DEV Community

GutsBerserker
GutsBerserker

Posted on

Generic Repositories in C#

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);
}
Enter fullscreen mode Exit fullscreen mode

}

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)

Collapse
 
tamerlan_rzayev_5c295b96d profile image
Tamerlan Rzayev

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>();
}
}

Collapse
 
tamerlan_rzayev_5c295b96d profile image
Tamerlan Rzayev

Registration in DAL
`public static class DALServiceRegistrations
{
public static void AddDALServices(this IServiceCollection services)
{
services.AddScoped();
services.AddScoped();

    }
}`
Enter fullscreen mode Exit fullscreen mode
Collapse
 
guts_002404a47707c9c88be6 profile image
GutsBerserker

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();`

Collapse
 
guts_002404a47707c9c88be6 profile image
GutsBerserker • Edited

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;
}

    if (file.ContentType != "image/jpeg" && file.ContentType != "image/png" && file.ContentType != "image/svg+xml")
    {
        return null;
    }

    if (!Directory.Exists(path))
    {
        Directory.CreateDirectory(path);
    }

    var fileExtension = Path.GetExtension(file.FileName); // .png .jpg .jpeg .svg

    var guid = Guid.NewGuid().ToString();

    var uniqueFileName = $"{guid}{fileExtension}";

    var filePath = Path.Combine(path, uniqueFileName);

    using var fileStream = new FileStream(filePath, FileMode.Create);
    file.CopyTo(fileStream);

    return uniqueFileName;
}
Enter fullscreen mode Exit fullscreen mode

}