DEV Community

Cover image for Build a Generic CRUD API with ASP.NET Core
Guillermo Verón
Guillermo Verón

Posted on • Updated on • Originally published at guivern.hashnode.dev

Build a Generic CRUD API with ASP.NET Core

In the process of creating CRUD controllers, we can repeat a lot of code and go against the DRY principle. To avoid that, I'm going to show you how to build a generic base controller, then you will only have to create controllers that inherit from the base controller, and that's it. The controllers will already have the basic CRUD methods.

I saw a lot of articles about the Generic Repository Pattern. What I'm going to show you is not a Repository Pattern. EF Core is already based on the Unit of Work and Repository Pattern. Thus, we can use the DbContext class directly from our controllers, and that's what we're going to do. Anyway, you can find more information about using a custom repository vs using EF DbContext directly here. With that said, let's start to build our Generic CRUD API.

Base Model

At first, let's define the base model for our entities. For operations like edit and delete we need an Id property, and we can have some timestamp properties for audit as well. So EntityBase looks like this:

using System;

namespace GenericCrudApi.Models
{
    public class EntityBase
    {
        public long Id { get; set; }
        public DateTime CreationDate { get; set; }
        public DateTime? ModificationDate { get; set; }
    }
}

Enter fullscreen mode Exit fullscreen mode

Generic Base Controller

Now, we are ready to implement our generic controller. It will be an abstract class because it will be the base class for our controllers, and we'll mark the CRUD methods as virtual since we need to be able to override them in our inherited controllers if necessary. So CrudControllerBase looks like this:

using System;
using System.Threading.Tasks;
using GenericCrudApi.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace GenericCrudApi.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class CrudControllerBase<T>: ControllerBase where T: EntityBase
    {
        protected readonly DataContext _context;

        public CrudControllerBase(DataContext context)
        {
            _context = context;
        }

        [HttpGet]
        public virtual async Task<IActionResult> List()
        {
            var entities = await _context.Set<T>().ToListAsync();

            return Ok(entities);
        }

        [HttpGet("{id}")]
        public virtual async Task<IActionResult> Detail(long id)
        {
            var entity = await _context.Set<T>().FindAsync(id);

            if (entity == null)
                return NotFound();

            return Ok(entity);
        }

        [HttpPost]
        public virtual async Task<IActionResult> Create(T entity)
        {
            entity.CreationDate = DateTime.Now;
            await _context.Set<T>().AddAsync(entity);
            await _context.SaveChangesAsync();

            return CreatedAtAction("Detail", new { id = entity.Id }, entity);
        }

        [HttpPut("{id}")]
        public virtual async Task<IActionResult> Update(long id, T entity)
        {
            if (id != entity.Id)
                return BadRequest();

            if (!await EntityExists(id))
                return NotFound();

            entity.ModificationDate = DateTime.Now;
            _context.Entry(entity).State = EntityState.Modified;
            await _context.SaveChangesAsync();

            return NoContent();
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(long id)
        {
            var entity = await _context.Set<T>().FindAsync(id);

            if (entity == null)
                return NotFound();

            _context.Set<T>().Remove(entity);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        private Task<bool> EntityExists(long id)
        {
            return _context.Set<T>().AnyAsync(e => e.Id == id);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Note: _context.Set<T>()

Returns a DbSet instance for access to entities of the given type in the context and the underlying store.

Inheriting from the Generic Base Controller

It's time to see our base controller in action, for this we're going to create a TodoItem controller by extending the CrudControllerBase.

namespace GenericCrudApi.Models
{
    public class TodoItem: EntityBase
    {
        public string Name { get; set; }
        public bool IsComplete { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode
using GenericCrudApi.Models;
using Microsoft.AspNetCore.Mvc;

namespace GenericCrudApi.Controllers
{
    public class TodoItemsController: CrudControllerBase<TodoItem>
    {
        public TodoItemsController(DataContext context) 
        : base(context){}
    }
}
Enter fullscreen mode Exit fullscreen mode

And that’s it. Now we can test the HTTP methods with Postman:

Captura de pantalla de 2021-05-09 15-28-26.png

Alright, the API is working but we still have some work to do. For real-world applications, we should be careful with data access. For instance, for a User entity we should not return the password hash in the GET method, as well as we might need some validations in the POST and PUT methods. For this, we can add Automapper and inject it in the CrudControllerBase. Also, we might add soft delete support to the DELETE method. And finally, to make our code completely reusable, we need to add filtering, sorting and pagination features.

In short, the to-do list:

  • Add Automapper and inject it in the CrudControllerBase.
  • Add filtering, sorting and pagination features.
  • Add soft delete support

I'd like to write about it in futures articles. So, if you liked this article, please let me know in the comments and stay around. If you are interested in contributing, feel free to fork the project and send me a Pull Request.

  • View or download the sample code on GitHub.

Thanks for reading!

Top comments (0)