Last active
May 24, 2016 15:30
-
-
Save margusmartsepp/675fe83952bbf0b444cbfba0a53b9962 to your computer and use it in GitHub Desktop.
Generic Specification pattern
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq.Expressions; | |
namespace GenericSpecificationPattern | |
{ | |
public interface ISpecification<T> | |
{ | |
Expression<Func<T, bool>> Act { get; } | |
bool IsSatisfiedBy(T entity); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
using System.Linq.Expressions; | |
namespace GenericSpecificationPattern | |
{ | |
public class Specification<T> : ISpecification<T> | |
{ | |
public static implicit operator Expression<Func<T, bool>>(Specification<T> specification) | |
{ | |
return specification.Act; | |
} | |
public Expression<Func<T, bool>> Act { get; } | |
protected Specification(Expression<Func<T, bool>> expression) | |
{ | |
Act = expression; | |
} | |
protected Specification(ISpecification<T> specification) : this(specification.Act) | |
{ | |
} | |
public bool IsSatisfiedBy(T entity) | |
{ | |
var query = new[] {entity}.AsQueryable(); | |
return query.Any(Act); | |
} | |
public Specification<T> And(ISpecification<T> specification) | |
{ | |
var andAlso = this.Binary<ISpecification<T>, T>(ExpressionType.AndAlso, specification); | |
return new Specification<T>(andAlso); | |
} | |
public Specification<T> Or(ISpecification<T> specification) | |
{ | |
var orElse = this.Binary<ISpecification<T>, T>(ExpressionType.OrElse, specification); | |
return new Specification<T>(orElse); | |
} | |
public Specification<T> Not => new Specification<T>(this.Unary<ISpecification<T>, T>(ExpressionType.IsFalse)); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
using System.Linq.Expressions; | |
namespace GenericSpecificationPattern | |
{ | |
public static class SpecificationExtensions | |
{ | |
internal static Expression<Func<T, bool>> Unary<TS, T>(this TS thisSpecification, ExpressionType operatorExpression) where TS : ISpecification<T> | |
{ | |
var expressionBody = Expression.MakeUnary(operatorExpression, thisSpecification.Act.Body, typeof(T)); | |
var expressionLambda = Expression.Lambda<Func<T, bool>>(expressionBody, LeftParameters<TS, T>(thisSpecification)); | |
return expressionLambda; | |
} | |
internal static Expression<Func<T, bool>> Binary<TS, T>(this TS thisSpecification, ExpressionType expressionType, TS thatSpecification) where TS : ISpecification<T> | |
{ | |
var leftParameters = LeftParameters<TS, T>(thisSpecification); | |
var rightParameters = GetRightParameters<TS, T>(thatSpecification, leftParameters); | |
var expressionBody = Expression.MakeBinary(expressionType, thisSpecification.Act.Body, rightParameters); | |
var expressionLambda = Expression.Lambda<Func<T, bool>>(expressionBody, leftParameters); | |
return expressionLambda; | |
} | |
private static ParameterExpression LeftParameters<TS, T>(TS thisSpecification) where TS : ISpecification<T> | |
{ | |
return thisSpecification.Act.Parameters.First(); | |
} | |
private static Expression GetRightParameters<TS, T>(TS thatSpecification, ParameterExpression leftParameters) where TS : ISpecification<T> | |
{ | |
return ReferenceEquals(leftParameters, thatSpecification.Act.Parameters.First()) | |
? thatSpecification.Act.Body | |
: Expression.Invoke(thatSpecification.Act, leftParameters); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Linq; | |
using System.Linq.Expressions; | |
using NUnit.Framework; | |
namespace GenericSpecificationPattern | |
{ | |
[TestFixture] | |
public class SpecificationTests | |
{ | |
private IQueryable<Movie> _movies; | |
private MpaaRatingExact _mpaaRatingG; | |
private MpaaRatingExact _mpaaRatingPg13; | |
private MpaaRatingExact _mpaaRatingR; | |
private MpaaRatingIsGreaterThan _mpaaRatingGreaterThanPg13; | |
[SetUp] | |
public void Init() | |
{ | |
_movies = new[] | |
{ | |
new Movie(MpaaRating.G), | |
new Movie(MpaaRating.Pg13), | |
new Movie(MpaaRating.R) | |
}.AsQueryable(); | |
_mpaaRatingG = new MpaaRatingExact(MpaaRating.G); | |
_mpaaRatingPg13 = new MpaaRatingExact(MpaaRating.Pg13); | |
_mpaaRatingR = new MpaaRatingExact(MpaaRating.R); | |
_mpaaRatingGreaterThanPg13 = new MpaaRatingIsGreaterThan(MpaaRating.Pg13); | |
} | |
[Test] | |
public void FilterExample() | |
{ | |
var isApredicate = _mpaaRatingG; | |
var isA = _movies.AsQueryable().Where(isApredicate).ToList(); | |
Assert.That(isA.Count, Is.EqualTo(1)); | |
Assert.That(isA.First().MpaaRating, Is.EqualTo(MpaaRating.G)); | |
var isBpredicate = _mpaaRatingPg13; | |
var isB = _movies.AsQueryable().Where(isBpredicate).ToList(); | |
Assert.That(isB.Count, Is.EqualTo(1)); | |
Assert.That(isB.First().MpaaRating, Is.EqualTo(MpaaRating.Pg13)); | |
var isCpredicate = _mpaaRatingR; | |
var isC = _movies.AsQueryable().Where(isCpredicate).ToList(); | |
Assert.That(isC.Count, Is.EqualTo(1)); | |
Assert.That(isC.First().MpaaRating, Is.EqualTo(MpaaRating.R)); | |
} | |
[Test] | |
public void NotExample() | |
{ | |
var isNotApredicate = _mpaaRatingG.Not; | |
var isNotA = _movies.Where(isNotApredicate).ToList(); | |
Assert.That(isNotA.Count, Is.EqualTo(2)); | |
} | |
[Test] | |
public void AndExample() | |
{ | |
var isApredicate = _mpaaRatingG; | |
var isBpredicate = _mpaaRatingPg13; | |
var isAandBpredicate = isApredicate.And(isBpredicate); | |
var isAandB = _movies.Where(isAandBpredicate).ToList(); | |
Assert.That(isAandB.Count, Is.EqualTo(0)); | |
} | |
[Test] | |
public void OrExample() | |
{ | |
var isAorBpredicate = _mpaaRatingPg13.Or(_mpaaRatingG); | |
var isAorB = _movies.AsQueryable().Where(isAorBpredicate).ToList(); | |
Assert.That(isAorB.Count, Is.EqualTo(2)); | |
} | |
[Test] | |
public void OrChainExample() | |
{ | |
var isAorBorCpredicate = _mpaaRatingPg13.Or(_mpaaRatingG).Or(_mpaaRatingR); | |
var isAorBorC = _movies.AsQueryable().Where(isAorBorCpredicate).ToList(); | |
Assert.That(isAorBorC.Count, Is.EqualTo(3)); | |
} | |
[Test] | |
[TestCase(MpaaRating.Pg13, true)] | |
[TestCase(MpaaRating.R, false)] | |
public void IsSatisfiedByExample(MpaaRating mpaaRating, bool expectedResult) | |
{ | |
var result = _mpaaRatingPg13.IsSatisfiedBy(new Movie(mpaaRating)); | |
Assert.That(result, Is.EqualTo(expectedResult)); | |
} | |
[Test] | |
public void OrChainMultiOriginExample() | |
{ | |
var isGreaterThanBorBpredicate = _mpaaRatingGreaterThanPg13.Or(_mpaaRatingPg13); | |
var isGreaterThanBorB = _movies.AsQueryable().Where(isGreaterThanBorBpredicate).ToList(); | |
Assert.That(isGreaterThanBorB.Count, Is.EqualTo(2)); | |
} | |
} | |
public class MpaaRatingExact : Specification<Movie> | |
{ | |
public MpaaRatingExact(MpaaRating rating) | |
: base(movie => movie.MpaaRating == rating) {} | |
} | |
public class MpaaRatingIsGreaterThan : Specification<Movie> | |
{ | |
public MpaaRatingIsGreaterThan(MpaaRating rating) | |
: base(movie => movie.MpaaRating > rating) {} | |
} | |
public class Movie | |
{ | |
public MpaaRating MpaaRating { get; } | |
public Movie(MpaaRating mpaaRating) | |
{ | |
MpaaRating = mpaaRating; | |
} | |
} | |
public enum MpaaRating | |
{ | |
G = 1, | |
Pg13 = 2, | |
R = 3 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment