Home > Back-end >  Generic method to retrieve an entity by Id with EF Core
Generic method to retrieve an entity by Id with EF Core

Time:04-10

I create an API in which I have several objects in the database (e.g. seller, product, etc.) and for each of these objects I provide an enpoint api to get a given object from the database using its Id (always of the Guid type). I noticed that in each class where I handle business logic, I repeat the same code:

//GetOneSellerQueryHandler class
...
var sellerDbItem = await Context.Seller.Where(x => x.Id == request.Id)
                .SingleOrDefaultAsync();
var seller = Mapper.Map<SellerDto>(sellerDbItem );
...

//GetOneProductQueryHandler class
...
var productDbItem = await Context.Product.Where(x => x.Id == request.Id)
                .SingleOrDefaultAsync();
var product = Mapper.Map<ProductDto>(productDbItem);
...

Does it make sense create abstract class with generic method to get element from db by passed id?

Moreover, is this implementation of the generic method correct?

//Abstract class
public async Task<F> GetElementById<T,F>(T obj, Guid id) where T : class
{
   T dbItem = (T) Convert.ChangeType(obj switch
   {
      Seller => await Context.Seller.Where(x => x.Id == id).SingleAsync(),
      Product=> await Context.Product.Where(x => x.Id == id).SingleAsync(),
      _=>throw new NotImplementedException()
   }, typeof(T));

   return Mapper.Map<F>(dbItem);
}


//GetOneProductQueryHandler class
...
var productDbItem = await GetElementById<Product, ProductDto>(new Product(), request.Id)
...
//GetOneSellerQueryHandler class
...
var sellerDbItem = await GetElementById<Seller, SellerDto>(new Seller(), request.Id)
...

CodePudding user response:

Does it make sense create abstract class with generic method to get element from db by passed id?

My advice : Don't be afraid to repeat yourself.

Generic reduce the line count... but it can hurt the maintainability.

Moreover, is this implementation of the generic method correct?

Not totally, because types are hard coded. Moreover the code that filter Context.XXX.Where(x => x.Id == id).SingleAsync() is duplicated. This combine all defaults, more line and less maintainable.

EF Core use Queryable, then you can generate a generic expression like :

public static class QueryableExtensions
{
    public static Task<T>? GetById<T>(this IQueryable<T> query, Guid id)
    {
        // This expression is lambad : e => e.Id == id
        var parameter = Expression.Parameter(typeof(T));
        var left = Expression.Property(parameter, "Id");
        var right = Expression.Constant(id);
        var equal = Expression.Equal(left, right);
        var byId = Expression.Lambda<Func<T, bool>>(equal, parameter);
        
        return query.SingleAsync(byId);
    }
}

//Use
var seller = await context.Sellers.GetById(Guid.NewGuid());
var product = await context.Products.GetById(Guid.NewGuid());

Expression is powerful, but hard.

CodePudding user response:

Usually I declare base class/interface for database entities to access shared properties and create generic method based on them

using AutoMapper;
using Microsoft.EntityFrameworkCore;

public interface IEntity
{
    public Guid Id { get; set; }
}

public class Seller : IEntity
{
    public Guid Id { get; set; }
}

public class Product : IEntity
{
    public Guid Id { get; set; }
}

public class ApplicationDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    public DbSet<Seller> Sellers { get; set; }
}

public class Repository
{
    private readonly ApplicationDbContext ctx;

    public Repository(ApplicationDbContext ctx)
    {
        this.ctx = ctx;
    }

    public Task<T> GetById<T>(Guid id) where T : class, IEntity
    {
        return ctx.Set<T>().SingleAsync<T>(x => x.Id == id);
    }
}

public class Handler<Entity, Dto> where Entity : class, IEntity
{
    private readonly Repository repository;
    private readonly IMapper mapper;

    public Handler(Repository repository, IMapper mapper)
    {
        this.repository = repository;
        this.mapper = mapper;
    }

    public async Task<Dto> GetDtoById(Guid id)
    {
        var entity = await repository.GetById<Entity>(id);

        var dto = mapper.Map<Dto>(entity);

        return dto;
    }
}
  • Related