In large enterprise applications, your domain isn’t just a few entities — it’s hundredsVendorsProductsOrdersInvoicesCustomers, etc.

If you:

  • Create one repository per entity
  • Create one service per entity
  • Create one controller per entity

👉 You’ll drown in boilerplate code and your solution will become unmanageable!

🚀 The Problem

Imagine:

IVendorRepository → VendorRepository → VendorService → VendorController  
IProductRepository → ProductRepository → ProductService → ProductController
IOrderRepository → OrderRepository → OrderService → OrderController
...

➡ Result? Hundreds of repetitive files, copy-paste logic everywhere.

💡 The Solution: Go Generic!

✅ 1️⃣ Generic Repository

Define a base repository for all entities:

public interface IRepository<TEntity> where TEntity : class
{
Task<TEntity> GetByIdAsync(Guid id);
Task<IEnumerable<TEntity>> GetAllAsync();
Task AddAsync(TEntity entity);
Task UpdateAsync(TEntity entity);
Task DeleteAsync(Guid id);
Task SaveChangesAsync();
}
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
private readonly AppDbContext _context;
private readonly DbSet<TEntity> _dbSet;

public Repository(AppDbContext context)
{
_context = context;
_dbSet = context.Set<TEntity>();
}

public async Task AddAsync(TEntity entity)
{
await _dbSet.AddAsync(entity);
}

public async Task DeleteAsync(Guid id)
{
var entity = await _dbSet.FindAsync(id);
if (entity == null) throw new KeyNotFoundException();
_dbSet.Remove(entity);
}

public async Task<IEnumerable<TEntity>> GetAllAsync()
=> await _dbSet.ToListAsync();

public async Task<TEntity> GetByIdAsync(Guid id)
{
var entity = await _dbSet.FindAsync(id);
if (entity == null) throw new KeyNotFoundException();
return entity;
}

public async Task UpdateAsync(TEntity entity)
{
_dbSet.Update(entity);
}

public async Task SaveChangesAsync()
=> await _context.SaveChangesAsync();
}

👉 One repository, reused everywhere:


IRepository<Vendor>
IRepository<Product>
IRepository<Order>

✅ 2️⃣ Generic Service

public interface IService<TEntity> where TEntity : class
{
Task<TEntity> GetByIdAsync(Guid id);
Task<IEnumerable<TEntity>> GetAllAsync();
Task AddAsync(TEntity entity);
Task UpdateAsync(TEntity entity);
Task DeleteAsync(Guid id);
}


//Service Class
public class Service<TEntity> : IService<TEntity> where TEntity : class
{
private readonly IRepository<TEntity> _repository;

public Service(IRepository<TEntity> repository)
{
_repository = repository;
}

public async Task<TEntity> GetByIdAsync(Guid id)
=> await _repository.GetByIdAsync(id);

public async Task<IEnumerable<TEntity>> GetAllAsync()
=> await _repository.GetAllAsync();

public async Task AddAsync(TEntity entity)
{
await _repository.AddAsync(entity);
await _repository.SaveChangesAsync();
}

public async Task UpdateAsync(TEntity entity)
{
await _repository.UpdateAsync(entity);
await _repository.SaveChangesAsync();
}

public async Task DeleteAsync(Guid id)
{
await _repository.DeleteAsync(id);
await _repository.SaveChangesAsync();
}
}

✅ 3️⃣ Generic Controller

[ApiController]
[Route("api/[controller]")]
public class GenericController<TEntity> : ControllerBase where TEntity : class
{
private readonly IService<TEntity> _service;

public GenericController(IService<TEntity> service)
{
_service = service;
}

[HttpGet("{id}")]
public async Task<IActionResult> GetById(Guid id)
=> Ok(await _service.GetByIdAsync(id));

[HttpGet]
public async Task<IActionResult> GetAll()
=> Ok(await _service.GetAllAsync());

[HttpPost]
public async Task<IActionResult> Create([FromBody] TEntity entity)
{
await _service.AddAsync(entity);
return Ok(entity);
}

[HttpPut]
public async Task<IActionResult> Update([FromBody] TEntity entity)
{
await _service.UpdateAsync(entity);
return Ok();
}

[HttpDelete("{id}")]
public async Task<IActionResult> Delete(Guid id)
{
await _service.DeleteAsync(id);
return Ok();
}
}
👉 Custom logic? Just override:

// Customer Controller Also
public class VendorController : GenericController<Vendor>
{
public VendorController(IService<Vendor> service) : base(service) { }

[HttpGet("special")]
public IActionResult GetSpecial()
{
return Ok("Special Vendor Data");
}
}

✅ DI Registration

builder.Services.AddDbContext<AppDbContext>(…);
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
builder.Services.AddScoped(typeof(IService<>), typeof(Service<>));

builder.Services.AddDbContext<AppDbContext>(…);
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
builder.Services.AddScoped(typeof(IService<>), typeof(Service<>));

⚡ Benefits

  • 📉 Less boilerplate — One set of logic, reused
  • 🛠 Easier maintenance — Changes in one place apply everywhere
  • 🚀 Scales cleanly — Add entities without code explosion
  • 🧩 Customizable — Override for special cases

🌟 Final Thought

When your domain grows, your architecture should scale with it. By introducing generic patterns, you reduce redundancy, improve clarity, and set yourself up for long-term success.