Blog dedicado a la programación .NET y la informática en general

Archivo para junio, 2019

Seguridad de Web API – JWT (Parte II de IV)

En la anterior entrada hablamos de la seguridad de WebApi con ApiKey. Hoy nos adentramos en Jason Web Token.

  • Comprobación de ApiKey.
  • Autenticación con Jason Web Token (JWT).
  • Azure Active Directory mediante los secretos del registro de “Aplicaciones Empresariales”.
  • Autenticación y Autorización con “Identity Server”.

Breve descripción de JWT

De JWT tenemos bastante información en internet, la definición oficial en español en la Wikipedia es basante completa pero creo que Santi Macías lo hace mejor en su blog. Resumiéndolo, tal vez demasiado, JWT es un estandar JSON para transmitir la identidad de un usuario de forma confiable, es decir, firmada y verificada digitalmente. Además también se incluye los privilegios que demanda, o en inglés «claims», como por ejemplo que su rol es de administrador, entre otras cosas. El contenido está dividido en tres partes, la cabecera con el tipo de codificación y token, el «payload» con los datos de la identificación e información útil, y la firma para verificar que el token es válido.

Si lo incluyo en los artículos de seguridad de WebApi es porque hoy en día es un estandar muy utilizado por los servicios de autenticación. Mi intención no es crer un servicio de autenticación completo, tan sólo cómo generar el token y cómo utilizarlo en una WebApi

Base del Proyecto

El proyecto va a utilizar las librerías de OWIN… esto me suena ¿pero qué es? «Open Web INterface» es una especificación que define las relaciones entre los servidores web y los componentes de las aplicaciones, reduciendo las interacciones entre éstas a un conjunto pequeño de tipos. Viéndolo desde una perpectiva práctica, utilizaremos la estructura que OWIN nos ofrece para incluir la gestión de la seguridad de la aplicación.

Esto se realiza mediante la clase «Startup.cs», en proyectos en «.Net Framework» hay que incluir en la raiz del proyecto un nuevo fichero de tipo «Clase de inicio OWIN» que depende de las librerías de NuGet y las instalará automáticamente si no las tiene:

  • Owin
  • Microsoft.Owin

En «.Net Core» esta clase viene por defecto incluida.

Para instalar las librerías que nos permitan trabajar con JWT se utiliza la siguiente línea de comandos:

PM> Install-Package System.IdentityModel.Tokens.Jwt

Que a su vez instalará los siguientes paquetes:

  • Microsoft.IdentityModel.Logging
  • Microsoft.IdentityModel.Tokens
  • Microsoft.IdentityModel.JsonWebTokens
  • System.IdentityModel.Tokens.Jwt

Por último para poder integrar JWT en OWIN se intalará el siguiente paquete de NuGet:

PM> Install-Package Microsoft.Owin.Security.Jwt

Este paquete también instalará:

  • Microsoft.Owin.Security
  • Microsoft.Owin.Security.OAuth

Ahora ya tenemos la base del proyecto preparado para poder codificar la solución.

Contrucción del código para generar JWT

Lo primero es crear una clase que retenga los datos que acreditan al usuario al que se le otorga la autorización. Para ellos genero la clase «Credencial.cs»

public class Credencial
{
    public int IdUsuario { get; set; }
    public string Nombre { get; set; }
    public string CorreoElectronico { get; set; }
    public IEnumerable<string> Roles { get; set; }
}

Antes de ponernos a configurar la seguridad del aplicativo codificaremos una clase que nos proporcione los «tokens». En la generación del «token» se incluye la firma digital. Para su composición se va a utlizar una clave o llave de seguridad que se encripta mediante un algoritmo simético, en este caso SHA256, para obtener un identificador único llamado «hash» y que nos proporciona la clave pública que será enviada en el «token». En este caso se crea una clase que extiende al tipo string para poder trabajar con las textos que contengan las claves.

public static class ExtensionesSeguridad
{
    public static SigningCredentials FormarSigningCredentials(this string jwtSecret)
    {
        var claveSimetrica = jwtSecret.FormarClaveSeguridadSimetrica();
        var credencialesFirmadas = new SigningCredentials(claveSimetrica, SecurityAlgorithms.HmacSha256);

        return credencialesFirmadas;
    }

    public static SymmetricSecurityKey FormarClaveSeguridadSimetrica(this string jwtSecret)
    {
        return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret));
    }
}

Ahora se codifica la clase que proporciona los «tokens».

public class ProveedorJwt
{
    #region Métodos Públicos
    /// <summary>
    /// Genera un token de tipo JSW Bearer
    /// </summary>
    /// <param name="credencial">Credencial de autenticación</param>
    /// <param name="dominio">Dominio web donde se autentica</param>
    /// <param name="secreto">Clave secreta para generar el hash</param>
    /// <param name="caducidad">Tiempo que dura el token como activo</param>
    /// <returns></returns>
    public string GenerarToken(Credencial credencial, string dominio, string secreto, TimeSpan caducidad)
    {
        var jwtSeguro = new JwtSecurityToken
        (
            issuer: dominio,
            audience: dominio,
            claims: GenerarClaims(credencial),
            // Cuando más corto sea el tiempo de caducidad más aumentará la seguridad
            expires: DateTime.UtcNow.Add(caducidad),
            signingCredentials: secreto.FormarSigningCredentials()
        );
        var token = new JwtSecurityTokenHandler().WriteToken(jwtSeguro);

        return token;
    }
    #endregion

    #region Métodos Auxiliares
    /// <summary>
    /// Genera una colección de claims basados en la credencial pasada como parámetro
    /// </summary>
    /// <param name="credencial">Credencial contiene la información del usuario</param>
    /// <returns>Devuleve una colección de Claims para JWT</returns>
    private IEnumerable<Claim> GenerarClaims(Credencial credencial)
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, credencial.IdUsuario.ToString()),
            new Claim(ClaimTypes.Name, credencial.Nombre),
            // Se pueden añadir más claims aquí
        };

        if (credencial.Roles != null)
        {
            foreach (var role in credencial.Roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role));
            }
        }

        return claims;
    }
    #endregion
}

Configuración de la seguridad en el proyecto

Ahora hay que aplicar JWT en la configuración del proyecto. Como anteriormente he dicho nos basaremos en OWIN o más concretamente en la clase Startup.

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions
        {
            TokenValidationParameters = new TokenValidationParameters
            {
                // AuthenticationType = "Bearer",
                // Deben coincidir el secreto y el dominio al generar el token para el cliente
                IssuerSigningKey = ConfigurationManager.AppSettings["SecretoJwt"].FormarClaveSeguridadSimetrica(),
                ValidIssuer = ConfigurationManager.AppSettings["DominioJwt"],
                ValidAudience = ConfigurationManager.AppSettings["DominioJwt"],
                ValidateLifetime = true,
                ClockSkew = TimeSpan.FromMinutes(0)
            }
        });
    }
}

Y ahora hay que «decorar» el controlador que se quiera asegurar.

[Authorize]
public class ValuesController : ApiController

Consumiendo el controlador segurizado

Haremos las pruebas de dos formas, primero con la aplicación PostMan y luego con código C# añadiendo un nuevo controlador.

Para generar el «token» de PostMan utilizaré la consola de C# Interactiva. Si no la tenéis a mano podéis encontrarla aquí:

Lo primero es haber compilado el proyecto y luego agregar la librería mediante el comando #r y la ruta del archivo dll. Dejo el ejemplo que he realizado en mi equipo con el «token» generado.

> #r "C:\GitHub\RublenX\Aprendiendo\PoCSeguridadWebApi\JwtNetFrk\bin\JwtNetFrk.dll"
> JwtNetFrk.Seguridad.Credencial credencial = new JwtNetFrk.Seguridad.Credencial { IdUsuario = 1, Nombre = "JuanRu", CorreoElectronico = "inventado@email.es", Roles = new string[] { "Aplicacion" } };
> JwtNetFrk.Seguridad.ProveedorJwt proveedor = new JwtNetFrk.Seguridad.ProveedorJwt();
> string token = proveedor.GenerarToken(credencial, "localhost:44342", "YWPgvOaLJNcubpz4duqCS7KRwgmCVRRLQ2sVRjPVI7ELOFcGHl2", TimeSpan.FromHours(1));
> token
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiSnVhblJ1IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQXBsaWNhY2lvbiIsImV4cCI6MTU2MDk0NjMxNSwiaXNzIjoibG9jYWxob3N0OjQ0MzQyIiwiYXVkIjoibG9jYWxob3N0OjQ0MzQyIn0.jHPhvm__IfufYwlSYuK70LvuUSj7trNMS4uy6swv-q4"
> 

Se configura PostMan con el «token» formado en Visual Studio de la siguiente manera:

Al lanzarlo podremos comprobar que devuelve los valores esperados.

Ahora probemos con C#. Se crea un nuevo controllador que solicitará por código el token, con una caducidad determinada, lo agregará al cliente y devolverá el resultado.

public class ConsumirJwtController : ApiController
{
    public async Task<IHttpActionResult> Get()
    {
        // Se obtiene el token
        ProveedorJwt jwTokenProv = new ProveedorJwt();
        string token = jwTokenProv.GenerarToken(
            new Credencial
            {
                IdUsuario = 1,
                Nombre = "nombre usuario",
                CorreoElectronico = "anonimo@email.es",
                Roles = new List<string> { "apiAccess" }
            },
            ConfigurationManager.AppSettings["DominioJwt"],
            ConfigurationManager.AppSettings["SecretoJwt"],
            TimeSpan.FromHours(1));

        HttpClient client = new HttpClient();

        // Se incluye el token en el header de la petición
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        HttpResponseMessage respuesta = await client.GetAsync("https://localhost:44342/api/Values");

        if (!respuesta.IsSuccessStatusCode)
        {
            return BadRequest();
        }

        // Al estar redireccionándolo, sólo que con la autenticacón incluida, se utiliza ResponseMessage
        return ResponseMessage(respuesta);
    }
}

Conclusión

No me he dedicado a realiar una autenticación completa con gestión de usuarios, contraseñas y roles, tan sólo he mostrado cómo realizar un JWT, la configuración del proyecto para que pueda utilizarlo y cómo consumirlo.

Lo interesante de JWT es que no permite la modificación del mismo sin ser detectado y tiene una caducidad, aún así siempre hay que utilizar el protocolo seguro HTTPS para evitar ataques «Man-in-the-middle» .

Referencias
Openiddict : Using OpenIddict with JWT Bearer Authentication with a .NET Framework 4.6 Resource server
Santi Macias : Construyendo una Web API REST segura con JSON Web Token en .NET Parte I y Parte II
Jon Hilton : Secure your ASP.NET Core 2.0 API
Bit of Technology : ASP.NET Web API 2 external logins with Facebook and Google in AngularJS app

Entradas relacionadas :

Anuncio publicitario

Seguridad de Web API – APIKey (Parte I de IV)

En el cliente en el que estoy ahora mismo ha surgido una necesidad de propuesta para la securización asegurar los accesos a las plataformas web API.

Había implementado algo de seguridad en ASP.Net anteriormente, pero ahora con la plataforma .Net Core se amplían las posibilidades.

La intención es poder autorizar el acceso a las APIs de la forma más sencilla posible, sin tener que construir «arcos de iglesia», y para que nos lleve unas pocas horas. De lo que he encontrado me quedo con cuatro formas de hacerlo:

  • Comprobación de ApiKey.
  • Autenticación con Jason Web Token (JWT).
  • Azure Active Directory mediante los secretos del registro de «Aplicaciones Empresariales».
  • Autenticación y Autorización con «Identity Server».

Para cada uno de ellos iré utilizando proyectos en .Net Framework o .Net Core en la medida que yo considere oportuno.

Pero ¿porqué no utilizar la autenticación mediante ASP.NET Identity mediante el tipo «Forms»? pues porque para ello usa el transporte de cookies, las cuales se gestionan de forma automática mediante los exploradores de internet, pero en WebApi hay que gestionarlos de forma manual y se transportan tanto en las solicitudes como las respuestas.

¿Y la autenticación HTTP Básica mediante los filtros de autorización? no queremos que una clave de usuario y su contraseña personales viajen de un sitio a otro de forma permanente, y en los filtros de autorización hay que generar un atributo personalizado para decorar los controladores. Además hablamos de APIs que normalmente serán consumidas por aplicaciones y no por personas.

Descripción de la autentición con API Key

Hoy comenzamos con la que creo que es la más sencilla «API Key», consiste en guardar localmente en el proyecto una clave única para autorizar el acceso a los distintos servicios.

Pero antes un poco de teoría para comprender su funcionamiento. En este enlace podéis comprobar cual es el flujo de una petición WebApi. Para esta implementación nosotros nos centraremos en dos sitios, la autenticación en «Message Handlers» y la autorización en «Controller».

Existe otra capa de seguridad en «HttpModule», pero está fuertemente ligada a IIS, y prefiero no tener que depender del servidor web.

Usamos «Message Handler» (DelegatingHandler) que se ejecuta antes de los filtros de autorización, eso sí sólo ve las peticiones de WebApi, la intención no es rechazar las peticiones no autenticadas, sino autorizar aquellas que sí lo están para que luego los controladores con Authorize deniguen la petición si no está autorizada. En el código creo que queda más claro.

Codificación

En este caso el proyecto que usaré será una aplicación WebApi hecha con .Net Framework que podéis encontrar en mi repositorio de GitHub. Lo primero que vamos a hacer es lo más común en estos casos, decorar nuestro controlador con [Authorize] ( System.Web.Http ), lo que implica que cualquier petición que no se haya autorizado previamente no podrá acceder al controlador.

[Authorize]
public class ValuesController : ApiController
{
    // GET api/values
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

¿Y dónde y cómo se autoriza? en nuestro caso se comprueba la autenticación de la petición en «Message Handlers», hacia dónde quiere acceder, y concediéndole el permiso si lo cumple. Cuando llega al controlador, si está asignado el permiso, le autoriza o lo rechaza. He creado una clase ApiKeyAuthMessageHandler que hereda de DelegatingHandler perteneciente al contexto de «Message Handlers».

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Dispatcher;

namespace ApiKeyNetFwk.Seguridad
{
    public class ApiKeyAuthMessageHandler : DelegatingHandler
    {
        /// <summary>
        /// Listado de API Keys autorizadas
        /// </summary>
        private readonly Dictionary<string, Guid> ApiKeys = GestorApiKeys.GetKeys();


        /// <summary>
        /// Cabecera http que indica que contiene la Key de la API
        /// </summary>
        private const string API_KEY = "API_KEY";

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
        {
            // Para obtener la API Key
            IEnumerable<string> listaCabeceras;
            var existeCabeceraApiKey = request.Headers.TryGetValues(API_KEY, out listaCabeceras);
            if (existeCabeceraApiKey && listaCabeceras.Any())
            {
                // El descriptor aquí contendrá información sobre el controlador al que se enrutará la solicitud.
                // Si es nulo (es decir, no se encontró el controlador), se lanzará una excepción
                var config = GlobalConfiguration.Configuration;
                var controllerSelector = new DefaultHttpControllerSelector(config);
                var controller = controllerSelector.SelectController(request);

                if (controller != null && ApiKeys.ContainsKey(controller.ControllerName))
                {
                    // Recupera el Guid de la API para comprobar su autenticación
                    Guid apiKey = ApiKeys[controller.ControllerName];
                    if (listaCabeceras.First().Equals(apiKey.ToString()))
                    {
                        // Valida el acceso a la API
                        var principal = new GenericPrincipal(new GenericIdentity("Auth_" + controller.ControllerName), null);
                        AutorizarAccesoApi(principal);
                    }
                }
            }

            return base.SendAsync(request, cancellationToken);
        }

        private void AutorizarAccesoApi(IPrincipal principal)
        {
            Thread.CurrentPrincipal = principal;
            if (HttpContext.Current != null)
            {
                HttpContext.Current.User = principal;
            }
        }
    }
}

En el código se puede ver que he creado un diccionario de ApiKeys (Dictionary<string, Guid> ApiKeys) para que el acceso a cada controlador tenga una key distinta, esto puede hacerse de mil maneras. Al principio del código, en SendAsync, se comprueba las cabeceras de la solicitud http para comprobar si está la que corresponde a ApiKey, se extrae la información y se contrasta con el diccionario. Si la autenticación es correcta se pasa a dar permiso a la solicitud en el contexto http.

private void AutorizarAccesoApi(IPrincipal principal)
{
    Thread.CurrentPrincipal = principal;
    if (HttpContext.Current != null)
    {
        HttpContext.Current.User = principal;
    }
}

Una vez creado el gestor ahora hay que integrarlo en la aplicación, por lo que se invoca en el inicio de la aplicación, en este caso en el Global.asax

using ApiKeyNetFwk.Seguridad;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace ApiKeyNetFwk
{
    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            // Autenticación por Message Handler
            GlobalConfiguration.Configuration.MessageHandlers.Add(new ApiKeyAuthMessageHandler());
        }
    }
}

Si se entra por el navegador de internet se comprobará que devuelve un error de no autorización. Para probarlo voy a usar la aplicación PostMan, dejo una captura de la prueba.

También voy a poner cómo probarlo mediante la codificación en C#:

HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("API_KEY", "5b7a521a-ccf5-483e-b250-447d5d5cbb81");
var response = await client.GetAsync("http://localhost:44331/api/Values");

if (!response.IsSuccessStatusCode)
{
    throw new ApplicationException("Hubo algún error");
}

var content = await response.Content.ReadAsStringAsync();

Y listo, ya lo tenemos montado.

Conclusiones

Esta es una forma muy rápida de hacer «segura» una WebApi, pero lo entrecomillo porque si no se pasa por un canal Https cualquier sniffer puede realizar una captura de la cabecera y la seguridad serviría de poco.

Referencias
Burbujas en .NET : Como hacer seguros tus servicios WebApi
Sovit Poudel : Securing ASP.NET Web API

Entidades clonables y comparables en listados hash

Hay ciertas ocasiones en las que necesitamos que nuestras entidades se puedan hacer copias de las mismas para poder manipularlas sin modificar el original. También en otras ocasiones necesitamos que éstas se puedan coleccionar en diccionarios para evitar la duplicidad de las mismas. El problema viene en que una clase de por sí no conlleva esta funcionalidad. Hoy voy a tratar de exponer el código con el que yo me siento cómodo para llevar a cabo ambas funcionalidades.

Para este fin me he decidido por utilizar una clase base a la que llamaré «EntidadBase». Se supone que heredarán todas mis clases de negocio de esta, pero OJO estas implementaciones no significan que sirvan en cualquier situación, se debe conocer bien la implementación ya que un mal uso puede llevar a un problema mayor.

Implementación de ICloneable

Lo primero que hay que advertir es que no está recomendada la implementación de ICloneable tal y como Microsoft advierte aquí en la sección de «Notas a los implementadores». ¿Por qué?, porque el método «Clone» no informa sobre la copia devuelta en sí, no indica si la copia se ha realizado en profuncidad, es intermedia o sólo superficial, por tanto en APIs públicas puede suponer un problema el uso de este método.

Pero en ciertas circunstancias si que es apropiado su uso, como por ejemplo cuando queremos modificar una entidad sin cambiar la original, de tal modo que podamos distinguir los cambios producidos.

Partamos de la necesidad y la practicidad. Podríamos usar el método object.MemberwiseClone si tan sólo necesitamos una copia simple de las propiedades del objeto, pero OJO, esto sólo va bien para campos de tipo valor, para los campos de tipo referencia copia la referencia tal cual y por tanto si cambiamos el campo también lo cambiaremos en el original.

Codificamos una clase sencilla EntidadSimple, con tipos valor:

public class EntidadSimple : EntidadBase 
{
	#region Propiedades
	public int Id { get; set; }
	public string Descripcion { get; set; }
	#endregion
}

Ahora una clase más compleja EntidadCompleja, una de sus propiedades es un listado y otra propiedad su tipo es otra clase. Además pondremos otros tipos de propiedad para que veamos cómo funcionan entre los campos de tipo valor y los campos de tipo referencia.

// SPOILER : MAL faltaría la sobrecarga del método Clone, pero lo dejamos para mas adelante
public class EntidadCompleja : EntidadBase 
{
	#region Propiedades
	public int Id { get; set; }
	public string Nombre { get; set; }
	public List<string> Coleccion { get; set; }
	public EntidadSimple Simple { get; set; }
	public DateTime Fecha { get; set; }
	#endregion
}

Implementemos ICloneable mediante MemberwiseClone y demos una vuelta de tuerca preparándolo para que devuelva la clase base EntidadBase:

public class EntidadBase : ICloneable
{
	#region Implementación ICloneable
	public virtual EntidadBase Clone()
	{
		// ATENCIÓN : Como no se puede predecir el comportamiento del método Clone se recomienda no
		// implementar ICloneable en APIs públicas
		// OJO para propiedades de colecciones, tipos por referencia o propiedades "IsReadOnly" hay que 
		// programarlo manualmente por esta razón se deja como "Virtual" el método, ya que MemberwiseClone 
		// tan sólo hace una copia superficial
		return this.MemberwiseClone() as EntidadBase;
	}

	object ICloneable.Clone()
	{
		return Clone();
	}
	#endregion
}

Podríamos clonar tal cual la clase EntidadSimple y no habría problema, crearía una copia superficial, el problema lo tendríamos si clonamos EntidadCompleja, el código que lo invoca sería el siguiente:

static void Main(string[] args)
{
	Console.WriteLine("┌────────────────────────────────────────────────────────────────────────────┐");
	Console.WriteLine("│                          Entidades Extendidas PoC                          │");
	Console.WriteLine("└────────────────────────────────────────────────────────────────────────────┘");

	try
	{
		#region Pruebas ICloneable
		// Clonado de la entidad simple
		EntidadSimple entSim = new EntidadSimple
		{
			Id = 1,
			Descripcion = "Simple 1"
		};

		EntidadSimple entSimCopia = entSim.Clone() as EntidadSimple;
		entSimCopia.Id = 2;
		entSimCopia.Descripcion = "Simple 2";
		if (entSim.Descripcion == entSimCopia.Descripcion)
		{
			Console.WriteLine("¡Vaya no ha hecho bien la copia simple!");
		}
		else
		{
			Console.WriteLine("Clonado de la entidad simple correcto.");
		}

		// Clonado de la entidad compleja
		bool clonadoComplejoCorrecto = true;
		EntidadCompleja entCom = new EntidadCompleja
		{
			Id = 1,
			Nombre = "Compleja 1",
			Coleccion = new List<string> { "Primero", "Segundo" },
			Simple = entSim,
			Fecha = new DateTime(2019, 6, 10)
		};

		EntidadCompleja entComCopia = entCom.Clone() as EntidadCompleja;
		entComCopia.Id = 2;
		if (entCom.Id == entComCopia.Id)
		{
			clonadoComplejoCorrecto = false;
			Console.WriteLine("¡Vaya no ha hecho bien la copia simple!");
		}

		entComCopia.Coleccion[0] = "Tercero";
		if (entCom.Coleccion[0] == entComCopia.Coleccion[0])
		{
			clonadoComplejoCorrecto = false;
			Console.WriteLine("¡Vaya no ha hecho bien la copia del listado!");
		}

		entComCopia.Simple.Id = 3;
		if (entCom.Simple.Id == entComCopia.Simple.Id)
		{
			clonadoComplejoCorrecto = false;
			Console.WriteLine("¡Vaya no ha hecho bien la copia del objeto!");
		}

		entComCopia.Fecha = entComCopia.Fecha.AddDays(1);
		if (entCom.Fecha == entComCopia.Fecha)
		{
			clonadoComplejoCorrecto = false;
			Console.WriteLine("¡Vaya no ha hecho bien la copia de la fecha!");
		}

		if (clonadoComplejoCorrecto)
		{
			Console.WriteLine("Clonado de la entidad compleja correcto.");
		}
		#endregion
	}
	catch (Exception ex)
	{
		Console.WriteLine($"ERROR : {ex.Message}");
	}
}

Obtendríamos en este punto lo siguiente:

┌────────────────────────────────────────────────────────────────────────────┐
│                          Entidades Extendidas PoC                          │
└────────────────────────────────────────────────────────────────────────────┘
Clonado de la entidad simple correcto.
¡Vaya no ha hecho bien la copia del listado!
¡Vaya no ha hecho bien la copia del objeto!

Se comprueba que a pesar que el tipo string es de tipo referencia realiza el clonado correctamente, también pasa lo mismo con el tipo DateTime que, a pesar de lo que algunos creen, es de tipo valor. Donde ya no realiza la copia bien son en el resto de objetos de tipo referencia como son el listado y la propiedad con la clase EntidadSimple.

Por tanto para que el clonado funcione correctamente se deja el método Clone en la clase base como virtual para poder sobrecargarla y tratar las propiedades de tipo referencia de forma manual.

public class EntidadCompleja : EntidadBase
{
	#region Propiedades
	public int Id { get; set; }
	public string Nombre { get; set; }
	public List<string> Coleccion1 { get; set; }
	public string[] Coleccion2 { get; set; }
	public ArrayList Coleccion3 { get; set; }
	public EntidadSimple Simple { get; set; }
	public DateTime Fecha { get; set; }
	#endregion

	#region Sobrecarga de métodos
	public override EntidadBase Clone()
	{
		EntidadCompleja copia = base.Clone() as EntidadCompleja;

		// Inicialización de propiedades de tipo referencia
		copia.Coleccion1 = null;
		copia.Simple = null;

		// Copia de valores si los objetos no son nulos
		if (this.Coleccion1 != null)
		{
			copia.Coleccion1 = this.Coleccion1.ToList();
		}

		if (this.Coleccion2 != null)
		{
			copia.Coleccion2 = this.Coleccion2.ToArray();
		}

		if (this.Coleccion3 != null)
		{
			copia.Coleccion3 = this.Coleccion3.Clone() as ArrayList;
		}

		if (this.Simple != null)
		{
			copia.Simple = this.Simple.Clone() as EntidadSimple;
		}

		return copia;
	}
	#endregion
}

