Generando búsquedas mediante el patrón especificación

Como ya comenté en mi anterior post, llevo un tiempo trabajando sobre la arquitectura DDD NLayer, con el objetivo de generar gran parte de los artefactos que la componen a partir de modelos. En la solución de ejemplo ya se adjuntan ciertas plantillas T4 para generar las self-tracking entities y el context de EF. Sin embargo, hay bastante más código «generable» que nos podría servir de soporte y que nos permitiría rápidamente centrarnos en los procesos de negocio realmente complejos. Una de estas partes con posibilidad de ser fácilmente generable es la de las búsquedas basadas en el patrón especificación. Para entender su implementación en DDD NLayer sólo hay que leer el post de su autor Unai.

La piedra angular de dicha implementación se basa en las clases Specification, que contienen los criterios de búsqueda (método SatisfiedBy), junto a los parámetros que se usan en dicha búsqueda. Por ejemplo:

public class BankAccountSearchSpecification
        :Specification<BankAccount>
    {
        string _CustomerName;
        string _BankAccountNumber;

        public BankAccountSearchSpecification(string bankAccountNumber, string customerName)
        {
            _CustomerName = customerName;
            _BankAccountNumber = bankAccountNumber;
        }

        public override System.Linq.Expressions.Expression<Func<BankAccount, bool>> SatisfiedBy()
        {
            Specification<BankAccount> spec = new TrueSpecification<BankAccount>();

            if (!String.IsNullOrEmpty(_BankAccountNumber) &&
                !String.IsNullOrWhiteSpace(_BankAccountNumber))
            {
                spec &= new BankAccountNumberSpecification(_BankAccountNumber);
            }
            if (!String.IsNullOrEmpty(_CustomerName) &&
                !String.IsNullOrWhiteSpace(_CustomerName))
            {
                spec &= new DirectSpecification<BankAccount>(ba => ba.Customer.ContactName.ToLower().Contains(_CustomerName.ToLower()));
            }

            return spec.SatisfiedBy();
        }
    }

Nuestro objetivo es generar una clase Specification por cada entidad, que nos proporcione las búsquedas parametrizadas básicas sobre sus propiedades y relaciones. El problema es que en el caso de entidades con un número alto de propiedades, los constructores de estas clases Specification se llenarían de parámetros. Una primera opción sería usar información del modelo para obtener grupos de propiedades que formen especificaciones autónomas dentro de cada entidad. Así obtendríamos conjuntos de especificaciones como los que encontramos en la solución de DDD NLayer. Por ejemplo, para la entidad Order podemos encontrar OrderDateSpecification, OrderFromCustomerDateRangeSpecification, OrderFromCustomerSpecification u OrderShippingSpecification. Con esta solución ganamos en la ubicuidad del lenguaje que tanto buscamos en DDD, pero no conseguimos nuestro objetivo de definir búsquedas de manera estándar y simple si no poseemos el nivel de expresividad adecuado en nuestros modelos.

Ese era nuestro caso, ya que partíamos de un modelo similar al de Entity Framework o al modelo de clases de UML. Para estos casos podemos introducir un nuevo artefacto por cada entidad al que llamamos SearchValues, y que tiene la única responsabilidad de encapsular los parámetros de búsqueda. De este modo, la clase Specification sólo tiene la responsabilidad de definir los criterios delegando el contenido de los parámetros, como hemos dicho, a la clase SearchValues. En el constructor recibirá un objeto de este tipo y en el método SatisfiedBy decidirá si los parámetros contenidos en ese objeto SearchValues cumplen o no con los criterios definidos.

De modo que para cada entidad, por un lado generamos una clase SearchValues, con una propiedad por cada propiedad de la entidad (strings para tipos string, rangos para decimal, int o datetime, e int para filtrar por claves en relaciones), ademas de otra propiedad de tipo SearchValues para hacer búsquedas por elementos que «no cumplan los criterios de ese otro SearchValues», propiedad a la que llamamos NotInSearch. Y por otro lado generamos una clase Specification que contiene la correspondiente propiedad de tipo SearchValues y define los criterios de búsqueda en el método SatisfiedBy a partir de los valores de esa propiedad. En código queda más claro:

public class BankAccountSearchSpecification
        :Specification<BankAccount>
    {
		public BankAccountSearchValues SearchValues { get; set; }
				
                public BankAccountSearchSpecification(BankAccountSearchValues searchValues)
                { 
			SearchValues=searchValues;
		}

               public override Expression<Func<BankAccount, bool>> SatisfiedBy()
               {
 			Specification<BankAccount> spec = new TrueSpecification<BankAccount>();
				if(SearchValues.CustomerName != string.Empty)
				{
					spec &= new DirectSpecification<BankAccount>(element => element.CustomerName == (SearchValues.CustomerName));
				}
				if(SearchValues.BankAccountNumber != string.Empty)
				{
					spec &= new DirectSpecification<BankAccount>(element => element.BankAccountNumber == (SearchValues.BankAccountNumber));
				}
				if (SearchValues.NotInSearch != null)
                               {
                                       spec &= new NotSpecification<BankAccount>(new BankAccountSearchSpecification(SearchValues.NotInSearch));
                               }  
			
	        return spec.SatisfiedBy();
        }
    }
    public class BankAccountSearchValues
    {	
	public string CustomerName { get; set;	}
	public string BankAccountNumber	{ get; set; }
	public BankAccountSearchValues NotInSearch { get; set; }
    }

Las expresiones que usamos para las búsquedas por propiedades de tipos básicos son triviales. En el código anterior se pueden ver para tipos string por criterio de «igual a». Otros criterios como «contenida en», «menor que», etc. son igualmente triviales, así como el uso de otros tipos como rangos de datetime, int, etc. En cuanto a las relaciones, la cosa se complica un poco cuando intentamos filtrar por extremos con cardinalidad «*» (Many). Por inercia la tendencia es usar una expresion del tipo:

spec &= new DirectSpecification<BankAccount>(element => element.BankTransferFromThis.Contains(SearchValues.BankTransfer));

Sin embargo, nos encontraremos con el error: «No se pudo crear un valor de constante de tipo ‘Namespace.Entities.Oferta’. Sólo se admiten los tipos primitivos (‘como Int32, String y Guid’) en este contexto».

La tendencia entonces cambia a crear el Join con Linq para filtrar por los elementos relacionados. El problema es que implicaría un severo golpe al diseño y la simplicidad de este motor de búsquedas.

La solución óptima que encontramos fue usar Any(), con lo que se obtiene una forma elegante y simple de buscar en este tipo de relaciones, además de ser independiente de si se trata de «One to Many» o «Many to Many». La sentencia quedaría así:

spec &= new DirectSpecification<BankAccount>(element => element.BankTransferFromThis.Any(bt=>bt.BankTransferId==SearchValues.BankTransferId));

Y la sentencia SQL que genera es algo como esto (un poco simplificada):

SELECT * FROM  BankAccount

LEFT OUTER JOIN BankTransfer ON BankTransfer.BankAccountId = BankAccount.BankAccountId

WHERE  EXISTS (SELECT 1 FROM BankTransfer WHERE (BankTransfer.BankAccountId = BankAccount.BankAccountId) AND (BankTransfer.BankAccountId = @p__linq__0)

Una vez que hemos contemplado los casos de cada tipo diferente de propiedad, tanto si es escalar, como si es de navegación, sólo nos queda generar la plantilla que recorra las clases o entidades del modelo y vaya tratando cada uno de los casos comentados. Gracias a las T4, nos resultará fácil y sólo dependerá del metamodelo que estemos usando. En el caso de usar UML podemos seguir aquí una guía de cómo hacerlo, si queremos usar el modelo de EF podemos usar la guía que suponen las plantillas de T4 para STE o POCO, y en el caso de que usemos modelos de DSL Tools simplemente al crear el proyecto de DSL Designer se nos creará un proyecto Debugging con ejemplos de dichas plantillas. No obstante, espero escribir algún ejemplo de estas aproximaciones en próximos posts. 

Algo que creo que deberíamos tener en cuenta (y que siempre se ha de aplicar cuando usamos código generado) es que ni todas las búsquedas se deberían de usar tal como nos la genera la plantilla desde el modelo, ni tampoco, por ello, deberíamos crearlas todas «a mano». Habrá siempre zonas en nuestra aplicación, más o menos amplias, que puedan ser automatizadas. La cuestión es encontrar la frontera correcta y no dejar de tener el control de qué está haciendo nuestro código por mucho que se haya generado automáticamente. Por ejemplo, en soluciones basadas en self-tracking en que no disponemos de lazy loading, el hecho de cargar todas las colecciones (por medio de Include()) de todas las relaciones de nuestras entidades para luego poder usar este tipo de búsquedas ya generadas automáticamente mediante plantillas, nos tiraría abajo el rendimiento de ciertas partes de nuestra aplicación. Ni por ello debemos descartar usar las búsquedas generadas para algunas otras partes de la aplicación, ni tampoco sacrificar al usuario por el hecho de usar ese código generado en todos sitios. De nuevo, como en muchas otras cosas, es cuestión de sensatez y ponderación…