Code Monkey home page Code Monkey logo

nspecifications's Introduction

NSpecifications - Specifications on .Net

What is the Specification Pattern?

When selecting a subset of objects, it allows to separate the statement of what kind of objects can be selected from the object that does the selection.

Ex:

A cargo has a separate storage specification to describe what kind of container can contain it. The specification object has a clear and limited responsibility, which can be separated and decoupled from the domain object that uses it.

What can you do with Specification Pattern?

  1. Validate an object or check that only suitable objects are used for a certain role
  2. Select a subset of objects based a specified criteria, and refresh the selection at various times
  3. Describe what an object might do, without explaining the details of how the object does it, but in such a way that a candidate might be built to fulfill the requirement

What are the advantages of this library over others

  1. One-liner specifications
  2. Query an in-memory collection and databases using same code
  3. Easy composition of specifications
  4. Use a Specification in place of any Expression<Func<T, bool>> and Func<T, bool>
  5. Combine a boolean with a Specification in order to negate it when value is false
  6. Is / Are extension methods

One-liner specifications

var greatWhiskey = new Spec<Drink>(drink => drink.Type == "Whiskey" && drink.Age >= 11);

Query an in-memory collection and databases using same code

Database query dataContext.Places.Where(cheapPlacesToEat & open)
Filter in-memory collection list.Where(cheapPlacesToEat & open)

Easy composition of specifications

var greatWhiskey = new Spec<Drink>(drink => drink.Type == "Whiskey" && drink.Age >= 11);
var fresh = new Spec<Drink>(drink => drink.Extras.Contains("Ice"));
var myFavourite = greatWhiskey & fresh;
var yourFavourite = greatWhiskey & !fresh;

Use a Specification in place of any Expression<Func<T, bool>> and Func<T, bool>

var books = new List<Book>();
(...) 
var boringBookSpec = new Spec<Book>(book => book.NumberOfPages > 300);
var boringBooks = books.Where(boringBookSpec);

Combine a boolean with a Specification in order to negate it when value is false

static ASpec<Place> CheapPlace = new Spec<Place>(p => p.Price < 10);

// If isCheap is not set it will simply return all places
// If isCheap is true it will return only Cheap places
// If isCheap is false it will return all places that are not Cheap
public Places[] FindPlaces(bool? isCheap = null) {
    
    // Initialize spec with an all-inclusive specification
    var spec = Spec.Any<Place>();
    
    // Apply filter only if a filter was specified
    if (isCheap.HasValue)
        spec = spec & (CheapPlace == isCheap.Value);
    
    // Let the repository search it in the DB
    return repository.Find(spec);
}

Is / Are extension methods

var cold = new Spec<Drink>(d => d.TemperatureCelsius < 2);

if (candidateDrink.Is(cold))
    Console.Write("Candidate drink is cold.");
    
if (new[] { blackberryJuice, appleJuice, orangeJuice }.Are(cold))
    Console.Write("All candidate drinks are cold.");

Other possible reasons

  • Easily finding all existing queries in the source code with a simple search for usages of Spec
  • Write more readable, manageable and elegant code

This Library was based on Eric Evans book

Specifications are described by Eric Evans as separate, combinable, rule objects, based on the concept of predicates but more specialized. "A SPECIFICATION is a predicate that determines if an object does or does not satisfy some criteria."

Technical Documentation

The Old Way: ISpecification<T>

The most basic form of Specification used to be implemented via a simple interface, the probem is that it could only be used for in-memory queries.

public class BoringBookSpec : ISpecification<Book> {
    public IsSatisfiedBy(Book book)
    {
        return book.Rating < 3 && book.Pages > 450;
    }
}

We could call it like this:

var isBoring = new BoringBookSpec().IsSatisfiedBy(book);

Or like this:

var boringBookSpec = new BoringBookSpec();
var boringBooks = allBooks.Where(boringBookSpec.IsSatisfiedBy);

ISpecification<T> could also do composition:

var boringBookSpec = lowRatedBookSpec.And(bigBookSpec);

This way of using Specifications had some cons:

  • it was very verbose, because every new specification had to be defined in a new class
  • it couldn't be converted to database queries
  • and, or and not "operators" were implemented as methods, not as real operators.