En esta nueva codificación de EntidadCompleja he agregado más propiedades de tipo listado para comprobar su comportamiento con cada una de ellas.

Ahora al volver a ejecutar la aplicación saldría el siguiente texto:

┌────────────────────────────────────────────────────────────────────────────┐
│                          Entidades Extendidas PoC                          │
└────────────────────────────────────────────────────────────────────────────┘
Clonado de la entidad simple correcto.
Clonado de la entidad compleja correcto.

Implementación para listados Hash y Diccionarios

Bien imaginemos que queremos hacer una caché simple de objetos de EntidadCompleja, lo primero que se nos ocurre es realizar un diccionario cualquiera e introducir valores esperando que el diccionario detecte si ya existe.

#region Pruebas Hash
Dictionary<EntidadCompleja, object> dic = new Dictionary<EntidadCompleja, object>();
entComCopia = entCom.Clone() as EntidadCompleja;
dic.Add(entCom, "Uno");
if(!dic.ContainsKey(entComCopia))
{
    dic.Add(entComCopia, "Dos");
    Console.WriteLine("Si pasa por aquí es que se ha duplicado la entidad en el diccionario.");
}
#endregion

Como la clase no está preparada para comprobar si otro objeto del mismo tipo es igual a él mismo, el diccionario entiende que son distintos y lo incluye, por lo que hay que codificar que de alguna forma distinga si son iguales o no.

ATENCIÓN : Partamos por tanto de la hipótesis que NECESITAMOS que para que un objeto de la misma clase sea igual a otro deberá tener los mismos valores en sus propiedades. Es decir, no vamos a entrar a evaluar variables, ni métodos, tan sólo propiedades

:

public class EntidadBase : ICloneable, IEquatable<EntidadBase>
{
	#region Implementación ICloneable
	public virtual EntidadBase Clone()
	{
		// ATENCIÓN : Como no se puede predecir el comportamiento del método Clone se recomienda no
		// implementar ICloneable en APIs públicas
		// OJO para propiedades de colecciones, tipos por referencia o propiedades "IsReadOnly" hay que 
		// programarlo manualmente por esta razón se deja como "Virtual" el método, ya que MemberwiseClone 
		// tan sólo hace una copia superficial
		return this.MemberwiseClone() as EntidadBase;
	}

	object ICloneable.Clone()
	{
		return Clone();
	}
	#endregion

	#region Implementación IEquatable
	public bool Equals(EntidadBase other)
	{
		if (other as object == null || GetType() != other.GetType())
		{
			return false;
		}

		Type tipo = this.GetType();

		foreach (PropertyInfo prop in tipo.GetProperties())
		{
			var valorEsperado = prop.GetValue(other);
			var valorRecibido = prop.GetValue(this);
			if (valorEsperado == null && valorRecibido != null)
			{
				return false;
			}
			if (valorEsperado != null && valorRecibido == null)
			{
				return false;
			}
			if (valorEsperado != null && valorRecibido != null)
			{
				// Comprueba si la propiedad es algún tipo de array
				var isGenericICollection = valorRecibido.GetType().GetInterfaces().Any(
					x => x.IsGenericType &&
					x.GetGenericTypeDefinition() == typeof(ICollection<>));
				var isICollection = valorRecibido.GetType().GetInterfaces().Any(
					x => x == typeof(ICollection));

				if (isGenericICollection || isICollection)
				{
					ICollection coleccionEsperada = valorEsperado as ICollection;
					ICollection coleccionRecibida = valorRecibido as ICollection;
					if (coleccionEsperada.Count != coleccionRecibida.Count)
					{
						return false;
					}

					object[] listaEsperada = new object[coleccionEsperada.Count];
					coleccionEsperada.CopyTo(listaEsperada, 0);
					object[] listaRecibida = new object[coleccionRecibida.Count];
					coleccionRecibida.CopyTo(listaRecibida, 0);

					for (int i = 0; i < coleccionRecibida.Count; i++)
					{
						if(!listaRecibida[i].Equals(listaEsperada[i]))
						{
							return false;
						}
					}
				}
				else if(!valorEsperado.Equals(valorRecibido))
				{
					return false;
				}
			}
		}

		return true;
	}
	#endregion

	#region Sobrecarga de Métodos
	public override int GetHashCode()
	{
		int hashCode = 0;

		foreach (PropertyInfo prop in this.GetType().GetProperties())
		{
			object value = prop.GetValue(this);
			if (value != null)
			{
				// Comprueba si la propiedad es algún tipo de array
				var isGenericICollection = value.GetType().GetInterfaces().Any(
					x => x.IsGenericType &&
					x.GetGenericTypeDefinition() == typeof(ICollection<>));
				var isICollection = value.GetType().GetInterfaces().Any(
					x => x == typeof(ICollection));

				if (isGenericICollection || isICollection)
				{
					ICollection col = value as ICollection;
					foreach (var valueColeccion in col)
					{
						// Se recorre los objetos del array obteniendo su código hash
						hashCode = CrearHash(hashCode, valueColeccion);
					}
				}
				else
				{
					hashCode = CrearHash(hashCode, value);
				}
			}
		}

		return hashCode;
	}

	public override bool Equals(object obj)
	{
		// Se sobreescribe el método para poder comprobar si dos objetos de la misma clase tienen las mismas propiedades
		return Equals(obj as EntidadBase);
	}

	public override string ToString()
	{
		// Este no hace falta pero lo he incluido para depurar las variables
		try
		{
			return JsonConvert.SerializeObject(this, Formatting.Indented);
		}
		catch (Exception ex)
		{
			return base.ToString() + $" - Excepción obteniendo los valores de las propiedades: {ex.Message}";
		}
	}
	#endregion

	#region Codificación de operadores
	public static bool operator ==(EntidadBase obj1, EntidadBase obj2)
	{
		if (obj1 as object == null && obj2 as object == null)
		{
			return true;
		}
		if (obj1 as object == null && obj2 as object != null)
		{
			return false;
		}
		return obj1.Equals(obj2);
	}

	public static bool operator !=(EntidadBase obj1, EntidadBase obj2)
	{
		if (obj1 as object == null && obj2 as object == null)
		{
			return false;
		}
		if (obj1 as object == null && obj2 as object != null)
		{
			return true;
		}
		return !obj1.Equals(obj2);
	}
	#endregion

	#region Métodos Auxiliares
	private static int CrearHash(int hashCode, object value)
	{
		if (hashCode == 0)
		{
			hashCode = value.GetHashCode();
		}
		else
		{
			hashCode = hashCode ^ value.GetHashCode();
		}

		return hashCode;
	}

	#endregion
}

He separado el código en regiones para que quede algo más claro.

Lo primero que hago es implementar la interfaz IEquatable. Esta va a llevar la responsabilidad mayor en la comparación de objetos. A través de foreach (PropertyInfo prop in tipo.GetProperties()) recorre todas las propieades, se podría hacer que sólo tuviera en cuenta las públicas, pero en esta ocasión no lo he hecho. Comprueba los posibles valores nulos en la comparación, y después de comprobar que no vienen a nulos distingue si la propiedad es una colección o no mediante las variables isGenericICollection y isICollection.

Cuando NO es una colección directamente se llama al método object.Equals, por tanto funcionará correctamente con campos tipo valor, y con campos tipo referencia siempre y cuando tengan implementado Equals en su clase. Si se respeta que las propiedades de la clase de tipo referencia sean de otra clase que herede de EntidadBase entonces no habrá problema para distinguirlas y sabrá si son iguales o no.

OJO esto significa que cualquier propiedad que contenga un campo de tipo referencia de una clase que no tenga implementada el método Equals dará como distinta.

Por esta razón las listas hay que tratarlas de otra forma, ya que un array es un campo de tipo referncia al que no tenemos acceso para implementarle el método Equals. Las propiedades detectadas como listados se convierten en ICollection para tratarlas individualmente en arrays ya recorribles.

ICollection coleccionEsperada = valorEsperado as ICollection;
ICollection coleccionRecibida = valorRecibido as ICollection;
if (coleccionEsperada.Count != coleccionRecibida.Count)
{
    return false;
}

object[] listaEsperada = new object[coleccionEsperada.Count];
coleccionEsperada.CopyTo(listaEsperada, 0);
object[] listaRecibida = new object[coleccionRecibida.Count];
coleccionRecibida.CopyTo(listaRecibida, 0);

for (int i = 0; i < coleccionRecibida.Count; i++)
{
    if(!listaRecibida[i].Equals(listaEsperada[i]))
    {
        return false;
    }
}

Si probamos esto en la aplicación de consola tendríamos el siguiente código:

entSimCopia = entSim.Clone() as EntidadSimple;
int hash1s = entSim.GetHashCode();
int hash2s = entSimCopia.GetHashCode();

Dictionary<EntidadCompleja, object> dic = new Dictionary<EntidadCompleja, object>();
entComCopia = entCom.Clone() as EntidadCompleja;
int hash1 = entCom.GetHashCode();
int hash2 = entComCopia.GetHashCode();
bool sonIguales = entCom == entComCopia;
dic.Add(entCom, "Uno");
if(!dic.ContainsKey(entComCopia))
{
	dic.Add(entComCopia, "Dos");
	Console.WriteLine("Si pasa por aquí es que se ha duplicado la entidad en el diccionario.");
}

En el código se compruba que los hash creados en las entidades simples son correctos, además se crea un diccionario para ver como se comporta con los objetos clonados y comprobar que efectivamente es capaz de distinguirlos.

Conclusión

Insisto que esto es una prueba de concepto totalmente personal, que me ha servido como base para otros desarrollos, pero antes de implementarlo en una solución profesional hay que tener en cuentas los siguientes criterios.

Criterios para la implementación de IClonable:

  • La clonación deberá tener en cuenta aquellas propiedades y variables que sean de tipo referencia (que no sean string) para sobrecargar el método base Clone y codificarlas individualmente.
  • De poco servirá si las clases de sus propiedades y variables no implementan IClonable.
  • No se recomienda su uso en APIs públicas.

Criterios para la implementación de IEquatable<>:

  • No es recomendable esta implementación en una clase que tiene como propiedades listas de longitud considerable si lo que queremos es usarla en tablas hash, ya que cada vez que realice una búsqueda tiene que recorrerse sus listas para comprobar si es igual y esto aumenta demasiado las búsquedas.
  • Esta implementación sólo tiene en cuenta las propiedades de la clase, no las variables, ni las de tipo estático o de sólo lectura.
  • Se debe tener especial cuidado con las propiedades de tipo referencia, sobre todo los listados, y considerar si realmente merece la pena usar esta implementación.

Espero que el que haya llegado hasta aquí haya entendido mejor el funcionamiento de IClonable e IEquatable y que le pueda servir en futuros desarrollos.

El código fuente está subido en GitHub y podéis descargarlo desde aquí.

Saludos.

El Patrón Especificación en .NET bajo proyectos DDD

Llevo tiempo detrás de este patrón, ya que como definición es bastante simple, pero a la hora de llevarla al código ya no lo es tanto si lo que pretendemos es que nos funcione de forma correcta en la capa de datos y trabajando con Entity Framework.

No me voy a centrar mucho en su definición, ya que hay bastante documentación al respecto en internet tanto en inglés como en español. Me voy a centrar más en la necesidad de su codificación y cómo poder usarlo de forma correcta. Este patrón lo he conocido cuando comencé con el libro de Cesar de la Torre sobre la arquitectura DDD. Así que recomiendo que entendáis este concepto antes.

Las clases aquí expuestas las podéis encontrar al completo en mi repositorio de GitHub. El proyecto viene preparado para ser ejecutado bajo SqlServer Express y genera la base de datos de forma automática. Además la solución tiene una estructura aproximada a DDD.

Comencemos por el principio ¿qué es el patrón especificación?

Martin Fowler y Eric Evans lo describen en el siguiente enlace. Yo lo voy a simplificar insultantemente desde el concepto de orientación al dominio:

Recordemos que DDD es un conglomerado de buenas prácticas en las que se encuentra SOLID, y precisamente una de ellas es la «separación de responsabilidades» o «Principio de Responsabilidad Única», y separar las operaciones de negocio (la funcionalidad que realiza una empresa con esa aplicación) de las operaciones de base de datos (la ejecución de scripts para recuperar información o manipularla) es una de ellas. Bien, pues bajo esta premisa se quiere facilitar en la capa de negocio una serie de operaciones sobre sus entidades que ignoren por completo cómo se va a resolver en las llamadas a base de datos. Es decir, y metiéndonos en la definición del patrón especificación, separa las responsabilidades de «qué se puede hacer con una entidad» de «cómo lo va a hacer». Por tanto hay que definir un mecanismo que nos permita traspasar esas expresiones de consulta hasta la capa de base de datos, para que allí las pueda interpretar y adaptar.

