I have the following Entity classes:
namespace Entities
{
public abstract class EntityBase
{
public EntityBase()
{
Id = Guid.NewGuid();
}
public Guid Id { get; set; }
}
}
public class Name : EntityBase
{
public DateTime LastUpdatedTime { get; set; }
public string Name { get; set; }
public Package Package { get; set; }
}
public class Owner : EntityBase
{
public DateTime LastUpdatedTime { get; set; }
public Guid ObjectId { get; set; }
public List<Package> Packages { get; set; }
}
public class Status : EntityBase
{
public string Name { get; set; }
public int SortPriority { get; set; }
}
public class Package : EntityBase
{
public Package()
{
}
public Guid? RequestorId { get; set; }
public string Requestor { get; set; }
public Name Name { get; set; }
public List<Owner> Owners { get; set; }
public Status StatusDetails { get; set; }
}
I have the following Model classes:
namespace Models
{
public abstract class ModelBase
{
public Guid Id { get; set; }
}
}
public class Name : ModelBase
{
public DateTime LastUpdatedTime { get; set; }
public string Name { get; set; }
public Package Package { get; set; }
}
public class Owner : ModelBase
{
public DateTime LastUpdatedTime { get; set; }
public Guid ObjectId { get; set; }
public List<Package> Packages { get; set; }
}
public class Status : ModelBase
{
public string Name { get; set; }
public int SortPriority { get; set; }
}
public class Package : ModelBase
{
public Package()
{
}
public Guid? RequestorId { get; set; }
public string Requestor { get; set; }
public Name Name { get; set; }
public List<Owner> Owners { get; set; }
public Status StatusDetails { get; set; }
}
Here is the DbContext class:
using Entities;
public class WriteContext : DbContext
{
public DbSet<Package> Packages { get; set; } = null!;
public DbSet<Name> Names { get; set; }
public DbSet<Owner> Owners { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Package>().Property(t => t.Id)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("NEWSEQUENTIALID()");
modelBuilder.Entity<Package>()
.HasOne(s => s.StatusDetails)
.WithOne()
.HasForeignKey<Package>(x => x.Status)
.HasPrincipalKey<Status>(x => x.Name)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
modelBuilder.Entity<Status>()
.ToTable("Statuses");
modelBuilder.Entity<JobNotification>().Property(t => t.Id)
modelBuilder.Entity<Package>()
.HasOne(package => package.Name)
.WithOne(name => name.Package)
.HasForeignKey<Name>(name => name.Id);
modelBuilder.Entity<Package>()
.HasMany(package => package.Owners)
.WithMany(owner => owner.Packages);
modelBuilder.Entity<Owner>().Property(owner => owner.Id)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("NEWSEQUENTIALID()");
modelBuilder.Entity<Owner>()
.HasIndex(owner => owner.ObjectId);
modelBuilder.Entity<Name>()
.HasIndex(name => name.Name);
}
public WrietContext(DbContextOptions<WriteContext> options)
: base(options)
{
}
}
Here is the Repository class:
using Models;
public class DatabasePackagesRepository : RepositoryBase<Entities.Package, Models.Package>, IDatabasePackagesRepository
{
private readonly WriteContext _writeContext;
public DatabasePackagesRepository(WriteContext writeContext)
: base(writeContext)
{
_readContext = readContext;
}
public override Entities.Package MapModelToEntity(Models.Package model)
{
return new Entities.Package
{
Id = model.Id,
RequestorId = model.RequestorId,
Requestor = model.Requestor
};
}
public override Models.Package MapEntityToModel(Entities.Package entity)
{
return new Models.Package
{
Id = entity.Id,
RequestorId = entity.RequestorId,
Requestor = entity.Requestor
};
}
public async Task<List<Models.Package>> GetPackagesAsync()
{
var entities = await _readContext.Packages.ToListAsync();
return entities.Select(MapEntityToModel).ToList();
}
On running this, I see the error
System.InvalidOperationException: The LINQ expression 'DbSet() .Where(s => DatabasePackagesRepository.MapEntityToModel(s).Owners .Any(o => o.ObjectId == __Parse_0))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'.
How do I update MapEntityToModel to include Owners, Name and StatusDetails?
Quick answer: Use a mapper that already supports working with EF's
IQueryableincluding Automapper, Mapperly, and Mapster to name a few.For instance with Automapper:
The reason your code doesn't work is because you are fetching just the top level Package without eager loading the related data. Though the error looks like it might be from different code than you provided. It would be what I would expect from something like:
which could be "fixed" by changing the code to be more like what you had:
But would result in issues where the related data isn't loaded, which could be "fixed" by using
.Include()for each of the related entities in thevar entities = ....Linq expression. However, this is a very inefficient approach.I highly recommend avoiding generic repository patterns like this. Abstracting EF to this degree leads to extremely inefficient code which will result in hard to use, slow, and excessive memory use. When a repository returns "Models" suitable for all occasions rather than entities then you end up with models that are strictly 1-to-1 copies of the entities they map to. In this case, what is their purpose??
View Models absolutely serve a purpose, but what they contain should be determined solely by the view, or what their consumer needs. Often these are cut-down views, or "flattened" representations of an entity and related data. It isn't the repository's job to serve these view models, it should just be a thin wrapper for the entities to either provide a simple abstraction point to serve as a boundary for unit testing, or ensuring that core querying rules are enforced.
The major issue with generic repositories, whether they return entities or view models, is that they make it very difficult and/or complex to support efficient querying. Think about what you will need to do to support:
EF and it's
IQueryablecan facilitate all of this capability through Linq where the typical generic repository pattern make supporting these capabilities a complex exercise of reinventing the wheel which still "leaks" EF dependent rules into your code (the usual argument for abstracting out EF), or lead to code that does without them. (slow, memory-hogging messes)