Как получить имя <T> из родового типа и передать его в JsonProperty()?

Я получаю следующую ошибку с кодом ниже:

"Ссылка на объект требуется для нестатического поля, метода или свойство 'Response.PropName'"

Код:

public class Response<T> : Response
{
    private string PropName
    {
        get
        {
            return typeof(T).Name;
        }
    }            
    [JsonProperty(PropName)]
    public T Data { get; set; }
}

Ответ 1

То, что вы пытаетесь сделать, возможно, но не тривиально и не может быть выполнено только с помощью встроенных атрибутов от JSON.NET. Вам понадобится настраиваемый атрибут и пользовательский разрешитель.

В этом решении я придумал:

Объявите этот настраиваемый атрибут:

[AttributeUsage(AttributeTargets.Property)]
class JsonPropertyGenericTypeNameAttribute : Attribute
{
    public int TypeParameterPosition { get; }

    public JsonPropertyGenericTypeNameAttribute(int position)
    {
        TypeParameterPosition = position;
    }
}

Примените его к свойству Data

public class Response<T> : Response
{
    [JsonPropertyGenericTypeName(0)]
    public T Data { get; set; }
}

(0 - позиция T в Response<T> параметрах типового типа)

Объявите следующий разрешитель контракта, который будет искать атрибут JsonPropertyGenericTypeName и получить фактическое имя аргумента типа:

class GenericTypeNameContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var prop = base.CreateProperty(member, memberSerialization);
        var attr = member.GetCustomAttribute<JsonPropertyGenericTypeNameAttribute>();
        if (attr != null)
        {
            var type = member.DeclaringType;
            if (!type.IsGenericType)
                throw new InvalidOperationException($"{type} is not a generic type");
            if (type.IsGenericTypeDefinition)
                throw new InvalidOperationException($"{type} is a generic type definition, it must be a constructed generic type");
            var typeArgs = type.GetGenericArguments();
            if (attr.TypeParameterPosition >= typeArgs.Length)
                throw new ArgumentException($"Can't get type argument at position {attr.TypeParameterPosition}; {type} has only {typeArgs.Length} type arguments");
            prop.PropertyName = typeArgs[attr.TypeParameterPosition].Name;
        }
        return prop;
    }
}

Сериализовать этот резольвер в настройках сериализации:

var settings = new JsonSerializerSettings { ContractResolver = new GenericTypeNameContractResolver() };
string json = JsonConvert.SerializeObject(response, settings);

Это даст следующий результат для Response<Foo>

{
  "Foo": {
    "Id": 0,
    "Name": null
  }
}

Ответ 2

Здесь возможно более простой способ его достижения. Все, что вам нужно сделать, - это расширить JObject Response, например:

public class Response<T>: Newtonsoft.Json.Linq.JObject
{
    private static string TypeName = (typeof(T)).Name;

    private T _data;

    public T Data {
        get { return _data; }
        set {
            _data = value;
            this[TypeName] = Newtonsoft.Json.Linq.JToken.FromObject(_data);   
        }
    }
}

Если вы это сделаете, следующее будет работать так, как вы ожидаете:

   static void Main(string[] args)
    {
        var p1 = new  Response<Int32>();
        p1.Data = 5;
        var p2 = new Response<string>();
        p2.Data = "Message";


        Console.Out.WriteLine("First: " + JsonConvert.SerializeObject(p1));
        Console.Out.WriteLine("Second: " + JsonConvert.SerializeObject(p2));
    }

Вывод:

First: {"Int32":5}
Second: {"String":"Message"}

Если вы не можете Response<T> расширять JObject, потому что вам действительно нужно расширить Response, вы могли бы сам Response расширять JObject, а затем Response<T> расширять Response, как раньше. Он должен работать одинаково.

Ответ 3

@Томас Левеске: Хорошо. Поэтому скажем, что вы не можете расширять JObject в Response<T>, потому что вам нужно расширить существующий класс Response. Здесь вы можете реализовать одно и то же решение:

public class Payload<T> : Newtonsoft.Json.Linq.JObject  {
    private static string TypeName = (typeof(T)).Name;
    private T _data;

    public T Data {
        get { return _data; }
        set {
            _data = value;
            this[TypeName] = Newtonsoft.Json.Linq.JToken.FromObject(_data);
        }
    }
}

 //Response is a pre-existing class...
public class Response<T>: Response { 
    private Payload<T> Value;

    public Response(T arg)  {
        Value = new Payload<T>() { Data = arg };            
    }

    public static implicit operator JObject(Response<T> arg) {
        return arg.Value;
    }

    public string Serialize() {
        return Value.ToString();
    }
}

Итак, теперь для Сериализации класса существуют следующие параметры:

   static void Main(string[] args) {
        var p1 = new Response<Int32>(5);
        var p2 = new Response<string>("Message");
        JObject p3 = new Response<double>(0.0);
        var p4 = (JObject) new Response<DateTime>(DateTime.Now);

        Console.Out.WriteLine(p1.Serialize());
        Console.Out.WriteLine(p2.Serialize());
        Console.Out.WriteLine(JsonConvert.SerializeObject(p3));
        Console.Out.WriteLine(JsonConvert.SerializeObject(p4));
    }

Результат будет выглядеть примерно так:

{"Int32":5}
{"String":"Message"}
{"Double":0.0}
{"DateTime":"2016-08-25T00:18:31.4882199-04:00"}