Projecting from an IDbAsyncQueryProvider - entity-framework

I have a project in which we use Automapper to Project() entities from our data access layer into our domain classes which are consumed by external callers (i.e. WebAPI, windows services, etc). The idea is that we want to abstract our domain model from the actual database implementation, but often the domain model is comprised of at least the members from the database, and so Automapper makes constructing those domain models a lot easier using it's projection features.
This results in code that looks something like:
public class DbTask
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Task
{
public int Id { get; set; }
public string Name { get; set; }
public User Assignee { get; set; }
public Priority CurrentPriority { get; set; }
}
And then a business service implementation that looks like:
public IQueryable<Task> QueryTasksByUser(int userId)
{
return dbContext
.Where(x => x.Assignee.Id == userId)
.Project()
.To<Task>();
}
So then at the API layer we want to leverage async/await as much as possible, and so I'm wondering if Automapper has any support for this. If I write my method like:
public async Task<IHttpActionResult> GetTopTaskForCurrentUser()
{
var task = await _taskService
.GetTasksByUser(Thread.CurrentPrincipal.AsUser().UserId)
.Where(x => x.CurrentPriority == Priority.Top)
.SingleOrDefaultAsync();
return Ok(task);
}
I get the following exception:
System.InvalidOperationException : The provider for the source IQueryable doesn't implement IDbAsyncQueryProvider. Only providers that implement IDbAsyncQueryProvider can be used for Entity Framework asynchronous operations. For more details see http://go.microsoft.com/fwlink/?LinkId=287068.
Is this something that Automapper just doesn't have support for [yet], or is there another method or extension that I'm missing?

You just need to make sure you call these methods in the correct order:
return await dbContext.Users
.Where(user => user.Id == id)
.Project().To<UserDto>()
.ToListAsync();
Put Project.To in the same place you'd put "Select" and it works fine.
I typically create an extension method to wrap the projection and async'ing:
public static async Task<List<TDestination>>
ToListAsync<TDestination>(
this IProjectionExpression projectionExpression)
{
return await projectionExpression.To<TDestination>().ToListAsync();
}
return await dbContext.Users
.Where(u => u.Id == id)
.Project().ToListAsync<UserDto>();

Related

Retrieve child entities from CrudAppService in abp.io using .Net 5 EF

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.

Include in Entity Framework and Cosmos DB

I have a problem with Entity Framework and Cosmos DB.
When I use .Include on an entity like this:
public async Task<People[]> GetAllPeopleAsync()
{
IQueryable<People> query = _context.Peoples.Include(p => p.Info);
return await query.ToArrayAsync();
}
I get this error:
Banco De dados Falhos The LINQ expression 'LeftJoin, TransparentIdentifier>(outer: DbSet, inner: DbSet,
outerKeySelector: (p) => Property>(p, "Id"),
innerKeySelector: (i) => Property>(i, "PeopleId"),
resultSelector: (o, i) => new TransparentIdentifier(
Outer = o,
Inner = i
))' 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 either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
Saving data is working normally as SQL:
public void Add<T>(T entity) where T : class
{
_context.Add(entity);
}
public async Task<bool> SaveChangesAsync()
{
return (await _context.SaveChangesAsync()) > 0;
}
My entities:
public class People
{
public Guid Id { get; set; }
public string Name { get; set; }
public Info Info { get; set; }
}
public class Info
{
public Guid Id { get; set; }
public string Street { get; set; }
public string Number { get; set; }
public Guid PeopleId { get; set; }
public People People { get; }
}
To solve this problem I made a function to get info from PeopleId and then put it in People and it worked as I needed:
public async Task<Info[]> GetInfoByPepleIdAsync(Guid guidPeople)
{
IQueryable<Info> query = _context.Infos.Where(i => i.PeopleId == guidPeople);
return await query.ToArrayAsync();
}
The GetAllPeopleAsync function looks like this:
public async Task<People[]> GetAllPeopleAsync()
{
IQueryable<People> query = _context.Peoples;
var Peoples = await query.ToArrayAsync();
foreach(People p in Peoples)
{
var Infos = await GetInfoByPepleIdAsync(p.Id);
p.Info = Infos.FirstOrDefault();
}
return Peoples;
}
I would like to know if there is any method that works like include in CosmosDB?
If there is no equivalent method, this method is viable or I will have some performance problem or something?
As of 2019-12-03, Include and Join are not supported by the EF Core Cosmos DB Provider.
https://learn.microsoft.com/en-us/ef/core/providers/cosmos/limitations
You can track the outstanding issues on Github to see if/when it gets implemented for EF Core 3.1. You might even get early access with a preview release. Include was not released with 3.1. You can still track the progress on that feature here:
https://github.com/aspnet/EntityFrameworkCore/issues?page=1&q=is%3Aissue+is%3Aopen+Cosmos+in%3Atitle+label%3Atype-enhancement+sort%3Areactions-%2B1-desc
Earlier this year, Julie Lerman posted a blog showing roughly how Include is expected to work with Cosmos. If the implementation resembles that preview, EF will add something similar to a foreign key when EF infers there are graph/node style relationships between entities in the context.
https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/january/data-points-a-peek-at-the-ef-core-cosmos-db-provider-preview

EF6:How to include subproperty with Select so that single instance is created. Avoid "same primary key" error

I'm trying to fetch (in disconnected way) an entity with its all related entities and then trying to update the entity. But I'm getting the following error:
Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
public class Person
{
public int PersonId { get; set; }
public string Personname { get; set }
public ICollection Addresses { get; set; }
}
public class Address
{
public int AddressId { get; set; }
public int PersonId { get; set; }
public string Line1 { get; set; }
public string City { get; set; }
public string State { get; set; }
public Person Person { get; set; }
public ICollection<Feature> Features { get; set; }
}
// Many to Many: Represented in database as AddressFeature (e.g Air Conditioning, Central Heating; User could select multiple features of a single address)
public class Feature
{
public int FeatureId { get; set; }
public string Featurename { get; set; }
public ICollection<Address> Addresses { get; set; } // Many-To-Many with Addresses
}
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
var person = dbContext.People.AsNoTracking().Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}
public void UpdateCandidate(Person newPerson)
{
Person existingPerson = GetPerson(person.Id); // Loading the existing candidate from database with ASNOTRACKING
dbContext.People.Attach(existingPerson); // This line is giving error
.....
.....
.....
}
Error:
Additional information: Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Kindly suggest.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Since you are using a short lived DbContext for retrieving the data, all you need is to remove AsNoTracking(), thus allowing EF to use the context cache and consolidate the Feature entities. EF tracking serves different purposes. One is to allow consolidating the entity instances with the same PK which you are interested in this case, and the second is to detect the modifications in case you modify the entities and call SaveChanges(), which apparently you are not interested when using the context simply to retrieve the data. When you disable the tracking for a query, EF cannot use the cache, thus generates separate object instances.
What you really not want is to let EF create proxies which hold reference to the context used to obtain them and will cause issues when trying to attach to another context. I don't see virtual navigation properties in your models, so most likely EF will not create proxies, but in order to be absolutely sure, I would turn ProxyCreationEnabled off:
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
dbContext.Configuration.ProxyCreationEnabled = false;
var person = dbContext.People.Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}

Using the Entry<TEntity>().CurrentValues.SetValues() is not updating collections

I have not run into this before, because I usually handled collections by them selves instead of modifying them directly on the entity.
public class Schedule: BaseEntity
{
public Guid Id {get;set;}
public virtual int? DayOfTheWeekTypeId { get; set; }
public virtual DayOfTheWeekType DayOfTheWeekType { get; set; }
public virtual ICollection<Instructor> Instructors { get; set; }
public DateTime? StartDateTime { get; set; }
public DateTime? EndDateTime { get; set; }
public string SpecialInstructions { get; set; }
}
Mapping class:
public ScheduleMapping()
{
HasMany(c => c.Instructors).WithMany().Map(m => { m.MapLeftKey("ScheduleId");
m.MapRightKey("InstructorId");
m.ToTable("Schedule_Instructors");
});
HasOptional(s => s.DayOfTheWeekType).WithMany().HasForeignKey(s => s.DayOfTheWeekTypeId).WillCascadeOnDelete(false);
Property(s => s.SpecialInstructions).IsMaxLength();
}
This is my update method:
public virtual void Update(TEntity entity)
{
if (entity == null)
throw new ArgumentNullException("entity");
//this is the original persisted entity
var persistedEntity = _repository.GetById(entity.Id);
if(originalEntity != null)
{
entity.Id = persistedEntity.Id;
UnitOfWork.ApplyCurrentValues<TEntity>(originalEntity,entity);
UnitOfWork.Commit();
}
}
This is the Method that handled the "merge"
public void ApplyCurrentValues<TEntity>(TEntity original, TEntity current) where TEntity : class
{
base.Entry<TEntity>(original).CurrentValues.SetValues(current);
}
If I modify the Instructors collection then try to apply the update, it seems to keep my original values. I have tried loading the the Schedule entity prior to the update and make my changes, but sometimes that causes a PK error (on the Instructors collection) in entity framework. As if it is trying to add an entity with the same key. So, instead I am rebuilding the Schedule entity (including the ID) manually and then updating it. When I do that I do not get any more errors, however, the Instructors collections doesn't change. I am thinking because CurrentValues. SetValues is being applied based on the persisted entity and not my updated version. the Should I handle my updates differently or do I need to manully
SetValues never updates navigation properties. When you execute your code it only knows about changes in simple / complex properties of the entity passed to your Update method. EF even don't know about related entities of the entity passed to your Update method.
You must manually tell EF about each change in your object graph - EF doesn't have any resolution mechanism for object graphs.

Fetching Strategy example in repository pattern with pure POCO Entity framework

I'm trying to roll out a strategy pattern with entity framework and the repository pattern using a simple example such as User and Post in which a user has many posts.
From this answer here, I have the following domain:
public interface IUser {
public Guid UserId { get; set; }
public string UserName { get; set; }
public IEnumerable<Post> Posts { get; set; }
}
Add interfaces to support the roles in which you will use the user.
public interface IAddPostsToUser : IUser {
public void AddPost(Post post);
}
Now my repository looks like this:
public interface IUserRepository {
User Get<TRole>(Guid userId) where TRole : IUser;
}
Strategy (Where I'm stuck). What do I do with this code? Can I have an example of how to implement this, where do I put this?
public interface IFetchingStrategy<TRole> {
TRole Fetch(Guid id, IRepository<TRole> role)
}
My basic problem was what was asked in this question. I'd like to be able to get Users without posts and users with posts using the strategy pattern.
If we talk about strategy pattern then IFetchingStrategy must be passed to IUserRepository so I think you should modify Get operation:
public interface IUserRepository
{
User Get<TRole>(Guid userId, IFetchingStrategy<TRole> strategy) where TRole : IUser;
}
But I'm not sure how to implement such interfaces with EF.
If we return to your former question, it can also be accomplished this way:
public interface IUserRepository
{
User Get(Guid userId, IEnumerable<Expression<Func<User,object>>> eagerLoading);
}
public class UserRepository : IUserRepository
{
public User Get(Guid userId, IEnumerable<Expression<Func<User,object>>> eagerLoading)
{
ObjectQuery<User> query = GetBaseQuery(); // get query somehow, for example from ObjectSet<User>
if (eagerLoading != null)
{
foreach(var expression in eagerLoading)
{
// This is not supported out of the box. You need this:
// http://msmvps.com/blogs/matthieu/archive/2008/06/06/entity-framework-include-with-func-next.aspx
query = query.Include(expression);
}
}
return query.SingleOrDefault(u => u.Id == userId);
}
}
You will use the method this way:
User userWithoutPosts = repository.Get(guid, null);
User userWithPosts = repository.Get(guid, new List<Expression<Func<User,object>>>
{
u => u.Posts
});
But I guess that this implementation works only for first level of navigation properties.
An answer in the following post uses strategy pattern to manipulate a query.
https://codereview.stackexchange.com/questions/3560/is-there-a-better-way-to-do-dynamic-filtering-and-sorting-with-entity-framework