Closed koraycsharp closed 2 years ago
with use seclection you can get rid of these:
.Include(dr => dr.Classrooms)
.ThenInclude(dr => dr.Students)
regarding your problem, i am not aware of the internals of Automapper. How does the proejction look like? Can you add a middleware before UseSelection and validate what is in context.Result after you called next?
I did similar think using auto mapper without problems.. I posted this on Slack channel maybe it will help you it was related to eliminate EF-core middle tables but i use auto-mapper for it.. In just copy paste it from slack:
The thing what i tried to solve was elimination of Join (middle) tables of many-to-many relation of EF-Core and projection to my custom DTO for graphql server..
Some picture what im talking about: https://excalidraw.com/#json=5532829123346432,9UuB9AkYRqZDtTW4TGTLGQ
I will write what i did to get what i have and why:
So now i have 3 blocks: HotchocolateGraphQL > IQueryable Mapper > EF Core
Good thing is that graphql server have no idea there is some mapper he is performing all operations on DTO model and mapper transform it to EF-core model and all is IQueryable
I will add small example:
Startup:
var mappingConfig = new MapperConfiguration(mc => {
mc.AddProfile(new MappingProfile());
});
IMapper mapper = mappingConfig.CreateMapper();
services.AddSingleton(mapper);
services.AddGraphQL(
SchemaBuilder.New()
.AddQueryType<UserQuerry>()
.Create(), new QueryExecutionOptions { ForceSerialExecution = true }).AddErrorFilter(error => {
Console.WriteLine(error.Exception);
return error;
});
Querry:
public class UserQuerry {
[UseSelection]
public IQueryable<User_DTO> GetGraphUsers([Service] IAppDbContext context, [Service] IMapper mapper) =>
mapper.ProjectTo<User_DTO>(context.Users.AsNoTracking());
}
Mapping:
namespace Aplication.GraphQL.Mapping {
public class MappingProfile : Profile {
public MappingProfile() {
// ------------------------------
// Mapping configuration
// ------------------------------
CreateMap<User, User_DTO>()
.ForMember(dest => dest.Code, opt => opt.MapFrom(src => src.Code))
.ForMember(dest => dest.FirstName, opt => opt.MapFrom(src => src.FirstName))
.ForMember(dest => dest.ID, opt => opt.MapFrom(src => src.ID))
.ForMember(dest => dest.LastName, opt => opt.MapFrom(src => src.LastName))
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.UserName))
.ForMember(dest => dest.Roles, opt => opt.MapFrom(src => src.Roles.Select(e => e.Role)))
.ForMember(dest => dest.Groups, opt => opt.MapFrom(src => src.Groups.Select(e => e.Group)))
.ForMember(dest => dest.Premissions, opt => opt.MapFrom(src => src.Claims.Select(e => e.Permission)));
CreateMap<Role_DTO, Role>()
.ForMember(dest => dest.Code, opt => opt.MapFrom(src => src.Code))
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
.ForMember(dest => dest.ID, opt => opt.MapFrom(src => src.ID))
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
.ReverseMap();
CreateMap<Group, Group_DTO>()
.ForMember(dest => dest.Code, opt => opt.MapFrom(src => src.Code))
.ForMember(dest => dest.ColorCode, opt => opt.MapFrom(src => src.ColorCode))
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
.ForMember(dest => dest.ID, opt => opt.MapFrom(src => src.ID))
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.Premissions, opt => opt.MapFrom(src => src.Claims.Select(e => e.Permission)))
.ReverseMap();
CreateMap<Permission, Premission_DTO>()
.ForMember(dest => dest.ID, opt => opt.MapFrom(src => src.ID))
.ForMember(dest => dest.Key, opt => opt.MapFrom(src => src.Key))
.ForMember(dest => dest.Value, opt => opt.MapFrom(src => src.Value))
.ReverseMap();
}
}
}
Querry only user name:
{
graphUsers{
firstName
}
}
Result:
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 3.1.2 initialized 'AppDbContext' using provider 'Microsoft.EntityFrameworkCore.Sqlite' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT "u"."FirstName"
FROM "Users" AS "u"
info: Aplication.Services.MetricHandler[0]
Querry more data example:
{
graphUsers{
firstName
groups{
iD
}
}
}
Result:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT "u"."FirstName", "u"."ID", "t"."ID", "t"."GroupID", "t"."UserID"
FROM "Users" AS "u"
LEFT JOIN (
SELECT "g0"."ID", "g"."GroupID", "g"."UserID"
FROM "GroupUsers" AS "g"
INNER JOIN "Groups" AS "g0" ON "g"."GroupID" = "g0"."ID"
) AS "t" ON "u"."ID" = "t"."UserID"
ORDER BY "u"."ID", "t"."GroupID", "t"."UserID", "t"."ID"
Some deeper DTO querry example projected to EFcore model select..
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT "u"."ID", "u"."Deactivated", "u"."Email", "u"."FirstName", "u"."LastName", "t0"."Code", "t0"."ColorCode", "t0"."Description", "t0"."ID", "t0"."Name", "t0"."GroupID", "t0"."UserID", "t0"."ID0", "t0"."Key", "t0"."Value", "t0"."GroupID0", "t0"."PermissionID", "t1"."ID", "t1"."Key", "t1"."Value", "t1"."UserID", "t1"."PermissionID", "t2"."Code", "t2"."Description", "t2"."ID", "t2"."Name", "t2"."UserID", "t2"."RoleID"
FROM "Users" AS "u"
LEFT JOIN (
SELECT "g0"."Code", "g0"."ColorCode", "g0"."Description", "g0"."ID", "g0"."Name", "g"."GroupID", "g"."UserID", "t"."ID" AS "ID0", "t"."Key", "t"."Value", "t"."GroupID" AS "GroupID0", "t"."PermissionID"
FROM "GroupUsers" AS "g"
INNER JOIN "Groups" AS "g0" ON "g"."GroupID" = "g0"."ID"
LEFT JOIN (
SELECT "p"."ID", "p"."Key", "p"."Value", "g1"."GroupID", "g1"."PermissionID"
FROM "GroupPermissions" AS "g1"
INNER JOIN "Permissions" AS "p" ON "g1"."PermissionID" = "p"."ID"
) AS "t" ON "g0"."ID" = "t"."GroupID"
) AS "t0" ON "u"."ID" = "t0"."UserID"
LEFT JOIN (
SELECT "p0"."ID", "p0"."Key", "p0"."Value", "u0"."UserID", "u0"."PermissionID"
FROM "UserPermissions" AS "u0"
INNER JOIN "Permissions" AS "p0" ON "u0"."PermissionID" = "p0"."ID"
) AS "t1" ON "u"."ID" = "t1"."UserID"
LEFT JOIN (
SELECT "r"."Code", "r"."Description", "r"."ID", "r"."Name", "u1"."UserID", "u1"."RoleID"
FROM "UserRoles" AS "u1"
INNER JOIN "Roles" AS "r" ON "u1"."RoleID" = "r"."ID"
) AS "t2" ON "u"."ID" = "t2"."UserID"
ORDER BY "u"."ID", "t0"."GroupID", "t0"."UserID", "t0"."ID", "t0"."GroupID0", "t0"."PermissionID", "t0"."ID0", "t1"."UserID",
"t1"."PermissionID", "t1"."ID", "t2"."UserID", "t2"."RoleID", "t2"."ID"
Just for info.. if you use [UseSelection] you dont need to specified this:
.Include(dr => dr.Classrooms)
.ThenInclude(dr => dr.Students)
.Where(dr => dr.Id == id)
.OrderBy(dr => dr.Id)
Just IQuriable
@damikun What version of HotChocolate and Automapper are you using? I tried the same and Automapper always expands my whole NavigationProperty tree
I'm not using any UseSelection (old-naming in v10) or UseProjections (current naming in V11+) so i cannot fully say what is the aspect of your problems.. Dont forget you must use ProjectTo and not Map and lazyloaded property must be marked as virtual... You need to call it on iQueriable not on dataloader! What i remember nasted object in that time didn't get loaded but all basic fields of root object was all loaded (string/int/enum)...
But your example has the [UseSelection]
attribute.
I am using the Mapper.ProjectTo
method and according to the AutoMapper documentation https://docs.automapper.org/en/stable/Queryable-Extensions.html#explicit-expansion this behavior is by design. It should not matter if I am using EF lazy loading or not because Automapper will actively request the navigation properties.
My Query method is just about the same as yours:
[UsePaging]
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<AddressModel> GetAddresses([Service] CacheDbContext db, [Service]IMapperService mapperService)
{
return db.Addresses
.AsSingleQuery()
.AsNoTracking()
.ProjectTo<AddressModel>(mapperService.ViewModelMapper.ConfigurationProvider);
}
But as ProjectTo
will expand all my mappings in that step, the query method does provide an IQuerable where everything is already expandend.
I am currently in a discussion on Stackoverflow with @PascalSenn https://stackoverflow.com/questions/67284037/using-automapper-10-1-1-and-hotchocolate-11-1-to-map-queryables/67284493?noredirect=1#comment118930085_67284493
If you have some idea on his approach it would be highly appreciated
Example is one year old... On that time i was on V10... This example is totally isolation of HC and automapper so they do not know about each other and are not connected only interface is ProjectTo abstraction... So it should work also in V11...
Try to just call your automapper ProjectTo directly for test if your mapping configuration does not miss some settings.. Normally only selected fields should be queried from DB.. This was real example from application what i posted hire in that time..
Can you try once #4563 is merged if it works with the new extension method?
dotnet add package HotChocolate.Data.AutoMapper
[UseProjection]
public IQueryable<PostDto> GetPosts(BloggingContext dbContext, IResolverContext context)
=> dbContext.Posts.ProjectTo<Post, PostDto>(context);
@PascalSenn #4563 looks like exactly what is required to get Automapper working with EF and HotChocolate. Unfortunately I am currently not in any project that plans to use HotChocolate but if i got the time, I will try to test it with one of my OpenSource projects.
Hi i tried AutoMapper 13.0.1 and HotChocolate 13.9.5. It works fine but there is difference in Queries. Without AutoMapper i feel the query is more optimized. See below
(With Automapper)
public class GetShippersResponseMapping : Profile
{
public GetShippersResponseMapping()
{
CreateMap<Shipper, GetShippersResponse>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id.ToString()))
.ForMember(dest => dest.LegalName, opt => opt.MapFrom(src => src.LegalName))
.ForMember(dest => dest.Admin, opt => opt.MapFrom(src => src.Users.FirstOrDefault(x => x.UserRoles.Any(ur => ur.Role == Role.ShipperAdmin))))
.ReverseMap();
}
}
SELECT CONVERT(varchar(20), [s].[Id]), [s].[LegalName], CASE
WHEN EXISTS (
SELECT 1
FROM [AspNetUsers] AS [a]
WHERE [s].[Id] = [a].[ShipperId] AND EXISTS (
SELECT 1
FROM [AppUserRoles] AS [a0]
WHERE [a].[Id] = [a0].[UserId] AND [a0].[Role] = N'ShipperAdmin')) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END, (
SELECT TOP(1) [a1].[Id]
FROM [AspNetUsers] AS [a1]
WHERE [s].[Id] = [a1].[ShipperId] AND EXISTS (
SELECT 1
FROM [AppUserRoles] AS [a2]
WHERE [a1].[Id] = [a2].[UserId] AND [a2].[Role] = N'ShipperAdmin')), (
SELECT TOP(1) [a3].[UserName]
FROM [AspNetUsers] AS [a3]
WHERE [s].[Id] = [a3].[ShipperId] AND EXISTS (
SELECT 1
FROM [AppUserRoles] AS [a4]
WHERE [a3].[Id] = [a4].[UserId] AND [a4].[Role] = N'ShipperAdmin'))
FROM [Shippers] AS [s]
WHERE [s].[IsDeleted] = CAST(0 AS bit)
No Automapper direct select
await _dbContext.Shippers.AsNoTracking()
.Where(x => !x.IsDeleted)
.Select(x => new
{
x.Id,
x.LegalName,
Admin = x.Users.FirstOrDefault(u => u.UserRoles.Any(ur => ur.Role == Role.ShipperAdmin))
})
.ToListAsync(cancellationToken);
SELECT [s].[Id], [s].[LegalName], [t0].[Id], [t0].[AccessFailedCount], [t0].[ConcurrencyStamp], [t0].[CreatedAt], [t0].[CreatedByUserId], [t0].[Email], [t0].[EmailConfirmed], [t0].[IsDeleted], [t0].[IsSuspended], [t0].[LockoutEnabled], [t0].[LockoutEnd], [t0].[NormalizedEmail], [t0].[NormalizedUserName], [t0].[PasswordHash], [t0].[PhoneNumber], [t0].[PhoneNumberConfirmed], [t0].[SecurityStamp], [t0].[ShipperId], [t0].[TwoFactorEnabled], [t0].[UpdatedAt], [t0].[UpdatedByUserId], [t0].[UserName]
FROM [Shippers] AS [s]
LEFT JOIN (
SELECT [t].[Id], [t].[AccessFailedCount], [t].[ConcurrencyStamp], [t].[CreatedAt], [t].[CreatedByUserId], [t].[Email], [t].[EmailConfirmed], [t].[IsDeleted], [t].[IsSuspended], [t].[LockoutEnabled], [t].[LockoutEnd], [t].[NormalizedEmail], [t].[NormalizedUserName], [t].[PasswordHash], [t].[PhoneNumber], [t].[PhoneNumberConfirmed], [t].[SecurityStamp], [t].[ShipperId], [t].[TwoFactorEnabled], [t].[UpdatedAt], [t].[UpdatedByUserId], [t].[UserName]
FROM (
SELECT [a].[Id], [a].[AccessFailedCount], [a].[ConcurrencyStamp], [a].[CreatedAt], [a].[CreatedByUserId], [a].[Email], [a].[EmailConfirmed], [a].[IsDeleted], [a].[IsSuspended], [a].[LockoutEnabled], [a].[LockoutEnd], [a].[NormalizedEmail], [a].[NormalizedUserName], [a].[PasswordHash], [a].[PhoneNumber], [a].[PhoneNumberConfirmed], [a].[SecurityStamp], [a].[ShipperId], [a].[TwoFactorEnabled], [a].[UpdatedAt], [a].[UpdatedByUserId], [a].[UserName], ROW_NUMBER() OVER(PARTITION BY [a].[ShipperId] ORDER BY [a].[Id]) AS [row]
FROM [AspNetUsers] AS [a]
WHERE EXISTS (
SELECT 1
FROM [AppUserRoles] AS [a0]
WHERE [a].[Id] = [a0].[UserId] AND [a0].[Role] = N'ShipperAdmin')
) AS [t]
WHERE [t].[row] <= 1
) AS [t0] ON [s].[Id] = [t0].[ShipperId]
WHERE [s].[IsDeleted] = CAST(0 AS bit)
This could most probably create a problem in future, with more data. With Automapper, query has cases and other one has not. I'm not sure why is this the case as of now
@damikun @PascalSenn
An update, after some testing, i don't think its an Automapper issue, even when i return IQueriable and do manual mapping without Automapper, it is still generating same query as of when using Automapper. This has something to do with [UseProjection]
[UseProjection]
[UseFiltering]
public async Task<IQueryable<GetShippersResponse>> GetShippers(
[Service] IAppDbContext dbContext,
[Service] ISender sender,
IResolverContext context)
{
await Task.CompletedTask;
var shippers = dbContext.Shippers.AsNoTracking()
.Where(x => !x.IsDeleted)
.Select(x => new GetShippersResponse
{
Id = x.Id.ToString(),
Address = x.Address,
DisplayName = x.DisplayName,
LegalName = x.LegalName,
EmailAddress = "dymmy",
PhoneNumber = x.PhoneNumber,
IsLinkedWithTFM = true,
Admin = x.Users
.Select(user => new AppUserDto
{
Id = user.Id.ToString(),
UserName = user.UserName,
Roles = user.UserRoles.Select(r => r.Role).ToArray()
})
.FirstOrDefault(userDto => userDto.Roles.Any(ur => ur == Role.ShipperAdmin))
})
;
return shippers;
}
When using Hotchocolate and Automapper, unnecessary database queries occur in the selection process.
Here is my dtos
And here is my mappings
And here is my query
And finally my graphql query
This query return me this db queries
1) Db query for school
2) Db query for classroom
3) Db queries for students
Select query for school is normal. Only id return me but selection over classroom and student dtos is not acting as desired i think. Because i want only floor and name fields in my graphql query but db return me name and surname field for student table and floor and name for classroom table.