Using Newtonsoft.Json, how can I use a SerializationBinder and CustomResolver together to deserialize abstract/interface types?

淺唱寂寞╮ 提交于 2021-02-08 05:43:25

问题


Building on this very helpful answer from Brian Rogers I wrote the this, but it throws an exception, see more below.

I re-wrote the code, because the original version unfortunately doesn't quite meet my requirements: I need to include an ISerializationBinder implementation, because I have to map types differently for use with an IOC ("Inversion Of Control") container:

  • For serialization I need to bind a component type (i.e. class) to a type alias.
  • For deserialization I need to bind the type alias to a service type (i.e. interface).

Below is my code, including comments on what I changed compared to Brian's original answer.

Main:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

public class Program
{
    public static void Main()
    {
        Assembly thing = new Assembly
        {
            Name = "Gizmo",
            Parts = new List<IWidget>
        {
            new Assembly
            {
                Name = "Upper Doodad",
                Parts = new List<IWidget>
                {
                    new Bolt { Name = "UB1", HeadSize = 2, Length = 6 },
                    new Washer { Name = "UW1", Thickness = 1 },
                    new Nut { Name = "UN1", Size = 2 }
                }
            },
            new Assembly
            {
                Name = "Lower Doodad",
                Parts = new List<IWidget>
                {
                    new Bolt { Name = "LB1", HeadSize = 3, Length = 5 },
                    new Washer { Name = "LW1", Thickness = 2 },
                    new Washer { Name = "LW2", Thickness = 1 },
                    new Nut { Name = "LN1", Size = 3 }
                }
            }
        }
        };

        var settings = new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.Objects,
            Formatting = Formatting.Indented,
            ContractResolver = new CustomResolver(),
            SerializationBinder = new CustomSerializationBinder(),
        };

        Console.WriteLine("----- Serializing widget tree to JSON -----");
        string json = JsonConvert.SerializeObject(thing, settings);
        Console.WriteLine(json);
        Console.WriteLine();

        Console.WriteLine("----- Deserializing JSON back to widgets -----");
        using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(json)))
        using (StreamReader sr = new StreamReader(stream))
        using (JsonTextReader reader = new JsonTextReader(sr))
        {
            var serializer = JsonSerializer.Create(settings);

            var widget = serializer.Deserialize<IAssembly>(reader);
            Console.WriteLine();
            Console.WriteLine(widget.ToString(""));
        }
    }
}

Changes:

  • Instead of using new JsonSerializer {...}; I used the JsonSerializer.Create factory method, which takes JsonSerializerSettings as a parameter, to avoid redundant settings specifications. I don't think this should have any impact, but I wanted to mention it.
  • I added my CustomSerializationBinder to the settings, used for both deserialization and serialization.

Contract Resolver:

public class CustomResolver : DefaultContractResolver
{
    private MockIoc _ioc = new MockIoc();

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

        // This is just to show that the CreateObjectContract is being called
        Console.WriteLine("Created a contract for '" + objectType.Name + "'.");

        contract.DefaultCreator = () =>
        {
            // Use your IOC container here to create the instance.
            var instance = _ioc.Resolve(objectType);

            // This is just to show that the DefaultCreator is being called
            Console.WriteLine("Created a '" + objectType.Name + "' instance.");

            return instance;
        };
        return contract;
    }
}

Changes:

  • The DefaultCreator delegate now uses a mock IOC, see below, to create the required instance.

Serialization Binder:

public class CustomSerializationBinder : ISerializationBinder
{
    Dictionary<string, Type> AliasToTypeMapping { get; }
    Dictionary<Type, string> TypeToAliasMapping { get; }

    public CustomSerializationBinder()
    {
        TypeToAliasMapping = new Dictionary<Type, string>
        {
            { typeof(Assembly), "A" },
            { typeof(Bolt), "B" },
            { typeof(Washer), "W" },
            { typeof(Nut), "N" },
        };

        AliasToTypeMapping = new Dictionary<string, Type>
        {
            { "A", typeof(IAssembly) },
            { "B", typeof(IBolt) },
            { "W", typeof(IWasher) },
            { "N", typeof(INut) },
        };
    }

    public void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        var alias = TypeToAliasMapping[serializedType];

        assemblyName = null;  // I don't care about the assembly name for this example
        typeName = alias;
        Console.WriteLine("Binding type " + serializedType.Name + " to alias " + alias);
    }

    public Type BindToType(string assemblyName, string typeName)
    {
        var type = AliasToTypeMapping[typeName];
        Console.WriteLine("Binding alias " + typeName + " to type " + type);
        return type;
    }
}

Changes:

  • The mappings were changed according to my requirements:
    • TypeToAliasMapping uses the component type (i.e. class) for serializing.
    • AliasToTypeMapping uses the service type (i.e. interface) for deserializing.

Mock IOC Container:

public class MockIoc
{
    public Dictionary<Type, Type> ServiceToComponentMapping { get; set; }
    public MockIoc()
    {
        ServiceToComponentMapping = new Dictionary<Type, Type>
        {
            { typeof(IAssembly), typeof(Assembly)   },
            { typeof(IBolt)    , typeof(Bolt)       },
            { typeof(IWasher)  , typeof(Washer)     },
            { typeof(INut)     , typeof(Nut)        },
        };
    }

    public object Resolve(Type serviceType)
    {
        var componentType = ServiceToComponentMapping[serviceType];
        var instance = Activator.CreateInstance(componentType);

        return instance;
    }
}       

This is new and simply maps the interface (obtained by the binding during deserialization) to a class (used for creating an actual instance).

Example classes:

public interface IWidget
{
    string Name { get; }
    string ToString(string indent = "");
}

public interface IAssembly : IWidget { }
public class Assembly : IAssembly
{
    public string Name { get; set; }
    public List<IWidget> Parts { get; set; }

    public string ToString(string indent = "")
    {
        var sb = new StringBuilder();
        sb.AppendLine(indent + "Assembly " + Name + ", containing the following parts:");
        foreach (IWidget part in Parts)
        {
            sb.AppendLine(part.ToString(indent + "  "));
        }
        return sb.ToString();
    }
}

public interface IBolt : IWidget { }
public class Bolt : IBolt
{
    public string Name { get; set; }
    public int HeadSize { get; set; }
    public int Length { get; set; }

    public string ToString(string indent = "")
    {
        return indent + "Bolt " + Name + " , head size + " + HeadSize + ", length "+ Length;
    }
}

public interface IWasher : IWidget { }
public class Washer : IWasher
{
    public string Name { get; set; }
    public int Thickness { get; set; }

    public string ToString(string indent = "")
    {
        return indent+ "Washer "+ Name + ", thickness " + Thickness;
    }
}

public interface INut : IWidget { }
public class Nut : INut
{
    public string Name { get; set; }
    public int Size { get; set; }

    public string ToString(string indent = "")
    {
        return indent + "Nut " +Name + ", size "  + Size;
    }
}

Changes:

  • I added specific interfaces for each class which all share the common interface, IWidget, used in List<IWidget> Assembly.Parts.

Output:

----- Serializing widget tree to JSON -----
Created a contract for 'Assembly'.
Binding type Assembly to alias A
Created a contract for 'IWidget'.
Binding type Assembly to alias A
Created a contract for 'Bolt'.
Binding type Bolt to alias B
Created a contract for 'Washer'.
Binding type Washer to alias W
Created a contract for 'Nut'.
Binding type Nut to alias N
Binding type Assembly to alias A
Binding type Bolt to alias B
Binding type Washer to alias W
Binding type Washer to alias W
Binding type Nut to alias N
{
  "$type": "A",
  "Name": "Gizmo",
  "Parts": [
    {
      "$type": "A",
      "Name": "Upper Doodad",
      "Parts": [
        {
          "$type": "B",
          "Name": "UB1",
          "HeadSize": 2,
          "Length": 6
        },
        {
          "$type": "W",
          "Name": "UW1",
          "Thickness": 1
        },
        {
          "$type": "N",
          "Name": "UN1",
          "Size": 2
        }
      ]
    },
    {
      "$type": "A",
      "Name": "Lower Doodad",
      "Parts": [
        {
          "$type": "B",
          "Name": "LB1",
          "HeadSize": 3,
          "Length": 5
        },
        {
          "$type": "W",
          "Name": "LW1",
          "Thickness": 2
        },
        {
          "$type": "W",
          "Name": "LW2",
          "Thickness": 1
        },
        {
          "$type": "N",
          "Name": "LN1",
          "Size": 3
        }
      ]
    }
  ]
}

----- Deserializing JSON back to widgets -----
Created a contract for 'IAssembly'.
Binding alias A to type IAssembly
Created a 'IAssembly' instance.
Run-time exception (line 179): Object reference not set to an instance of an object.

Stack Trace:

[System.NullReferenceException: Object reference not set to an instance of an object.]
   at Assembly.ToString(String indent) :line 179
   at Program.Main() :line 68

Serialization appears to be fully correct, but the exception and console outputs indicate that while the "root" Assembly instance was created correctly, its members were not populated.

As mentioned in my previous question, I'm currently using a JsonConverter in combination with a SerializationBinder - and it works fine, except for the performance (see link above). It's been a while since I implemented that solution and I remember finding it hard to wrap my head around the intended or actual data flow, esp. while deserializing. And, likewise, I don't yet quite understand how the ContractResolver comes into play here.

What am I missing?


回答1:


This is very close to working. The problem is that you have made your IWidget derivative interfaces empty. Since the SerializationBinder is binding the $type codes in the JSON to interfaces instead of concrete types, the contract resolver is going to look at the properties on those interfaces to determine what can be deserialized. (The contract resolver's main responsibility is to map properties in the JSON to their respective properties in the class structure. It assigns a "contract" to each type, which governs how that type is handled during de/serialization: how it is created, whether to apply a JsonConverter to it, and so forth.)

IAssembly doesn't have a Parts list on it, so that's as far as the deserialization gets. Also, the Name property on IWidget does not have a setter, so the name of the root assembly object doesn't get populated either. The NRE is being thrown in the ToString() method in the demo code which is trying to dump out the null Parts list without an appropriate null check (that one's on me). Anyway, if you add the public properties from the concrete Widget types to their respective interfaces, then it works. So:

public interface IWidget
{
    string Name { get; set; }
    string ToString(string indent = "");
}

public interface IAssembly : IWidget 
{
    List<IWidget> Parts { get; set; }
}

public interface IBolt : IWidget 
{
    int HeadSize { get; set; }
    int Length { get; set; }
}

public interface IWasher : IWidget 
{
    public int Thickness { get; set; }
}

public interface INut : IWidget 
{
    public int Size { get; set; }
}

Here's the working Fiddle: https://dotnetfiddle.net/oJ54VD




回答2:


Brian's answer is wonderful and helped me understand the cause of my problems and how to resolve them. Here's my recipe that I've destilled from the things I've learned.

A noteable difference to the code in the question above or Brian's solution is that here the type alias is associated with two types. Brian's bug fix above was to include the members that needed to be populated in the type that was used by the contract resolver (see below), the service interface type. But these members may not be part of the service interface (which I may not have control over); they may just include some extra data that my class needs internally that need to be (de)serialized.

This is (as always with recipes) just a suggestion based on my specific requirements.

Goal:

  • Implement a JSON serialization framework that is bulletproof in the sense that the $type alias in the JSON file is only loosely coupled ("bound") to the actual classes defined in code. This will help us to maintain backwards compatibility between the current code base and JSON files created with an older version.
  • Include an IOC ("Inversion Of Control") container for creating the required instances, allowing us to exchange the actual components we want to use for specific services, e.g. for testing with mock classes.

Requirements:

A mechanism to translate/convert the type information throughout the (de)serialization process, specifically:

  1. During serialization, determine how the type alias of a specific instance is stored in the $type properties of the JSON file.
  2. During deserialization, use the type alias to determine which classes to build.
  3. During deserialization, determine how the instances' members are to be populated.

Components:

The following components are going to be used, see this fiddle for a working example.

public class MockIoc
{
    public Dictionary<Type, Type> ServiceToComponentMapping { get; }

    public MockIoc()
    {
        ServiceToComponentMapping = new Dictionary<Type, Type>
        {
            { typeof( IAssembly), typeof(Assembly)   },
            { typeof( IBolt)    , typeof(Bolt)       },
            { typeof( IWasher)  , typeof(Washer)     },
            { typeof( INut)     , typeof(Nut)        },
        };
    }

    public object Resolve(Type serviceType)
    {
        var componentType = ServiceToComponentMapping[serviceType];
        var instance = Activator.CreateInstance(componentType);

