Ignore second level children of object when using Automapper and ProjectTo - c#

I am using Automapper in order to map a BookingDbModel to a BookingViewModel. The Models looks like this:
public class BookingDbModel : DatabaseModel
{
public ResourceDbModel Resource { get; set; }
public int? ResourceId { get; set; }
//...more
}
public class ResourceDbModel : DatabaseModel
{
public ICollection<ChildDbModel> Children { get; set; }
//...more
}
public class ChildDbModel : DatabaseModel
{
public string name { get; set; }
//...more
}
When mapping from the BookingDbModel to the BookingViewModel, I am using a default Manager and .ProjectTo, that also takes care of all other tables on the database:
(T = ViewModel)
public virtual async Task<List<T>> GetAll()
{
List<T> allEntities = await DbAccess
.GetAll()
.AsNoTracking()
.ProjectTo<T>(Mapper.ConfigurationProvider)
.ToListAsync();
return allEntities;
}
My Problem is now that I would like to include the ResourceObject, but exclude the children that are attached to the Resource whenever I map explicitly the Booking. When I retrieve the Resource itself, the children should be included.
What can I do in my MappingProfile or what parameters for the projectTo Method can I use in order to achieve these results?
CreateMap<BookingDbModel, BookingViewModel>();

Related

Mapping CHILD collection to a separate DTO (asp net core API automapper projection)

