Creando CRUD para nuestra API de forma rápida

  • tips

Estos últimos días he estado trabajando con Visual Studio 2022 en Windows en un pet project y he descubierto una funcionalidad que me está ahorrando algo de tiempo a la hora de crear CRUD para mi API. Porque, admitámoslo, aunque nuestro proyecto sea muy complejo, siempre es necesario crear algún CRUD básico que nos quita algo de tiempo a pesar de ser una tarea sencilla. Es en este punto donde entra la funcionalidad que he descubierto, new scaffolded item…

Para comenzar a usar esta funcionalidad, el primer paso es crear nuestro modelo, a partir del cual crearemos la funcionalidad CRUD. Para este ejemplo, he creado un modelo sencillo para almacenar tareas.

public class TaskItem
{
    public int Id { get; set; }
    public DateTimeOffset CreationDate { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
}

Una vez creado el modelo, estamos en condiciones de empezar a utilizar el asistente de Visual Studio new scaffolded item… que aparece en el menú contextual al hacer clic derecho sobre la solución.

Una vez que se pulsa sobre el elemento, aparece un asistente que nos irá guiando por el proceso de creación del scaffolding. Aunque en este post me he centrado en crearlo para un API, el asistente nos ofrece más opciones que pueden ser interesantes, por ejemplo, componentes o páginas Razor.

Si nos centramos en las opciones para un API, vemos que podemos crear un controlador vacío, con acciones de lectura/escritura y con esas acciones usando entity framework. También tenemos opciones para crear estas acciones utilizando minimal API, por lo que no se crearía un controlador.

Para alcanzar el objetivo marcado en el título, crear un CRUD, las opciones que nos interesan son las que permiten crear las acciones utilizando entity framework. En el ejemplo sobre el que estoy trabajando, no utilizo minimal API, pero los pasos serían totalmente análogos.

Una vez que seleccionamos la opción que hemos comentado, pasamos al siguiente paso del asistente, en el que se nos pide que le facilitemos algo de información.

  • Model class: Modelo a partir del que crearemos nuestro CRUD. Para nuestro ejemplo, el TaskItem que hemos creado anteriormente.

  • DbContext: Contexto de entity framework en el que almacenaremos el modelo. Si en nuestro API todavía no hemos creado un contexto, el asistente nos guiará para crear uno, solicitándonos únicamente el nombre de éste, incluyendo el namespace.

  • Controller name: Nombre del controlador que se creará.

El asistente con todos los datos informados, quedaría de la forma que muestra la siguiente imagen.

Tras terminar el asistente, si analizamos nuestro código, vemos que se han creado dos nuevas clases. Una es el contexto, si no lo habíamos creado previamente.

public class CrudExampleContext : DbContext
{
    public CrudExampleContext (DbContextOptions<CrudExampleContext> options)
        : base(options)
    {
    }

    public DbSet<CrudExample.Models.TaskItem> TaskItem { get; set; } = default!;
}

Y la otra es el controlador, que expone las distintas operaciones para tener un CRUD, sobre el DbSet creado en el contexto.

[Route("api/[controller]")]
[ApiController]
public class TaskItemsController : ControllerBase
{
    private readonly CrudExampleContext _context;

    public TaskItemsController(CrudExampleContext context)
    {
        _context = context;
    }

    // GET: api/TaskItems
    [HttpGet]
    public async Task<ActionResult<IEnumerable<TaskItem>>> GetTaskItem()
    {
        return await _context.TaskItem.ToListAsync();
    }

    // GET: api/TaskItems/5
    [HttpGet("{id}")]
    public async Task<ActionResult<TaskItem>> GetTaskItem(int id)
    {
        var taskItem = await _context.TaskItem.FindAsync(id);

        if (taskItem == null)
        {
            return NotFound();
        }

        return taskItem;
    }

    // PUT: api/TaskItems/5
    // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
    [HttpPut("{id}")]
    public async Task<IActionResult> PutTaskItem(int id, TaskItem taskItem)
    {
        if (id != taskItem.Id)
        {
            return BadRequest();
        }

        _context.Entry(taskItem).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!TaskItemExists(id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return NoContent();
    }

    // POST: api/TaskItems
    // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
    [HttpPost]
    public async Task<ActionResult<TaskItem>> PostTaskItem(TaskItem taskItem)
    {
        _context.TaskItem.Add(taskItem);
        await _context.SaveChangesAsync();

        return CreatedAtAction("GetTaskItem", new { id = taskItem.Id }, taskItem);
    }

    // DELETE: api/TaskItems/5
    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteTaskItem(int id)
    {
        var taskItem = await _context.TaskItem.FindAsync(id);
        if (taskItem == null)
        {
            return NotFound();
        }

        _context.TaskItem.Remove(taskItem);
        await _context.SaveChangesAsync();

        return NoContent();
    }

    private bool TaskItemExists(int id)
    {
        return _context.TaskItem.Any(e => e.Id == id);
    }
}

Por último, hay que destacar que, si no teníamos un contexto creado y hemos delegado esta tarea en el asistente, tenemos que tener en cuenta que es necesario revisar la conexión que éste ha creado. Si abres tu appsettings.json podrás ver la cadena de conexión. Para nuestro ejemplo es la siguiente:

"ConnectionStrings": {
    "CrudExampleContext": "Server=(localdb)\\mssqllocaldb;Database=CrudExample.Data;Trusted_Connection=True;MultipleActiveResultSets=true"
}

Es una cadena de conexión para trabajar con SqlServer. No obstante, podemos cambiarla si lo deseamos. Y si queremos usar otro proveedor, tendríamos que cambiar la conexión aquí y revisar el Program para indicarle que utilice el proveedor que nos interese, ya que, como se ha comentado anteriormente, por defecto es SqlServer.

builder.Services.AddDbContext<CrudExampleContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("CrudExampleContext") ?? throw new InvalidOperationException("Connection string 'CrudExampleContext' not found.")));

Llegado a este punto, ya hemos creado un CRUD completo. Para verlo y probarlo, bastaría con ejecutar nuestra aplicación y echar un ojo a swagger, que ahora nos muestra las nuevas operaciones.

Foto de Douglas Lopes en Unsplash