Implementando OData en .Net #1

¿Qué es OData y cómo podemos integrarlo en .NET?. OData es una vía para estandarizar REST mediante un protocolo abierto que facilita tanto la creación como el consumo de APIs Restful de manera sencilla.

OData nos permite expresar nuestras necesidades a través del protocolo HTTP. Utilizamos los verbos para indicar lo que requerimos, similar a REST. Sin embargo, a su vez, podemos adaptar las consultas de datos o el almacenamiento mediante la URL o lo que enviamos en el cuerpo de la 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 nuestro 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 click 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 nuestra 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. Esto nos 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

En la próxima publicación, revisaremos el demo completo y llevaremos a cabo pruebas exhaustivas para verificar las funcionalidades implementadas en nuestras APIs.

0 0 votos
Valora la Publicación
Suscribirse
Notificación de
guest
0 Comentarios
Más votados
Nuevos Viejos
Feedback en línea
Ver todos los Comentarios

Comentarios Recientes

0
Nos encantaría conocer tu opinión: ¡comenta!x
Ir a la barra de herramientas