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

How to use filtered include in specification? #67

Closed
BobbyBahrami opened this issue Dec 12, 2020 · 12 comments
Closed

How to use filtered include in specification? #67

BobbyBahrami opened this issue Dec 12, 2020 · 12 comments
Milestone

Comments

@BobbyBahrami
Copy link

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!

@ardalis
Copy link
Owner

ardalis commented Dec 12, 2020

Are you on .NET 5? This feature has only just been added to EF Core:
https://stackoverflow.com/a/61147681/13729

I haven't tried it yet, myself.

@fiseni
Copy link
Collaborator

fiseni commented Dec 12, 2020

What you're requiring is the Filtered Include feature. As @ardalis mentioned this is available only in the new version EF Core 5. Read about it here.

Regardless of that, for now, we have no support for it in this package. All Include and ThenInclude expressions are parsed and stored as navigationPropertyPaths (string value) in the specification. They are evaluated in EF Core using the method overloads which accept navigationPropertyPath. Read here why it's done that way.

We need to refactor and add support, but honestly, I have no bright idea of how to do it. In order to implement it, we have to store the Include/ThenInclude expressions, and that's quite a big issue. We have no easy way to store multi-level Includes as expressions. EF internally implements this by building up the IQueryable incrementally, just adding up each next expression. We have no luxury of doing it since we'll have to reference EFCore in the base package.

@ardalis
Copy link
Owner

ardalis commented Dec 13, 2020

The Include and ThenInclude functionality is only needed/used by EF, though. Does it make it easier/possible if we restrict those to that separate package (Ardalis.Specification.EntityFrameworkCore)?

@fiseni
Copy link
Collaborator

fiseni commented Dec 13, 2020

Indeed the evaluation of the specification and the translation is done in the plugin package.
But, we have to store the input as part of the specification first. And this is usually done in the Core project, right? So, this information should be stored in an agnostic way somehow. Then the evaluator can evaluate it however it wants (it's out of this problem's scope).

Let me be more specific. You can store the first level Include as Expression<Func<T, object>, no problem here. But once you enter the next level, the input is not T anymore.
Here is how I used it in the builder

public static IIncludableSpecificationBuilder<TEntity, TProperty> ThenInclude<TEntity, TPreviousProperty, TProperty>(
	this IIncludableSpecificationBuilder<TEntity, TPreviousProperty> previousBuilder,
	Expression<Func<TPreviousProperty, TProperty>> thenIncludeExpression)
	where TEntity : class
{
	var propertyName = (thenIncludeExpression.Body as MemberExpression)?.Member?.Name;
	previousBuilder.Aggregator.AddNavigationPropertyName(propertyName);

	return new IncludableSpecificationBuilder<TEntity, TProperty>(previousBuilder.Specification, previousBuilder.Aggregator);
}

Here TProperty is covariant generic, and also TProperty and TPreviousProperty are method scope generics. For an instance, you don't have the mechanism to store this Expression<Func<TPreviousProperty, TProperty>> in the specification. Those parameters are unknown for the specification. The only way is to store it as Expression<Func<object, object>, but this won't be evaluated correctly by EF. The input parameter of Func shouldn't be an object.

So, as a solution, on each level, we're extracting the property name and concatenating it to the navigation path. Each IncludeAggregator holds information for one Include chain.

I'm out of ideas here :(

@BobbyBahrami
Copy link
Author

BobbyBahrami commented Dec 13, 2020

So unfortunately for a person like me that is completely based his project on this package and has implemented eshopOnWeb DDD style in his project layers (Core and Infrastructure) there is no option available right now. true? Is there a way to use new .NET 5 feature to limit my includes and not totally revamp entire relevant project parts?

@fiseni
Copy link
Collaborator

fiseni commented Dec 13, 2020

You always can create custom repositories for these specific scenarios and write your queries using EF directly.

You can totally do DDD without this feature, it's not a deal breaker. As a matter of fact, in terms of DDD, I would suggest to try to avoid this feature. If you're using aggregates, you want to get the whole aggregate, not only portion of the children. If you have loaded only part of it, you will end up with implicit rules in your code. The rest of the code will have to be aware and keep track what is loaded. Imagine using Clear() method on some of the collections. Your intention will be to delete all order items, but only the filtered and loaded ones will be cleared. You get ambigious usages.

This feature is handy for reading and reporting tasks. In those cases, not necessarily you want to include your domain model in the process, thus you don't need specifications either.

@BobbyBahrami
Copy link
Author

I should thank you for for your time, its appreciated.
I'm wondering if you're intended to support where clause in Includes in near future, I can live with that and continue implementing this package in my project with hope of coming back and just revise the specifications with relevant where clauses in Includes and problem is solved; but if not maybe its better to go all the way back and change core/infrastructure layers to support custom repository using .NET 5 features.

@fiseni
Copy link
Collaborator

fiseni commented Dec 13, 2020

@ardalis will decide on that. I do believe if we come up with a solution, we'll include it in the package, why not.

Btw, you can use specifications and custom repositories side by side. They are not mutually exclusive. Refer to the sample app in this repo. We have CustomerRepository to demonstrate this. You keep the implementation in the infrastructure project, and the interface in the core project.

@BobbyBahrami
Copy link
Author

Thanks a lot.

@ardalis
Copy link
Owner

ardalis commented Dec 13, 2020

If we can figure out how, we will support it for sure.

@fiseni
Copy link
Collaborator

fiseni commented Feb 13, 2021

Ok, this issue bothers me for some time, so I gave it another try. I finally figured out how to do it. We'll store information for each Include expression. Then, during the evaluation, we'll utilize this info to call Include/ThenInclude of EntityFramework by reflection.

public class IncludeExpressionInfo
{
    public Expression ExpressionBody { get; set; }
    public ReadOnlyCollection<ParameterExpression> ExpressionParameters { get; set; }
    public Type EntityType { get; set; }
    public Type PropertyType { get; set; }
    public Type PreviousPropertyType { get; set; }
}

I tested it, it works fine. Should do some further testing, and probably next week will post PR for this.

@fiseni fiseni changed the title How to use specific where in includes? How to use filtered include in specification? Feb 13, 2021
@fiseni fiseni added this to the 5.0 milestone Mar 2, 2021
@fiseni
Copy link
Collaborator

fiseni commented Mar 5, 2021

Implemented in version 5.0.3

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

3 participants