Let's see now a more intuitive way to create and manage Specifications.

The New Way: new Spec<T>(expression)

ISpecification<T> is now extended by the ASpec<T> abstract class. This abstract class enables a set of new features such as:

  • real operators (&, |, !, ==, !=)
  • implicit operators that make any Specification to be interchangeable with Expression<Func<T, bool>> and Func<T, bool>

ASpec<T> is an abstract class therefore it can't be directly instantiated. Instead we use Spec<T> to create a new instance. This generic class should be good for 99.9% of your specifications but if you need to make your own implementation of Specification you can always extend it from ASpec<T>.

ASpec<Car> fastCar = new Spec<Car>(c => c.VelocityKmH > 200);

Pros of using this generic implementation

  • only one line needed to define a new specification
  • it returns ASpec<T> class (and not just an interface) so that we can now use real operators for making composition;
  • it stores a Linq Expression in the created instance, therefore it can be easily used by any IQueryable<T>, suitable for querying DBs.

Example:

var greatWhiskey = new Spec<Drink>(drink => drink.Type == "Whiskey" && drink.Age >= 11);
var fresh = new Spec<Drink>(drink => drink.Extras.Contains("Ice"));
var myFavouriteDrink = repository.FindOne(greatWhiskey & fresh);

Let me dig into the details:

  • Following the example from Eric Evans book I usually name my specifications as objects rather than predicates. I could name it greatWhiskeySpec or greatWhiskey for short but not isGreatWhiskey. My aim is to make it clear that a specification is be a bit more than just a simple boolean expression.
  • As you may have noticed by now this code now is much less verbose than when we were using ISpecification<T>.
  • I can now compose specifications using friendly operators: ! (not), & (and), | (or), == (compare spec with a boolean).
  • I'm passing my specifications directly as a parameter to a repository method that expects a Linq Expression, but it receives a specification instead and that's converted automatically.

Real Use Cases

Let's say that I need to search for users by name (if specified) and by their locked-out state. This is how I'd do it.

First I'd have to find a meaningful place to store my specifications:

  • It could be in a static class called Specifications or Specs for short. I could invoke it like this: Specs.LockedOutUser.
  • Or it could be in a static classe per entity type like (UserSpecs, UserGroupSpecs, ...). Ex: UserSpecs.LockedOut .
  • It could be in a static members inside the User entity. Ex: User.LockedOut. This is my favourite, because specifications are always tightly coupled to entities and this can make maintenance easier.

The only thing I'd like to note here is that hosting specifications in static members do not present any problem for Unit Testing. Specifications usually don't need to be mocked.

Let's blend the specifications into the User class.

public class User 
{
    public string Name { get; }
    public bool IsLockedOut { get; }
	
    // Spec for LockedOut
    public static readonly ASpec<User> LockedOut = new Spec<User>(user => user.IsLockedOut);  
    	
    // Spec for NamedLike
    public static ASpec<User> NamedLike(string text) 
    {
    	return new Spec<User>(user => name.Contains(text));
    }
}

While in the first member LockedOut is instantiated once (it's a readonly static field), the second member NamedLike need to be instantiated for every given text parameter (it's a static factory method). That's the way that specifications need to be done when they need to receive parameters.

When I need to make my query I can do it like this:

public IEnumerable<User> FindByNameAndLockedStatus(string name = null, bool? isLockedOut = null) {
    
    // Initialize the spec with an all inclusive spec
    var spec = Spec.Any<User>;
    
    // Apply Name filter
    if (!string.IsNullOrEmpty(name))
    	spec = spec & User.NamedLike(name);
    
    // Apply LockeOut filter
    if (isLockedOut.HasValue)
    	spec = spec & (User.LockedOut == isLockedOut.Value);
    
    return _repository.Find(spec);
}

Install it from NuGet Gallery

Install-Package NSpecifications

References:

http://martinfowler.com/apsupp/spec.pdf

https://domainlanguage.com/ddd/

nspecifications's People

Contributors

alanparr avatar jnicolau avatar miholler avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

nspecifications's Issues

Reusing ASpec for ternary models

Hi,

I am currently exploring whether this library fits my project's requirement.

How would you suggest reusing the specification pattern on ternary models or related models? For example, I have Users, Businesses and BusinessUser ternary table.

If we already have this spec on User domain model,
public static ASpec<User> Active => new Spec<User>(user => user.IsActive);

How do I reuse the spec above for my BusinessUser ternary model?

Currently, below seems to be the only way to define the spec again.
public static ASpec<BusinessUser> ActiveUser => new Spec<BusinessUser>(bizUser => bizUser.User.IsActive);

'And<T>' specification implements 'IOrSpecification<T>' instead of 'IAndSpecification<T>'

I realized during tests that the ASpec<T>.And<T> inner class implements the wrong interface, IOrSpecification<T>, instead of implementing IAndSpecification<T> as it should.

Here is the offending line:
https://github.com/jnicolau/NSpecifications/blob/68e56cef95f52b23a0231bd49752bb2d9997b348/Nspecifications/ASpec.cs#L143

Compare to Or<T>, which is correct:
https://github.com/jnicolau/NSpecifications/blob/68e56cef95f52b23a0231bd49752bb2d9997b348/Nspecifications/ASpec.cs#L176

As I understand it, these interfaces are not actively used anywhere so it doesn't represent a bug for NSpecifications itself. However, since they are public (as are their implementations) consuming code could attempt using them (like I did) and run into this weird situation.

Nuget. Warning!

package 'NSpecifications 1.0.1' was restored using '.netframework version=v4.6.1' instead of the project target framework '.NetCoreApp,version=v2.0' This package may not be fully compatible with your project.

Please update nuget package

Nuget Updates

Is there a recommended workflow for consuming the updates that have been made? I see plenty of commits that have been made since that last push to the NuGet feed.

Would the maintainer be receptive to a PR with a CI pipeline that would deploy to the NuGet feed?

I'm starting a .NET 8 project, and I believe this package would serve especially well for us. I'd rather not clone it or fork it.

Please let me know if there's any small help I can provide.

Any plans to support Async Specifications?

Hi João Oliveira
Thanks for the great job!
Am I doing it wrong if I need to make a DB/API call in one of my Specs.
I know that I might be able to make those calls before using the Spec, however it would be much cleaner if I can keep that knowledge to the Speck as well.
Thanks.

Add 'because' to ASpec

Hi, great framework :)

Would it be possible/meaningful to add 'because' parameter to the ASpec implementation?

ASpec<Drink> whiskeySpec = new Spec<Drink>(d => d.Name.ToLower() == "whiskey", because = "I like whiskey");

The background story:
We are developing API and our DTO`s have a validation pipeline build-out of specifications. If one of them fails, I would like to know which one.

For example: I do not accept this drink, because: I like whiskey
Thanks

Support OrderBy, OrderByDescending, Take, Skip

Would it be possible to support OrderBy, OrderByDescending, Take, Skip methods?

I could do it, but I would need some guidance on how to approach it. I suspect it would require some change since this would expand the functionality beyond the where predicate.

Expects the database model to match the domain model

Your implementation is nicely done, however it seems to require that the object type used in the data access code will match exactly with the type used in the domain model. This is not necessarily true, particularly when working with a clean architecture approach.

One of the benefits you are seeking with NSpecifications is for the domain model to be able to specify which documents it needs without any dependency on the database, but also to be able to apply the specification in the database rather than only in memory. This means either that the database and domain need to be using the same object types, or else the repository that accepts the specification needs to do some additional complex mapping before sending the specification on to the database through a mapping object.

Is there any way to use NSpecifications that bypasses this problem, i.e. avoids a dependency on the database from the application domain?

Using Specification on related collections as subquery that's database safe

To give a little bit of context, I'm utilizing Entity Framework (EF) and am using this library and am having some trouble with the expressions not being properly translated into SQL when working with nested related objects and their collections and wonder if you knew how to resolve or if it's a known issue. In the scenario outlined below, EF throws: System.InvalidOperationException : Internal .NET Framework Data Provider error 1025.

Let's say I had the following entity classes:

class Student {
     public Employee Employee {get; set; }
}

class Employee {
     public string Name {get; set;}
     public ICollection<Position> Positions {get; set;}
}

class Position {
      public DateTime HireDate {get;set;}
}

And let's say I wanted to write a spec that gets passed to EF that gets students that are employees that have positions after a certain hire date. The following doesn't translate or work because it's nested:

class StudentEmployeeHiredAfter2000: ASpec<Student> {
     private readonly PositionHiredAfter2000 positionHiredAfter2000spec;

     public WasHiredAfter2000() {
          positionHiredAfter2000spec= new PositionHiredAfter2000();
     }

     public override Expression<Func<Student, bool>> Expression => s => s.Employee != null && s.Employee.Positions.Any(positionHiredAfter2000spec)
}

class PositionHiredAfter2000 : ASpec<Position> {
     private readonly DateTime dateToCheck;

     public WasHiredAfter2000() {
          dateToCheck = new DateTime(2000, 12, 31);
     }

     public override Expression<Func<Position, bool>> Expression => p => p.HireDate > dateToCheck;
}

and run
dbContext.Students.Where(new StudentEmployeeHiredAfter2000()).ToList();

Is there any way to re-use the specifications nested inside of another specification on a nested related collection like above?

Reusing Spec for children models throws an Exception

I have uploaded the repo for this issue here: https://github.com/ronnypmuliawan/nspec-issue

Environment:
ASP.NET Core 2.2.0
EF Core 2.2.4

Assuming simple classes below:

public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }

        public ICollection<Post> Posts { get; set; }
    }

public class Post
    {
        public static ASpec<Post> ContainsFirst => new Spec<Post>(s => s.Title.Contains("First"));

        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }

I am expecting that a query like this should work:

var blogs = await db.Blogs
                    .Include(s => s.Posts)
                    .Where(s => s.Posts.Any(Post.ContainsFirst))
                    .ToListAsync();

But unfortunately, it throws the following error:
System.NotSupportedException
HResult=0x80131515
Message=Could not parse expression 's.Posts.Any(Convert(__ContainsFirst_0, Func2))': The given arguments did not match the expected arguments: Object of type 'System.Linq.Expressions.UnaryExpression' cannot be converted to type 'System.Linq.Expressions.LambdaExpression'. Source=Remotion.Linq StackTrace: at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.CreateExpressionNode(Type nodeType, MethodCallExpressionParseInfo parseInfo, Object[] additionalConstructorParameters) at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.Parse(String associatedIdentifier, IExpressionNode source, IEnumerable1 arguments, MethodCallExpression expressionToParse)
at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseMethodCallExpression(MethodCallExpression methodCallExpression, String associatedIdentifier)
at Remotion.Linq.Parsing.Structure.QueryParser.GetParsedQuery(Expression expressionTreeRoot)
at Remotion.Linq.Parsing.ExpressionVisitors.SubQueryFindingExpressionVisitor.Visit(Expression expression)
at System.Linq.Expressions.ExpressionVisitor.VisitLambda[T](Expression1 node) at System.Linq.Expressions.Expression1.Accept(ExpressionVisitor visitor)
at System.Linq.Enumerable.SelectListPartitionIterator2.ToArray() at System.Linq.Enumerable.ToArray[TSource](IEnumerable1 source)
at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.Parse(String associatedIdentifier, IExpressionNode source, IEnumerable1 arguments, MethodCallExpression expressionToParse) at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseMethodCallExpression(MethodCallExpression methodCallExpression, String associatedIdentifier) at Remotion.Linq.Parsing.Structure.QueryParser.GetParsedQuery(Expression expressionTreeRoot) at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileAsyncQueryCore[TResult](Expression query, IQueryModelGenerator queryModelGenerator, IDatabase database) at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable1.System.Collections.Generic.IAsyncEnumerable<TResult>.GetEnumerator() at System.Linq.AsyncEnumerable.<Aggregate_>d__63.MoveNext() in D:\a\1\s\Ix.NET\Source\System.Interactive.Async\Aggregate.cs:line 128
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at ASpecInvestigation.Program.

d__0.MoveNext() in C:\Users\ronny\Documents\Visual Studio 2017\Projects\Playground1\ASpecInvestigation\Program.cs:line 26
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at ASpecInvestigation.Program.(String[] args)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.