0

Implementando OData en .Net 6 #1

Muchos me han consultado que es OData y cómo podemos usarlo en .Net. OData es un camino de estandarizar REST con un protocolo abierto que permite tanto la creación, como también el consumo de APIs Restful de una manera sencilla.

OData nos ayuda a describir lo que necesitamos por medio del protocolo HTTP, usaremos los verbos para saber que necesitamos, como en REST, pero a su vez por medio de la url o por medio de lo que le enviamos en el body adaptar las consultas de datos o el almacenamiento según nuestra solicitud.

Desde OData tenemos las siguiente opciones:

  • $select, selecciona las columnas de una entidad que deseamos.
  • $orderby, ordenar los registros por una columna de forma ascendente o descendente.
  • $skip, cantidad de registros que queremos omitir, por ejemplo, si tenemos 1000 registros, pero necesitamos del 101 al 200 usaremos skip para evitar los 100 primeros.
  • $top, tomar una n cantidad de primeros, si tengo 1000 registros, tomará los primeros 100 por ejemplo.
  • $expand,  expande las entidad, similar a un inner join.
  • $filter, similar a la cláusula Where en el SQL, por ejemplo, podemos filtrar todos los registros que sean de algún valor, o por ejemplo entre fechas de creación.
  • $inlinecount, este lo usaremos mucho en paginaciones de registros, marca el total de registros obtenidos.

Para hacer la implementación usaremos Visual Studio 2022 con .Net 6. Lo primero que debemos hacer es crear un proyecto del tipo “ASP.Net Core WEB API”. Abrimos nuestro Visual Studio y seleccionamos la opción.

Presionamos aceptar y en la siguiente pantalla le daremos un nombre. Yo llamare a mi proyecto Demo.OData, ustedes pueden seleccionar el que deseen.

El siguiente paso es configurar nuestros proyectos, seleccionaremos las opciones que estan en la siguiente imagen:

Le damos lo siguiente y tendremos nuestro proyecto listo para comenzar. Para este demo, utilice la base de datos AdventureWorks. Esta es una base de datos modelo que Microsoft nos brinda en SQL Server para hacer algunas pruebas. Pueden descargarla desde haciendo clic aquí.

El siguiente paso es agregar las referencias necesarias desde los paquetes nuget. Las que necesitaremos son las siguiente:

El siguiente paso será construir nuestro modelo de datos. Vamos a utilizar Database First y vamos a crear nuestro por medio de Scaffolding de Entity Framework. Utilizaremos el siguiente comando desde la consola en la ruta del proyecto. No olvides completar tu connectionstring.

dotnet ef dbcontext scaffold "{connectstring}" Microsoft.EntityFrameworkCore.SqlServer -o Models

Este comando creará todas entidades necesarias y el DBContext dentro de la carpeta Models de nuestro proyecto. Lo siguiente será agregar el connection string en nuestro appsetting.json para que luego nuestro api pueda conectarse a la base de datos.

{
  "ConnectionStrings": {
    "{connectstring}"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Nosotros en este demo estaremos usando la entidad Product que encontrarás en la carpeta Models. Siguiente paso, debemos dejar disponible nuestro DBContext, AdventureWorlsDBContext, y para esto lo inyectamos. Debemos agregar las siguientes líneas en nuestro program.cs:

builder.Services.AddDbContext<AdventureWorksContext>(
        options =>
            options.UseSqlServer(builder.Configuration.GetConnectionString("Default"))
    );

Es el momento de preparar nuestros APIs. En la ventana de Explorador de soluciones, hacemos clic derecho sobre la carpeta Controllers y seleccionaremos la opción agregar -> controlador. Nos aparecerá la siguiente pantalla:

Seleccionaremos API, luego, controlado de API con acciones que usan Entity Framework. Esto nos hará seleccionar una entidad y un contexto para generar automáticamente todos los métodos.

Puedes limpiar todos los comentarios creados si lo deseas. Solo habrá un cambio, en lugar de Usar Put, usaremos el verbo Patch. Esto nos ayuda a actualizar solamente lo que realmente deseamos cambiar y no toda la entidad.

Modificaremos el primer Get, debe quedarte algo así:

[EnableQuery(PageSize = 15)]
public IQueryable<Product> Get()
{
    return _context.Products;
}

Fijate que tenemos una nueva decoración [EnableQuery]. Esta decoración pertenece al espacio de nombres Microsoft.AspNetCore.OData.Query que indica que tiene que activar OData para ese método. Estenos permitirá poder usar los parámetros que nombramos en el inicio del post como $select, $filter, $orderby, etc.

No olvides de hacer el using, igualmente, no te preocupes al final te dejaré el código completo del controller. Este tiene un parámetro llamado PageSize  que tiene el valor 15. Este le dirá que solamente devuelva los 15 primeros registros evitando traer todos los registros de una tabla.

Ahora veamos el siguiente get que nos permitirá devolver un registro por el ID o por la Primary Key:

 [EnableQuery]
        public SingleResult<Product> Get([FromODataUri]int key)
        {
            var product = _context.Products.Where(c => c.ProductId == key);
            return SingleResult.Create(product);
        }

En este caso vemos que retornaremos SingleResult<Product>. Esto permite devolver un estado parcial. Por ejemplo, si usamos $select=Name, no devolverá todas las propiedades de la entidad, solamente Name reduciendo el tamaño de lo que enviamos por HTTP.

El siguiente será guardar un registro, para eso usaremos el verbo POST:

 [EnableQuery]
        public async Task<ActionResult<Product>> PostProduct([FromBody] Product product)
        {
            _context.Products.Add(product);
            await _context.SaveChangesAsync();

            return Created(product);
        }

El siguiente es PATH. te preguntarás ¿Por qué no PUT?. Usaremos patch para poder actualizar solamente algo que cambie, si por ejemplo, solamente cambiamos la propiedad color solo se hará sobre esa propiedad.

[EnableQuery]
public async Task<IActionResult> PatchProduct([FromODataUri] int key, Delta<Product> product)
        {

            var actualProduct = await _context.Products.FindAsync(key);

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

            product.Patch(actualProduct);

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

            return Updated(actualProduct);
        }

Por último, borrar con el verbo DELETE:

[EnableQuery]
public async Task<IActionResult> DeleteProduct([FromODataUri] int key)
        {
            var product = await _context.Products.FindAsync(key);
            if (product == null)
            {
                return NotFound();
            }

            _context.Products.Remove(product);
            await _context.SaveChangesAsync();

            return NoContent();
        }

Como prometí, aquí tienes el controlador completo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Demo.OData.Models;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Formatter;
using Microsoft.AspNetCore.OData.Results;
using Microsoft.AspNetCore.OData.Deltas;

namespace Demo.OData.Controllers
{    
    public class ProductsController : ODataController
    {
        private readonly AdventureWorksContext _context;

        public ProductsController(AdventureWorksContext context)
        {
            _context = context;
        }
        
        [EnableQuery(PageSize = 15)]
        public IQueryable<Product> Get()
        {
            return _context.Products;
        }
        
        [EnableQuery]
        public SingleResult<Product> Get([FromODataUri]int key)
        {
            var product = _context.Products.Where(c => c.ProductId == key);
            return SingleResult.Create(product);
        }

        [EnableQuery]
        public async Task<ActionResult<Product>> PostProduct([FromBody] Product product)
        {
            _context.Products.Add(product);
            await _context.SaveChangesAsync();

            return Created(product);
        }

        [EnableQuery]
        public async Task<IActionResult> PatchProduct([FromODataUri] int key, Delta<Product> product)
        {

            var actualProduct = await _context.Products.FindAsync(key);

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

            product.Patch(actualProduct);

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

            return Updated(actualProduct);
        }
        
        [EnableQuery]
        public async Task<IActionResult> DeleteProduct([FromODataUri] int key)
        {
            var product = await _context.Products.FindAsync(key);
            if (product == null)
            {
                return NotFound();
            }

            _context.Products.Remove(product);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        private bool ProductExists(int key)
        {
            return _context.Products.Any(p => p.ProductId == key);
        }

    }
}

Conclusiones

El próximo post veremos todo el demo completo y realizaremos las pruebas para comprobar las funcionalidades implementadas en nuestras apis.

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *