Reputation: 1297
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; }
}
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
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:
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; }
}
[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.
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
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.ReferenceLoopHandling
= 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