简体   繁体   中英

Using ContractResolver to realize an equivalent to MongoDB's BSON class map in Newtonsoft's Json.NET

This is a follow-up question to this one . What I am trying to achieve is to substitue [JsonConstructor] from my classes in order to get rid of the dependency to Json.Net in projects where I am not doing any serialization. With what I have gathered from other sources, using a ContractResolver is the way to go to implement some kind of "for type Foo use delegate Bar to create an instance of it"-mapping (see MongoDB's class map ). What's important to me is, that I want to stick with my readonly fields in my entities and therefore I must use some factory methods with parameters to create instances while the given arguments get assigned to the readonly fields during the creation process.

I have come up with the following solution (I went from having a public parameterless constructor, to a attributed constructor, to a private parameterless constructor, to no attributed constructor at all and with every step I explored the behavior in the overriden methods of my contract resolver):

class Thing
{
  public int Id { get; private set; }
  public string Name { get; private set; }

  //private Thing() { }

  //[JsonConstructor]
  private Thing(int id, string name)
  {
    this.Id = id;
    this.Name = name;
  }

  public static Thing Create(int id, string name)
  {
    return new Thing(id, name);
  }

  public static object Create2(object[] values)
  {
    return new Thing((int)values[0], (string)values[1]);
  }
}

My contract resolver looks as follows:

class CustomResolver : DefaultContractResolver
{
  public override JsonContract ResolveContract(Type type)
  {
    var b = base.ResolveContract(type);

    if(type == typeof(Thing))
    {
      var bb = b as JsonObjectContract;

      // figured out the name of the backing field using ILSpy
      var prop = typeof(JsonObjectContract).GetField("_creatorParameters",
        System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

      // in order to set the value of property CreatorParameters
      // which is used as parameter when OverrideCreator is invoked
      prop.SetValue(bb, bb.Properties); 

      bb.OverrideCreator = new ObjectConstructor<object>(Thing.Create2);
    }

    return b;
  }
}

And this is the usage of it:

Thing t1 = Thing.Create("Flim", 1);

// Serializer settings
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.ContractResolver = new CustomResolver();
settings.PreserveReferencesHandling = PreserveReferencesHandling.None;
settings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
settings.Formatting = Newtonsoft.Json.Formatting.Indented;

string json = JsonConvert.SerializeObject(t1, settings);
Thing t2 = JsonConvert.DeserializeObject<Thing>(json, settings);

It works as I expect it, but since I have hacked the system by using reflection I am convinced that I am doing something completely wrong and that there must be a cleaner way. Any ideas?

EDIT

What I have not thought of until now is, that I have some classes which are attributed with [JsonConverter(typeof(SomeSubstituteType))] which I would also get rid of if I want to elmininate the depency completely. These classes are so far not included in the type check in the contract resolver class. Is there an elegant way to make it work without the attribute and without adding the additional checks in the resolver by somehow configuring the JsonSerializerSettings additionally to what is configured so far?

It's true that DefaultContractResolver doesn't currently make it easy to inject logic to choose a custom constructor, since:

Nevertheless you can make the following improvements to your code:

  1. Override CreateObjectContract rather than ResolveContract . The former is called the first time a object of a given type type is encountered to construct a contract for the type which is subsequently cached. The latter is called for every object encountered including primitives. Thus overriding the former will be more performant. Also, CreateObjectContract() is within a thread-safe lock while ResolveContract() is not and can be called from multiple threads simultaneously. Thus your current code might not be thread safe.

  2. JsonObjectContract.CreatorParameters is a mutable collection so you can just clear the contents and add new properties.

Thus your contract resolver could look like:

class CustomResolver : DefaultContractResolver
{
    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var contract = base.CreateObjectContract(objectType);

        if (objectType == typeof(Thing))
        {
            contract.CreatorParameters.Clear();
            // For safety, specify the order concretely.
            contract.CreatorParameters.AddProperty(contract.Properties["Id"]); // Use nameof() in latest version.
            contract.CreatorParameters.AddProperty(contract.Properties["Name"]); // Use nameof() in latest version.
            contract.OverrideCreator = new ObjectConstructor<object>(Thing.Create2);
        }

        return contract;
    }
}

Example fiddle .

Update

I have some classes which are attributed with [JsonConverter(typeof(SomeSubstituteType))] which I would also get rid of if I want to elmininate the depency completely... Is there an elegant way to make it work without the attribute and without adding the additional checks in the resolver by somehow configuring the JsonSerializerSettings additionally to what is configured so far?

Yes, make sure that JsonConverter.CanConvert() is implemented for your converters, then add them to JsonSerializerSettings.Converters :

public class SomeSubstituteType : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(SomeOriginalType).IsAssignableFrom(objectType);
    }

    // Remainder as before
}

And then do:

var settings = new JsonSerializerSettings
{
    Converters = { new SomeSubstituteType(), new SecondConverter(), ... },
};

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM