问题
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 theJsonSerializer.Create
factory method, which takesJsonSerializerSettings
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 thesettings
, 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 inList<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:
- During serialization, determine how the type alias of a specific instance is stored in the
$type
properties of the JSON file. - During deserialization, use the type alias to determine which classes to build.
- 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