Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for the (logical) combination of specifications #254

Closed
AndreErb opened this issue Mar 2, 2022 · 2 comments
Closed

Add support for the (logical) combination of specifications #254

AndreErb opened this issue Mar 2, 2022 · 2 comments

Comments

@AndreErb
Copy link

AndreErb commented Mar 2, 2022

I wonder if it would be possible to add support for the (logical) combination of specifications, using operators like (&, |, !, ==, !=)

I have watched your guys' YouTube videos about this Specification Library and you too mentioned the problem, that specification names tend to become large, when multiple logic/rules are contained. You discussed different naming approaches to mitigate this, however it feels like the real problem is somehow a consequence of the pattern itself. If one has "SpecA" and "SpecB" it seems to violate the DRY principle as well, if one had to create "SpecAB" with both rules repeated, just in order to combine them.

This seems to be related to Add support for updating specifications
Also ASbeletsky's NSpecification library has this kind feature and I saw this approach on different web blogs, too.

Not sure if it is possible (diserable) at all, since specs also project the result and therefore might not even be compatible at all.
However, it seems hard to extend this for ourselves and if it could or should be done, it feels like should be done in the library itself.

BTW: Thanks for this library and your great work!

@fiseni
Copy link
Collaborator

fiseni commented Mar 12, 2022

Hey @AndreErb ,

We had lengthy conversations on this topic on many occasions. There are several issues with that approach, and I do believe it will do more harm than good.

  • The specifications in this library are not intended to hold only criteria and validation rules, but also information required for query generation. That said, specA can contain Take(10) and specB contains Take(20). What do you expect to happen during the logical combination of these specs? You may say, ok let's ignore all the query-related state and apply only Where and Search perhaps? But, I'm sure many consumers would request to combine Include statements too (or something else). You get my point, it will be hard to find a consensus here. On top of that, suddenly library becomes full of implicit conventions. The user should have some prior knowledge of how the library works in order to use it correctly. And that's definitely not the place you want to be.
  • Some of the logical operators can not be applied without modifying your original expressions. In the case of AND operator, you can just append it during evaluation Where(x => logic1).Where(x => logic2). But, that's not the case with OR, you have to create a new expression out of logic1 and logic, e.g. Where(x => logic1 || logic2). We tend to minimize these actions wherever possible, since they may have performance drawbacks. We want specs to be really lightweight.
  • If you start combining the specs all over your code, how is that different than just simply using LINQ all over the place? Not saying that using LINQ everywhere is bad per se, it's your choice. But, probably you started to use specs to avoid doing that in your code. So, it defies the purpose somehow.

Anyhow, we still don't want to confine the usage, and we tried to create an extensible infrastructure so the users can define their desired custom behavior.

  • We also introduced specification validators (check here). Since this has to do purely with validation, you can apply all kinds of logical operations, e.g. spec1.IsSatisfiedBy(customer) || spec2.IsSatisfiedBy(customer). The validation infrastructure is also extensible, so you can write your own custom rules where you can implement all kinds of compositions.
  • We'll allow updating the specifications, more specifically we'll expose the builder. So, you will be able to write your extensions for various composition rules. Sample here.
  • We should be aware that DRY creates an implicit coupling. So, not necessarily want to apply that principle everywhere. But, if you don't like having repeating logic in the specs, you can always define a base spec that holds the common rules
public class CustomerBaseSpec : Specification<Customer>
{
  public CustomerBaseSpec()
  {
    Query.Include(x => x.Stores);
  }
}

public class CustomerByIdSpec : CustomerBaseSpec
{
  public CustomerByIdSpec(int id)
  {
    Query.Where(x => x.Id == id);
  }
}
  • If you don't like inheritance and you prefer composition, you can do it by creating specific extensions to the builder.
public static class CustomerSpecExtensions
{
  public static ISpecificationBuilder<Customer> ApplyBaseRules(
  this ISpecificationBuilder<Customer> specificationBuilder)
  {
    specificationBuilder.Include(x => x.Stores);

    return specificationBuilder;
  }
}

public class CustomerByIdSpec : Specification<Customer>
{
  public CustomerByIdSpec(int id)
  {
    Query.ApplyBaseRules()
        .Where(x => x.Id == id);
  }
}

As you can notice, the infrastructure is quite flexible and you have many options to achieve the same results. Let me know if this helps.

@AndreErb
Copy link
Author

@fiseni
Thank you for detailed answer and sorry for my late response.

Some of your arguments are are something which I already had foreseen (e.g. the "query generation" argument). I agree with your explanations and arguments, so that's why I am closing this issue as well.

Thank you again for your time and effort!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants