How can Json.NET perform dependency injection during deserialization?
Asked Answered
C

3

24

When I have a class with no default constructor, i.e. using dependency injection to pass its dependencies, can Newtonsoft.Json create such an object?

For example:

public class SomeFoo
{
    private readonly IFooDependency _dependency;

    public SomeFoo(IFooDependency dependency){
        if(dependency == null)
            throw new ArgumentNullException("dependency");

        _dependency = dependency;
    }

    public string Data { get; set; }
    public int MoreData { get; set; }

    public void DoFoo(){
        Data = _dependency.GetFooData();
        MoreData = _dependency.GetMoreFooDate();
    }
}

During serialization, I only care of storing Data and MoreData (and the type of the object, but let's don't complicate things for the moment). Now, to deserialize, can I call something like

var obj = JsonConvert.DeserializeObject<SomeFoo>(jsonText);

How can I let JsonConvert know about my DI container?

(Note: A work-around would be to always have default constructors in my classes, and call Service Locator in there to get any dependencies I need. I'm just looking for some more clean solution without poluting my classes with such constructors).

Catachresis answered 18/2, 2014 at 16:33 Comment(2)
possible duplicate of DI and JSON.NETBanal
See this answer by CodeFuller to How can I use dependency injection with .NET Core 2 API options? for an elegant solution using a custom contract resolver which replaces JsonObjectContract.DefaultCreator() with the injected creation method. Unlike solutions that use CustomCreationConverter<T> this works correctly with PreserveReferencesHandling and TypeNameHandling.Huneycutt
U
13

I agree with the separation of concerns posted by Steven, and the answer Mark Seemann has posted here. However, if you still want to go this way, here is a solution that may help:

Inherit a CustomCreationConverter<T>:

internal class NinjectCustomConverter<T> : CustomCreationConverter<T> where T : class
{
    private readonly IResolutionRoot _serviceLocator;

    public NinjectCustomConverter(IResolutionRoot serviceLocator)
    {
        _serviceLocator = serviceLocator;
    }

    public override T Create(Type objectType)
    {
        return _serviceLocator.Get(objectType) as T;
    }
}

Then make sure you retrieve this converter instance via your DI container as well. The code below will deserialize and perform DI on your object:

var ninjectConverter = kernel.Get<NinjectCustomConverter<SerializedObject>>();
var settings = new JsonSerializerSettings();
settings.Converters.Add(ninjectConverter);

var instance = JsonConvert.DeserializeObject<SerializedObject>(json, settings);

Here is a complete working example.

Unswerving answered 18/2, 2014 at 18:12 Comment(5)
My personal alternative is to create a single IContractResolver that either maintains its own mapping for dependancies or that uses the DI resolver directly. This way you can simply inject an IContractResolver into the types that perform deserialization and the IoC is still strictly maintained. It's a very similar approach, but removes the 'dependancy' on a specific converter.Croat
This only makes sense if you have access to the SerializedObjectQuechuan
@AndrewHanlon - care to share your IContractResolver impleentation, it sounds like what I am looking forDundee
@Dundee Sorry, I don't have a good example on hand. But in essence, I had overridden the Create Parameter/Property methods in the DefaultContractResolver to inject the dependencies.Croat
No prob, we got there by overriding CreateObjectContract in our custom DefaultContractResolver, getting the JsonObjectContract from the base method, then overriding the contract.DefaultCreator with a func to call the constructor of the type with our injected values.Dundee
B
16

You shouldn't let JsonConvert know anything about your DI container. The problems you're experiencing are caused by a flaw in the design of your application. The flaw here is that you mix data and behavior.

If you separate the data from the behavior your problem (and many other problems) will simply go away. You can do this by creating two classes: one for the data, and one for the behavior:

public class SomeFoo
{
    public string Data { get; set; }
    public int MoreData { get; set; }
}

public class SomeFooHandler
{
    private readonly IFooDependency _dependency;

    public SomeFooHandler(IFooDependency dependency) {
        _dependency = dependency;
    }

    public void Handle(SomeFoo foo) {
        foo.Data = _dependency.GetFooData();
        foo.MoreData = _dependency.GetMoreFooDate();
    }
}

Since now data and behavior are separated, SomeFoo can be serialized without any problem and SomeFooHandler can simply be injected. SomeFoo has becomes a Parameter Object.

Banal answered 18/2, 2014 at 17:45 Comment(6)
Rings a bell for the visitor pattern, which could be quite helpful for what I want to achieve, thanks!Catachresis
Ugh! Duh, I can't believe I didn't realize this. I'm making a task execution runner and I had tasks take in their dependencies along with options. I'll separate them so this is much more clean. Thanks for the reminder.Irrupt
Why would you always want to separate data and behaviour?Newport
@MarkT: This argument is expessed more clearly here.Banal
This argument only holds true if the behavior does not need to be serialized. In the case where it does, this point becomes invalid.Quechuan
When using the pattern, how would you then apply it best to a collection. If I have a List<SomeFoo>, how best to apply SomeFooHandler to all?Sumrall
U
13

I agree with the separation of concerns posted by Steven, and the answer Mark Seemann has posted here. However, if you still want to go this way, here is a solution that may help:

Inherit a CustomCreationConverter<T>:

internal class NinjectCustomConverter<T> : CustomCreationConverter<T> where T : class
{
    private readonly IResolutionRoot _serviceLocator;

    public NinjectCustomConverter(IResolutionRoot serviceLocator)
    {
        _serviceLocator = serviceLocator;
    }

    public override T Create(Type objectType)
    {
        return _serviceLocator.Get(objectType) as T;
    }
}

Then make sure you retrieve this converter instance via your DI container as well. The code below will deserialize and perform DI on your object:

var ninjectConverter = kernel.Get<NinjectCustomConverter<SerializedObject>>();
var settings = new JsonSerializerSettings();
settings.Converters.Add(ninjectConverter);

var instance = JsonConvert.DeserializeObject<SerializedObject>(json, settings);

Here is a complete working example.

Unswerving answered 18/2, 2014 at 18:12 Comment(5)
My personal alternative is to create a single IContractResolver that either maintains its own mapping for dependancies or that uses the DI resolver directly. This way you can simply inject an IContractResolver into the types that perform deserialization and the IoC is still strictly maintained. It's a very similar approach, but removes the 'dependancy' on a specific converter.Croat
This only makes sense if you have access to the SerializedObjectQuechuan
@AndrewHanlon - care to share your IContractResolver impleentation, it sounds like what I am looking forDundee
@Dundee Sorry, I don't have a good example on hand. But in essence, I had overridden the Create Parameter/Property methods in the DefaultContractResolver to inject the dependencies.Croat
No prob, we got there by overriding CreateObjectContract in our custom DefaultContractResolver, getting the JsonObjectContract from the base method, then overriding the contract.DefaultCreator with a func to call the constructor of the type with our injected values.Dundee
G
0

If your objective is to use the injected dependency to modify the data, then you can create a custom Converter.

With this, you should be able to inject your dependency. Similar to the code below:

 var settings = new JsonSerializerSettings
            {
                Converters = { new FooConverter<T>(injectedDependency) }
            };
 return JsonConvert.DeserializeObject<Dto>(json, settings);

There're many samples of how to create a custom Converters, so you can refer to them.

Ganiats answered 26/6, 2019 at 3:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.