JsonConverter Attribute : deserialize using custom

2020-04-14 02:41发布

Am using a custom JsonConverter to convert my JSON object. This is achieved via the JsonConverter attribute to the IQuery object below

[JsonConverter(typeof(CustomConverter<IQuery>))]
public interface IQuery
{
}

The custom generic class is below (some bits removed for brevity)

public class CustomConverter<T> : JsonConverter
{
    // This should be created via AutoFac
    public ICustomObjectCreator<T> ObjectCreator { get; set; }

    // This default constructr always gets called
    public CustomConverter() {}

    // I want to call this constructor
    [JsonConstructor]
    public CustomConverter(ICustomObjectCreator<T> objectCreator)
    {
        Context = context;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;

        // Load JObject from stream 
        var jObject = JObject.Load(reader);

        // Create target object based on JObject 
        var target = Create(objectType, jObject);

        // Populate the object properties 
        serializer.Populate(jObject.CreateReader(), target);

        return target;
    }

    protected T Create(Type objectType, JObject jObject)
    {
        var type = jObject.GetValue("type", StringComparison.OrdinalIgnoreCase)?.Value<string>();
        return ObjectCreator.Create(type);
    }
}

The ICustomObjectConverter interface is simple

public interface ICustomObjectCreator<out T>
{
    T Create(string type);
}

and one of its implementation

public class QueryObjectCreator : ICustomObjectCreator<IQuery>
{
    public IQuery Create(string type)
    {
        // ... some logic to create a concrete object
        return (IQuery)concreteObject;
    }
}

Finally, Autofac is wired to honor the above

builder.RegisterType<QueryObjectCreator>()
       .As<ICustomObjectCreator<IQuery>>()
       .InstancePerLifetimeScope();

Problems:

  1. When CustomJsonConverter is called, only its default constructor is called. The JsonConstructor is NEVER called.
  2. If I remove the default constructor, then the whole JsonConverter is NEVER called!

I have an inklinkg that AutoFac is never being called when JsonConverter is being invoked. I even tried property injection to construct QueryObjectConstruct explicitly, but even that is never called. How can I make it work so that my QueryObjectCretor is injected via DI?

I found this article about Dependency Injection and JSON.net deserialization. However, that is for manual resolve using DeserializeObject<>() call, how can I, if it works, make it work with JsonConverter attribute?

Thanks

2条回答
何必那么认真
2楼-- · 2020-04-14 02:54

You could do the following steps to achieve your goal:

  1. Create a non generic interface for your ICustomObjectCreator interface, to make creating objects more convenient.
  2. Introduce a generic ObjectCreatorBase<T> base class which calls your generic Create method.
  3. Create and set default settings which are used by JsonConvert.
  4. Set the AutofacContractResolver as ContractResolver.

See the following example to get you started:

void Main()
{
    var builder = new ContainerBuilder();

    builder.RegisterType<QueryObjectCreator>()
        .As<ICustomObjectCreator<IQuery>>()
        .InstancePerLifetimeScope();

    var container = builder.Build();

    Func<JsonSerializerSettings> settingsFactory = () =>
    {
        var settings = new JsonSerializerSettings();
        settings.ContractResolver = new AutofacContractResolver(container);

        return settings;
    };

    JsonConvert.DefaultSettings = settingsFactory;

    var myObject = new MyObject { Query = new Query(42) };

    var json = JsonConvert.SerializeObject(myObject);

    myObject = JsonConvert.DeserializeObject<MyObject>(json);
    Console.WriteLine(myObject.Query.MyProperty);
}

// Define other methods and classes here
public class AutofacContractResolver : DefaultContractResolver
{
    private readonly IContainer _container;

    public AutofacContractResolver(IContainer container)
    {
        _container = container;
    }

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        JsonObjectContract contract = base.CreateObjectContract(objectType);

        var customObjectCreatorType = typeof(ICustomObjectCreator<>).MakeGenericType(objectType);

        if (!_container.IsRegistered(customObjectCreatorType))
            return contract;

        var customObjectCreator = (ICustomObjectCreator) _container.Resolve(customObjectCreatorType);

        // I don't know how you want to obtain the string which shall be passed to CreateObject
        contract.DefaultCreator = () => customObjectCreator.CreateObject("XYZ");
        return contract;
    }
}

public interface ICustomObjectCreator
{
    object CreateObject(string type);
}

public interface ICustomObjectCreator<out T> : ICustomObjectCreator
{
    T Create(string type);
}

public abstract class ObjectCreatorBase<T> : ICustomObjectCreator<T>
{
    public object CreateObject(string type)
    {
        return Create(type);
    }

    public abstract T Create(string type);
}

public class QueryObjectCreator : ObjectCreatorBase<IQuery>
{
    public override IQuery Create(string type)
    {
        Console.WriteLine("Create called");

        // ... some logic to create a concrete object
        var concreteObject = new Query();
        return (IQuery)concreteObject;
    }
}

public interface IQuery
{
    int MyProperty { get; set; }
}

public class Query : IQuery
{
    public int MyProperty { get; set; }

    public Query()
    {
    }

    public Query(int myProperty)
    {
        MyProperty = myProperty;
    }
}

public class MyObject
{
    public IQuery Query { get; set; }
}

The output should be

Create called
42

Maybe you could simplify the code by removing all ICustomObjectCreator instances by simply using Autofac to create your objects directly.

Update

The first approach works, but it does not take into account that you need to get a string to decide which kind of object you are creating (type).

To get this to work you could the following:

  1. Register the CustomConverter as generic.
  2. Overwrite the ResolveContractConverter method to return an instance of the converter in case any ICustomObjectCreator has been registered for the type.
  3. Alter the DefaultSettings so that the AutofacContractResolver will be used.

See the following example:

void Main()
{
    var builder = new ContainerBuilder();

    builder.RegisterType<QueryObjectCreator>()
        .As<ICustomObjectCreator<IQuery>>()
        .InstancePerLifetimeScope();

    builder.RegisterGeneric(typeof(CustomConverter<>)).AsSelf().InstancePerLifetimeScope();

    var container = builder.Build();

    Func<JsonSerializerSettings> settingsFactory = () =>
    {
        var settings = new JsonSerializerSettings();
        settings.ContractResolver = new AutofacContractResolver(container);

        return settings;
    };

    JsonConvert.DefaultSettings = settingsFactory;

    var myObject = new MyObject { Query = new Query(42) };

    var json = JsonConvert.SerializeObject(myObject);

    myObject = JsonConvert.DeserializeObject<MyObject>(json);
    Console.WriteLine(myObject.Query.MyProperty);
}

// Define other methods and classes here
public class AutofacContractResolver : DefaultContractResolver
{
    private readonly IContainer _container;

    public AutofacContractResolver(IContainer container)
    {
        _container = container;
    }

    protected override JsonConverter ResolveContractConverter(Type objectType)
    {
        var customObjectCreatorType = typeof(ICustomObjectCreator<>).MakeGenericType(objectType);
        if (!_container.IsRegistered(customObjectCreatorType))
            return base.ResolveContractConverter(objectType);

        var customConverterType = typeof(CustomConverter<>).MakeGenericType(objectType);
        return (JsonConverter) _container.Resolve(customConverterType);
    }
}

public class CustomConverter<T> : JsonConverter
{
    // This should be created via AutoFac
    public ICustomObjectCreator<T> ObjectCreator { get; }

    // This default constructr always gets called
    public CustomConverter() { }

    // I want to call this constructor
    public CustomConverter(ICustomObjectCreator<T> objectCreator)
    {
        Console.WriteLine("Constructor called");
        ObjectCreator = objectCreator;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;

        // Load JObject from stream 
        var jObject = JObject.Load(reader);

        // Create target object based on JObject 
        var target = Create(objectType, jObject);

        // Populate the object properties 
        serializer.Populate(jObject.CreateReader(), target);

        return target;
    }

    protected T Create(Type objectType, JObject jObject)
    {
        var type = jObject.GetValue("type", StringComparison.OrdinalIgnoreCase)?.Value<string>();
        return ObjectCreator.Create(type);
    }
}

public interface ICustomObjectCreator<out T> 
{
    T Create(string type);
}

public class QueryObjectCreator : ICustomObjectCreator<IQuery>
{
    public IQuery Create(string type)
    {
        Console.WriteLine("Create called");

        // ... some logic to create a concrete object
        var concreteObject = new Query();
        return (IQuery)concreteObject;
    }
}

public interface IQuery
{
    int MyProperty { get; set; }
}

public class Query : IQuery
{
    public int MyProperty { get; set; }

    public Query()
    {
    }

    public Query(int myProperty)
    {
        MyProperty = myProperty;
    }
}

public class MyObject
{
    public IQuery Query { get; set; }
}

The output should be

Constructor called
Create called
42

Here is the .NET Fiddle link for the sample.

查看更多
ら.Afraid
3楼-- · 2020-04-14 02:58

Suppose an autofac setting like the following:

public class AutofacContractResolver : DefaultContractResolver
{
    private readonly IContainer _container;

    public AutofacContractResolver(IContainer container)
    {
        _container = container;
    }

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        JsonObjectContract contract = base.CreateObjectContract(objectType);

        // use Autofac to create types that have been registered with it
        if (_container.IsRegistered(objectType))
        {
           contract.DefaultCreator = () => _container.Resolve(objectType);
        }  

        return contract;
    }
}

Then, suppose your class is like this:

public class TaskController
{
    private readonly ITaskRepository _repository;
    private readonly ILogger _logger;

    public TaskController(ITaskRepository repository, ILogger logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public ITaskRepository Repository
    {
        get { return _repository; }
    }

    public ILogger Logger
    {
        get { return _logger; }
    }
}

Therefore, the usage of the resolver in deserialization could be like:

ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<TaskRepository>().As<ITaskRepository>();
builder.RegisterType<TaskController>();
builder.Register(c => new LogService(new DateTime(2000, 12, 12))).As<ILogger>();

IContainer container = builder.Build();

AutofacContractResolver contractResolver = new AutofacContractResolver(container);

string json = @"{
      'Logger': {
        'Level':'Debug'
      }
}";

// ITaskRespository and ILogger constructor parameters are injected by Autofac 
TaskController controller = JsonConvert.DeserializeObject<TaskController>(json, new JsonSerializerSettings
{
    ContractResolver = contractResolver
});

Console.WriteLine(controller.Repository.GetType().Name);

You can see more details in http://www.newtonsoft.com/json/help/html/DeserializeWithDependencyInjection.htm

查看更多
登录 后发表回答