I have a one-to-many relationship between two entities: Maps and MapNodes. When I eager load MapNodes with Maps (via an Include()), I get Map, and I get related MapNodes, but in the related MapNodes, I also get the full parent Map again. That Map then has associated MapNodes (again), and so on...
I need to limit the contents of the query to one level (i.e MapNodes) and not have the eager loading go deeper.
[Table("maps")]
public partial class Maps
{
public Maps()
{
MapNodes = new HashSet<MapNodes>();
}
<...>
[InverseProperty("Map")]
public virtual ICollection<MapNodes> MapNodes { get; set; }
}
[Table("map_nodes")]
public partial class MapNodes
{
public MapNodes()
{
}
<...>
[Column("map_id", TypeName = "int(10) unsigned")]
public uint MapId { get; set; }
public Maps Map { get; set; }
}
When I execute the following query:
var map = await context.Maps.Include( x => x.MapNodes).FirstOrDefaultAsync( x => x.Id == id);
I get an infinate eager loading:
{
"id": 1063,
"mapNodes": [
{
"id": 25784,
"mapId": 1063,
"map": {
"id": 1063,
"mapNodes": [
{
"id": 25784,
"mapId": 1063,
"map": {
"id": 1063,
"mapNodes": [
...
Microsoft replaced Microsoft.AspNetCore.Mvc.NewtonsoftJson in ASP.NET Core 3 with their own implementation which is System.Text.Json but it doesn't support Reference Loop Handling handling yet.
So in order to configure Reference Loop Handling you need to add the nuget package for Microsoft.AspNetCore.Mvc.NewtonsoftJson then configure it this way:
services.AddControllersWithViews().AddNewtonsoftJson(options => {
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
Related
I have a code first Blazor WebAssembly application in which I have a Many-to-Many relationship.
public class A
{
public A()
{
this.Bs = new HashSet<B>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<B> Bs { get; set; }
}
public class B
{
public B()
{
this.As= new HashSet<A>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<A> As { get; set; }
}
In my client, I call the Get method of the server AController. I would like to have in each A object, the Bs ICollection.
If the Get method is like this, the Bs collection is null :
[HttpGet]
public IEnumerable<A> Get()
{
return _context.A.ToList();
}
If it is like this, to inlude Bs, I have an exception ("System.Text.Json.JsonException: A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles.")
[HttpGet]
public IEnumerable<A> Get()
{
return _context.A.Include(a => a.Bs).ToList();
}
So in my Startup.cs (on the server) I had the following in the ConfigureServices method
services.AddControllers().AddJsonOptions(o =>
o.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve);
So now, the serialization works, but the deserialization fails because the JSON is different, not only a List of A.
In the client, I call the get method of the AController like this :
var response = await _httpClient.GetAsync("my_api_adresse");
return await response.Content.ReadFromJsonAsync<List<A>>();
But du to the ReferenceHandler.Preserve the JSON is like this, so the desirialization can't work and raises an exception :
{
"$id": "1",
"$values": [
{
"$id": "2",
"id": 4,
"name": "nameA3",
"Bs": {
"$id": "3",
"$values": [
{
"$id": "4",
"id": 1,
"Name": "NameB1",
"As": {
"$id": "5",
"$values": [
{
"$ref": "2"
}
]
}
}
]
}
}
]
}
What could I do to be able to include Bs collection into A objects and be able to serialize and deserialize the response without any trouble ?
Try using DTO objects (data transfer objects).
There are many ways. One is:
Create two new classes that are similar to class A and class B, but this one without a list of A objects. Instead, use a list of A Ids. That would solve your problem.
Comment gere if you need code sample
I'm using the latest version of ABP from abp.io and have two entities with a many-many relationship. These are:
public class GroupDto : AuditedEntityDto<Guid>
{
public GroupDto()
{
this.Students = new HashSet<Students.StudentDto>();
}
public string Name { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<Students.StudentDto> Students { get; set; }
}
and
public class StudentDto : AuditedEntityDto<Guid>
{
public StudentDto()
{
this.Groups = new HashSet<Groups.GroupDto>();
}
public string Name { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<Groups.GroupDto> Groups { get; set; }
}
I set up the following test to check that I am retrieving the related entities, and unfortunately the Students property is always empty.
public async Task Should_Get_List_Of_Groups()
{
//Act
var result = await _groupAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(g => g.Name == "13Ck" && g.Students.Any(s => s.Name == "Michael Studentman"));
}
The same is true of the equivalent test for a List of Students, the Groups property is always empty.
I found one single related answer for abp.io (which is not the same as ABP, it's a newer/different framework) https://stackoverflow.com/a/62913782/7801941 but unfortunately when I add an equivalent to my StudentAppService I get the error -
CS1061 'IRepository<Student, Guid>' does not contain a definition for
'Include' and no accessible extension method 'Include' accepting a
first argument of type 'IRepository<Student, Guid>' could be found
(are you missing a using directive or an assembly reference?)
The code for this is below, and the error is being thrown on the line that begins .Include
public class StudentAppService :
CrudAppService<
Student, //The Student entity
StudentDto, //Used to show students
Guid, //Primary key of the student entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateStudentDto>, //Used to create/update a student
IStudentAppService //implement the IStudentAppService
{
private readonly IRepository<Students.Student, Guid> _studentRepository;
public StudentAppService(IRepository<Student, Guid> repository)
: base(repository)
{
_studentRepository = repository;
}
protected override IQueryable<Student> CreateFilteredQuery(PagedAndSortedResultRequestDto input)
{
return _studentRepository
.Include(s => s.Groups);
}
}
This implements this interface
public interface IStudentAppService :
ICrudAppService< // Defines CRUD methods
StudentDto, // Used to show students
Guid, // Primary key of the student entity
PagedAndSortedResultRequestDto, // Used for paging/sorting
CreateUpdateStudentDto> // Used to create/update a student
{
//
}
Can anyone shed any light on how I should be accessing the related entities using the AppServices?
Edit: Thank you to those who have responded. To clarify, I am looking for a solution/explanation for how to access entities that have a many-many relationship using the AppService, not the repository.
To aid with this, I have uploaded a zip file of my whole source code, along with many of the changes I've tried in order to get this to work, here.
You can lazy load, eagerly load or configure default behaviour for the entity for sub-collections.
Default configuration:
Configure<AbpEntityOptions>(options =>
{
options.Entity<Student>(studentOptions =>
{
studentOptions.DefaultWithDetailsFunc = query => query.Include(o => o.Groups);
});
});
Eager Load:
//Get a IQueryable<T> by including sub collections
var queryable = await _studentRepository.WithDetailsAsync(x => x.Groups);
//Apply additional LINQ extension methods
var query = queryable.Where(x => x.Id == id);
//Execute the query and get the result
var student = await AsyncExecuter.FirstOrDefaultAsync(query);
Or Lazy Load:
var student = await _studentRepository.GetAsync(id, includeDetails: false);
//student.Groups is empty on this stage
await _studentRepository.EnsureCollectionLoadedAsync(student, x => x.Groups);
//student.Groups is filled now
You can check docs for more information.
Edit:
You may have forgotten to add default repositories like:
services.AddAbpDbContext<MyDbContext>(options =>
{
options.AddDefaultRepositories();
});
Though I would like to suggest you to use custom repositories like
IStudentRepository:IRepository<Student,Guid>
So that you can scale your repository much better.
Here is my query with Entity Framework :
var transportlist = await _context.Set<Transport>()
.Include(transport => transport.TransportState)
.Select(transport =>
new
{
Transport = _mapper.Map<TransportDto>(transport),
TransportState = _mapper.Map<TransportStateForTransportDto>(transport.TransportState),
}
)
.Take(limitNumber)
.ToListAsync();
return transportlist;
Here is my TransportDto :
public class TransportDto
{
public int TransportId { get; set; }
public TransportStateForTransportDto TransportState { get; set; }
public bool Locked { get; set; }
}
Here is my TransportStateForTransportDto :
public class TransportStateForTransportDto
{
public string Label { get; set; }
}
I would like to have something like that as result :
[
{
"transportId": 123456,
"transportState" : {
"Label": "Deleted"
},
"locked" : false
}
]
But, I have this one (and it is a bit logical...) :
[
{
"transport": {
"transportId": 123456,
"transportState" : {
"Label": "Deleted"
},
"locked" : false
},
"transportstate": {
"Label": "Deleted"
}
}
]
I thought about doing like this :
var transportlist = await _context.Set<Transport>()
.Include(transport => transport.TransportState)
.Select(transport =>
new
{
_mapper.Map<TransportDto>(transport),
}
)
.Take(limitNumber)
.ToListAsync();
return transportlist;
But, if I do, the property transportstate in JSON equals NULL.
So I am trying to do nested mapping where Transport should be mapped with its object properties mapped too. Note that all the properties have exactly the same name.
I wrote a Profile for mapping which is :
public class TransportsProfile : Profile
{
public TransportsProfile()
{
CreateMap<TransportState, TransportStateForTransportDto>();
CreateMap<Transport, TransportDto>();
}
}
Thank you in advance for your help.
If you are using Automapper 8 or above you should leverage ProjectTo rather than Map to translate entities into DTOs.
var transportlist = await _context.Set<Transport>()
.ProjectTo<TransportDto>(_mapper.ConfigurationProvider)
.Take(limitNumber)
.ToListAsync();
Edit: had _mapper.Configuration above, looks like the config is provided by ConfigurationProvider.
Given that your TransportDTO contains a reference to the related TransportState DTO, automapper should be able to tell EF exactly what fields are needed from the related entities to ensure that they are loaded properly. The issue with Map is that this is really geared towards loaded objects, not expressions that EF could otherwise turn into SQL.
With using Take you should also have an Order By clause to ensure the items returned are predictable when more than that limit are present.
Like using Select for projection, there is no need to eager load any of the related details or disable lazy loading. EF will simply compose a query to pull the required fields provided your DTOs are purely simple data containers and don't contain references to entities within them.
I inherited a EF CF project and my knowledge of EF is inadequate. I am almost there. I have been reading and experimenting and I just can't figure out what I am doing wrong.
I have read numerous articles, tried reverse navigation, experimented and discovered strange new errors but no luck so far.
This is my setup. Of course the models have many more properties like Name that I left out for brevity.
public class VendorModel
{
[key]
public int Id { get; set; }
[ForeignKey("VendorId")]
public virtual List<VendorPersonnelModel> VendorPersonnel { get; set; }
}
//This model represents an intersect table in the DB
public class VendorPersonnelModel
{
[Key]
public int Id { get; set; }
public int VendorId { get; set; } //This is a FK to Vendor table defined in DB
public int PersonnelId { get; set; } //This is a FK to Personnel table defined in DB
}
//This model definition is here but not used till the second half of the question
public class PersonnelModel
{
[key]
public int Id { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
}
This is how I am navigating in the controller:
var models = await _db.Vendors
.Include(o => o.Addresses)
.Include(o => o.VendorPersonnel)
.ToListAsync();
This is db context definition:
var ent = modelBuilder.Entity<VendorPersonnelModel>();
ent.HasKey(obj => obj.Id);
ent.ToTable("VendorPersonnel");
//Navigation properties...do I put something here like this???
//ent.HasMany().HasForeignKey(y => y.PersonnelId);
This produces a json return like this:
[{
"name": "Vendor ABC",
"vendorPersonnel": [
{
"id": 1,
"vendorId": 1001001,
"personnelId": 1231
},
{
"id": 2,
"vendorId": 1001001,
"personnelId": 1776
}
]
}]
This is very close to what I want. Now I want to resolve the other half of the intersect table...the personnel. I do NOT need the intersect details I was just trying to step my way towards the solution.
I want the json to look something like this:
[{
"name": "Vendor ABC",
"personnelDetails": [
{
"id": 1,
"Name": "Jane",
"Phone": "333-123-4567"
},
{
"id": 2,
"Name": "Joe",
"Phone": "675-943-6732"
}
]
}]
This is where I start getting all sorts of strange errors. I CAN make it work by doing two DIFFERENT queries and then mashing them together in a new object and return the new object but that just seems to me to be poor coding.
I'm sure there is a way to do this with EF I just don't know what to look for / read up on.
I have now been able to read up and answer my question. This article helped me with what was missing:
https://www.entityframeworktutorial.net/code-first/configure-many-to-many-relationship-in-code-first.aspx
This mapping will create a self referencing "appearance" that will freak a json formatter out.
Simply add:
HttpConfiguration config = GlobalConfiguration.Configuration;
config.Formatters.JsonFormatter
.SerializerSettings
.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
Source: Entity framework self referencing loop detected
Domain model:
public class Course
{
public int CourseId { get; set; }
public virtual ICollection<TeeSet> TeeSets { get; set; }
}
public class TeeSet
{
public int TeeSetId { get; set; }
public int CourseId { get; set; }
public CourseRating MensRating { get; set; }
}
The following query does not include the CourseRating complex type when Courses are expanded to include TeeSets.
GET /api/courses?$expand=TeeSets
public class CoursesController : ApiController
{
[Queryable]
public IQueryable<Course> Get()
{
return _uow.Courses.GetAll();
}
}
The JSON serialized result does not include the MensRating complex type (CourseRating):
[
{
"teeSets": [
{
"teeSetId": 1,
"courseId": 7
},
{
"teeSetId": 2,
"courseId": 7
}
],
"courseId": 7,
}
]
However, a quick test against the DbContext returns the CourseRating complex type on TeeSets like I would expect:
[TestMethod]
public void Get_Course_With_TeeSets()
{
using (CoursesContext ctx = new CoursesContext())
{
var courses = ctx.Courses.Where(x => x.CourseId == 7).Include(x => x.TeeSets).FirstOrDefault();
}
}
Entity Framework 6 and Web API 2 used.
You should expand MensRating as well like this, GET /api/courses?$expand=TeeSets/MensRating
When we build an implicit EDM model for supporting QueryableAttribute with ApiController, we treat every type as an entity type to get around the OData V3 limitation that complex types cannot refer to entity types. And, this means that you have to expand explicitly every non primitive type.
Add AutoExpand on MensRating Property can make this work.