        return instance;
    }
}

The MockIoc class is for demo purposes only. The types defined here, must be "sync'd" with the types defined in the 'CustomBindings`, see below. They relationships/translations are defined here, seperating concerns between the IOC and serialization framework elements.

public class CustomBinding
{
    public string Alias { get; set; }
    public Type SerializationType { get; set; }
    public Type ServiceType { get; set; }
}

The CustomBinding class holds all the information we need to define the desired translations. Note that ServiceType is the type that will be handed to the IOC container and is usually an interface or abstract class; the final translation to the desired, concrete component is defined seperately, as part of the IOC configuration.

public class CustomBindings
{
    public CustomBindings()
    {
        _bindings = new List<CustomBinding>()
        {
            new CustomBinding() { Alias = "A", SerializationType = typeof(Assembly), ServiceType = typeof(IAssembly)},
            new CustomBinding() { Alias = "B", SerializationType = typeof(Bolt), ServiceType = typeof(IBolt)},
            new CustomBinding() { Alias = "W", SerializationType = typeof(Washer), ServiceType = typeof(IWasher)},
            new CustomBinding() { Alias = "N", SerializationType = typeof(Nut), ServiceType = typeof(INut)},

        };
    }

    private readonly List<CustomBinding> _bindings;

    public String SerializedTypeToAlias(Type type) => _bindings.Single(b => b.SerializationType == type).Alias;

    public Type AliasToDeserializedType(string alias) => _bindings.Single(b => b.Alias == alias).SerializationType;

    public Type DeserializedTypeToService(Type type) => _bindings.Single(b => b.SerializationType == type).ServiceType;
}

The CustomBindings class defines the intended bindings and provides methods for translating the type information.

The types handed to SerializedTypeToAlias will be the actual class types in the current code base. The type returned by AliasToDeserializedType will determine which members are populated. The type returned by DeserializedTypeToService will be used to resolve a concrete component for the service type by the IOC container.

public class CustomSerializationBinder : ISerializationBinder
{
    private readonly CustomBindings _bindings;

    public CustomSerializationBinder(CustomBindings bindings)
    {
        _bindings = bindings;
    }
    public void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        var alias = _bindings.SerializedTypeToAlias(serializedType);

        assemblyName = null;  // Not required for custom type aliases
        typeName = alias;
        Console.WriteLine("Binding type " + serializedType.Name + " to alias " + alias);
    }

    public Type BindToType(string assemblyName, string typeName)
    {
        var type = _bindings.AliasToDeserializedType(typeName);
        Console.WriteLine("Binding alias " + typeName + " to type " + type);
        return type;
    }
}

The CustomSerializationBinder is used by the JSON framework: BindToName is called during serialization and provides the type alias to the JSON file. BindToType is called during deserialization and determines the type to be used (by the contract resolver, see below) to specify how the created instance's members are to be populated. It is also the type handed to the IOC container. So this can be an interface, abstract class or concrete class; it is not the type will ultimately be built.

public class CustomResolver : DefaultContractResolver
{
    public CustomResolver(MockIoc ioc, CustomBindings bindings)
    {
        _ioc = ioc;
        _bindings = bindings;
    }

    private readonly MockIoc _ioc;
    private readonly CustomBindings _bindings;

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

        // This is just to show that the CreateObjectContract method is being called
        Console.WriteLine("Created a contract for '" + objectType.Name + "'.");

        contract.DefaultCreator = () =>
        {
            var serviceType = _bindings.DeserializedTypeToService(objectType);

            // Use your IOC container here to create the instance.
            var instance = _ioc.Resolve(serviceType);

            // This is just to show that the DefaultCreator is being called
            Console.WriteLine("Resolved '" + objectType.Name + "' to a '" + instance.GetType().Name + "' instance.");

            return instance;
        };
        return contract;
    }
}

The CustomResolver class is also used by the JSON framework. The objectType handed in here is the type returned by CustomBindings.AliasToDeserializedType, see above, and determines how the newly created instance is to be populated. The actual type of that instance will be determined by the IOC, after (at least here, in my recipe) being converted again to serviceType.

public class CustomSettings : JsonSerializerSettings
{
    public CustomSettings(MockIoc ioc, CustomBindings bindings) : base()
    {
        TypeNameHandling = TypeNameHandling.Objects;
        Formatting = Formatting.Indented;
        ContractResolver = new CustomResolver(ioc, bindings);
        SerializationBinder = new CustomSerializationBinder(bindings);
    }
}

These are the settings required by the JSON framework to tie it all together. Note, of course, the ContractResolver and SerializationBinder.

Main and demo classes

public class JsonContactResolverTests
{
    public static void Main()
    {
        Assembly input = getDemoAssembly();
        var ioc = new MockIoc();
        var bindings = new CustomBindings();
        var settings = new CustomSettings(ioc, bindings);

        Console.WriteLine("----- Serializing widget tree to JSON -----");
        string json = JsonConvert.SerializeObject(input, settings);
        Console.WriteLine(json);
        Console.WriteLine();

        Console.WriteLine("----- Deserializing JSON back to widgets -----");
        using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(json)))
        using (StreamReader sr = new StreamReader(stream))
        using (JsonTextReader reader = new JsonTextReader(sr))
        {
            var serializer = JsonSerializer.Create(settings);
            var loaded = serializer.Deserialize<IAssembly>(reader);
            Console.WriteLine();
            Console.WriteLine(loaded.ToString(""));

            Console.WriteLine("----- Serializing loaded instance to JSON ----");
            string json2 = JsonConvert.SerializeObject(loaded, settings);

            Console.WriteLine("----- Comparing JSON outputs ----");
            if (json != json2) throw new Exception("JSON outputs are not the same");
            Console.WriteLine("ok");

        }
    }

    private static Assembly getDemoAssembly()
    {
        return new Assembly
        {
            Name = "Gizmo",
            Parts = new List<IWidget>
            {
                new Assembly
                {
                    Name = "Upper Doodad",
                    Parts = new List<IWidget>
                    {
                        new Bolt { Name = "UB1", HeadSize = 2, Length = 6 },
                        new Washer { Name = "UW1", Thickness = 1 },
                        new Nut { Name = "UN1", Size = 2 }
                    }
                },
                new Assembly
                {
                    Name = "Lower Doodad",
                    Parts = new List<IWidget>
                    {
                        new Bolt { Name = "LB1", HeadSize = 3, Length = 5 },
                        new Washer { Name = "LW1", Thickness = 2 },
                        new Washer { Name = "LW2", Thickness = 1 },
                        new Nut { Name = "LN1", Size = 3 }
                    }
                }
            }
        };
    }
}

public interface IWidget
{
    string Name { get; set; }
    string ToString(string indent = "");
}

public interface IAssembly : IWidget { }
public class Assembly : IAssembly
{
    public string Name { get; set; }
    public List<IWidget> Parts { get; set; }

    public string ToString(string indent = "")
    {
        var sb = new StringBuilder();
        sb.AppendLine(indent + "Assembly " + Name + ", containing the following parts:");
        if (Parts != null)
        {
            foreach (IWidget part in Parts)
            {
                sb.AppendLine(part.ToString(indent + "  "));
            }
        }
        return sb.ToString();
    }
}

public interface IBolt : IWidget { }
public class Bolt : IBolt
{
    public string Name { get; set; }
    public int HeadSize { get; set; }
    public int Length { get; set; }

    public string ToString(string indent = "")
    {
        return indent + "Bolt " + Name + " , head size + " + HeadSize + ", length " + Length;
    }
}

public interface IWasher : IWidget { }
public class Washer : IWasher
{
    public string Name { get; set; }
    public int Thickness { get; set; }

    public string ToString(string indent = "")
    {
        return indent + "Washer " + Name + ", thickness " + Thickness;
    }
}

public interface INut : IWidget { }
public class Nut : INut
{
    public string Name { get; set; }
    public int Size { get; set; }

    public string ToString(string indent = "")
    {
        return indent + "Nut " + Name + ", size " + Size;
    }
}

Finally: the binding associations may appear to be complicated or unnecessarily complex, but a) it is just a contrived repro example and b) it shows just one possible way of "interfering" with JSON's out-of-the-box behavior and the relevant points-of-entry.



来源:https://stackoverflow.com/questions/65243789/using-newtonsoft-json-how-can-i-use-a-serializationbinder-and-customresolver-to

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!