Tenéis más detalles del patrón en el libro «Arquitectura N-Capas DDD .NET 4.0» en las secciones 2.2.6 y 3.6.

Implementación básica

La principal premisa que debe realizar una especificación es indicar si satisface una condición. Por lo que empezaremos por una implementación sencilla, aunque incompleta, que aproveche las bondades de los árboles de expresión, ya que nos ofrece una potencia de codificación importante. Comenzamos con la interfaz:

using System;
using System.Linq.Expressions;

namespace PatronEspecificacion.Dominio.Consultas.PatronBasico
{
    public interface ISpecification<TEntity>
    {
        Expression<Func<TEntity, bool>> IsSatisfiedBy();
    }
}

Ahora la clase que la va a implementar:

using System;
using System.Linq.Expressions;

namespace PatronEspecificacion.Dominio.Consultas.PatronBasico
{
    public abstract class Specification<TEntity> : ISpecification<TEntity>
    {
        public abstract Expression<Func<TEntity, bool>> IsSatisfiedBy();
    }
}

Bien ya tenemos los cimientos, ahora vamos a crear una especificación de consulta bajo su estructura:

using PatronEspecificacion.Dominio.Consultas.PatronBasico;
using PatronEspecificacion.Dominio.Entidades;
using System;
using System.Linq.Expressions;

namespace PatronEspecificacion.Dominio.Consultas
{
    public class DireccionesPorProvinciaSpecificationBasico : Specification<DireccionEspanolaEntity>
    {
        private readonly string provincia;

        public DireccionesPorProvinciaSpecificationBasico(string provincia) : base()
        {
            this.provincia = provincia;
        }

        public override Expression<Func<DireccionEspanolaEntity, bool>> IsSatisfiedBy()
        {
            return (x) => x.Provincia == this.provincia;
        }
    }
}

En este punto tenemos los cimientos y una consulta, vamos a aplicarlo en la capa de datos para que Entity Framework lo utilice:

using PatronEspecificacion.InfraestructuraDatos.Persistencia;
using System;
using System.Collections.Generic;
using System.Linq;
using PatronEspecificacion.Dominio.Contratos;
using PatronEspecificacion.Dominio.Entidades;
using System.Linq.Expressions;
using PatronEspecificacion.InfraestructuraDatos.Base;

namespace PatronEspecificacion.InfraestructuraDatos.Repositorios
{
    public class DireccionesRepository : IDireccionesRepository
    {
        public ICollection<DireccionEspanolaEntity> GetDireccionesBasico(Dominio.Consultas.PatronBasico.ISpecification<DireccionEspanolaEntity> especificacion)
        {
            ICollection<DireccionEspanolaEntity> salida;

            using (PoCEspecificacionContext ctx = new PoCEspecificacionContext())
            {
                IQueryable<DireccionEspanolaEntity> query = ctx.Direcciones.Select(d => new DireccionEspanolaEntity
                {
                    Id = d.DireccionId,
                    Provincia = d.Provincia,
                    Municipio = d.Municipio,
                    Calle = d.Calle
                })
                .Where(especificacion.IsSatisfiedBy());
                string parada = query.ToSql();
                salida = query.ToList();
            }

            return salida;
        }
    }
}

Quiero ver qué tal se acoplaba dicha consulta en las consultas que terminan generando «Entity Framework». Para ello hay dos formas de comprobarlo, ejecutando «SQLProfiler» y depurando las llamadas que recibe el servidor de base de datos, o implementando una extensión a «IQueryableExtensions» que he encontrado aquí y que extrae el script que finalmente se lanza sobre la BBDD. Por tanto extraigo la consulta SQL en la variable «parada» y pararé la depuración en ese punto para comprobarlo.

Ahora consumimos los datos desde el proyecto de negocio:

using PatronEspecificacion.Dominio.Entidades;
using PatronEspecificacion.Dominio.Servicios.Interfaces;
using System.Collections.Generic;
using PatronEspecificacion.Dominio.Contratos;
using PatronEspecificacion.Dominio.Consultas;

namespace PatronEspecificacion.Dominio.Servicios
{
    public class GestorDirecciones : IGestorDirecciones
    {
        private IDireccionesRepository direccionesRepository;

        public GestorDirecciones(IDireccionesRepository repo)
        {
            direccionesRepository = repo;
        }

        public string MyProperty { get; set; } = "Un valor";

        public ICollection<DireccionEspanolaEntity> ObtenerDirecciones()
        {
            // Consulta por la implementación básica, no admite combinaciones
            var espBas = new DireccionesPorProvinciaSpecificationBasico("Madrid");
            var dirBas = direccionesRepository.GetDireccionesBasico(espBas);

            return dirBas ;
        }
    }
}

Cuando esto se ejecuta se puede comprobar que funciona correctamente. Si nos detenemos incluso en la variable «parada» (antes comentada) se puede ver que efectivamente la consulta SQL integra el filtro.

SELECT [d].[DireccionId] AS [Id], [d].[Provincia], [d].[Municipio], [d].[Calle]
FROM [Direcciones] AS [d]
WHERE [d].[Provincia] = N'Madrid'

Implementación de Wikipedia

El problema viene cuando queremos realizar varias consultas y que interactúen entre ellas, aquí entra la implementación más compleja para incluir operaciones AND, OR y NOT, y se necesita una especificación compuesta de otras especificaciones. Por lo que el código se enfoca en desarrollar la siguente estructura obtenida de la WikiPedia:

El código lo podéis consultar en su web. El ejemplo que he tomado utiliza las expresiones en arbol y los tipos genéricos. Si se implementa en el proyecto, el resultado es que se pueden realizar consultas complejas de otras más simples.

// Combinación de consultas por la implementación según Wikipedia
var espWiki1 = new DireccionesPorProvinciaSpecificationWiki("Madrid");
var espWiki2 = new DireccionesPorMunicipioSpecificationWiki("Madrid");
var dirWiki = direccionesRepository.GetDireccionesWiki(espWiki1.And(espWiki2));

Pero ¿qué tal se integra en las consultas de «Entity Framework»? Veamos la variable «parada«.

SELECT [d].[DireccionId] AS [Id], [d].[Provincia], [d].[Municipio], [d].[Calle]
FROM [Direcciones] AS [d]

