Implementar OData con funciones de Azure v2

En esta entrega, aprenderemos cómo implementar un endpoint OData v4 con una función de azure v2 (utilizando un disparador HTTP), capaz de traducir una consulta OData como https://myapi.myapp.com/v1/products?$filter=Category eq 'Mountain Bicycle' en una consulta LINQ y aplicarla a un IQueryable.

De manera oficial, actualmente no existe soporte para OData sobre Azure Functions en un entorno serverless.

Se han realizado varias solicitudes a Microsoft para que implemente esta funcionalidad, pero hasta el momento no hay respuesta oficial:

Como las funciones de azure v2 están basadas en ASP.NET Core, podemos habilitar OData de manera similar a como lo haríamos en ASP.NET Core, pero haciendo un pequeño hack para hacer creer a la implementación de OData de Microsoft que ejecuta en un controlador de ASP.NET Core y no sobre una función de azure. Asumiremos que se ha creado un proyecto de Azure Functions v2 en Visual Studio y la función de ejemplo ejecuta correctamente.

Antes de hablar del hack, primeramente vamos a repasar cómo quedaría la función con soporte para OData:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static class FunctionODataExample
{
    [FunctionName("Function1")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "products")] HttpRequest req,
        ILogger log)
    {
        // Parte 1 - Inicializamos una lista de productos (Puede ser una tabla de SQL o CosmosDB a través de Entity Framework Core)
        var data = new List<Product>() {
            new Product() { Title = "Mountain Bike SERIOUS ROCKVILLE", Category = "Mountain Bicycle" },
            new Product() { Title = "Mountain Bike eléctrica HAIBIKE SDURO HARD SEVEN", Category = "Mountain Bicycle" },
            new Product() { Title = "Sillín BROOKS CAMBIUM C15 CARVED ALL WEATHER", Category = "Sillin" },
            new Product() { Title = "Poncho VAUDE COVERO II Amarillo", Category = "Chaquetas" },
        };

        // Parte 2 - Aplicamos la consulta OData al IQueryable<Product> a la fuente de datos anterior
        var result = req.ApplyTo<Product>(data.AsQueryable());

        // Parte 3 - Se retorna el resultado
        return new OkObjectResult(result);
    }
}

Como podéis apreciar, la función no tiene nada especial, simplemente un HttpTrigger para configurar la ruta /products.

Hemos actualizado el host.json con routePrefix: "" para deshacernos del molesto prefijo /api/, aunque este cambio no aporta nada a la solución.

La parte 1 (líneas 9 - 14), simplemente configura una lista de productos (objetos POCO) para utilizarla como fuente de datos. En la práctica, esto puede sustituirse por un IDbSet<T> de Entity Framework Core o cualquier fuente de datos que soporte consultas IQueryable<T>.

En la parte 2 (línea 17) es donde ocurre la magia. El método extensor HttpRequest.ApplyTo<T>(IQueryable<T> query) es quien se encarga de traducir la consulta OData que viene expresada en la URL y la aplica al IQueryable<T> que pasemos como parámetro.

La parte 3 simplemente realiza implícitamente el .ToList() (Ejecuta la consulta) y convierte el resultado a un JSON.

Si ejecutamos la siguiente consulta:

http://localhost:7071/products?$filter=Category eq 'Mountain Bicycle'

Veremos que el resultado es un JSON filtrado 😮:

Figura 1 - Resultado de la consulta OData $filter=Category eq 'Mountain Bicycle'

Se pueden incluso realizar consultas y proyecciones más complejas, y la implementación de OData, realizada por el equipo de ASP.NET Core de Microsoft, se encargará de incluso generar una consulta SQL eficiente, en caso de aplicar la consulta a un IDbSet<T> de Entity Framework:

http://localhost:7071/products?$filter=Category eq 'Mountain Bicycle'&$select=Title&$top=1

Figura 2 - Resultado de la consulta OData $filter=Category eq 'Mountain Bicycle'&$select=Title&$top=1

Una vez visto el funcionamiento, desvelemos la implementación del método extensor HttpRequest.ApplyTo<T>(IQueryable<T> query):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public static class Extensions {
    private static IServiceProvider _provider = null;
    private static RouteBuilder _routeBuilder = null;

    public static IQueryable ApplyTo<TEntity>(this HttpRequest request, IQueryable<TEntity> query)
        where TEntity : class
    {
        // Parte 1 - Se registran los componentes requeridos por la implementación de 
        // Microsoft ASP.NET Core OData y se memorizan en una variable estática
        if (_provider == null)
        {
            var collection = new ServiceCollection();
            collection.AddMvcCore();
            collection.AddOData();
            collection.AddTransient<ODataUriResolver>();
            collection.AddTransient<ODataQueryValidator>();
            collection.AddTransient<TopQueryValidator>();
            collection.AddTransient<FilterQueryValidator>();
            collection.AddTransient<SkipQueryValidator>();
            collection.AddTransient<OrderByQueryValidator>();
            _provider = collection.BuildServiceProvider();
        }

        // Parte 2 - Se configura la ruta de ASP.NET Core OData
        if (_routeBuilder == null)
        {
            _routeBuilder = new RouteBuilder(new ApplicationBuilder(_provider));
            _routeBuilder.EnableDependencyInjection();
        }

        // Parte 3 - Se simula un pedido HTTP como si viniese desde ASP.NET Core
        var modelBuilder = new ODataConventionModelBuilder(_provider);
        modelBuilder.AddEntityType(typeof(TEntity));
        var edmModel = modelBuilder.GetEdmModel();

        var httpContext = new DefaultHttpContext
        {
            RequestServices = _provider
        };
        HttpRequest req = new DefaultHttpRequest(httpContext)
        {
            Method = "GET",
            Host = request.Host,
            Path = request.Path,
            QueryString = request.QueryString
        };

        var oDataQueryContext = new ODataQueryContext(edmModel, typeof(TEntity), new Microsoft.AspNet.OData.Routing.ODataPath());
        var odataQuery = new ODataQueryOptions<TEntity>(oDataQueryContext, req);

        // Parte 4 - Se aplica la consulta OData al queryable que nos pasan por parámetro
        return odataQuery.ApplyTo(query.AsQueryable());
    }
}

Para que el código anterior funcione, deberemos añadir el paquete Nuget Microsoft.AspNetCore.OData a nuestra Function App (ya sea a través de la línea de comandos o a través del Visual Studio):

Install-Package Microsoft.AspNetCore.OData

El ejemplo completo se puede encontrar en: https://github.com/aletc1/examples-odata-azure-functions

Decimos que el método extensor es un hack, ya que simulamos un pedido HTTP como si proviniese desde ASP.NET Core para hacer funcionar la implementación de OData de Microsoft. Así que mientras Microsoft no facilite una implementación oficial, podéis utilizar ésta, que algo hace 😂.

Compartir con:

Alejandro Tamayo Castillo

Profesor, Investigador, Arquitecto de software, Consultor y entusiasta de la tecnología. Master of Science (MSc) in Computer Science actualmente trabajando con tecnología full-stack de Microsoft y otros fabricantes, como .NET Core, ASP.NET Core, Azure, Serverless, SPA/ReactJS, HTML5, Xamarin (Android/iOS/Forms), Orchard, SharePoint, HoloLens, Bots, y otros temas chulos.

Seguir a Alejandro Tamayo Castillo en LinkedIn