my first StackOverflow question so please bear with me, and thank you for your help in advance! :)
How on earth can I get my asp.net core controller to respond with the DTO of the child collection within the DTO of the parent response? I need two separate DTOs because of some business logic constraints that call for this many-to-many relationship situation.
Tried automapper and spent the last two days of my life researching this to no avail.
I tried the following in my Controller but always get an empty child collection. I can get the child collection to display if I return the entity class which is not great with many to many relationships.
I want to end up with JSON that looks like...
[
{ prop : ..,
prop: ..,
collection[
{
prop:..,
prop:..
}
]
}
]
This is what I have in my controller:
public ActionResult<IEnumerable<LogEntryDto>> GetAllEntries()
{
var entryList = _context.Entries.ToList();
return Ok(_mapper.Map<IEnumerable<RiskGetDto>>(entryList));
}
My Automapper profile classes contains simple mapping between the entities and DTOs
CreateMap<LogEntry, LogEntryDto>();
CreateMap<Tag, TagDto>();
I have a the following class
public class LogEntry
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<Tag> Tags { get; set; } = new List<Tag>();
}
And another
public class Tag
{
public int Id { get; set; }
public string Name { get; set; }
public string Value { get; set; }
public List<LogEntry> LogEntries{ get; set; } = new List<LogEntry>();
}
And the following DTOs for each class
public class LogEntryDto
{
public string Name { get; set; }
public string Description { get; set; }
public List<Tag> Tags { get; set; } = new List<Tag>();
}
and...
public class TagDto
{
public string Name { get; set; }
public string Value { get; set; }
}
Thank you for your help.
Ok, just in case someone will be helped by this fix. My silly mistake was driven by my lack of lambda expression mileage(!!);
I needed to retrieve the full set of properties in the GetAllEntries() method in the service class like so (notice the ToList() on the Tags collection):
public List<Risk> GetAllEntries()
{
var entryList = _context.Entries.Select(e=> new Risk
{
Id = e.Id,
RiskName = e.RiskName,
RiskDescription = e.RiskDescription,
---->>> **Tags = e.Tags.ToList()**
}).ToList();
return entryList;
}
This is then passed to the controller and mapped like so:
[HttpGet]
public ActionResult<List<LogEntryDto>> GetAllEntries()
{
var entries = _entryService.GetAllEntries();
return Ok(_mapper.Map<List<LogEntryDto>>(entries));
}
And then let AutoMapper do its magic with the simple mapping profile...:
public EntriesProfile()
{
CreateMap<LogEntry, LogEntryDto>()
.ForMember
(dto => dto.EntryId,
dbEntity => dbEntity.MapFrom(src => ....)
.ReverseMap();
CreateMap<Tag, TagDto>()
.ForMember
(dto => dto.Name,
dbEntity => dbEntity.MapFrom(src => src.Name))
.ForMember
(dto => dto.Value,
dbEntity => dbEntity.MapFrom(src => src.Value))
.ReverseMap();
}

EF Core: detached lazy-loading navigation entity. Why?

I have the following model:
public partial class Device
{
public int Id { get; set; }
public virtual Tablet Tablet { get; set; }
where Tablet is the following:
public class Tablet
{
public string TabletId { get; set; }
public int DeviceId { get; set; }
public virtual Device Device { get; set; }
private ICollection<TabletTransferRequest> _tabletTransferRequests;
public virtual ICollection<TabletTransferRequest> TabletTransferRequests { get => _tabletTransferRequests ?? (_tabletTransferRequests = new List<TabletTransferRequest>()); protected set => _tabletTransferRequests = value; }
}
and the mapping class:
public class TabletMap : IEntityTypeConfiguration<Tablet>
{
public void Configure(EntityTypeBuilder<Tablet> builder)
{
builder.ToTable(nameof(Tablet));
builder.HasKey(p => p.TabletId);
builder.HasOne(p => p.Device)
.WithOne(o => o.Tablet)
.HasForeignKey<Tablet>(p => p.DeviceId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict)
;
}
}
DTO classes:
public class DeviceDisplayDto
{
public int Id { get; set; }
public TabletPartDto Tablet { get; set; }
}
public class TabletPartDto
{
public string TabletId { get; set; }
public List<TabletTransferRequestElementDto> TabletTransferRequests { get; set; }
}
public class TabletTransferRequestElementDto : DeviceRequestElementAbstractDto
{
public string TabletId { get; set; }
public int DeviceId { get; set; }
}
when I try to do the following
var query = _context.Devices.Include(d => d.Tablet).ThenInclude(d => d.TabletTransferRequests);
var devices = new PagedList<DeviceDisplayDto>(query.ProjectTo<DeviceDisplayDto>(_mapperConfig), pageIndex, pageSize);
I got the following:
Error generated for warning
'Microsoft.EntityFrameworkCore.Infrastructure.DetachedLazyLoadingWarning:
An attempt was made to lazy-load navigation property
'TabletTransferRequests' on detached entity of type 'TabletProxy'.
Lazy-loading is not supported for detached entities or entities that
are loaded with 'AsNoTracking()'.'. This exception can be suppressed
or logged by passing event ID 'CoreEventId.DetachedLazyLoadingWarning'
to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or
'AddDbContext'.
why it's detached?
Is the error occurring on the var devices = new PagedList(...) or later, such as when the controller method completes? I don't recognize that PagedList implementation, but from what you've typed that rendition appears to be initializing with IQueryable<T> rather than a static collection IList<T> which may be an issue if the PagedList ends up attempting to resolve paging from the IQueryable after an initial load.
Normally PagedList would utilize a ToPagedList(page, pageSize) method when working with IQueryable which would trigger a one-off load of a Page. If written as a class initialized with an IQueryable, a data retrieval query could be non-fillable if queried after the DbContext is disposed.

Prevent circular references when getting result from database

If I have the following simple model:
public class Company
{
public Guid CompanyId { get; set; }
public ICollection<CompanyUser> Users { get; set; }
}
public class CompanyUser
{
public Guid CompanyId { get; set; }
public Guid UserId { get; set; }
public Company Company { get; set; }
public User User { get; set; }
}
public class User
{
public Guid UserId { get; set; }
public ICollection<CompanyUser> Companies { get; set; }
}
To get a list of companies + their users + the user object I run the following query:
return await _dataContext.Companies
.Include(m => m.Users)
.ThenInclude(m => m.User)
.OrderBy(m => m.Name)
.ToListAsync();
The results work, but I use a mapper to map the results to a view model by going recurisvely through the model.
What happens is that the Company object has a reference to a list of CompanyUser, in each of those CompanyUser objects we have a Company which has a list of CompanyUser again, which just keeps repeating until we get a stack overflow.
The mapper is a very simple one:
var results = companies.ToViewModel<Company, CompanyViewModel>();
public static IList<TModel> ToViewModel<TEntity, TModel>(this IEnumerable<TEntity> entities)
where TEntity : class
where TModel : class, IViewModel<TEntity>, new()
{
return entities?.Select(entity => entity.ToViewModel<TEntity, TModel>()).ToList();
}
public static TModel ToViewModel<TEntity, TModel>(this TEntity entity)
where TEntity : class
where TModel : class, IViewModel<TEntity>, new()
{
if (entity == null)
{
return null;
}
var model = new TModel();
model.ToViewModel(entity);
return model;
}
public interface IViewModel<in TEntity>
where TEntity : class
{
void ToViewModel(TEntity entity);
}
public class CompanyViewModel : IViewModel<Company>
{
public Guid CompanyId { get; set; }
public IList<CompanyUserViewModel> Users { get; set; }
public void ToViewModel(Company entity)
{
CompanyId = entity.CompanyId;
Users = entity.Users.ToViewModel<CompanyUser, CompanyUserViewModel>();
}
}
public class CompanyUserViewModel : IViewModel<CompanyUser>
{
public Guid CompanyId { get; set; }
public Guid UserId { get; set; }
public CompanyViewModel Company { get; set; }
public UserViewModel User { get; set; }
public void ToViewModel(CompanyUser entity)
{
CompanyId = entity.CompanyId;
UserId = entity.UserId;
Company = entity.Company.ToViewModel<Company, CompanyViewModel>();
User = entity.User.ToViewModel<User, UserViewModel>();
}
}
public class UserViewModel : IViewModel<User>
{
public Guid UserId { get; set; }
public void ToViewModel(User entity)
{
UserId = entity.Id;
}
}
Is there a way to prevent these references to be resolved?
There are multiple solutions:
1) You can use automapper instead of own mapper. It has MaxDepth property which will prevents from this problem:
Mapper.CreateMap<Source, Destination>().MaxDepth(1);
2) You can remove dependencies from your entities and use shadow properties in one direction.
Are you open to changing your data model? I think the best solution would be to remove the circular reference.
If a company contains a list of users, does the User also need both the CompanyId and the Company object he is contained in? I would remove public Company Company { get; set; } from your CompanyUser object and Companies from your User object.
Your issue is that you are mapping to CompanyViewModel which then maps to CompanyUserViewModel but this then maps back again to CompanyViewModel which creates an infinite loop.
If you expect to always start at Company (to CompanyView) then remove the recursion back from CompanyUserViewModel.
public void ToViewModel(CompanyUser entity)
{
CompanyId = entity.CompanyId;
UserId = entity.UserId;
// Company = entity.Company.ToViewModel<Company, CompanyViewModel>();
User = entity.User.ToViewModel<User, UserViewModel>();
}
Alternatively do not map the relations in your ToViewModel mapping, wire up the relations after based on the Ids.

What's the proper way to get data using Entity Framework so you can navigate through objects?

I have a .Net 4.5 MVC 5 database first project that I'm playing around with. There's a data access layer (Entity Framework 6), a business logic layer and the MVC layer.
If I have an object with relationships in the data layer:
namespace DataAccess
{
public class Course
{
public int CourseID { get; set; }
public string Title { get; set; }
public ICollection<Lecture> Lectures { get; set; }
public ICollection<Tutor> Tutors { get; set; }
}
public class Lecture
{
public int LectureID { get; set; }
public string Title { get; set; }
public DateTime Date { get; set; }
public ICollection<Student> Students { get; set; }
}
public class Tutor
{
public int TutorID { get; set; }
public string Name { get; set; }
}
public class Student
{
public int StudentID { get; set; }
public string Name { get; set; }
}
}
And in my business logic layer I have a method that gets courses:
namespace BusinessLogic
{
public static IEnumerable<Course> GetCourses()
{
using (var db = new MyEntities())
{
return db.Courses.Include("Lectures").Include("Lectures.Students").Include("Tutors").ToList();
}
}
}
And I get the data using my controller like this:
public class HomeController : Controller
{
public ActionResult Index()
{
var courses = BusinessLogic.GetCourses();
return View(courses);
}
}
Why is it, when I query my data in the Razor view like this:
var numLectures = courses.Lectures.Count;
var numStudents = courses.Lectures.Students.Count;
var tutorName = courses.Tutors.LastOrDefault().Name;
I get the application error System.ObjectDisposedException: The ObjectContext instance has been disposed and can no longer be used for operations that require a connection.
I know the connection is disposed after the using statement has finished and that .ToList() will let me navigate the courses object, but how do I navigate the objects inside each course (i.e. lectures, students, tutors etc.)?
Your navigation properties need to be declared as virtual:
namespace DataAccess
{
public class Course
{
public int CourseID { get; set; }
public string Title { get; set; }
public virtual ICollection<Lecture> Lectures { get; set; }
public virtual ICollection<Tutor> Tutors { get; set; }
}
public class Lecture
{
public int LectureID { get; set; }
public string Title { get; set; }
public DateTime Date { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
...
}
When these lazy loadable properties are not marked as virtual, the EF dynamic proxies cannot override them and you will never be able to navigate from one entity to a (set of) another.
Another bit of advice: use the strongly-typed .Include when eager loading:
namespace BusinessLogic
{
public static IEnumerable<Course> GetCourses()
{
using (var db = new MyEntities())
{
return db.Courses
.Include(x => x.Lectures.Select(y => y.Students))
.Include(x => x.Tutors)
.ToList();
}
}
}
I think the problem is because one (or more than one) property that you are calling in your View is (are) not included in your query. Make sure you are including all the navigation properties you need in the view. Try with this query:
using (var db = new MyEntities())
{
return db.Courses.db.Courses.Include(c=>c.Lectures.Select(l=>l.Students)).Include(c=>c.Tutors‌​).ToList()
}
If you need to add another relative property that you use in your View, then add another Include call for that property.
Another thing, when you need to eager load two levels (like Lectures.Students), you don't need to add a Include call for each level, with the call that you do for the second level is enough to include both. Could be this way:
.Include("Lectures.Students") // as you did it
Or:
.Include(c=>c.Lectures.Select(l=>l.Students))

Entity Framework storing collection size in 1-* relationships

I'm creating a forum website. More for a learning project than anything else.
My database is fairly simple: a Board has many Thread, and a Thread has many Post. So my domain objects look like this:
public class Board
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Thread> Threads { get; set; }
}
public class Thread
{
public int Id { get; set; }
public string Title { get; set; }
public int BoardId { get; set; }
public virtual Board Board { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Author { get; set; }
public DateTime CreationDate { get; set; }
public string Content { get; set; }
public int ThreadId { get; set; }
public virtual Thread Thread { get; set; }
}
I want my Board ViewModel to look like this:
public class BoardViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public int ThreadCount { get; set; }
// Date of the latest post.
public DateTime LastUpdatedDate { get; set; }
}
EDIT: My View will use an IEnumerable<BoardViewModel>. I don't need to use the Post or Thread collections directly.
Creating this view model when using the DbContext object directly is easy, but I don't know what to do if I want to refactor this behind a repository.
I know that returning a view model from the data layer is a bad idea, but just returning a board object would mean the Threads and Posts collections aren't initialized.
Should I just include the collections in the database query (which sounds expensive), or should I include the information in the Board class? Or are there other options?
That depend what you are going to do in your view, if you are going to use the collection of Threads in your view, then you need to load them before use it. In my humble opinion, to achieve the escenario that you want, you should disable lazy loading, wich is the default behavior. It can be turned off for all entities in the context by setting a flag on the Configuration property:
public class YourContext : DbContext
{
public YourContext()
{
this.Configuration.LazyLoadingEnabled = false;
}
}
Then, you could load the navigation properties by demand in your queries using, for example, one of these extension methods:
public static class IQueryableExtensions
{
public static IQueryable<TEntity> Includes<TEntity>(this IQueryable<TEntity> queryable, params string[] includeProperties)
{
return includeProperties.Aggregate(queryable, (current, includeProperty) => current.Include(includeProperty));
}
public static IQueryable<TEntity> IncludesWithFunc<TEntity>(this IQueryable<TEntity> queryable, params Expression<Func<TEntity, object>>[] includeProperties)
{
return includeProperties.Aggregate(queryable, (current, includeProperty) => current.Include(includeProperty));
}
}
To give you some examples, if you need to load the boards with their Threads to create your ViewModel instances, you could do this:
db.Boards.IncludesWithFunc(b=>b.Threads);
Or this:
db.Boards.Includes("Threads");
And if you need to load another level, then you could do this:
db.Boards.IncludesWithFunc(b=>b.Threads.Select(t=>t.Posts));
Or this:
db.Boards.Includes("Threads.Posts");
If you are using a Generic Repository, you can adapt those methods in your class, for example:
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
private readonly DbSet<TEntity> _dbSet;
//...
public IQueryable<TEntity> IncludesWithFunc<TEntity>(params Expression<Func<TEntity, object>>[] includeProperties)
{
IQueryable<TEntity> queryable = _dbSet;
return includeProperties.Aggregate(queryable, (current, includeProperty) => current.Include(includeProperty));
}
}
Using a class like this you can do something like this in your Business Layer (where I guess you are creating the ViewModel instances):
var rep=new Repository<Board>();
var someBoard=rep.IncludesWithFunc(b=>b.Threads).FirstOrDefault(b=>b.Name="SomeName");

Categories