Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
740 views
in Technique[技术] by (71.8m points)

.net - How to flatten a referenced object into two json.net properties on the referer?

Consider the following class:

public class User
{
  public virtual int Id {get;set;}
  public virtual string Name {get;set;}
  public virtual User Superior {get;set;}
}

My goal is to serialize this as json using newtonsofts json.net like so:

{
  Id: 101,
  Name: 'Mithon',
  SuperiorId: 100,
  SuperiorName: 'TheMan'
}

Why do I want to do this? Because I want to use the Json as my DTO's without generating an intermediate layer of dynamic objects. Generating the DTO's should be done dynamically by convention rather than explicitly, imho. I know some might strongly disagree with this, but discussing my approach is besides the point. I just want to know if and how it can be done.

The challenge is that using JsonPropertyAttribute for the Superior property will yield only one property as output, where I need two. If I use a JsonObjectAttribute I will get a nested attribute and I would have trouble with the top level User also being flattened.

Luckily it seems there are enough protected and/or public properties and methods in the json.net library that I can extend something to get the desired result. The question then is which classes and methods should I start with to get where I want to go? Would deriving from DefaultContractResolver, and overriding the GetProperties method be good places, or should I look elsewhere?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The short answer is yes, that would be appropriate places to start. Here's what I ended up with (for now):

public class MyContractResolver : DefaultContractResolver
{
    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var properties = base.CreateProperties(type, memberSerialization);

        foreach (var pi in type.GetProperties().Where(pi => typeof (Entity).IsAssignableFrom(pi.DeclaringType) && Attribute.IsDefined((MemberInfo) pi, typeof(IdNameMemberAttribute))))
        {
            properties.Add(CreateReferenceProperty(pi, Reflect.GetProperty<Entity>(e => e.Id)));
            properties.Add(CreateReferenceProperty(pi, Reflect.GetProperty<Entity>(e => e.Name)));
        }

        return properties;
    }

    private JsonProperty CreateReferenceProperty(PropertyInfo reference, PropertyInfo referenceMember)
    {
        var jsonProperty = base.CreateProperty(reference, MemberSerialization.OptOut);
        jsonProperty.PropertyName += referenceMember.Name;
        jsonProperty.ValueProvider = new ReferencedValueProvider(reference, referenceMember);
        jsonProperty.Writable = false;

        return jsonProperty;
    }
}

IdNameMemberAttribute is just an empty attribute that I use to annotate which reference properties I want to serialize. The important bit is that I do NOT annotate it with anything that Json.NET will recognize and use to generate a JsonProperty. That way I don't end up with a duplicate JsonProperty from my CreateProperties.

Alternatively I could have derived from DataMemberAttribute and looked for, modified and cloned the JsonProperty to represent my Id and Name.

For my asp.net web api I then set this MyContractResolver as the ContractResolver of JsonFormatter.SerializerSettings.

So that fixed me up for serialization. For deserialization I have a custom ChangeSet object where I store PropertyInfo and objects. During deserialization I then make sure I keep the Ids, and later resolve those from my data store, in my case using custom ActionFilter with access to the data store session.

Here is the essence of my serialization:

var jsonSource = streamReader.ReadToEnd();
var deserializedObject = JsonConvert.DeserializeObject(jsonSource, type, SerializerSettings);
var changeSet = deserializedObject as PropertyChangeSet;
if (changeSet != null)
{
    var jsonChange = JObject.Parse(jsonSource)["Change"].Cast<JProperty>().ToArray();

    IDictionary<string, int> references = jsonChange.Where(IsReferenceId).ToDictionary(t => t.Name.Substring(0, t.Name.Length - 2), t => t.Value.ToObject<int>());
    changeSet.References = references;

    var properties = jsonChange.Where(jp => !IsReferenceId(jp)).Select(t => t.Name).ToList();
    changeSet.Primitives = properties;
}

And presto, all the gory details of my clean entities and dynamic serialization are encapsulated, sadly in two places, but it couldn't be helped since I don't want to access my data source from the serializer.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...