Continuamos con la serie de posts relacionados con Asp.Net Core y OData. El gran Adrián Díaz hace poco escribió el siguiente artículo sobre Cómo beneficiar nuestro perfomance con Entity Framework.
Uno de los problemas que veo cuando empleamos OData es que exponemos en el controlador las entidades de nuestra base de datos…¿qué os parecería si os dijera que podemos definir nuestros modelos y poder filtrar con los mismos en nuestras consultas OData? Suena bien, ¿verdad? Vamos al lío 🙂
Lo primero que vamos a ver es la diferencia que veo entre un modelo o DTO y un entidad de base de datos.
En nuestro ejemplo vamos a definir una entidad en nuestra base de datos, dicha entidad la llamaremos Tarea.
namespace AspNetCoreODataWithModel.Shared.Models.Database { public abstract class BaseEntity { public int Id { get; set; } } } using AspNetCoreODataWithModel.Shared.Models.Database; using System; namespace AspNetCoreODataWithModel.Data.Entities { public class Tarea : BaseEntity { public string Nombre { get; set; } public string Observaciones { get; set; } public DateTime? Fecha{ get; set; } public bool Facturable { get; set; } } }
Definiremos nuestro modelo que será el que expongamos en nuestra Api y lo llamaramos TaskModel.
using System; namespace AspNetCoreODataWithModel.Model { public class TaskModel { public int Id { get; set; } public string Name { get; set; } public string Observations { get; set; } public DateTime? Date { get; set; } public bool Billable { get; set; } } }
Bien, ya tenemos nuestra entidad y nuestro modelo definidos. Como véis, el modelo posee atributos o propiedades con nombres en inglés y la entidad en español. Así reflejaremos mejor el comportamiento del filtrado OData usando propiedades en inglés 🙂
Os explicaré el truco… al final lo que hay que hacer es generar un nuevo filtro OData a partir del filtro OData de entrada con nuestro modelo, haciendo un mapeo de las propiedades.
Para ello pondré el código y lo analizaremos juntos, ¿ok?
[HttpGet("")] [ProducesResponseType(typeof(IEnumerable<TaskModel>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(BadRequestObjectResult), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(NotFoundObjectResult), StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)] public IActionResult Get(ODataQueryOptions<TaskModel> filter) { var tasks = repository.ListAll(); if (!string.IsNullOrWhiteSpace(filter.Filter?.RawValue)) { IEdmModel model = EdmModelHelper.GetEdmModel(); IEdmType type = model.FindDeclaredType("AspNetCoreODataWithModel.Data.Entities.Tarea"); IEdmNavigationSource source = model.FindDeclaredEntitySet("Tareas"); ODataQueryOptionParser parser = new ODataQueryOptionParser(model, type, source, new Dictionary<string, string> { { "$filter", filter.Filter?.RawValue } }); ODataQueryContext context = new ODataQueryContext(model, typeof(Tarea), filter.Context.Path); FilterQueryOption newfilter = new FilterQueryOption(filter.Filter?.RawValue, context, parser); tasks = newfilter.ApplyTo(tasks, new ODataQuerySettings()) as IQueryable<Tarea>; } var results = tasks.Select(p => mapper.Map<TaskMode>(p)); var page = new PageResult<TaskModel>(mapper.Map<IEnumerable&<TaskModel>>(results.ToList()), Request.HttpContext.ODataFeature().NextLink, Request.HttpContext.ODataFeature().TotalCount); return Ok(page); } ........................... public static IEdmModel GetEdmModel() { ODataModelBuilder builder = new ODataConventionModelBuilder().EnableLowerCamelCase(NameResolverOptions.ProcessReflectedPropertyNames | NameResolverOptions.ProcessExplicitPropertyNames); return GetEdmModel(builder); } private static IEdmModel GetEdmModel(ODataModelBuilder builder) { EntitySetConfiguration<Tarea> tasks = builder.EntitySet<Tarea>("Tareas"); builder.ContainerName = "DefaultContainer"; tasks.EntityType.Name = "Tarea"; tasks.EntityType.Namespace = "AspNetCoreODataWithModel.Data.Entities"; tasks.EntityType.Property(p => p.Id).Name = "Id"; tasks.EntityType.Property(p => p.Nombre).Name = "Name"; tasks.EntityType.Property(p => p.Fecha).Name = "Date"; tasks.EntityType.Property(p => p.Facturable).Name = "Billable"; tasks.EntityType.Property(p => p.Observaciones).Name = "Observations"; tasks.EntityType .Filter() .Count() .Expand() .OrderBy() .Page() .Select(); return builder.GetEdmModel(); }
En el código del controlador lo primero que observamos es que llamamos al repositorio al método ListAll() que lo único que hace es devolver un IQueryable.
Luego con el método EdmModelHelper.GetEdmModel(); lo que hacemos es establecer el modelo Edm que usa OData para la entidad Tarea, realizando un «mapeo» (estableciendo en su propiedad Name el nombre de la propiedad de nuestro modelo). Esto es muy importante para poder crear el nuevo filtro OData.
El siguiente bloque lo que hace es que, si se recibe un filtro OData desde el modelo de entrada, generamos un nuevo filtro con la entidad y aplicamos sobre el IQueryable inicial los filtros correctos.
Mola mucho, ¿eh?
Como regalito de cumpleaños (hoy 28 de julio es el mío jejejeje) os voy a proporcionar una clase para definir los parámetros OData y lo podáis usar con Swashbuckle y Swagger.
using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using System.Linq; namespace AspNetCoreODataWithModel.Infrastructure.Swagger { public class ODataQueryOptionsFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { var queryAttribute = context.MethodInfo.GetCustomAttributes(true) .Union(context.MethodInfo.DeclaringType.GetCustomAttributes(true)) .OfType<EnableQueryAttribute>().FirstOrDefault(); if (queryAttribute != null) { operation.Parameters?.RemoveAt(0); if (queryAttribute.AllowedQueryOptions.HasFlag(AllowedQueryOptions.Select)) { operation.Parameters.Add(new OpenApiParameter { Name = "$select", In = ParameterLocation.Query, Description = "Selects which properties to include in the response.", Schema = new OpenApiSchema { Type = "string" } }); } if (queryAttribute.AllowedQueryOptions.HasFlag(AllowedQueryOptions.Expand)) { operation.Parameters.Add(new OpenApiParameter { Name = "$expand", In = ParameterLocation.Query, Description = "Expands related entities inline.", Schema = new OpenApiSchema { Type = "string" } }); } // Additional OData query options are available for collections of entities only if (queryAttribute.AllowedQueryOptions.HasFlag(AllowedQueryOptions.Filter)) { operation.Parameters.Add(new OpenApiParameter { Name = "$filter", In = ParameterLocation.Query, Description = "Filters the results, based on a Boolean condition.", Schema = new OpenApiSchema { Type = "string" } }); } if (queryAttribute.AllowedQueryOptions.HasFlag(AllowedQueryOptions.OrderBy)) { operation.Parameters.Add(new OpenApiParameter { Name = "$orderby", In = ParameterLocation.Query, Description = "Determines what values are used to order a collection of results.", Schema = new OpenApiSchema { Type = "string" } }); } if (queryAttribute.AllowedQueryOptions.HasFlag(AllowedQueryOptions.Top)) { operation.Parameters.Add(new OpenApiParameter { Name = "$top", In = ParameterLocation.Query, Description = "The max number of results.", Schema = new OpenApiSchema { Type = "string" } }); } if (queryAttribute.AllowedQueryOptions.HasFlag(AllowedQueryOptions.Skip)) { operation.Parameters.Add(new OpenApiParameter { Name = "$skip", In = ParameterLocation.Query, Description = "The number of results to skip.", Schema = new OpenApiSchema { Type = "string" } }); } if (queryAttribute.AllowedQueryOptions.HasFlag(AllowedQueryOptions.Count)) { operation.Parameters.Add(new OpenApiParameter { Name = "$count", In = ParameterLocation.Query, Description = "Returns count of results.", Schema = new OpenApiSchema { Type = "string" } }); } } } } } ....... public static IServiceCollection AddSwagger(this IServiceCollection services) { services.AddSwaggerGen(options => { options.SwaggerDoc(ApiConstants.Swagger.ApiVersion, new OpenApiInfo { Version = ApiConstants.Swagger.ApiVersion, Title = ApiConstants.Swagger.ApiName, Description = ApiConstants.Swagger.ApiName }); options.EnableAnnotations(); options.IgnoreObsoleteProperties(); options.IgnoreObsoleteActions(); options.DescribeAllParametersInCamelCase(); options.EnableAnnotations(); options.OperationFilter<ODataQueryOptionsFilter>(); // Aquí nuestro filtro OData // Set the comments path for the Swagger JSON and UI. var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(System.AppContext.BaseDirectory, xmlFile); options.IncludeXmlComments(xmlPath); }); services.AddSwaggerGen(); return services; }
Para que veáis cómo queda en Swagger
Realizaremos una consulta sencilla para probar que todo funciona de lujo. Establecermos un filtro en nuestro Swagger, como por ejemplo en el campo
$filter ponemos billable eq true
y obtenemos la siguiente salida:
{ "id": 1, "name": "Tarea 1", "observations": "Sin observaciones", "date": "2020-07-25T10:11:27.7125366+02:00", "billable": true }, { "id": 3, "name": "Tarea 3", "observations": "Esto es una prueba de observaciones", "date": "2020-07-21T10:11:27.7127657+02:00", "billable": true } ]
Si establecemos el campo $filter a billable eq false
{ "id": 2, "name": "Tarea 2", "observations": "Sin observaciones", "date": "2020-07-28T10:11:27.7127637+02:00", "billable": false } ]
¿Quieres sólo devolver el identificador y el nombre de las tareas?
En el campo $select estableceremos id, name
{ "id": 1, "name": "Tarea 1" }, { "id": 2, "name": "Tarea 2" }, { "id": 3, "name": "Tarea 3" } ]
Hemos visto cómo usar nuestros modelos en los controladores y poder realizar los filtros OData sobre entidades de base de datos sin exponerlas al exterior.
El código podéis descargarlo en Github https://github.com/Encamina/Blogs/tree/master/Desarrolla%20en%20colores/AspNetCoreODataWithModel
Happy coding!!!!
Este sitio web utiliza cookies para que tengas la mejor experiencia de usuario. Si continuas navegando, estás dando tu consentimiento para aceptar las cookies y también nuestra política de cookies (esperemos que no te empaches con tanta cookie 😊)