Giter Site home page Giter Site logo

ardalis / specification Goto Github PK

View Code? Open in Web Editor NEW
1.8K 26.0 235.0 715 KB

Base class with tests for adding specifications to a DDD model

License: MIT License

C# 99.10% Dockerfile 0.23% Batchfile 0.07% Shell 0.17% PowerShell 0.43%
ddd design-patterns domain-driven-design dotnet repository-pattern specification-pattern hacktoberfest

specification's Introduction

NuGetNuGet Actions Status Generic badge

Follow @ardalis   Follow @fiseni   Follow @nimblepros

Stars Sparkline

Give a Star! ⭐

If you like or are using this project please give it a star. Thanks!

Specification

Base class with tests for adding specifications to a DDD model. Also includes a default generic Repository base class with support for EF6 and EF Core. Currently used in Microsoft reference application eShopOnWeb, which is the best place to see it in action, as well as the Clean Architecture solution template. Check out Steve "ardalis" Smith's associated (free!) eBook, Architecting Modern Web Applications with ASP.NET Core and Azure, as well.

Documentation

Videos

🎥 Watch What's New in v5 of Ardalis.Specification

🎥 Watch an Overview of the Pattern and this Package

Version 7 Release Notes

Version 7 is now available on NuGet.org! We have had a lot of confusion about the need to have the version of Ardalis.Specification (and/or the EF6/EFCore packages) match the consuming project's version of .NET. We intend to version this package more frequently in the near future to make it clear that it need not match.

Breaking Changes

  • Updated projects, drop support for old TFMs. by @fiseni in #326

Other updates

  • Patch 2 by @davidhenley in #283
  • Fix Just the Docs link in docs home page by @snowfrogdev in #293
  • Update url path by @ta1H3n in #303
  • Implement SelectMany support by @amdavie in #320
  • Add two methods for consuming repositories in scenarios where repositories could be longer lived (e.g. Blazor component Injections) by @jasonsummers in #289
  • Added support for AsAsyncEnumerable by @nkz-soft in #316
  • Lamadelrae/doc faq ef versions by @Lamadelrae in #324
  • Update the search feature to generate parameterized query. by @fiseni in #327
  • Add support for extending default evaluator list by @fiseni in #328
  • Ardalis/cleanup by @ardalis in #332

Version 6 Release Notes

See Releases

Sample Usage

The Specification pattern pulls query-specific logic out of other places in the application where it currently exists. For applications with minimal abstraction that use EF Core directly, the specification will eliminate Where, Include, Select and similar expressions from almost all places where they're being used. In applications that abstract database query logic behind a Repository abstraction, the specification will typically eliminate the need for many custom Repository implementation classes as well as custom query methods on Repository implementations. Instead of many different ways to filter and shape data using various methods, the same capability is achieved with few core methods.

Example implementation in your repository using specifications

public async Task<List<T>> ListAsync(ISpecification<T> specification, CancellationToken cancellationToken = default)
{
	return await ApplySpecification(specification).ToListAsync(cancellationToken);
}

private IQueryable<T> ApplySpecification(ISpecification<T> specification)
{
	return SpecificationEvaluator.Default.GetQuery(dbContext.Set<T>().AsQueryable(), specification);
}

Now to use this method, the calling code simply instantiates and passes the appropriate specification.

var spec = new CustomerByNameSpec("customerName");
var customers = await _repository.ListAsync(spec, cancellationToken);

Specifications should be defined in an easily-discovered location in the application, so developers can easily reuse them. The use of this pattern helps to eliminate many commonly duplicated lambda expressions in applications, reducing bugs associated with this duplication.

We're shipping a built-in repository implementation RepositoryBase, ready to be consumed in your apps. You can use it as a reference and create your own custom repository implementation.

Running the tests

This project needs a database to test, since a lot of the tests validate that a specification is translated from LINQ to SQL by EF Core. To run the tests, we're using docker containers, including a docker-hosted SQL Server instance. You run the tests by simply running RunTests.bat or RunTests.sh.

Reference

Some free video streams in which this package has been developed and discussed on YouTube.com/ardalis.

Pluralsight resources:

specification's People

Contributors

amdavie avatar ardalis avatar clivar avatar connerorth avatar davidhenley avatar deepumi avatar devbased avatar efleming18 avatar fiseni avatar fretje avatar gabrielheming avatar halilkocaoz avatar ilyanadev avatar janschreier avatar jasonsummers avatar kylemcmaster avatar lamadelrae avatar markusgnigler avatar misinformeddna avatar mrukas avatar mustafaelshobaky avatar ngruson avatar nkz-soft avatar rowe2rywa avatar sadukie avatar shadynagy avatar snowfrogdev avatar ta1h3n avatar thorstenalpers avatar vittorelli 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

specification's Issues

Multiple property Where clause not working with WebApplicationFactory and InMemory database

If I use the following Query statement
Query.Where(x => x.PropOne == propOne && x.PropTwo == propTwo);
I get a null result when using WebApplicationFactory in my tests when the table has a row satisfying the where clause.

If I use
FirstOrDefault(Expression<Func<T, bool>> predicate)
directly from EF core with the same predicate
FirstOrDefault(x => x.PropOne == propOne && x.PropTwo == propTwo)
I get the result I expect.

The issue is only isolated to using WebApplicationFactory and a inMemory database.
The specification where clause works in the my App(InMemory and Sql).
Or in my tests when I switch the WebApplicationFactory to use a SQL database.

Allow specifying QuerySplitBehaviour for specifications

When retrieving one-to-many EFCore starting from v5 allows us to specify whether or not we want to execute the query as a single statement or as a split statement.
see: MS docs on split-queries

Currently at the dbcontext-level we are able to set this globally.
Would like to be able to set this per specification.

What are your thoughts on this, @ardalis ?

Copied over from Ardalis/Specification.EFCore due to that repo no longer being maintained, and this repo might be more suited to handle this request. @fiseni

Select query doesnt return TResult object

Thank you for this library.

I followed the code samples and created a specification class that is supposed to return a view model after querying the database. The

  public class StaffsInTeamSpecification : Specification<AppUser, CompanySettingsStaffVM>
    {
        public StaffsInTeamSpecification(int companyId)
        {
            Query.Select(b => new CompanySettingsStaffVM { Email = b.Email, Id = b.Id, Name = b.FullName, UserType = b.StaffType })
                .Where(b => b.CompanyId == companyId);
        }
    }

This is where I used the spec.

 var stSpec = new StaffsInTeamSpecification(companyId);

var staffs = await _userRepository.ListAsync(stSpec);

I noticed staffs is still a List<AppUser> class instead of List<CompanySettingsStaffVM>. This shows to my understanding that I cant run a select query in my spec class. If this is not the case, please can I get a little guidance on how to run a select query that returns a different object? Thanks

Is there any way I can OrderBy a SmartEnum name?

Hi,

So I'm using both this library in addition to Steve's SmartEnum. I have a SmartEnum Status class in my .Net CORE 3.1 project:

public class BookingStatus : SmartEnum<BookingStatus>
   {
       public string ShortName { get; }
       public bool IsConfirmed { get; }
       public StatusUpdateType UpdateUpdateType { get; }

       public static readonly BookingStatus Submitted = new BookingStatus("Booking request submitted", "Submitted", 0, false);
       public static readonly BookingStatus Confirmed = new BookingStatus("Booking confirmed", "Confirmed", 1, true);
       public static readonly BookingStatus Cancelled = new BookingStatus("Booking cancelled", "Cancelled", 2, false);
       public static readonly BookingStatus ConfirmedDateChanged = new BookingStatus("Confirmed date amended", "Confirmed date amended", 3, true, StatusUpdateType.LogOnly);

       private BookingStatus(string name, string shortName, int value, bool isConfirmed) : base(name, value)
       {
           ShortName = shortName;
           IsConfirmed = isConfirmed;
           UpdateUpdateType = StatusUpdateType.LogAndUpdate;
       }
   }

I also have a Specification class which I want to OrderBy the BookingStatus.ShortName property. EF doesn't like this in 3.1 due to the error "Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly".

I could do this client side using LINQ, but I am using the Skip() and Take() feature of this library for pagination, so I cannot order client side as the results set is incorrect

Is there any way around this or force my query to use client side evaluation or another solution possibly?

Thanks.

How to chain Where and Search functions with an or condition

I am trying to make one search box to find Patients by different properties.
So I need an "Or method" to chain between Query.Where and Query.Search.

I know there is a similar issue #52, but I can't figure it out the best way to resolve it.

In the example below the test return 0 when it should be 1, because there aren't a clinicHistory that contains 12345 search term.

The entity:

public class Patient : BaseEntity, IAggregateRoot
{
    public int ClinicHistory { get; private set; }
    public string Nif { get; private set; }
    public string Name { get; private set; }

    public Patient(int clinicHistory, string nif, string name)
    {...}
}

The specification look like:

public class PatientSearchSpecification : Specification<Patient>
{
    public PatientSearchSpecification(string searchTerm)
    {
        if (int.TryParse(searchTerm, out int clinicHistory))
            Query.Where(p => p.ClinicHistory == clinicHistory);

        Query
            .Search(p => p.Nif, "%" + searchTerm + "%")
            .Search(p => p.Name, "%" + searchTerm + "%");
    }
}

The test

public class PatientSearchSpecificationTest
{
    private readonly SpecificationEvaluator<Patient> _evaluator = new SpecificationEvaluator<Patient>();

    [Fact]
    public void MatchesOneNif()
    {
        var patients = new List<Patient>();
        patients.Add(new Patient(2001,"12345678X", "Patient1"));
        patients.Add(new Patient(2002,"87654321A", "Patient2"));
        patients.Add(new Patient(2003,"75395182Z", "Patient3"));

        var spec = new PatientSearchSpecification("12345");

        var result = _evaluator.GetQuery(patients.AsQueryable(), spec);

        result.Should().HaveCount(1);
    }
}

First of all, you have a very useful library and thank you for share it with us.
I hope you can help me

Missing XML comments.

We should add XML comments for all publicly visible types or members. This will improve the usage, intellisense will provide information to the end users. If you want to contribute, you are welcome to do so.

Instructions:

  • On each public class, public property, public method, we should add XML comment.
  • Just on top o the name of class/property/method start typing three slashes ///. The VS automatically will create the skeleton for the XML comment. It looks as following:
        /// <summary>
        /// 
        /// </summary>
        /// <param name="id"></param>
  • Try to analyze what that class or method does, and accordingly write in the comment.

In the end, anyhow we will try to review it, so don't get stuck too much :)

GetBySpec with select missing

I noticed that there is a Task<T> GetBySpecAsync(ISpecification<T> specification).
Shouldn't there also be a Task<TResult> GetBySpecAsync<TResult>(ISpecification<T, TResult> specification).

Otherwise the select is not working when returning a single entity given a specification.

Using library for non-SQL data sources

Is it possible to use this library for building requests to non-SQL data sources? In the eShopOnWeb, example I see that the EfRepository uses an EfSpecificationEvaluator:
image

If I wanted to create a Repository that had an HTTP-based API source, is that possible using this library? As an example, if the HTTP-based API allowed me to search for users or their pets, each by their ID, I'd like to build a specification for each of those because their query parameters would be different (?user= or ?pet=). Or if I hated myself, could i use this library to build SOAP requests that a repository implementation used?

How to use filtered include in specification?

I want to use a where clause on when using Include as below:
Query.Where(r => r.Id == resourceId).Include(r => r.Banks.Where(b => b.Id == bankId))

but it doesn't filter out the result and fetches all banks related to specified resource (Where(r => r.Id == resourceId))
which is awful!

Order functionality improvements.

For now we can order by max two columns (two levels). We may keep list of expressions, so we don't have such limitation.
We would need initial expressions for OrderBy and OrderByDescending. These methods probably should return newly defined interface, and then we define extensions to the interface "ThenBy" and "ThenByDescending". These also would return the interface so we can cascade.
We just have to think of the efficiently way to store this information in the BaseSpec.

The 'PackageIconUrl'/'iconUrl' element is deprecated

The issue is about a deprecated tag which is 'PackageIconUrl' tag in the Specification.csproj file.

<PackageIconUrl>https://user-images.githubusercontent.com/782127/33497760-facf6550-d69c-11e7-94e4-b3856da259a9.png</PackageIconUrl>

The 'PackageIconUrl'/'iconUrl' element is deprecated. Consider using the 'PackageIcon'/'icon' element instead. Learn more at https://aka.ms/deprecateIconUrl Ardalis.Specification

image

Features/improvements for version 5

I'm listing a few features and changes to consider for the next major version v5.

  • Refactoring the include infrastructure (see PR #83). Contains breaking changes.
  • Refactor the evaluation infrastructure. Will contain breaking changes.
    The evaluator's content grew with time, and right now everything is squeezed within a single method. It's not easy to extend/modify the evaluator, since the order of the actions is important, and users are forced to fully implement their own evaluators (if needed). We should improve this, extract the bits into partial evaluators and make it more modular and open to extensions.
  • Add "transient" evaluator in the base package, which will act on IEnumerable types (see issue #57). Most probably will be a breaking change.
  • Add generic constraints wherever is required. Right now we have no constraints, e.g. Specification<int> does not make sense, right? Breaking change.
  • Add addon/plugin package for EF Core 5
  • Add support for AsSplitQuery
  • Various other improvements.

@ardalis ping me whenever you're free and willing to discuss/work on this. I think we can fix this within a couple of hours.

How to use Caching here?

I can see options for caching in ISpecification like CacheEnabled and CacheKey. Please can you assist with any examples or docs?

[version4] In EF plugin project add reference to the actual Specification package.

Once version 4.0.0 of Ardalis.Specification package is published to Nuget, we should update the references in Ardalis.Specification.EF project. Remove the project references and add Ardalis.Specification nuget package instead. Only then publish it to Nuget.

If Nuget picks up the package dependency automatically, than no need for this. But, I'm not sure on that.

Specification usage improvement for preloaded data.

These few days I dedicated some time to specifications, and I want to elaborate one more idea/feature. Then I finally have to get back to my real work :)

The usage (as it is for now), is quite straightforward for retrieving data from DB. You instantiate a specification, pass it to a repository method and that's it.
But, if you need to apply the specification to some collection that you already have in hand, then the usage is a bit tricky. Let's examine this example that you might have in some of your services

var spec = new CustomerSpecification(customerID);

var evaluator = new SpecificationEvaluator<Customer>();
var result = evaluator.GetQuery(SomeCollectionOfCustomers().AsQueryable(), spec).ToList();

This, at best looks ugly. First of all, the evaluator lives in the plugin package (in infrastructure project), so you won't be able to reference it here directly. You actually have to inject the ISpecificationEvaluator in the constructor, and work with that instead. And that means you have to wire up them in you IoC container. The next issue is that SpecificationEvaluator also will try to evaluate EF related IQueryable extensions like Include, Like etc. An in this scenario, that will throw an exception. And lastly, the usage is just complicated, too many conversions done by hand.

Suggestion:

  • The base package will have its own specification evaluator, which instead of operating on top of IQueryable, will operate on IEnumerable/List. And it will evaluate only standard operations (excluding Include, Like).
  • We should name this evaluator with distinct name, so it can be obvious that if specification contains Include or Like, they will be omitted during this evaluation.
  • Finally, the specification internally will hold an instance of this evaluator, and will expose the evaluation functionality through a public construct.

So, the usage would be something like:

var result = new CustomerSpecification(customerID).Evaluate(SomeCollectionOfCustomers);

I think this is much more clear, and will improve the usage of specification in these scenarios.
The Evaluate() method should have some better name to make it clear that some stuff are excluded.

Create common project for tests.

Sample entities and sample specs are defined within each test project. Tests should be cleaned up and refactored.

  • Create common test project which will hold sample entities and sample specs.
  • Refactor the tests to use only one Database (update the docker file). We're using 2 databases now just because different entities are defined in each of the integration test projects.

How can I filter the included entity?

According to this documentation , filtering is possible when including with the ef core.

I checked the repo and from what I saw this feature was not added (or I could not see it).

I wonder what kind of solutions those who use the repo have come up with for such situations.

@ardalis do you plan to add this feature too?

Skip not working together with Include

I have one-to-many relationship configured in my entities. I'm trying to Include parent entity from child entity and also doing pagination. But I'm getting compile time error as shown below

'IIncludableSpecificationBuilder<ItemEntity, CategoryEntity>' does not contain a definition for 'Skip' and no accessible extension method 'Skip' accepting a first argument of type 'IIncludableSpecificationBuilder<ItemEntity, CategoryEntity>' could be found (are you missing a using directive or an assembly reference?)

Item Entity:

public sealed class ItemEntity : BaseEntity
{
    public ItemName Name { get; private set; }
    public ItemCode Code { get; private set; }
    public ItemBarCode BarCode { get; private set; }
    public ItemHsnCode HsnCode { get; private set; }
    public ItemQuantity Quantity { get; private set; }
    public Money UnitPrice { get; private set; }
    public Money Tax { get; private set; }
    public bool TaxIncludedWithPrice { get; private set; }
    public bool Product { get; private set; }

    public Guid CategoryId { get; private set; }
    public CategoryEntity Category { get; private set; }
}

Specification:

public class ItemFilterPaginatedSpecification : Specification<ItemEntity>
{
    public ItemFilterPaginatedSpecification(int skip, int take,
        string? itemName, string? itemCode)
        : base()
    {
        Query
            .Where(c => (string.IsNullOrWhiteSpace(itemName) && string.IsNullOrWhiteSpace(itemCode)) ||
                        (c.Name == itemName) || c.Code == itemCode)
            .Include(c => c.Category)
            .Skip(skip)
            .Take(take);
    }
}

Screenshot:
image

Please assist on what I'm doing wrong?

AsNoTracking() support

As title states: Some bulk operations do not require to be persisted back into the repository. In that case, it would be nice to fetch untracked entities without having to explicitly implement a repository for it.

Query.Where(x => x.IsNew).AsNoTracking()

Add capability for utilizing "DbFunctions"

I wanted this feature for quite some time. I want to utilize DbFunctions (e.g. Like) through specifications somehow. In production not rarely we need "Like" queries. It offers some quasi full-text search. Not applicable everywhere, but there are cases where it's quite useful.
The idea is to have the following usage. In specification, we should have the capability to define what we want to search for:

public class CustomerSpec : Specification<Customer>
{
	public CustomerSpec(string searchTerm)
	{
		Query.Search(x => x.Name, searchTerm)
			.Search(x => x.BIC, searchTerm);
	}
}

And once you have those information stored somehow, it should be utilized into something as below

dbContext.Customers.Where(x => EF.Functions.Like(x.Name, searchTerm) ||
                                EF.Functions.Like(x.BIC, searchTerm));

The way how DbFunctions are parsed internally in EF, makes it really tricky to implement this. Also, if you noticed, you should use the "x" of Where's Func argument (not just provide expression in Like), otherwise won't be parsed correctly. I tried to implement this, and it includes complete expression reconstruction, a lot of reflection.. and shortly said, I failed badly :)

I do believe someone can come up with much brighter idea, and you're more than welcome to do so.

Feature improvements.

Hi there,

The package offers great functionalities for now. I think we can do few improvements though. For the next major release we might think of the following subjects. If you think that any of this makes sense, then we can open separate issues and work on them.

  • Naming convention
    People are already quite familiar with LINQ. Why would we introduce new method names for them to remember? Instead of AddCriteria, let just be Where; instead of AddInclude, let be Include, etc. Other than that, usually it's not quite obvious what functionalities the base classes offer, and users constantly look it up to see what's available. Let simply define property which would encapsulate everything. Simply, it can be this. The statements look better and more familiar to the users. Might be as following
    public abstract class BaseSpecification<T> : ISpecification<T>
    {
        public BaseSpecification<T> Query { get; }

        protected BaseSpecification()
        {
            Query = this;
        }
    }

// Usage

    public class MySpec : BaseSpecification<MyEntity>
    {
        public MySpec() : base()
        {
            Query.Where(x => x.ID == 1);
            Query.Include(x => x.MyEntityCollection);
        }
    }
  • Include functionalities
    One of contributors did great job with "ThenInclude", but kinda I'm still not satisfied :). In the current implementation, ultimately everything is still converted to include strings. Let's keep the additional expressions and evaluate properly in the spec evaluators, utilize "ThenBy" of EF. The implementation would be a bit more complex, but it's possible.

  • Specification Evaluator
    Since this grew to be a separate construct, then let just do it properly. Lets make the methods non-static, and define an interface. If you decide to go with infrastructure as described in issue #10 and include the repository implementation in the package, then the interface would be injected in the constructor.
    The included repository actually would be abstract, and the users will have to inherit it, so they can use their own DbContext. It might be something as below

    public abstract class Repository<T> : IRepository<T> where T : BaseEntity, IAggregateRoot
    {
        private readonly DbContext dbContext;
        private readonly ISpecificationEvaluator<T> specificationEvaluator;

        public RepositoryEF(DbContext dbContext)
        {
            this.dbContext = dbContext;
            this.specificationEvaluator = new SpecificationEvaluator<T>();
        }

        public RepositoryEF(DbContext dbContext, ISpecificationEvaluator<T> specificationEvaluator)
        {
            this.dbContext = dbContext;
            this.specificationEvaluator = specificationEvaluator;
        }

        // Interface implementation

    }

    public class MyRepository<T> where T : BaseEntity, IAggregateRoot
    {
        private readonly MyDbContext myDbContext;

        public RepositoryEF(MyDbContext myDbContext)
            : base(myDbContext)
        {
            this.myDbContext = myDbContext;
        }

        public RepositoryEF(MyDbContext myDbContext, ISpecificationEvaluator<T> specificationEvaluator)
            : base(myDbContext, specificationEvaluator)
        {
            this.myDbContext = myDbContext;
        }

        // Not required to implement anything. Override or add additional functionalities.
    }

If the user has not defined their own implementation for the SpecificationEvaluator, then the default one will be instantiated. And yet again, they can implement their own. This structure offers much more flexibility.

  • Ordering
    For now we can order by max two columns (two levels). We may keep list of expressions, so we don't have such limitation.

There are few other features that we might consider.. but we can elaborate them by time.

Lambda expression for multiple Include

Hi Steve,

I appreciate all the work on this that you (and community) have put into this.

Is it possible to have multiple includes using expression syntax? I was able to chain includes using string then expression, but would really like to use the typed expressions if possible.

Expected behavior:

Query.Include(x => x.foo).Include(x => x.bar)

should compile and produce the same results as:

Query.Include("foo").Include(x => x.bar)

I'm happy to look into what it would take and will try to spend some time with it this weekend.

Add support for EntityFramework 6 and EntityFramework Core 5

In this issue, we should continue the discussion and decide in which way we will proceed with plugin/addon packages.
Plugin packages contain the implementation for a given ORM, more specifically, the evaluation logic for the specification and any other ORM dependent features. For now, we're providing only one package, the implementation for EntityFramework Core 3. We should add support for EntityFramework 6 and EntityFramework Core 5.

  • EntityFramework 6

Naming: The official name is EntityFramework, but while installing it we are aware of the version "v6". In our case, it will be something like "Ardalis.Specification.EntityFramework v1.0", the version of our package won't necessarily correspond to the version of the EF, and it may lead to confusion. I believe we should name this plugin package EntityFramework6.

TFM: The last version of EF is v6.4.4. This version is not compatible with .NET Standard 2.0, instead, it requires .NET Standard 2.1. I assume folks that are still using EF are the ones that are still unable to migrate to .NET Core, and still using .NET Framework (for whatever reasons, and there can be plenty). Since .NET Standard 2.1 can't be referenced by .NET Framework projects, I suggest this plugin package should target .NET Framework (the lowest supported version). Officially it can run on v4.5 and newer, but I do think v4.6.2 is the "sweet" spot in terms of stability and compatibility. But, if we want to add support for Nullable Reference Types, then I guess we have to go for v4.8.

  • EntityFramework Core 5

Naming: The choices are either we create new package and name it "EntityFrameworkCore5", or we update the existing EntityFrameworkCore package to the latest EF Core version v5. The cons of having one consolidated package are that the users that are unable to migrate to EF Core 5 will be locked to the features that we provide up to that point. So, if we add additional features they won't be able to use them until they migrate to EF Core 5. On the other hand, the cons of having separate packages are simply having too many of them, Core, Core5, tomorrow will be Core6, and so on.

TFM: EntityFramework Core 5 is compatible with .NET Standard 2.1, so probably we should target that one.

Feature request to chain And and Or Conditions in Where Clause

It would be great if we can add dynamic where clause chaining into specification. For example, if we need to dynamically include search values in where clause based on condition or to support advanced search scenarios; can something like this would make sense?

public class ItemFilterSpecification : Specification<ItemEntity>
{
    public ItemFilterSpecification(string? itemName, string? itemCode) : base()
    {
        if (!string.IsNullOrWhiteSpace(itemName))
        {
            Query.Where(i => i.Name == itemName);
        }

        if (!string.IsNullOrWhiteSpace(itemCode))
        {
            Query.Where(i => i.Code== itemCode); // WhereAnd or Where can be defaulted to And search
            Query.WhereOr(i => i.Code== itemCode); // WhereOr to include Or conditions in search 
        }

        // Till this point if both values are not null then query should be composed like 
        // Query.Where(i => i.Name == itemName && i.Code == itemCode);
        // For Or search
        // Query.Where(i => i.Name == itemName || i.Code == itemCode);
    }
}

This would add great value to the package. And help to build query for advanced search or dynamic search

Virtualize RepositoryBase methods

What do you feel about making the methods in RepositoryBase.cs virtual?
I ran into a NullReferenceException when unit testing my concrete EF6 implementation of the repository with a mocked DbContext.

public virtual async Task UpdateAsync(T entity)
{
  dbContext.Entry(entity).State = EntityState.Modified; // Entry call to mocked DbContext returns null

  await SaveChangesAsync();
}

I'm not able to mock the Entry method of DbContext because the constructor of DbEntityEntry is not public.

If UpdateAsync was virtual in RepositoryBase, I could override it in my concrete repository:

public override async Task UpdateAsync(T entity)
{
  ((MyDbContext)DbContext).SetModified(entity);
  await SaveChangesAsync();
}

SetModified in my concrete repository:

public virtual void SetModified(object entity)
{
  Entry(entity).State = EntityState.Modified;
}

I would need a protected property to get to the DbContext.
In this setup, I'm able to unit test my concrete repository without issues.

OrderBy string column name

Is there any easy way to pass a string column name to OrderBy or OrderByDescending, or provide an extension method to do so?

Expected:
Query.OrderBy("Name");

Would effectively be the same as calling:
Query.OrderBy(x => x.Name);

This would allow for passing in different column names into the specification and not having to modify, if/else, or switch on columns.

Naming conventions and specification infrastructure

People are already quite familiar with LINQ. Why would we introduce new method names for them to remember? Instead of AddCriteria, let just be Where; instead of AddInclude, let be Include, etc. Other than that, usually it's not quite obvious what functionalities the base classes offer, and users constantly look it up to see what's available. Let simply define property which would encapsulate everything. Simply, it can be this. The statements would look better and more familiar to the users. Might be as following

    public abstract class BaseSpecification<T> : ISpecification<T>
    {
        protected BaseSpecification<T> Query { get; }

        protected BaseSpecification()
        {
            Query = this;
        }
    }

// Usage

    public class MySpec : BaseSpecification<MyEntity>
    {
        public MySpec() : base()
        {
            Query.Where(x => x.ID == 1);
            Query.Include(x => x.MyEntityCollection);
        }
    }

Obviously, we don't need extra property Query, the user simply can use this. It's just a matter of what's more visually appealing.

Specification evaluator improvements

Since this grew to be a separate construct, then let just do it properly. Lets make the methods non-static, and define an interface. If you decide to go with infrastructure as described in issue #10 and include the repository implementation in the package, then the interface would be injected in the constructor.
The included repository actually would be abstract, and the users will have to inherit it, so they can use their own DbContext. It might be something as below

    public abstract class Repository<T> : IRepository<T> where T : BaseEntity, IAggregateRoot
    {
        private readonly DbContext dbContext;
        protected readonly ISpecificationEvaluator<T> specificationEvaluator;

        public Repository(DbContext dbContext)
        {
            this.dbContext = dbContext;
            this.specificationEvaluator = new SpecificationEvaluator<T>();
        }

        public Repository(DbContext dbContext, ISpecificationEvaluator<T> specificationEvaluator)
        {
            this.dbContext = dbContext;
            this.specificationEvaluator = specificationEvaluator;
        }

        // Interface implementation
        // We should define all the methods as virtual, or most of them.

    }

    public class MyRepository<T> where T : BaseEntity, IAggregateRoot
    {
        private readonly MyDbContext myDbContext;

        public MyRepository(MyDbContext myDbContext)
            : base(myDbContext)
        {
            this.myDbContext = myDbContext;
        }

        public MyRepository(MyDbContext myDbContext, ISpecificationEvaluator<T> specificationEvaluator)
            : base(myDbContext, specificationEvaluator)
        {
            this.myDbContext = myDbContext;
        }

        // Not required to implement anything. Override or add additional functionalities.
    }

If the user has not defined their own implementation for the SpecificationEvaluator, then the default one will be instantiated. And yet again, they can implement their own. This structure offers much more flexibility.

Expose Top or Take

Looking to select example: 4 items from my specification. I've resorted to using Query.Paginate(0, 4).

@ardalis suggested raising an Issue here to see if it might be a trivial item to implement.

Improve AsNoTracking integration tests after EFCore5 upgrade

Integration tests for EFCore have all entities tracked by default as they are seeded, To make tracking tests work, you are required to manually detach the tested entity first. This is not a perfect solution and may produce side effects.

EFCore5 supplies ChangeTracker.Clear for cases like this which is much faster and side-effect free (for example while working with multiple entities or Include's)

Involved PR #72
Documentation about ChangeTracker.Clear Method

Extract EfSpecificationEvaluator in own repository

Wouldn't it make sense to extract the specification evaluators in an own repository? It may lead to a cleaner separation. Now this repository has a direct dependency to EF Core which may not be necessary. A specification may be evaluated to EF queries, ODATA queries, file system access or whatever.

I think an own Nuget package per evaluator would be a great idea.

Include functionality improvements

One of contributors did great job with "ThenInclude". We might improve this a bit. In the current implementation, ultimately everything is still converted to include strings. Let's keep the additional expressions and evaluate properly in the spec evaluators, utilize "ThenBy" of EF.
I still didn't think through of the implementation, but since the user already is typing the expression within "ThenInclude", then we might keep the same to utilize it in the evaluator.

Cast specification

If

Dog : Animal

So are there some way to cast/convert/parse

Specification<Animal> to Specification<Dog> ?

Thanks!

Answers to some common questions!

This is not an issue per se. I'll just try to provide some additional information and answer some questions I received regarding the topics we had in the following stream.

Does the use of filters break the Open-Close Principle?

This is a totally valid question. If the intention by using the specifications is to conform to this principle, then by adding the concept of filters, aren't we doing the opposite? We go back and update the specification by adding additional conditions.
As a brief recap, the OCP predicates that we should have constructs that are open to extensions and close to changes. This means, if we need to add a "behavior" to a class, we should be able to do that without changing the class itself. Even more simplified, if you have switch statements and too many conditional logic; it might be a sign that the behavior is too much hardcoded, and might be refactored in a better way.

In our case, indeed we have too many conditions within the specification, so this concern is partially true. The catch here is that we have to do with single and atomic business requirement. The user has demanded from us that we change the behavior and add an additional condition by which the customers can be queried. The requirement is that the filters on the customer's UI page be extended. Whenever you have business requirement changes, undoubtedly you will have code changes in the domain model as well. That particular specification will change only when this exact business requirement changes, and never otherwise; it's wired up to this functionality only. Even though not the perfect structure, I might say it's an acceptable solution.

This is quite different from the case when you have to change/add/delete behavior in a "classic" repository, in which case in order to update one business requirement, you're forced to update a construct that holds a collection of business requirements. That clearly violates SRP and OCP.

Why using ThenInclude in one instance broke the application? What is the proper usage?

In one instance, while describing different usages we updated the specification by adding ThenInclude (as shown below), which in turn resulted in a runtime error.

Query.Include(x => x.Stores).ThenInclude(x => x.Address);

The error here has to do with the fact that Address property is not navigation, but a string property. Obviously, you should not include simple properties. And, this is the same behavior that you will have by using EF directly. Including simple properties won't result in a compile-time error but a runtime error. It's up to you to be careful, make proper usage of it, and thoroughly test your queries.
We can constrain this usage and throw an exception, but we don't want to alter the behavior that much. What if the EF in some further version makes use of this usage? So, the ultimate usage constraints should be the responsibility of the ORM you're using.

How many Include statements are OK to have?

This is not directly related to this package, but I'll try to answer anyhow.
While creating JOINs in SQL, the real issue is not about how many tables, but the cardinality. If the dependent tables are configured as one-to-one relationships, that's quite OK. But if you're including dependent tables, where there are many rows for each principal row, then it can have quite an impact on the performance. On top of that, you should be careful what SQL queries are being produced by EF as well. The EF Core 1&2 uses split queries, while EF Core 3 uses a single query. If you have a lot of 1:n relationships and use a single query, then you might end with a "cartesian explosion" (consider split queries in these cases).

During the stream, I showcased the following specification, and the question was if this is OK?

public class AwbForInvoiceSpec : Specification<Awb>
{
    public AwbForInvoiceSpec(int ID)
    {
        Query.Where(x => x.ID == ID);
        Query.Include(x => x.Packages);
        Query.Include(x => x.AwbCargoServices);
        Query.Include(x => x.AwbPurchase);
        Query.Include(x => x.CargoManifest);
        Query.Include(x => x.Customer).ThenInclude(x => x.Addresses);
    }
}

In the context of that particular application, the Awb has quite significant importance in the overall business workflow, and it might be a bit more complicated than it should be. First of all, the AwbPurchase and CargoManifest represent 1:1 relationships. So, we end up with two 1:n navigations. This is relatively OK if you're retrieving one Awb record (as in this case). On the other hand, if you're trying to get a list of records, then you should reconsider if you need the child collections or not. Try to measure the performance, consider the usage of the application, number of users, peak usages, etc, and then you can decide if that meets your criteria or not.

Anti-pattern usage

In the above example, the actual anti-pattern is not the usage of several Include statements, but including the Customer. That implies that Awb aggregate has direct navigation to the Customer aggregate. If you follow DDD, you should strive to have as independent aggregates as possible. If required, one particular aggregate should hold only the identifier of some other aggregate root and not have a direct reference.

In our case, it was a deliberate design decision to break this rule (for Customer aggregate), in order to improve the performance in particular cases and to reduce roundtrips to DB. But, it's not something I would advise you to do. Anyhow, it's up to you to weigh the pros/cons and make your own elaborate decisions for your applications.

Do I need private constructors for the entities (e.g. for the EF code-first approach)

I got this question related to one particular scenario which happened during the stream. Once we added the DateTime birthdate parameter in the constructor, we were forced to add an additional parameterless constructor so the EF could work properly.

EF requires a parameterless constructor, so it can create the instance of the entity and then populate the properties. So, it might be wise always to add one private parameterless constructor.
In our case it was working totally fine without it since all the parameters were of type string, and EF could just pass null values while creating the instance. But, once the DateTime parameter was added, that was no longer true, EF no longer could create the instance.

[EDIT]
The above explanation is completely wrong :), so just ignore it.
It turns out EF is smart enough to utilize the constructor and will try to pass the values as ctor arguments. That's how EF handles the immutability (if your props have only getters). But, the ctor arguments should be named the same as the properties. The first character can be lowercase, and that's ok, EF will map to it correctly. But, the case of the rest of the characters should be exactly the same. So, in our case, if we have named the argument birthDate instead of birthdate, would have worked with no issues.

public Customer(string name, string email, string address, DateTime birthdate)
{
    Guard.Against.NullOrEmpty(name, nameof(name));
    Guard.Against.NullOrEmpty(email, nameof(email));

    this.BirthDate = birthdate;
    this.Name = name;
    this.Email = email;
    this.Address = address;
}

How to cast from Specification<DomainEntity> to Specification<DBEntity> where DBEntity derives from DomainEntity

Hi,

I'm trying to use the library to connect to a CosmosDB database in a "Clean Architecture" solution. The database is using database entities (implemented in the infrastructure layer) that are derived from the domain entities (implemented in the domain layer). The DB entities contain extra properties like "DataType" (so that we can differentiate from other entities stored in the same Cosmos DB container).

The problem that I have is that the Specification object is defined in the application layer using domain entities but the repositories, in the infrastructure layer, need to be able to convert them into Specification objects using DB entities ( the repos need to add a value to the "EntityType" field in the DB entity). Any ideas on how we can convert from Specification<DomainEntity> into Specification<DBEntity>?

Many thanks.

Nullable reference types enabled, but not used

I noticed that nullable reference types are enabled in the project, but are not used where it would be necessary.
Shouldn't the methods of the IRepositoryBase interface for example somtimes declare their return types as nullable?

For example in this case: Link
It would be more appropriate for this method Task<T> GetByIdAsync(int id); to be declared with a nullable return type like this: Task<T?> GetByIdAsync(int id);. The ef implementation of the repository uses the FindAsync method for this under the hood and this method returns null if the entry has not been found. With the current implementation someone would get wrong warnings in Visual Studio like this:

grafik

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.