Introduction
In modern software development, maintaining a clean and scalable architecture is crucial. One of the most effective ways to achieve this in C# applications using Entity Framework Core is through the implementation of generic repositories. A generic repository provides a reusable and flexible way to handle data access logic while keeping the codebase maintainable.
This blog will explore the concept of generic repositories in C# and demonstrate an implementation using IRepository and Repository.
What is a Generic Repository?
A generic repository is a design pattern that abstracts database operations into a single class, making it reusable across different entity types. This approach follows the DRY (Don't Repeat Yourself) principle and ensures consistency in data access logic.
A generic repository typically supports CRUD (Create, Read, Update, Delete) operations and allows querying of entities while minimizing boilerplate code.
Implementing a Generic Repository in C#
1. Defining the IRepository Interface
The IRepository interface defines the contract that all repositories must follow. This interface ensures that every entity repository provides fundamental data access operations.
public interface IRepository<TEntity> where TEntity : BaseEntity, new()
{
public DbSet<TEntity> Table { get; }
public Task<TEntity?> GetById(Guid id, params string[] includes);
public IQueryable<TEntity> GetAll(params string[] includes);
public IQueryable<TEntity> FindAll(Expression<Func<TEntity, bool>> expression = null, params string[] includes);
public Task<TEntity> Create(TEntity entity);
public void Update(TEntity entity);
public void Delete(TEntity entity);
public Task<int> SaveChangesAsync();
public Task<bool> IsExist(Expression<Func<TEntity, bool>> expression);
}
Explanation:
GetById: Retrieves an entity by its ID, optionally including related entities.
GetAll: Returns all entities from the database.
FindAll: Fetches entities based on a filter expression.
Create: Adds a new entity to the database.
Update: Updates an existing entity.
Delete: Removes an entity from the database.
SaveChangesAsync: Saves all changes asynchronously.
IsExist: Checks if an entity exists based on a given condition.
2. Implementing the Repository Class
The Repository class implements IRepository and provides actual database interaction using Entity Framework Core.
public class Repository : IRepository where TEntity : BaseEntity, new()
{
private readonly AppDbContext _context;
public Repository(AppDbContext context)
{
_context = context;
}
public DbSet<TEntity> Table => _context.Set<TEntity>();
public async Task<TEntity> Create(TEntity entity)
{
await Table.AddAsync(entity);
return entity;
}
public void Delete(TEntity entity)
{
Table.Remove(entity);
}
public IQueryable<TEntity> FindAll(Expression<Func<TEntity, bool>> expression = null, params string[] includes)
{
IQueryable<TEntity> query = Table.AsNoTracking();
foreach (var include in includes)
{
if (!string.IsNullOrWhiteSpace(include))
{
query = query.Include(include);
}
}
if (expression != null)
{
query = query.Where(expression);
}
return query;
}
public IQueryable<TEntity> GetAll(params string[] includes)
{
IQueryable<TEntity> query = Table.AsNoTracking();
foreach (var include in includes)
{
query = query.Include(include);
}
return query;
}
public async Task<TEntity?> GetById(Guid id, string[] includes)
{
IQueryable<TEntity> query = Table.AsNoTracking();
foreach (var include in includes)
{
query = query.Include(include);
}
return await query.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
}
public async Task<bool> IsExist(Expression<Func<TEntity, bool>> expression)
{
return await Table.AnyAsync(expression);
}
public async Task<int> SaveChangesAsync()
{
return await _context.SaveChangesAsync();
}
public void Update(TEntity entity)
{
Table.Update(entity);
}
}
Explanation:
Uses DbSet to interact with the database table corresponding to TEntity.
AsNoTracking() ensures that retrieved entities are not tracked by EF Core, improving performance for read operations.
Includes support navigation properties, allowing eager loading of related entities.
Uses async methods for database interactions to optimize performance in web applications.
Advantages of Using a Generic Repository
Code Reusability: Eliminates repetitive CRUD logic for different entities.
Scalability: Makes it easier to extend functionalities as the application grows.
Consistency: Ensures a uniform data access approach across the codebase.
Separation of Concerns: Decouples business logic from data access logic.
Testability: Facilitates unit testing by allowing dependency injection of repositories.
In case if you have some specific methods, you can create a new repository:
public class PositionRepository : Repository<Position>, IPositionRepository
{
public PositionRepository(AppDbContext context) : base(context)
{
}
}
Conclusion
A generic repository pattern is a powerful tool in C# development with Entity Framework Core. It streamlines data access, improves maintainability, and ensures consistency across different parts of an application.
By implementing IRepository and Repository, we can manage database operations efficiently while adhering to clean architecture principles.
Do you use a generic repository in your projects? Let me know in the comments!
Here's the full example of my application which you can download from this link:
My Project
Here's the example of Program.cs from my project:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext(options => options.UseSqlServer(
builder.Configuration.GetConnectionString("Default")
));
builder.Services.AddDALServices();
builder.Services.AddBusinessServices();
builder.Services.AddIdentity()
.AddEntityFrameworkStores()
.AddDefaultTokenProviders();
builder.Services.AddAuthentication()
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.ExpireTimeSpan = TimeSpan.FromDays(7);
options.AccessDeniedPath = "/Account/AccessDenied";
});
builder.Services.Configure(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredLength = 6;
options.User.RequireUniqueEmail = true;
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Dashboard}/{action=Index}/{id?}"
);
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
What is the best practice of file upload in my case for .NET Web API?
Top comments (5)
Buisness layer registration:
public static class BusinessServiceRegistrations
{
public static void AddBusinessServices(this IServiceCollection services)
{
services.AddAutoMapper(typeof(BusinessServiceRegistrations));
services.AddScoped<IAuthService, AuthService>();
services.AddScoped<IPositionService, PositionService>();
services.AddScoped<IEmployeeService, EmployeeService>();
}
}
Registration in DAL
`public static class DALServiceRegistrations
{
public static void AddDALServices(this IServiceCollection services)
{
services.AddScoped();
services.AddScoped();
My default Program.cs code for MVC:
`var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext(options => options.UseSqlServer(
builder.Configuration.GetConnectionString("Default")
));
builder.Services.AddDALServices();
builder.Services.AddBusinessServices();
builder.Services.AddIdentity()
.AddEntityFrameworkStores()
.AddDefaultTokenProviders();
builder.Services.AddAuthentication()
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.ExpireTimeSpan = TimeSpan.FromDays(7);
options.AccessDeniedPath = "/Account/AccessDenied";
});
builder.Services.Configure(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredLength = 6;
options.User.RequireUniqueEmail = true;
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Dashboard}/{action=Index}/{id?}"
);
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();`
File upload:
public static class FileExtension
{
public static string? CreateFile(
this IFormFile file,
string path
)
{
if (file.Length > 10 * 1024 * 1024) // 10 MB
{
return null;
}
}