Entity Framework Core returning object with many to many relationship

asp.net-core c# entity-framework-core

Question

I'm not sure how to get the information I need using EF Core with a .NET Core app. I have two database tables - OBJECTS and TAGS with a many-to-many relationship (using a third join table OBJECTTAGS). I want to get all the objects (about 1400) along with their associated tags.

These are my three models:

using System;
using System.Collections.Generic;

namespace ContentMarketplace.Models
{
    public partial class Object
    {
        public int ObjectId { get; set; }
        ...
        public virtual List<ObjectTag> ObjectTags { get; set; }
    }
}

namespace ContentMarketplace.Models
{
    public partial class Tag
    {
        public int TagId { get; set; }
        ...
        public virtual List<ObjectTag> ObjectTags { get; set; }
    }
}

namespace ContentMarketplace.Models
{
    public partial class ObjectTag
    {
        public int ObjectId { get; set; }
        public virtual Object Object { get; set; }
        public int TagId { get; set; }
        public virtual Tag Tag { get; set; }
    }
}

And this is what's in the OnModelCreating() method in my context:

modelBuilder.Entity<ObjectTag>(entity =>
{
    entity.HasKey(e => new { e.ObjectId, e.TagId });

    entity.HasOne(ot => ot.Object)
        .WithMany(o => o.ObjectTags)
        .HasForeignKey(ot => ot.ObjectId);

    entity.HasOne(ot => ot.Tag)
        .WithMany(t => t.ObjectTags)
        .HasForeignKey(ot => ot.TagId);
});

This issue arises when I try to return data in my ObjectController.cs

namespace ContentMarketplace.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ObjectController : ControllerBase
    {
        private readonly ContentMarketplaceContext _context;

        public ObjectController(ContentMarketplaceContext context)
        {
            _context = context;
        }       

        [HttpGet]
        public ActionResult<List<ContentMarketplace.Models.Object>> GetAll()
        {
            return _context.Objects
                .Include(o => o.ObjectTags)
                .ThenInclude(ot => ot.Tag)
                .ToList();
        }
    }
}

The Include().ThenInclude() creates a circular relationship where an HTTP request doesn't just return the tag information associated with each object but then all the objects associated with each of those tags and so on, crashing the browser.

If I take out the ThenInclude() it works fine but doesn't return all the tag info not in my ObjectTag model.

I know this has to do with EF Core auto loading stuff that's already in the context (like the "tip" notes here https://docs.microsoft.com/en-us/ef/core/querying/related-data) but I don't know how else to return JUST the object and tags without going any further.

1
1
9/6/2018 9:14:47 PM

Accepted Answer

If you use Include or Theninclude in your query it will create circular references. JSON cannot handle circular reference. You can easily overcome this problem using Select query.

Without DTO:

Write your GetAll() controller-method as follows:

[HttpGet]
public IActionResult GetAll()
{
    var objectList =  _context.Objects.Select(o => new
            {
               o.ObjectId,
               Tags = o.ObjectTags.Select(ot => ot.Tag).ToList()
            }).ToList();

     return Ok(objectList);
}

With DTO:

Write your DTO class as follows:

public class ObjectDto
{
    public int ObjectId { get; set; }
    ....
    public List<Tag> Tags { get; set; }
}

Then your GetAll() controller-method should be as follows:

[HttpGet]
public ActionResult<List<ObjectDto>> GetAll()
{
    var objectList =  _context.Objects.Select(o => new ObjectDto
            {
               ObjectId = o.ObjectId,
               Tags = o.ObjectTags.Select(ot => ot.Tag).ToList()
            }).ToList();

     return objectList;
}

Note: If you use Select inside your query you don't need to use Include or Theninclude.

Hope it will now work as expected!

0
9/7/2018 8:26:17 AM

Popular Answer

Your issue is caused by the loop reference, you could try to below to ignore the loop reference.

            services.AddMvc()
                .AddJsonOptions(opt => {
                    opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
                });

For another option, you could try to return ObjectModel with List<Tag> directly instead of List<ObjectTag>

ObjectModel

    public partial class ObjectModel
{
    public int ObjectId { get; set; }
    public string Name { get; set; }
    public virtual List<Tag> Tags { get; set; }
}

Query

        public List<Models.ObjectModel> GetAll()
    {
        //return _db.Object
        //    .Include(o => o.ObjectTags)
        //    .ThenInclude(ot => ot.Tag)
        //    .ToList();
        return _db.Object
            .Include(o => o.ObjectTags)
            .ThenInclude(ot => ot.Tag)
            .Select(r => new Models.ObjectModel
            {
                ObjectId = r.ObjectId,
                Name = r.Name,
                Tags = r.ObjectTags.Select(ot => ot.Tag).ToList()
            })
            .ToList();
    }


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow