Circular Reference error when serializing objects

2019-02-22 00:45发布

I'm writing a Web API project in C# that uses Entity Framework to pull data from a DB, serialize it and send it to a client.

My project has 2 classes, Post and Comment (foreign key from Post).

These are my classes.

Post class:

public partial class Post
{
    public Post()
    {
        this.Attachment = new HashSet<Attachment>();
        this.Comment = new HashSet<Comment>();
    }

    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public System.DateTime Created { get; set; }
    public Nullable<System.DateTime> Modified { get; set; }

    public virtual ICollection<Attachment> Attachment { get; set; }
    public virtual ICollection<Comment> Comment { get; set; }
}

Comment class:

public partial class Comment
{
    public int CommentId { get; set; }
    public string Content { get; set; }
    public System.DateTime Posted { get; set; }
    public bool Approved { get; set; }
    public int AnswersTo { get; set; }
    public int PostId { get; set; }

    public virtual Post Post { get; set; }
}

My problem is that when I try to get via Web API a Post, it spits me the following error:

Object graph for type 'APIServer.Models.Comment' contains cycles and cannot be serialized if reference tracking is disabled.

And when I try to get a Comment via Web API, the error is as follows:

Object graph for type 'System.Collections.Generic.HashSet`1[[APIServer.Models.Comment, APIServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]'  contains cycles and cannot be serialized if reference tracking is disabled.

If I annotate the Comment class with

[DataContract(IsReference = true)]

the errors disappear, but the serialization only returns the ID of the comment and ignores the other fields.

Any suggestions on how to solve this?

Thanks in advance,

Léster

2条回答
ら.Afraid
2楼-- · 2019-02-22 01:18

You can disable Lazy Loading on your Comment class by removing virtual from the Post property definition...

public partial class Comment
{
    public int CommentId { get; set; }
    public string Content { get; set; }
    public System.DateTime Posted { get; set; }
    public bool Approved { get; set; }
    public int AnswersTo { get; set; }
    public int PostId { get; set; }

    public Post Post { get; set; }
}

This should sort out the circular reference exception.

查看更多
仙女界的扛把子
3楼-- · 2019-02-22 01:31

Here are 2 solutions

Solution #1:

I had this same problem and so I decorated my class with DataContract and the members with DataMember like you mention. HOWEVER, I don't like editing auto-generated code directly because I have to redo it every time I regenerate the file. In order to get around this, I used the MetadataType attribute. In your case, it would look like this...

First, you will keep the auto generated entity as is:

public partial class Comment
{
    public int CommentId { get; set; }
    public string Content { get; set; }
    public System.DateTime Posted { get; set; }
    public bool Approved { get; set; }
    public int AnswersTo { get; set; }
    public int PostId { get; set; }

    public virtual Post Post { get; set; }
}

Next, in another file, you will create another partial class and decorate it like this:

[MetadataType(typeof(Metadata))]
[DataContract(IsReference = true)]
public partial class Comment
{
    private class Metadata
    {
        [DataMember]
        public int CommentId { get; set; }
        [DataMember]
        public string Content { get; set; }
        [DataMember]
        public System.DateTime Posted { get; set; }
        [DataMember]
        public bool Approved { get; set; }
        [DataMember]
        public int AnswersTo { get; set; }
        [DataMember]
        public int PostId { get; set; }

        [DataMember]
        public virtual Post Post { get; set; } // you can remove "virtual" if you wish
    }
}

MetadataType will essentially add the attributes from the Metadata buddy class to the ones with the same name in Comment (not directly, but for our purposes, it's close enough... that's a topic for a different post). Of course, if your Comment entity changes, you'll need to update this accordingly.

Solution #2:

Having to edit your second file every time you make a change is only a slight improvement from directly editing auto-generated files. Fortunately, there is another approach that is much easier to maintain. Details can be found here but as a summary, all you need to do is decorate your OperationContract that is consuming Comment with an additional attribute, ReferencePreservingDataContractFormat. Note that there is a slight error in the code provided on that page that would cause infinite recursion. As noted in this post, the fix is quite simple: instead of recursing at all, just create a new DataContractSerializer

The advantage to this approach is that no matter how much you change Comment, you still don't need update anything.

As an example for your code, let's say you are using Comment as follows:

[OperationContract]
Comment FindComment(string criteria);

All you need to do is add

[OperationContract]
[ReferencePreservingDataContractFormat]
Comment FindComment(string criteria);

And then somewhere else you need to define ReferencePreservingDataContractFormat which will look like this:

//From http://blogs.msdn.com/b/sowmy/archive/2006/03/26/561188.aspx and https://stackoverflow.com/questions/4266008/endless-loop-in-a-code-sample-on-serialization
public class ReferencePreservingDataContractFormatAttribute : Attribute, IOperationBehavior
{
    public void AddBindingParameters(OperationDescription description, BindingParameterCollection parameters)
    {
    }

    public void ApplyClientBehavior(OperationDescription description, System.ServiceModel.Dispatcher.ClientOperation proxy)
    {
        IOperationBehavior innerBehavior = new ReferencePreservingDataContractSerializerOperationBehavior(description);
        innerBehavior.ApplyClientBehavior(description, proxy);
    }

    public void ApplyDispatchBehavior(OperationDescription description, System.ServiceModel.Dispatcher.DispatchOperation dispatch)
    {
        IOperationBehavior innerBehavior = new ReferencePreservingDataContractSerializerOperationBehavior(description);
        innerBehavior.ApplyDispatchBehavior(description, dispatch);
    }

    public void Validate(OperationDescription description)
    {
    }

}
class ReferencePreservingDataContractSerializerOperationBehavior : DataContractSerializerOperationBehavior
{
    public ReferencePreservingDataContractSerializerOperationBehavior(OperationDescription operationDescription) : base(operationDescription) { }
    public override XmlObjectSerializer CreateSerializer(Type type, string name, string ns, IList<Type> knownTypes)
    {
        return new DataContractSerializer(type, name, ns, knownTypes,
            0x7FFF, //maxItemsInObjectGraph
            false,  //ignoreExtensionDataObject
            true,   //preserveObjectReferences
            null    //dataContractSurrogate
            );
    }

    public override XmlObjectSerializer CreateSerializer(Type type, XmlDictionaryString name, XmlDictionaryString ns, IList<Type> knownTypes)
    {
        return new DataContractSerializer(type, name, ns, knownTypes,
            0x7FFF, //maxItemsInObjectGraph
            false,  //ignoreExtensionDataObject
            true,   //preserveObjectReferences
            null    //dataContractSurrogate
            );
    }
}

And that's it!

Either method will work just fine--pick the one that works for you.

查看更多
登录 后发表回答