This short post (or rather code block) is just a slightly adapted generic design pattern from MSDN without Unit of Work pattern implementation. I've adapted it because of the requirement for asynchronous APIs for a single Context. I post it primarily to me personally for later use - to find it quickly, but if you find it useful too, I'm glad. If you have any suggestions for further improvements, please comment.
Here is the code snippet:
class Repository<TEntity> : IDisposable where TEntity : class
{
private bool isDisposed;
private ILogger<Repository<TEntity>> Logger { get; }
private ApplicationDbContext Context { get; }
private DbSet<TEntity> DbSet { get; }
public Repository(
ILogger<Repository<TEntity>> logger,
ApplicationDbContext context)
{
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
Context = context ?? throw new ArgumentNullException(nameof(context));
DbSet = Context.Set<TEntity>();
}
public virtual Task<List<TEntity>> GetAsync(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
string includeProperties = "")
{
IQueryable<TEntity> query = DbSet;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split(",", StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToListAsync();
}
else
{
return query.ToListAsync();
}
}
public virtual ValueTask<TEntity?> GetByIdAsync(object id, CancellationToken cancellationToken = default) =>
DbSet.FindAsync(id, cancellationToken);
public virtual ValueTask<EntityEntry<TEntity>> InsertAsync(TEntity entity, CancellationToken cancellationToken = default) =>
DbSet.AddAsync(entity, cancellationToken);
public virtual async Task DeleteAsync(object id, CancellationToken cancellationToken = default)
{
var entityToDelete = await DbSet.FindAsync(id, cancellationToken);
if (entityToDelete != null) Delete(entityToDelete);
}
public virtual void Delete(TEntity entityToDelete)
{
if (Context.Entry(entityToDelete).State == EntityState.Detached)
{
DbSet.Attach(entityToDelete);
}
DbSet.Remove(entityToDelete);
}
public virtual void Update(TEntity entityToUpdate)
{
DbSet.Attach(entityToUpdate);
Context.Entry(entityToUpdate).State = EntityState.Modified;
}
public virtual async Task SaveChangesAsync(CancellationToken cancellationToken = default) =>
await Context.SaveChangesAsync();
protected virtual void Dispose(bool disposing)
{
if (!isDisposed)
{
if (disposing)
{
Context.Dispose();
}
isDisposed = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
Top comments (3)
Yours is a very good generic repository, save for the fact that uses the annoying Entity Framework, which is of course, my very own personal opinion. Personally I dislike it (EF). I'd rather use Dapper. I too create generic repositories, but I do them using Model Features. See if you like it.
If you can extract a model feature out of your models, you can create base code for that feature, regardless of the feature, and most likely regardless of the ORM. Soft deletion, UPSERT, search by name and even apply RLS (Row-Level Security).
Thank you very much for your comment! This is very interesting. I do like your Model Features. There are some good aspects to consider.
Just curious, why do you dislike the Entity Framework, or why do you think Dapper is better? I'd really like to read your opinion.
Basically I find its Unit of Work implementation useless for REST. Here.