-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
DbFunctions: self-bootstrapping functions #9810
Comments
@pmiddleton Let us know if you're interested in working on this. |
@ajcvickers Yes I am. I actually have already started looking into something along the lines of the second example. public int PostReadCount(int postId)
{
return Execute(db => db.PostReadCount(postId));
} I ran into some issues with ReLinq and then put it aside. I was going to pick it up again once I had the instance methods on DbContext (#9213) working as it is required first. |
This feature is what is really needed to make niladic functions (#9022) useful. It was my work on that item which started me looking at what is in effect this feature. |
Hi @pmiddleton I would like to call function mapped by |
DbFunctions that are a part of a larger query were added as a part of 2.0. Those you can use today. This issue tracks the ability to directly call a function outside of a query. If you need this functionality I can't give you a good eta as I am doing this in my free time. I have started on it and am making progress, I would like to have it ready for the 2.1 release but no promises. If you need help just making a regular dbFunction call let me know and I can help out. |
I have a prototype put together that allows running scalar methods directly via syntax similar to what you outlined above.
There are two limitations I have run into with this approach which are worth discussing.
There is an alternative approach I have been thinking about, but I want to run it past you before I spend much time digging into it. What if we add a special DbSet to DbContext which we can use to build and run the queries. This DbSet would not be added to the model and could be used to run the two types of queries I outlined above.
|
@ajcvickers @smitpatel @anpete - Do any of you have any input on this? Thanks |
@pmiddleton Cool, thank you very much! Some answers:
I guess you are saying that is the case if you take advantage of the self-bootstrapping capability of one of these functions (i.e. you invoke it directly) and the function returns a scalar. That is expected, but you should still be able to use the function inside the body of a regular query (e.g. one bootstrapped from a
Based on the example you provided what I expect to happen is that the two functions will be invoked in succession, with the result of the first one passed to the first one. Is that the case, or am I missing some other detail? If we (or the user) want to enable composition in this manner, we would need to tweak the argument for the second function to be an expression (scalars parameters are not composable in the right way because they don't capture expressions). Then you should be able to write it like this: var count = context.PostReadCount(() => context.FindNewestPostId()); This could in fact be an additional overload of
I see this "null" You should be able to achieve something functionally similar by taking any regular |
While current SelectExpression gives ability to create without FromExpression, though the only issue to encounter the assumptions made around root being there. My suspicion is QueryModelVisitor is more prone to the assumption than SelectExpression. |
any news on this ? |
@pfaisant - This just missed the cut for 2.2. A redesign for query generation is happening in 3.0. Depending on the time frame of when that is done will determine if there is enough time for me to update my PR to work with the new approach before release. Therefore I can't say if this will make 3.0 or not. |
Pulling this in 5.0. Most of the work has been done with TVF work which are self-bootstrapping. We probably just need to iron out kink to finish this feature. The self-bootstrapping db functions would work for scalars also. |
Related #20363 |
Review whether |
Researching more on this:
Which leaves us with rest 2 patterns
I am proposing following, private IQueryable<TElement> BootstrapFunction<TElement>(Expression<Func<IQueryable<TElement>>> expression)
{
var queryProvider = this.GetService<IAsyncQueryProvider>();
return queryProvider.CreateQuery<TElement>(expression.Body);
}
private TResult BootstrapFunction<TResult>(Expression<Func<TResult>> expression)
{
var queryProvider = this.GetService<IAsyncQueryProvider>();
return queryProvider.Execute<TResult>(expression.Body);
}
private Task<TResult> BootstrapFunctionAsync<TResult>(Expression<Func<TResult>> expression, CancellationToken cancellationToken = default)
{
var queryProvider = this.GetService<IAsyncQueryProvider>();
return queryProvider.ExecuteAsync<Task<TResult>>(expression.Body, cancellationToken);
} Definition in application DbContext public IQueryable<Blog> TVF(int id)
{
return BootstrapFunction(() => TVF(id));
}
public int Aggregate(IEnumerable<Blog> blogs)
{
return BootstrapFunction(() => Aggregate(blogs));
}
public Task<int> AggregateAsync(IEnumerable<Blog> blogs, CancellationToken cancellationToken = default)
{
return BootstrapFunctionAsync(() => Aggregate(blogs), cancellationToken);
} Usage in application code var query1 = db.TVF(1).ToList();
var query2 = db.Aggregate(db.Blogs);
var query3 = await db.AggregateAsync(db.Blogs); M1: works like TVF and injects query provider M2, M3: works like aggregate by invoking Execute* methods and returning a scalar result. User methods take IEnumerable rather than IQueryable in order to use navigation properties in subqueries. Though this is upto users to decide. I verified inside our pipeline and with proper DbFunction mapping, we get expression tree as expected. While testing I put BootstrapFunction methods on DbContext directly but since it is relational it should live somewhere else and not on DbContext directly as long as we can resolve IAsyncQueryProvider service there. |
In the original queryable functions code I had for 2.2 I did have the ability to directly run scalar functions. It would be possible to add it back, but how useful would it be to an end user. There are some metadata and security functions that might be worth calling directly, say SCOPE_IDENTITY, SCHEMA_NAME, or CURRENT_USER. I've never seen anyone ask for this so the demand is probably pretty low. I don't follow how you're aggregate methods example works. Aggregates require a scalar column as input and you are passing in a DbSet. Your last sentence is the thing I was never able to find a good answer to when I originally wrote this feature. Where can you put the bootstrap function, besides on DbContext, and still have it easily accessible to the end user inside of DbContext. You don't want to force the user to DI something into their context in order to use queryable functions. You could try making them static methods on DbFunctions and creating a new service scope in order to get access to an IAsyncQueryProvider. I have no idea how well creating another scope would play with the rest of the system but I'm going to guess not well. |
That is trying to use EF Core to connect to database to implement a calculator. On serious note,
Aggregates may require scalar column but even EF6 pattern did not take it since any aggregate with Scalar can be written as I think there are some places like |
Remove CreateQuery API. |
Have a PR out yet? Would love to see how you tie that back into IAsyncQueryProvider |
It is same as CreateQuery API just public and different name. |
Options discussed in 1h55m long design meeting.
We decided to use
|
Public method FromExpression on DbContext to tie up our query provider with the expression Resolves #9810
Public method FromExpression on DbContext to tie up our query provider with the expression Resolves #9810
Public method FromExpression on DbContext to tie up our query provider with the expression Resolves #9810
Glad to know I'm not the only one who has issues naming things :) |
@smitpatel Are there any open issues to address self-bootstrapping scalar functions? It seems like TVFs were solved, but scalars were not. |
@MisinformedDNA - #11624 tracks that. |
TL;DR
We can enable instance functions and extension methods defined on the
DbContext
to be called directly outside of queries, since access to "this"DbContext
instance can infuse the ability to bootstrap server LINQ queries without having to go through aDbSet<TEntity>
:This applies to scalar functions as well as TVFs (which would look like any other self-bootstrapping function but would return
IQueryable<T>
).Details
(originally from comment at #9213 (comment))
One of the reasons to support instance functions based on
DbContext
(#9213) is to be able to pass theDbContext
to the implementation of the function, which may involve querying for data that exists in the database.There is another click-stop beyond that which is to be able to use the function directly. E.g. instead of always having to call the function like this:
We can enable calling the function directly in code:
(I understand the scenario above is a bit contrived, because in theory calling the function in the query may avoid additional round-trips, but I don't think this example negates the value of self-bootstrapping functions in general)
Since in the body of the method we have access to "this"
DbContext
, we can use it to execute a query in which the method is invoked:Although ideally the user should be able to do something like this some day:
Where assuming there is a
PostReadCount()
scalar function in the database would translate to something like this:We actually had this kind of capability in EF6 and even previous versions, and it was useful for mapping scalar functions as well as TVFs, although it wasn't at all easy to setup.
Impact of client evaluation
Note that it was also safer in EF6 to invoke the method through the LINQ provider as part of the implementation of the method, because there was no automatic client evaluation. In EF Core we should probably have a way to fork re-entrant calls to either evaluate in memory or throw if there is no reasonable in-memory implementation. Otherwise there are scenarios that would result into infinite recursion.
The text was updated successfully, but these errors were encountered: