So if i understood it correctly Projections are used to get rid of the Over/ Under fetching problems you would have with normal REST-APIs.
I already implemented a GetAll Functionality for my Author-ObjectType, and there it works pretty fine. My Problem is I wont get it working for my GetById Functionality.
What I mean with not working is that always the full SQL statement gets fired into the database and not only the requested fields are selected.
Maybe I understood it wrong and Projections are only useable for a list of Entities? or in this case IQueryables.
If this is only useable for Lists / IQueryables, what would be a way to implement it for filtered Entities (like if I would want a Author by ID or name, etc.)
CallHierarchy: AuthorQuery -> AuthorService -> Repository
AuthorQuery:
[ExtendObjectType(typeof(Query))]
public class AuthorQuery {
[UseProjection]
public async Task<IQueryable<Author>> Authors([Service] IAuthorService authorService) {
return await authorService.GetAsync();
}
[UseProjection]
public async Task<Author> AuthorById([Service] IAuthorService authorService, int id) {
var result = await authorService.GetAsync(author => author.Id == id);
return result.Single();
}
}
AuthorService (at this Point just the base service cause AuthorService calls the parent method):
public class BaseService<TEntity> : IBaseService<TEntity> where TEntity : BaseEntity {
protected readonly IRepository<TEntity> repository;
public BaseService(IRepository<TEntity> repository) {
this.repository = repository;
}
public virtual async Task<IQueryable<TEntity>> GetAsync(Expression<Func<TEntity, bool>> filter = null, params Expression<Func<TEntity, object>>[] includes) {
return await repository.GetAsync(filter, includes);
}
public virtual async Task<TEntity> GetFirstAsync(Expression<Func<TEntity, bool>> filter = null, params Expression<Func<TEntity, object>>[] includes) {
return await repository.GetFirstAsync(filter, includes);
}
}
public class Repository<TEntity> : IRepository<TEntity> where TEntity : BaseEntity {
protected readonly LibraryContext context;
public Repository(IDbContextFactory<LibraryContext> contextFactory) {
this.context = contextFactory.CreateDbContext();
}
public async Task<IQueryable<TEntity>> GetAsync(Expression<Func<TEntity, bool>> filter = null, params Expression<Func<TEntity, object>>[] includes) {
IQueryable<TEntity> query = context.Set<TEntity>();
foreach (var include in includes) {
query = query.Include(include);
}
if (filter != null) {
query = query.Where(filter);
}
return query.AsQueryable();
}
public async Task<TEntity> GetFirstAsync(Expression<Func<TEntity, bool>> filter = null, params Expression<Func<TEntity, object>>[] includes) {
IQueryable<TEntity> query = context.Set<TEntity>();
foreach (var include in includes) {
query = query.Include(include);
}
if (filter != null) {
query = query.Where(filter);
}
return await query.AsQueryable().FirstOrDefaultAsync();
}
}
AuthorType Definition:
public class AuthorType: ObjectType<Author> { }
Program.cs --> only the Definition of Services and GraphQL specific stuff
builder.Services.AddTransient(typeof(IRepository<>), typeof(Repository<>));
builder.Services.AddTransient(typeof(IBookRepository), typeof(BookRepository));
builder.Services.AddTransient(typeof(IAuthorRepository), typeof(AuthorRepository));
builder.Services.AddTransient(typeof(IBaseService<>), typeof(BaseService<>));
builder.Services.AddTransient<IBookService, BookService>();
builder.Services.AddTransient<IAuthorService, AuthorService>();
builder.Services
.AddGraphQLServer()
.AddProjections()
.AddQueryType<Query>()
.AddTypeExtension<BookQuery>()
.AddTypeExtension<AuthorQuery>()
.AddMutationType<Mutation>()
.AddTypeExtension<BookMutation>()
.AddTypeExtension<AuthorMutation>()
.AddType<BookType>()
.AddType<AuthorType>()
.AddType<BookCreate>()
.AddType<BookUpdate>()
.AddType<AuthorCreate>()
.AddType<AuthorUpdate>();
This are the generated sql statements for the following requested fields:
{
id
firstName
books {
id
title
}
}
Update: (generated sql querys)
ByAuthorId:
LastName is in Query
SELECT [a].[Id], [a].[FirstName], [a].[LastName], [t].[AuthorsId], [t].[BooksId], [t].[Id], [t].[Title]
FROM [Authors] AS [a]
LEFT JOIN (
SELECT [a0].[AuthorsId], [a0].[BooksId], [b].[Id], [b].[Title]
FROM [AuthorBook] AS [a0]
INNER JOIN [Books] AS [b] ON [a0].[BooksId] = [b].[Id]
) AS [t] ON [a].[Id] = [t].[AuthorsId]
WHERE [a].[Id] = @__id_0
ORDER BY [a].[Id], [t].[AuthorsId], [t].[BooksId]
LastName has been ignored GetAll:
SELECT [a].[Id], [a].[FirstName], [t].[Id], [t].[Title], [t].[AuthorsId], [t].[BooksId]
FROM [Authors] AS [a]
LEFT JOIN (
SELECT [b].[Id], [b].[Title], [a0].[AuthorsId], [a0].[BooksId]
FROM [AuthorBook] AS [a0]
INNER JOIN [Books] AS [b] ON [a0].[BooksId] = [b].[Id]
) AS [t] ON [a].[Id] = [t].[AuthorsId]
ORDER BY [a].[Id], [t].[AuthorsId], [t].[BooksId]
Update (2) Code from BaseService, AuthorService and AuthorRepository
public class BaseService<TEntity> : IBaseService<TEntity> where TEntity : BaseEntity {
protected readonly IRepository<TEntity> repository;
public BaseService(IRepository<TEntity> repository) {
this.repository = repository;
}
public virtual async Task<IQueryable<TEntity>> GetAsync(Expression<Func<TEntity, bool>> filter = null, params Expression<Func<TEntity, object>>[] includes) {
return await repository.GetAsync(filter, includes);
}
public virtual async Task<TEntity> GetFirstAsync(Expression<Func<TEntity, bool>> filter = null, params Expression<Func<TEntity, object>>[] includes) {
return await repository.GetFirstAsync(filter, includes);
}
public virtual Task<TEntity> AddAsync(TEntity entity) {
return repository.AddAsync(entity);
}
public virtual async Task<TEntity> UpdateAsync(TEntity entity) {
return await repository.UpdateAsync(entity);
}
public virtual async Task<bool> ExistsAsync(int id) {
return await repository.ExistsAsync(id);
}
public virtual async Task RemoveAsync(TEntity entity) {
await repository.RemoveAsync(entity);
}
}
public class AuthorService : BaseService<Author>, IAuthorService {
public AuthorService(IAuthorRepository repository) : base(repository) {
}
}
AuthorRepository:
public class AuthorRepository : Repository<Author>, IAuthorRepository {
public AuthorRepository(IDbContextFactory<LibraryContext> contextFactory) : base(contextFactory) { }
public override async Task<Author> AddAsync(Author author) {
author.Books = await context.Books.Where(book => author.Books.Select(x => x.Id).ToList().Contains(book.Id)).ToListAsync();
return await base.AddAsync(author);
}
public override async Task<Author> UpdateAsync(Author author) {
var authorToUpdate = await GetFirstAsync(a => a.Id == author.Id, a => a.Books);
if (authorToUpdate == null) {
throw new ArgumentNullException(nameof(authorToUpdate));
}
authorToUpdate.FirstName = author.FirstName;
authorToUpdate.LastName = author.LastName;
if (author.Books.Count != authorToUpdate.Books.Count || !authorToUpdate.Books.All(author.Books.Contains)) {
authorToUpdate.Books.UpdateManyToMany(author.Books, b => b.Id);
authorToUpdate.Books = await context.Books.Where(book => author.Books.Select(a => a.Id).ToList().Contains(book.Id)).ToListAsync();
}
return await base.UpdateAsync(authorToUpdate);
}
}
please notice that I updated the AuthorById Function of AuthorQuery like the following, like it was suggested
[UseProjection]
[UseSingleOrDefault]
public async Task<IQueryable<Author>> AuthorById([Service] IAuthorService authorService, int id) {
return await authorService.GetAsync(author => author.Id == id, author => author.Books);
}
CodePudding user response:
Did you try this?
[ExtendObjectType(typeof(Query))]
public class AuthorQuery {
[UseProjection]
public async Task<IQueryable<Author>> Authors([Service] IAuthorService authorService) {
return await authorService.GetAsync();
}
[UseSingleOrDefault]
[UseProjection]
public async Task<IQueryable<Author>> AuthorById([Service] IAuthorService authorService, int id) {
return await authorService.GetAsync(author => author.Id == id);
}
}