I'm always excited to take on new projects and collaborate with innovative minds.
If you’ve been working with ASP.NET Core APIs, chances are you’ve written dozens of controllers—each doing pretty much the same thing: CRUD (Create, Read, Update, Delete) operations for different entities.
Let’s be honest—this gets repetitive fast.
Wouldn’t it be better if there was a smarter way to handle all entities using just one generic controller?
Well, you can! Let’s walk through this clean and efficient approach.
Instead of creating separate controllers for each entity, we’ll use generics and a bit of clever abstraction to build a universal controller.
The benefits?
To make this work, we need a common ground among all our entities. A simple interface can do the trick:
public interface IEntity
{
int Id { get; set; }
}
Now, all your entities (like Product, Customer, etc.) just need to implement this interface.
We’ll also use a generic repository pattern to abstract data access:
public interface IRepository<TEntity> where TEntity : class, IEntity
{
Task<IEnumerable<TEntity>> GetAllAsync();
Task<TEntity?> GetByIdAsync(int id);
Task<TEntity> AddAsync(TEntity entity);
Task UpdateAsync(TEntity entity);
Task DeleteAsync(int id);
}
This lets you plug in any entity without worrying about duplication.
Here’s where the magic happens.
[ApiController]
[Route("api/[controller]")]
public class GenericController<TEntity> : ControllerBase where TEntity : class, IEntity
{
private readonly IRepository<TEntity> _repository;
public GenericController(IRepository<TEntity> repository)
{
_repository = repository;
}
[HttpGet]
public async Task<IActionResult> Get() => Ok(await _repository.GetAllAsync());
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var entity = await _repository.GetByIdAsync(id);
return entity == null ? NotFound() : Ok(entity);
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] TEntity entity)
{
var result = await _repository.AddAsync(entity);
return CreatedAtAction(nameof(Get), new { id = result.Id }, result);
}
[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, [FromBody] TEntity entity)
{
if (id != entity.Id) return BadRequest();
await _repository.UpdateAsync(entity);
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
await _repository.DeleteAsync(id);
return NoContent();
}
}
This one controller can now handle any entity you throw at it.
For every new entity, you simply:
IEntityIRepository<TEntity> implementation in the DI containerThis approach works best for standard CRUD operations.
If your API needs heavy customization, business logic, or specific authorization per endpoint, a traditional per-entity controller might still be a better fit.
I’ve prepared a complete working example of this approach, including:
Fully working ASP.NET Core API
In-memory data storage
Swagger documentation
👉 View the GitHub Repo here
Writing generic controllers in ASP.NET Core isn’t just a “cool trick.” It’s a powerful way to embrace the DRY (Don’t Repeat Yourself) principle and keep your APIs clean and scalable.
If you’re tired of repetitive code, give this approach a shot—you’ll be amazed how much time and effort it can save.
👉 Have you tried this pattern in your projects? Share your experiences below!
Your email address will not be published. Required fields are marked *