¡Vaya se trae toda la tabla sin filtrar! El ejemplo de la WikiPedia está orientado a objetos en memoria, no en base de datos, por lo que no está optimizado para usarlo con «Entity Framework» y esto con una tabla de millones de registros puede ser un problema.

Implementación del proyecto ejemplo de DDD

Demos una vuelta de tuerca más, tomemos la implementación del patrón según el proyecto de ejemplo DDD que hace unos años estaba publicado por Cesar de la Torre. El código fuente lo dejo subido en GitHub, aquí tan sólo comento las diferencias.

A primera vista vemos que implementa los comando AND, OR y NOT mediante los operadores ‘&’, ‘|’ y ‘!’ con lo cual la codificación queda de la siguiente manera:

// Combinación de consultas por la implementación del proyecto DDD
var espDdd1 = new DireccionesPorProvinciaSpecificationDdd("Madrid");
var espddd2 = new DireccionesPorMunicipioSpecificationDdd("Madrid");
var dirDdd = direccionesRepository.GetDireccionesDdd(espDdd1&espddd2);

Pero ¿solo es esto?… pues NO. Esta implementación también tiene en cuenta que va a ejecutarse sobre «Entity Framework» por LinQ y por tanto están integradas las composiciones de especificaciónes en el código SQL resultante. Para ello usa las clases «ExpressionBuilder» y «ParameterRebinder «.

Volvamos a parar la depuración en la variable «parada» y comprobémoslo:

SELECT [d].[DireccionId] AS [Id], [d].[Provincia], [d].[Municipio], [d].[Calle]
FROM [Direcciones] AS [d]
WHERE (CASE
    WHEN [d].[Provincia] = N'Madrid'
    THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END & CASE
    WHEN [d].[Municipio] = N'Madrid'
    THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END) = 1

Efectivamente, los datos ya vienen filtrados desde SqlServer.

Pero aquí no queda la cosa, también disponemos de otra clase llamada «DirectSpecification» que nos permite potenciar las especificaciones de consulta. Por tanto en el código donde disponía de dos especificaciones, «DireccionesPorProvinciaSpecificationDdd» y «DireccionesPorMunicipioSpecificationDdd» las termino uniendo en una sola de la siguiente forma:

using PatronEspecificacion.Dominio.Consultas.PatronDdd;
using PatronEspecificacion.Dominio.Entidades;
using System;
using System.Linq.Expressions;

namespace PatronEspecificacion.Dominio.Consultas
{
    public class DireccionesFiltradasSpecificationDdd : Specification<DireccionEspanolaEntity>
    {
        private readonly DireccionEspanolaFiltro filtro;

        DireccionesFiltradasSpecificationDdd(DireccionEspanolaFiltro filtro)
        {
            this.filtro = filtro;
        }

        public override Expression<Func<DireccionEspanolaEntity, bool>> SatisfiedBy()
        {
            Specification<DireccionEspanolaEntity> spec = new TrueSpecification<DireccionEspanolaEntity>();

            if (!string.IsNullOrWhiteSpace(filtro.Provincia))
            {
                spec &= new DirectSpecification<DireccionEspanolaEntity>(d => d.Provincia == (filtro.Provincia));
            }
            if (!string.IsNullOrWhiteSpace(filtro.Municipio))
            {
                spec &= new DirectSpecification<DireccionEspanolaEntity>(d => d.Municipio == (filtro.Municipio));
            }
            if (filtro.Exclusion != null)
            {
                spec &= new NotSpecification<DireccionEspanolaEntity>(new DireccionesFiltradasSpecificationDdd(filtro.Exclusion));
            }

            return spec.SatisfiedBy();
        }
    }
}

En la misma se puede ver como utilizar las clases «DirectSpecification» y «NotSpecification«, esto da muchas posibilidades de codificación.

Ahora codifico una prueba donde obtengo las direcciones de la provincia de Madrid excluyendo al municipio de Madrid:

// Ahora excluyamos a Madrid capital
filtro = new DireccionEspanolaFiltro
{
    Provincia = "Madrid",
    Exclusion = new DireccionEspanolaFiltro() { Municipio = "Madrid"}
};
var espddd4 = new DireccionesFiltradasSpecificationDdd(filtro);
var dirExclus = direccionesRepository.GetDireccionesDdd(espddd4);

La consulta SQL que termina generando es la siguiente:

SELECT [d].[DireccionId] AS [Id], [d].[Provincia], [d].[Municipio], [d].[Calle]
FROM [Direcciones] AS [d]
WHERE ((1 & CASE
    WHEN [d].[Provincia] = N'Madrid'
    THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END) & CASE
    WHEN NOT ((1 & CASE
        WHEN [d].[Municipio] = N'Madrid'
        THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
    END) = 1)
    THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END) = 1

Por tanto se integra perfectamente y nos permite consultas con exclusiones en los resultados.

¿Pero esto no se puede hacer pasando los árboles de expresión como parámetro?

Pues SI, al menos en la prueba que yo he realizado he conseguido que la expresión se integre correctamente en la consulta de «Entity Framework».

public ICollection<DireccionEspanolaEntity> GetDirecciones(Expression<Func<DireccionEspanolaEntity, bool>> exp)
{
	ICollection<DireccionEspanolaEntity> salida;

	using (PoCEspecificacionContext ctx = new PoCEspecificacionContext())
	{
		IQueryable<DireccionEspanolaEntity> query = ctx.Direcciones
			.Select(d => new DireccionEspanolaEntity
			{
				Id = d.DireccionId,
				Provincia = d.Provincia,
				Municipio = d.Municipio,
				Calle = d.Calle
			})
			.Where(exp);
		string parada = query.ToSql();
		salida = query.ToList();
	}

	return salida;
}

El resultado del SQL es el siguiente:

SELECT [d].[DireccionId] AS [Id], [d].[Provincia], [d].[Municipio], [d].[Calle]
FROM [Direcciones] AS [d]
WHERE ([d].[Provincia] = N'Madrid') AND ([d].[Municipio] = N'Madrid')

Pero esto no quita interés al patrón, creo que la experiencia conociéndolo ha sido interesante.

Conclusión

Este artículo se ha escrito con un fin didáctico personal y si lo publico es porque creo que puede resultar interesante a otras personas, no soy un experto del patrón, por lo que cualquier crítica constructiva será bienvenida. Por tanto queda abierta a posibles ediciones a posteriori.

El patrón me ha permitido averiguar cómo poder separar las responsabilidades de consultas de negocio respecto a su implementación en base de datos y adentrarme más profundamente en los árboles de expresiones y su uso.

Saludos.

A %d blogueros les gusta esto: