Introducción
Hace ya unos cuantos dias que vengo dándole vueltas a la cabeza para escribir un post sobre especificaciones y como construirlas sobre la infraestructura de Entity Framework, aunque en realidad sería igual sobre cualquier elemento IQueryable, aunque con alguna nota de implementación. Pensando en esta entrada no sabía si comenzar con una explicación sobre este patrón o ceñirme directamente a una posible implementación, puesto que, me parece magnifica la documentación sobre el mismo que Fowler y Evans tienen en el documento de referencia. Al final, y despues de darle muchas vueltas me quedaré entre medias puesto que creo que es necesario realizar una pequeña introducción, aunque aconsejo encarecidamente desde aquí leerse el documento anterior si nunca ha visto información acerca de este patrón.
Sintetizándolo, aunque sea demasiada síntesis, podríamos decir que el patrón espeficación trata de lograr una separación entre la sentencia que unos objetos deberían de cumplir y el objeto que realiza la selección. Por ejemplo una especificación podría describir “los clientes que pertenecen a una determinada situación geográfica” pero no saber nada acerca de como realizar dicha selección. En definitiva no se trata nada más y nada menos que agregar una responsabilidad desacoplada de los objetos de dominio que la usan.
Este patrón es muy escuchado y usado en arquitecturas basadas en modelos de dominio y puesto en práctica en muchas arquitecturas para definir “criterios” de selección. Basándonos en la definición formal de este patrón, mostrada en la Figura 1, podríamos ver a primeras que una implementación de este patrón trabajando con IQueryable no tendría mucho sentido.
La razón principal de la afirmación anterior viene de la propia definición del patron, la cual implica trabajar con objetos directamente en memoria puesto que el método IsSatisfiedBy tomaría una instancia del objeto en el cual queremos comprobar si cumple un determinado criterio y devolver true o false según se cumpla o no, algo que por supuesto no deseamos por la sobrecarga que esto implicaría. Por todo esto podríamos modificar un poco nuestra definición de Specification para que en vez de devolver un booleano negando o afirmando el cumplimiento de una especificación determinada podríamos devolver una “expression” con el criterio a cumplir. En el siguiente fragmento de código tendríamos un esqueleto de nuestro contrato base con esta ligera modificación.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
<span class="rem">/// <summary></span> <span class="rem">/// Base contract for Specification pattern, for more information</span> <span class="rem">/// about this pattern see http://martinfowler.com/apsupp/spec.pdf</span> <span class="rem">/// or http://en.wikipedia.org/wiki/Specification_pattern.</span> <span class="rem">/// Really this is variant implementation for add feature of linq and</span> <span class="rem">/// lambda expression into this pattern.</span> <span class="rem">/// </summary></span> <span class="rem">/// <typeparam name="TEntity">Type of entity</typeparam></span> <span class="kwrd">public</span> <span class="kwrd">interface</span> ISpecification<TEntity> <span class="kwrd">where</span> TEntity : <span class="kwrd">class</span>,<span class="kwrd">new</span>() { <span class="rem">/// <summary></span> <span class="rem">/// Check if this specification is satisfied by a </span> <span class="rem">/// specific expression lambda</span> <span class="rem">/// </summary></span> <span class="rem">/// <returns></returns></span> Expression<Func<TEntity, <span class="kwrd">bool</span>>> SatisfiedBy(); } |
Profundizando
Llegados a este punto podríamos decir que ya tenemos la base y la idea de lo que queremos construir, ahora, solamente falta seguir las propias normas y guias de este patrón empezándonos a crear nuestras especificaciones directas o “hard coded specifications” y nuestras especificaciones compuestas, al estilo And, Or …
Según avanzas en esta aproximación uno se da cuenta de que tendrá que realizar un buen esfuerzo de trabajo con árboles de expresiones, algo en el que no todo el mundo está muy puesto, y a lo que se le tiene algo de miedo, del que uno se puede curar leyendo el excelente libro de mi amigo y colega Octavio 🙂
Mi primera intentona, y fallida a mi pesar, era similar a lo siguiente:
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
<span class="rem">/// <summary></span> <span class="rem">/// A logic AND Specification</span> <span class="rem">/// </summary></span> <span class="rem">/// <typeparam name="T">Type of entity that check this specification</typeparam></span> <span class="kwrd">public</span> <span class="kwrd">class</span> AndSpecification<T> : CompositeSpecification<T> <span class="kwrd">where</span> T : <span class="kwrd">class</span>,<span class="kwrd">new</span>() { <span class="preproc">#region</span> Members <span class="kwrd">private</span> ISpecification<T> _RightSideSpecification = <span class="kwrd">null</span>; <span class="kwrd">private</span> ISpecification<T> _LeftSideSpecification = <span class="kwrd">null</span>; <span class="preproc">#endregion</span> <span class="preproc">#region</span> Public Constructor <span class="rem">/// <summary></span> <span class="rem">/// Default constructor for AndSpecification</span> <span class="rem">/// </summary></span> <span class="rem">/// <param name="leftSide">Left side specification</param></span> <span class="rem">/// <param name="rightSide">Right side specification</param></span> <span class="kwrd">public</span> AndSpecification(ISpecification<T> leftSide, ISpecification<T> rightSide) { <span class="kwrd">if</span> (leftSide == (ISpecification<T>)<span class="kwrd">null</span>) <span class="kwrd">throw</span> <span class="kwrd">new</span> ArgumentNullException(<span class="str">"leftSide"</span>); <span class="kwrd">if</span> (rightSide == (ISpecification<T>)<span class="kwrd">null</span>) <span class="kwrd">throw</span> <span class="kwrd">new</span> ArgumentNullException(<span class="str">"rightSide"</span>); <span class="kwrd">this</span>._LeftSideSpecification = leftSide; <span class="kwrd">this</span>._RightSideSpecification = rightSide; } <span class="preproc">#endregion</span> <span class="preproc">#region</span> Composite Specification overrides <span class="rem">/// <summary></span> <span class="rem">/// Left side specification</span> <span class="rem">/// </summary></span> <span class="kwrd">public</span> <span class="kwrd">override</span> ISpecification<T> LeftSideSpecification { get { <span class="kwrd">return</span> _LeftSideSpecification; } } <span class="rem">/// <summary></span> <span class="rem">/// Right side specification</span> <span class="rem">/// </summary></span> <span class="kwrd">public</span> <span class="kwrd">override</span> ISpecification<T> RightSideSpecification { get { <span class="kwrd">return</span> _RightSideSpecification; } } <span class="rem">/// <summary></span> <span class="rem">/// <see cref="Microsoft.Samples.NLayerApp.Domain.Core.Specification.ISpecification"/></span> <span class="rem">/// </summary></span> <span class="rem">/// <returns><see cref="Microsoft.Samples.NLayerApp.Domain.Core.Specification.ISpecification"/></returns></span> <span class="kwrd">public</span> <span class="kwrd">override</span> Expression<Func<T, <span class="kwrd">bool</span>>> SatisfiedBy() { Expression<Func<T, <span class="kwrd">bool</span>>> left = _LeftSideSpecification.SatisfiedBy(); Expression<Func<T, <span class="kwrd">bool</span>>> right = _RightSideSpecification.SatisfiedBy(); InvocationExpression invokedExpr = Expression.Invoke(right, left.Parameters.Cast<Expression>()); <span class="kwrd">return</span> Expression.Lambda<Func<T, <span class="kwrd">bool</span>>>(Expression.AndAlso(left.Body, invokedExpr), left.Parameters); } <span class="preproc">#endregion</span> } |
Os podreís imaginar que la especificacion OR era similar a esta, simplemente modificando Expression.AndAlso por Expression.Or. Todo funcionaba de maravilla en mis test contra mi mock de los ObjectSet de Entity Framework hasta que pasando las pruebas de integración me di cuenta que el QueryProvider de Entity Framework no soportaba el método Invoke, al contrario que un QueryProvider sobre objetos en memoria, y que por lo tanto esta forma no me servía :-(.
Aunque ahora mismo pondré la solución fijaros como, de una forma elegante, manteniendo el principio de separación de responsabilidades y dejando un concepto de negocio como es un tipo especial de búsqueda perfectamente explícito, se podrían declarar especificaciones como la siguiente
NOTA: Seguramente que alguna vez ha pensado como hacer consultas distintas en función de parámetros usando conjunciones o disjunciones de expressiones, pues esta es una posible solución.
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 55 56 57 58 59 60 61 62 63
|
<span class="rem">/// <summary></span> <span class="rem">/// AdHoc specification for finding orders</span> <span class="rem">/// by shipping values</span> <span class="rem">/// </summary></span> <span class="kwrd">public</span> <span class="kwrd">class</span> OrderShippingSpecification :Specification<Order> { <span class="preproc">#region</span> Members <span class="kwrd">string</span> _ShippingName = <span class="kwrd">default</span>(String); <span class="kwrd">string</span> _ShippingAddress = <span class="kwrd">default</span>(String); <span class="kwrd">string</span> _ShippingCity = <span class="kwrd">default</span>(String); <span class="kwrd">string</span> _ShippingZip = <span class="kwrd">default</span>(String); <span class="preproc">#endregion</span> <span class="preproc">#region</span> Constructor <span class="rem">/// <summary></span> <span class="rem">/// Default constructor for this specification</span> <span class="rem">/// </summary></span> <span class="rem">/// <param name="shippingName">Shipping Name or null for not include this value in search</param></span> <span class="rem">/// <param name="shippingAddress">Shipping Address or null for not include this vlaue in search</param></span> <span class="rem">/// <param name="shippingCity">Shipping City or null for not include this value in search</param></span> <span class="rem">/// <param name="shippingZip">Shipping Zip or null for not include this value in search</param></span> <span class="kwrd">public</span> OrderShippingSpecification(<span class="kwrd">string</span> shippingName,<span class="kwrd">string</span> shippingAddress,<span class="kwrd">string</span> shippingCity,<span class="kwrd">string</span> shippingZip) { _ShippingName = shippingName; _ShippingAddress = shippingAddress; _ShippingCity = shippingCity; _ShippingZip = shippingZip; } <span class="preproc">#endregion</span> <span class="preproc">#region</span> Specification Overrides <span class="rem">/// <summary></span> <span class="rem">/// <see cref=" Microsoft.Samples.NLayerApp.Domain.Core.Specification.Specification{TEntity}"/></span> <span class="rem">/// </summary></span> <span class="rem">/// <returns><see cref=" Microsoft.Samples.NLayerApp.Domain.Core.Specification.Specification{TEntity}"/></returns></span> <span class="kwrd">public</span> <span class="kwrd">override</span> System.Linq.Expressions.Expression<Func<Order, <span class="kwrd">bool</span>>> SatisfiedBy() { Specification<Order> beginSpec = <span class="kwrd">new</span> TrueSpecification<Order>(); <span class="kwrd">if</span> (_ShippingName != <span class="kwrd">null</span>) beginSpec &= <span class="kwrd">new</span> DirectSpecification<Order>(o =>o.ShippingName!=<span class="kwrd">null</span> && o.ShippingName.Contains(_ShippingName)); <span class="kwrd">if</span> (_ShippingAddress != <span class="kwrd">null</span>) beginSpec &= <span class="kwrd">new</span> DirectSpecification<Order>(o => o.ShippingAddress !=<span class="kwrd">null</span> && o.ShippingAddress.Contains(_ShippingAddress)); <span class="kwrd">if</span> (_ShippingCity != <span class="kwrd">null</span>) beginSpec &= <span class="kwrd">new</span> DirectSpecification<Order>(o => o.ShippingCity != <span class="kwrd">null</span> && o.ShippingCity.Contains(_ShippingCity)); <span class="kwrd">if</span> (_ShippingZip != <span class="kwrd">null</span>) beginSpec &= <span class="kwrd">new</span> DirectSpecification<Order>(o => o.ShippingZip != <span class="kwrd">null</span> && o.ShippingZip.Contains(_ShippingZip)); <span class="kwrd">return</span> beginSpec.SatisfiedBy(); } <span class="preproc">#endregion</span> } |
Una posible solución para especificaciones And y OR
Os podreís imaginar que seguramente existe más de una aproximación para este tema y que yo probablemente me haya decantado por una aproximación algo dura, pero la verdad es que me parecía la más adecuada. Releyendo algún post sobre el tema me acorde de la serie que Matt Warren tenía sobre el tema y como hacía uso del patrón Visitor para evaluar las expresiones, ExpressionVisitor. Además, navegando, me encontré con un ejemplo aceptable que resolvía este tema en el blog de Colling Meek el cual fué la solución que adopte.
Dada la explicación, lo que necesitamos es la siguiente clase que nos haga una recomposición de las expressiones en vez de un InvocationExpression, esta clase de apoyo es la siguiente:
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
|
<span class="rem">/// <summary></span> <span class="rem">/// Extension methods for add And and Or with parameters rebinder</span> <span class="rem">/// </summary></span> <span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">class</span> ExpressionBuilder { <span class="kwrd">public</span> <span class="kwrd">static</span> Expression<T> Compose<T>(<span class="kwrd">this</span> Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge) { <span class="rem">// build parameter map (from parameters of second to parameters of first)</span> var map = first.Parameters.Select((f, i) => <span class="kwrd">new</span> { f, s = second.Parameters[i] }).ToDictionary(p => p.s, p => p.f); <span class="rem">// replace parameters in the second lambda expression with parameters from the first</span> var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body); <span class="rem">// apply composition of lambda expression bodies to parameters from the first expression </span> <span class="kwrd">return</span> Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters); } <span class="kwrd">public</span> <span class="kwrd">static</span> Expression<Func<T, <span class="kwrd">bool</span>>> And<T>(<span class="kwrd">this</span> Expression<Func<T, <span class="kwrd">bool</span>>> first, Expression<Func<T, <span class="kwrd">bool</span>>> second) { <span class="kwrd">return</span> first.Compose(second, Expression.And); } <span class="kwrd">public</span> <span class="kwrd">static</span> Expression<Func<T, <span class="kwrd">bool</span>>> Or<T>(<span class="kwrd">this</span> Expression<Func<T, <span class="kwrd">bool</span>>> first, Expression<Func<T, <span class="kwrd">bool</span>>> second) { <span class="kwrd">return</span> first.Compose(second, Expression.Or); } } |
La definición completa por lo tanto de una especificación And queda como sigue:
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68
|
<span class="rem">/// <summary></span> <span class="rem">/// A logic AND Specification</span> <span class="rem">/// </summary></span> <span class="rem">/// <typeparam name="T">Type of entity that check this specification</typeparam></span> <span class="kwrd">public</span> <span class="kwrd">class</span> AndSpecification<T> : CompositeSpecification<T> <span class="kwrd">where</span> T : <span class="kwrd">class</span>,<span class="kwrd">new</span>() { <span class="preproc">#region</span> Members <span class="kwrd">private</span> ISpecification<T> _RightSideSpecification = <span class="kwrd">null</span>; <span class="kwrd">private</span> ISpecification<T> _LeftSideSpecification = <span class="kwrd">null</span>; <span class="preproc">#endregion</span> <span class="preproc">#region</span> Public Constructor <span class="rem">/// <summary></span> <span class="rem">/// Default constructor for AndSpecification</span> <span class="rem">/// </summary></span> <span class="rem">/// <param name="leftSide">Left side specification</param></span> <span class="rem">/// <param name="rightSide">Right side specification</param></span> <span class="kwrd">public</span> AndSpecification(ISpecification<T> leftSide, ISpecification<T> rightSide) { <span class="kwrd">if</span> (leftSide == (ISpecification<T>)<span class="kwrd">null</span>) <span class="kwrd">throw</span> <span class="kwrd">new</span> ArgumentNullException(<span class="str">"leftSide"</span>); <span class="kwrd">if</span> (rightSide == (ISpecification<T>)<span class="kwrd">null</span>) <span class="kwrd">throw</span> <span class="kwrd">new</span> ArgumentNullException(<span class="str">"rightSide"</span>); <span class="kwrd">this</span>._LeftSideSpecification = leftSide; <span class="kwrd">this</span>._RightSideSpecification = rightSide; } <span class="preproc">#endregion</span> <span class="preproc">#region</span> Composite Specification overrides <span class="rem">/// <summary></span> <span class="rem">/// Left side specification</span> <span class="rem">/// </summary></span> <span class="kwrd">public</span> <span class="kwrd">override</span> ISpecification<T> LeftSideSpecification { get { <span class="kwrd">return</span> _LeftSideSpecification; } } <span class="rem">/// <summary></span> <span class="rem">/// Right side specification</span> <span class="rem">/// </summary></span> <span class="kwrd">public</span> <span class="kwrd">override</span> ISpecification<T> RightSideSpecification { get { <span class="kwrd">return</span> _RightSideSpecification; } } <span class="rem">/// <summary></span> <span class="rem">/// <see cref="Microsoft.Samples.NLayerApp.Domain.Core.Specification.ISpecification"/></span> <span class="rem">/// </summary></span> <span class="rem">/// <returns><see cref="Microsoft.Samples.NLayerApp.Domain.Core.Specification.ISpecification"/></returns></span> <span class="kwrd">public</span> <span class="kwrd">override</span> Expression<Func<T, <span class="kwrd">bool</span>>> SatisfiedBy() { Expression<Func<T, <span class="kwrd">bool</span>>> left = _LeftSideSpecification.SatisfiedBy(); Expression<Func<T, <span class="kwrd">bool</span>>> right = _RightSideSpecification.SatisfiedBy(); <span class="kwrd">return</span> (left.And(right)); } <span class="preproc">#endregion</span> } |
¿Por dónde continuamos?
Dentro de la jerarquía de especificaciones que se propone en el documento de Eric y Fowler podemos encontrar desde la especificación Not hasta una base para LeafSpecifications que tendríamos que construir… es decir, un poquito más de curro para poder disfrutar…
Un resumen
La verdad espero que le encontréis utilidad a esta forma de implementar el patrón Specification, y por supuesto, si no lo conociais espero que os levante un poco la curiosidad y veais las bondades que este puede tener dentro de vuestros desarrollos, por supuesto me encantaría ver vuestras opiniones al respecto y si realmente esta forma de encajar criterios es adecuado para vuestras soluciones…
Saludos
Unai Zorrilla