LogicalDesk
LogicalDesk

Reputation: 1297

Self referencing loop detected for property in WebApi 2

I've created a Web Api to save new products and reviews in database. Below is the WebApi code:

POST api/Products

[ResponseType(typeof(Product))]
public IHttpActionResult PostProduct(Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    db.Products.Add(product);
    db.SaveChanges();

    return CreatedAtRoute("DefaultApi", new { id = product.ProductId }, product);
}

Product class

public class Product
{
    public int ProductId { get; set; }
    [Required]
    public string Name { get; set; }
    public string Category { get; set; }
    public int Price { get; set; }
    //Navigation Property
    public ICollection<Review> Reviews { get; set; }
}

Review class

public class Review
{
    public int ReviewId { get; set; }
    public int ProductId { get; set; }
    [Required]
    public string Title { get; set; }
    public string Description { get; set; }
    //Navigation Property
    public Product Product { get; set; }
}

entity diagram

I'm using google chrome extension 'POSTMAN' to test the api. When I try to save details by creating a POST request in POSTMAN:

{
    "Name": "Product 4",
        "Category": "Category 4",
        "Price": 200,
        "Reviews": [
            {
                "ReviewId": 1,
                "ProductId": 1,
                "Title": "Review 1",
                "Description": "Test review 1",
                "Product": null
            },
            {
                "ReviewId": 2,
                "ProductId": 1,
                "Title": "Review 2",
                "Description": "Test review 2",
                "Product": null
            }
        ]
}

Which shows following error:

"Message":"An error has occurred.",
"ExceptionMessage":"The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json; charset=utf-8'.",
"ExceptionType":"System.InvalidOperationException",
"StackTrace":null,
"InnerException":{
    "Message":"An error has occurred.",
    "ExceptionMessage":"Self referencing loop detected for property 'Product' with type 'HelloWebAPI.Models.Product'.

How can I resolve this error?

Upvotes: 5

Views: 19507

Answers (2)

Alisson Reinaldo Silva
Alisson Reinaldo Silva

Reputation: 10705

Avoid using the same class you use in Entity Framework to map your entities in the API methods. Create DTO classes for using with API, then convert them to your Entity class manually or using tools like Auto Mapper which help you doing this.

Anyway, if you still want to use your Product and Review classes, the two simplest options I could remember are:

Removing the circular reference property

As identied by Stuart:

public class Review
{
    public int ReviewId { get; set; }
    public int ProductId { get; set; }
      [Required]
    public string Title { get; set; }
    public string Description { get; set; }
}

Decorate circular reference property with [IgnoreDataMember]

public class Review
{
    public int ReviewId { get; set; }
    public int ProductId { get; set; }
      [Required]
    public string Title { get; set; }
    public string Description { get; set; }

    //Navigation Property
    [IgnoreDataMember]
    public Product Product { get; set; }
}

About ignoring properties when (de)serializing, you can refer to this question/answer.


Using DTO classes

You create two new classes:

public class CreateProductRequest
{
    [Required]
    public string Name { get; set; }
    public string Category { get; set; }
    public int Price { get; set; }
    //Navigation Property
    public IEnumerable<CreateReviewRequest> Reviews { get; set; }
}

public class CreateReviewRequest
{
    [Required]
    public string Title { get; set; }
    public string Description { get; set; }
}

Then fix your controller action like this:

[ResponseType(typeof(Product))]
public IHttpActionResult PostProduct(CreateProductRequest request)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    var product = new Product
    {
        Name = request.Name,
        Category = request.Category,
        Price = request.Price
    }
    if (request.Reviews != null)
        product.Reviews = request.Reviews.Select(r => new Review
        {
            Title = r.Title,
            Description = r.Description
        });

    db.Products.Add(product);
    db.SaveChanges();

    return CreatedAtRoute("DefaultApi", new { id = product.ProductId }, product);
}

I know it looks redundant, but this is because I'm doing everything manually. If I was using something like Auto Mapper, we could reduce it to:

[ResponseType(typeof(Product))]
public IHttpActionResult PostProduct(CreateProductRequest request)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    var product = AutoMapper.Map<Product>(request);

    db.Products.Add(product);
    db.SaveChanges();

    return CreatedAtRoute("DefaultApi", new { id = product.ProductId }, product);
}

Mixing Entity Framework (or any other ORM) classes with service layer classes (e.g Web API, MVC, WCF) often causes problems for people which still don't know how serialization occurs.

There are situations where I do use directly the classes, for simple scenarios, because this is not a rule that should be strictly followed. I believe each situation has its own needs.

Upvotes: 9

Afzaal Ahmad Zeeshan
Afzaal Ahmad Zeeshan

Reputation: 15860

First of all, change the Navigation properties to virtual, that would provide lazy loading,

public virtual ICollection<Review> Reviews { get; set; }

// In the review, make some changes as well
public virtual Product Product { get; set; }

Secondly, since you know that a Product is not going to always have a review in the collection, can't you set it to nullable? — just saying.

Now back to your question, a pretty much easy way to handle this would be to just ignore the objects which cannot be serialized... Again! Do that using the ReferenceLoopHandling.Ignore setting in the JsonSerializer of Json.NET. For ASP.NET Web API, a global setting can be done (taken from this SO thread),

GlobalConfiguration.Configuration.Formatters
                   .JsonFormatter.SerializerSettings.Re‌​ferenceLoopHandling 
                   = ReferenceLoopHandling.Ignore;

This error comes from Json.NET when it tried to serialize an object, which had already been serialized (your loop of objects!), and the documentation makes this pretty much clear as well,

Json.NET will ignore objects in reference loops and not serialize them. The first time an object is encountered it will be serialized as usual but if the object is encountered as a child object of itself the serializer will skip serializing it.

Reference captured from, http://www.newtonsoft.com/json/help/html/SerializationSettings.htm

Upvotes: 9

Related Questions