C# 自定义引用循环处理

C# 自定义引用循环处理,c#,.net,json,serialization,json.net,C#,.net,Json,Serialization,Json.net,我正在尝试实现自定义引用循环处理。我所需要的就是写空对象来代替嵌套对象 预期结果 { Id:1, Field:"Value", NestedObject:{Id:1}} 我创建了JsonConverter public class SerializationConverter : JsonConverter { public override bool CanRead { get { return false; } } public override bool CanWrit

我正在尝试实现自定义引用循环处理。我所需要的就是写空对象来代替嵌套对象

预期结果

 { Id:1, Field:"Value", NestedObject:{Id:1}}
我创建了
JsonConverter

public class SerializationConverter : JsonConverter
{
    public override bool CanRead { get { return false; } }
    public override bool CanWrite { get { return true; } }


    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Form) || typeof(Form).IsAssignableFrom(objectType);
    }

    private HashSet<Form> serializedForms = new HashSet<Form>();

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value == null)
            writer.WriteNull();

        var f = (Form)value;
        if (!serializedForms.Add(f))
            writer.WriteRawValue("{Id:" + f.Id.Value + "}");
        else
            serializer.Serialize(writer, value);
    }
}
公共类序列化转换器:JsonConverter
{
公共重写bool CanRead{get{return false;}}
公共覆盖布尔可写{get{return true;}}
公共覆盖布尔CanConvert(类型objectType)
{
return objectType==typeof(Form)| typeof(Form).IsAssignableFrom(objectType);
}
私有HashSet serializedForms=新HashSet();
公共重写void WriteJson(JsonWriter编写器、对象值、JsonSerializer序列化器)
{
如果(值==null)
WriteNull();
var f=(形式)值;
如果(!serializedForms.Add(f))
writer.WriteRawValue(“{Id:+f.Id.Value+”}”);
其他的
serializer.Serialize(writer,value);
}
}
但是,正如预期的那样,
serializer.Serialize(writer,value)
的内部调用上的序列化程序再次调用我的转换器


我试图仅在对象已序列化时替换序列化结果,否则使用默认的序列化行为。

首先,我要提到的是,Json.Net具有内置的
PreserveReferencesHandling
设置,可以自动处理这类事情,而不需要特殊的转换器。当
PreserveReferencesHandling
设置为
All
时,Json.Net为每个对象分配内部引用id,并将特殊的
$id
$ref
属性写入Json以跟踪引用。对于您的示例,JSON输出如下所示:

{"$id":"1","Id":1,"Field":"Value","NestedObject":{"$ref":"1"}}
您会注意到这与您的问题的期望输出非常相似。这还有一个优点,即可以轻松地将其反序列化回原始对象图,并保留所有引用,同样不需要实现任何特殊的功能

但是,现在让我们假设您有自己的理由想要实现自定义引用循环处理,并看看为什么您的代码不能工作

当Json.Net遇到对象的
JsonConverter
时,它假定转换器将处理写入该对象所需的任何Json。所以,如果你想包括某些属性,你必须自己写出来。您可以使用序列化程序来帮助编写对象的一部分,但您不能将整个对象交给序列化程序并说“序列化此对象”,因为它最终将调用回转换器

在大多数情况下,这样做将导致无限循环。在您的情况下,它不会,因为您在第一次调用
WriteJson
时将表单添加到了HashSet。当序列化程序第二次回调时,将采用另一个分支,因为表单已在集合中。因此,对象的整个JSON最终都是
{Id:1}
,而不是您真正想要的

防止序列化程序回调转换器的一种方法是在转换器内创建
JsonSerializer
的新实例,并使用该实例,而不是传递给
WriteJson
方法的实例。新实例将没有对转换器的引用,因此
表单将正常序列化

不幸的是,这种想法也行不通:如果内部序列化程序中没有对转换器的引用,那么Json.Net就无法知道如何对
NestedObject
执行特殊的序列化处理!相反,它将被忽略,因为我们将被迫将
ReferenceLoopHandling
设置为
Ignore
,以避免错误。你看,你有第二十二条军规

那么我们怎样才能让它起作用呢?好吧,让我们后退一步,重新定义你真正想要的产出:

  • 如果我们遇到一个已经看到的表单,我们只想输出
    Id
  • 否则,将表单添加到我们看到的表单列表中,然后输出
    Id
    字段
    NestedObject
  • 请注意,在这两种情况下,我们都希望输出
    Id
    ,因此我们可以将逻辑简化为:

  • 始终输出Id
  • 如果遇到尚未看到的表单,请将该表单添加到已看到的表单列表中,然后输出
    字段
    嵌套对象
  • 为了使事情变得简单,我们可以使用
    JObject
    来收集我们想要输出的属性,然后只需在最后将其写入
    writer

    修订后的守则如下:

    public class SerializationConverter : JsonConverter
    {
        public override bool CanRead { get { return false; } }
        public override bool CanWrite { get { return true; } }
    
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(Form) || typeof(Form).IsAssignableFrom(objectType);
        }
    
        private HashSet<Form> serializedForms = new HashSet<Form>();
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            Form f = (Form)value;
    
            JObject jo = new JObject();
            jo.Add("Id", f.Id);
    
            if (serializedForms.Add(f))
            {
                jo.Add("Field", f.Field);
                if (f.NestedObject != null)
                {
                    jo.Add("NestedObject", JToken.FromObject(f.NestedObject, serializer));
                }
            }
    
            jo.WriteTo(writer);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    
    到目前为止看起来不错。更严格一点怎么样:

    class Program
    {
        static void Main(string[] args)
        {
            List<Form> forms = new List<Form>
            {
                new Form 
                { 
                    Id = 1, 
                    Field = "One", 
                    NestedObject = new Form
                    {
                        Id = 2,
                        Field = "Two"
                    }
                },
                new Form
                {
                    Id = 3,
                    Field = "Three"
                },
                new Form
                {
                    Id = 4,
                    Field = "Four"
                },
                new Form
                {
                    Id = 5,
                    Field = "Five"
                }
            };
    
            forms[0].NestedObject.NestedObject = forms[3];
            forms[1].NestedObject = forms[0].NestedObject;
            forms[2].NestedObject = forms[1];
    
            JsonSerializerSettings settings = new JsonSerializerSettings
            {
                Converters = new List<JsonConverter> { new SerializationConverter() },
                ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
                Formatting = Formatting.Indented
            };
    
            string json = JsonConvert.SerializeObject(forms, settings);
            Console.WriteLine(json);
        }
    }
    
    编辑

    如果您的
    表单
    类有大量字段,您可能希望使用反射,而不是在转换器中单独列出属性。下面是使用反射的
    WriteJson
    方法的外观:

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        Form f = (Form)value;
    
        JObject jo = new JObject();
        if (serializedForms.Add(f))
        {
            foreach (PropertyInfo prop in value.GetType().GetProperties())
            {
                if (prop.CanRead)
                {
                    object propVal = prop.GetValue(value);
                    if (propVal != null)
                    {
                        jo.Add(prop.Name, JToken.FromObject(propVal, serializer));
                    }
                }
            }
        }
        else
        {
            jo.Add("Id", f.Id);
        }
    
        jo.WriteTo(writer);
    }
    

    回答得好。它需要比我预期的更多的工作(实际的对象类型和字段可能会有所不同,因此它需要反射和LCG来填充JObject),但多亏了方向。我需要自定义解决方案,因为有些客户端不是.NET,但在反序列化过程中已经有了对象Id映射机制。我不喜欢这个对象有两个“Id”字段:)事实上,我的第一个版本的转换器使用了反射,但我不想使这个例子过于复杂。我在回答的末尾添加了反射版本,以防有用。
    class Program
    {
        static void Main(string[] args)
        {
            List<Form> forms = new List<Form>
            {
                new Form 
                { 
                    Id = 1, 
                    Field = "One", 
                    NestedObject = new Form
                    {
                        Id = 2,
                        Field = "Two"
                    }
                },
                new Form
                {
                    Id = 3,
                    Field = "Three"
                },
                new Form
                {
                    Id = 4,
                    Field = "Four"
                },
                new Form
                {
                    Id = 5,
                    Field = "Five"
                }
            };
    
            forms[0].NestedObject.NestedObject = forms[3];
            forms[1].NestedObject = forms[0].NestedObject;
            forms[2].NestedObject = forms[1];
    
            JsonSerializerSettings settings = new JsonSerializerSettings
            {
                Converters = new List<JsonConverter> { new SerializationConverter() },
                ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
                Formatting = Formatting.Indented
            };
    
            string json = JsonConvert.SerializeObject(forms, settings);
            Console.WriteLine(json);
        }
    }
    
    [
      {
        "Id": 1,
        "Field": "One",
        "NestedObject": {
          "Id": 2,
          "Field": "Two",
          "NestedObject": {
            "Id": 5,
            "Field": "Five"
          }
        }
      },
      {
        "Id": 3,
        "Field": "Three",
        "NestedObject": {
          "Id": 2
        }
      },
      {
        "Id": 4,
        "Field": "Four",
        "NestedObject": {
          "Id": 3
        }
      },
      {
        "Id": 5
      }
    ]
    
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        Form f = (Form)value;
    
        JObject jo = new JObject();
        if (serializedForms.Add(f))
        {
            foreach (PropertyInfo prop in value.GetType().GetProperties())
            {
                if (prop.CanRead)
                {
                    object propVal = prop.GetValue(value);
                    if (propVal != null)
                    {
                        jo.Add(prop.Name, JToken.FromObject(propVal, serializer));
                    }
                }
            }
        }
        else
        {
            jo.Add("Id", f.Id);
        }
    
        jo.WriteTo(writer);
    }