How can I reset a JsonReader instance instead of calling JToken.CreateReader?

依然范特西╮ 提交于 2021-01-28 21:52:44

问题


I have a large JSON file that I'm deserializing using Newtonsoft.Json - and I'm experiencing performance issues, esp. regarding one specific detail:

While deserializing I need to use an IOC ("Inversion Of Control") Container to create the required instances.

Inspired by https://skrift.io/issues/bulletproof-interface-deserialization-in-jsonnet/ and other sources I implemented a custom JsonConverter that overrides ReadJson. The method signature includes Type objectType, but e.g. in the case of arrays, this may well only be some common interface and not a specific class type.

I serialized the data using the option TypeNameHandling.Objects, which includes a "$type" token for each class.

If I detect that the type is not sufficient, I do something like the following:

JObject o = JObject.Load(reader);

var typeName = o["$type"].ToString();

if (!tryGetBindingByName(typeName, out var detectedBinding))
    throw new SpecificBindingNotFoundBinderException(typeName, binding);

result = detectedBinding.DeserializeType;

//"reset" reader, because due to `JObject.Load(reader)` call above the reader's current position has changed
reader = o.CreateReader();

I think the performance critical part - esp. regarding heap allocations - is where I manually read the JSON object - and then the need to call reader = o.CreateReader().

My question: can an existing JTokenReader be "reset" so that I can continue using it, instead of creating a new instance all the time?


回答1:


No. Neither JTokenReader, nor the abstract JsonReader on which it is based, provide a means to "reset" or go backward to an earlier point. They are forward-only readers by design.

However, it sounds like the problem you are really trying to solve is just getting Json.Net to use your IOC container to create concrete instances of your classes in a performant manner. For that, I think you might be using the wrong tool for the job. Instead of a JsonConverter, try using a custom ContractResolver. You can supply your own DefaultCreator function to handle the instantiation for each Type, as shown below.

class CustomResolver : DefaultContractResolver
{
    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        JsonObjectContract contract = base.CreateObjectContract(objectType);
        contract.DefaultCreator = () =>
        {
            // Change this to use your IOC container to create the instance.
            var instance = Activator.CreateInstance(objectType);

            return instance;
        };
        return contract;
    }
}

To use the resolver, you just need to add it to the JsonSerializerSettings alongside the TypeNameHandling.Objects option you are already using:

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

Here is a round-trip demo: https://dotnetfiddle.net/eHcLMb


If you also need to customize the type names that are written into the JSON, you can implement a custom SerializationBinder to do that part.

Here is a contrived example to demonstrate the concept. The CustomSerializationBinder below uses some hardcoded dictionaries to map some known types to an "alias", which is just a unique string I chose to represent that type. The "known types" here are the same ones from the "Bulletproof Interface Deserialization in Json.Net" article you linked to in your question. You can replace the dictionaries with whatever type lookup mechanism you require. Note that I'm not using the assemblyName in the example below just to keep the JSON simple.

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

    public CustomSerializationBinder()
    {
        TypeToAliasMapping = new Dictionary<Type, string>
        {
            { typeof(Person), "Peep" },
            { typeof(Programming), "Coder" },
            { typeof(Writing), "Wordsmith" }
        };

        AliasToTypeMapping = new Dictionary<string, Type>(
            TypeToAliasMapping.Select(kvp => new KeyValuePair<string, Type>(kvp.Value, kvp.Key))
        );
    }

    public void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        if (TypeToAliasMapping.TryGetValue(serializedType, out string alias))
        {
            assemblyName = null;  // I don't care about the assembly name for this example
            typeName = alias;
            return;
        };
        throw new Exception($"Type {serializedType.Name} is not mapped to an alias");
    }

    public Type BindToType(string assemblyName, string typeName)
    {
        if (AliasToTypeMapping.TryGetValue(typeName, out Type type))
        {
            return type;
        }
        throw new Exception("No type was found matching the alias {typeName}");
    }
}

To use the custom binder, you guessed it, just add it to the settings with the other stuff:

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

Here is the same round-trip demo with the binder: https://dotnetfiddle.net/YC9IAT



来源:https://stackoverflow.com/questions/65226298/how-can-i-reset-a-jsonreader-instance-instead-of-calling-jtoken-createreader

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