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

ExecuteUpdate for Dynamic Entity #33626

Closed
aslooni opened this issue Apr 27, 2024 · 3 comments
Closed

ExecuteUpdate for Dynamic Entity #33626

aslooni opened this issue Apr 27, 2024 · 3 comments
Assignees
Labels
area-bulkcud closed-no-further-action The issue is closed and no further action is planned. customer-reported

Comments

@aslooni
Copy link

aslooni commented Apr 27, 2024

I am trying to use the ExecuteUpdate method with a dynamic entity. When using the following code snippet where my entity is dynamic, I encounter the following error:

var parameterExpression = Expression.Parameter(typeof(T), "x"); 
var memberExpression = Expression.Property(parameterExpression, "MyProp"); 
var conversion = Expression.Convert(memberExpression, typeof(object)); 
var lambda = Expression.Lambda<Func<T, object>>(conversion, parameterExpression).Compile();
await query.ExecuteUpdateAsync(c => c.SetProperty(lambda, newValue));

Error message:
Unable to cast object of type 'System.Linq.Expressions.TypedParameterExpression' to type 'System.Linq.Expressions.LambdaExpression'.

How can I resolve this issue?

@vigouredelaruse
Copy link

vigouredelaruse commented Apr 28, 2024

because all those lambda expressions end up looking like greek to me, i wrote an extension method based on this
https://stackoverflow.com/questions/47673524/ef-core-soft-delete-with-shadow-properties-and-query-filters/48744644#48744644

        public static LambdaExpression ConvertFilterExpression<TInterface>(
               this Expression<Func<TInterface, bool>> filterExpression,
               Type entityType)
        {
            var newParam = Expression.Parameter(entityType);
            var newBody = ReplacingExpressionVisitor.Replace(filterExpression.Parameters.Single(), newParam, filterExpression.Body);

            return Expression.Lambda(newBody, newParam);
        }

then i write the prediates in plain english like this

Expression<Func<IContentRowLevelSecured, bool>> newFilter = f =>
// filter everyone deny
adminPredicate(this.claimsPrincipal) || !f.AccessControlEntries
    .Where(w => w.Scope == ACEPermissionScope.EVERYONE
    && w.Permission == ACEPermission.READ
    && w.PermissionType == ACEPermissionType.DENY
    && w.IsActive == true).Any()
// filter anonymous deny for logged in scnario
|| (adminPredicate(this.claimsPrincipal) || !f.AccessControlEntries
    .Where(w => w.Scope == ACEPermissionScope.ANONYMOUS
    && w.Permission == ACEPermission.READ
    && w.PermissionType == ACEPermissionType.DENY
    && w.IsActive == true).Any())
// filter explict deny to current user
&& !f.AccessControlEntries 
// etc

then

                                    var newFilterLambda = newFilter.ConvertFilterExpression<IContentRowLevelSecured>(entity.ClrType);
                                    newFilterLambda.Compile();

// now ef core will let you use the lambda in a global query filter for example
// ACHTUNG - you shouldn't call .addqueryfilter without first combining your new filter with existing filters
  builder.Entity(entity.ClrType).AddQueryFilter(newFilter); 

καλή τύχη

@roji
Copy link
Member

roji commented Apr 29, 2024

@aslooni ExecuteUpdate accepts the following parameter for the setters:

Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>> setPropertyCalls

In other words, the entire parameter is a single expression tree, whereas you're trying to compile a lambda and insert a reference to the compiled delegate inside it. Here's a code sample that shows dynamic construction of ExecuteUpdate:

await using var context = new BlogContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

// First the reflection MethodInfo for the SetProperty() method that we need to call - this isn't trivial (note
// that there are two overloads of SetProperty() - here we get the simpler one that gets a value directly, rather than
// a lambda returning the value.
var setPropertyMethod = typeof(SetPropertyCalls<>)
    .MakeGenericType(typeof(Blog))
    .GetMethods()
    .Single(m => m is
                 {
                     Name: nameof(SetPropertyCalls<object>.SetProperty),
                     IsGenericMethod: true
                 }
                 && m.GetGenericArguments() is [var valueType]
                 && m.GetParameters() is
                 [
                     _, // Property expression type
                     { ParameterType: var valueParameterType },
                 ]
                 && valueParameterType == valueType)
    .MakeGenericMethod(typeof(string));

var setPropertyParameter = Expression.Parameter(typeof(Blog), "x");
var memberExpression = Expression.Property(setPropertyParameter, "Name");
var setterPropertyLambda = Expression.Lambda<Func<Blog, string>>(memberExpression, setPropertyParameter);

var parameter = Expression.Parameter(typeof(SetPropertyCalls<Blog>), "c");
var setPropertyCall = Expression.Call(
    parameter,
    setPropertyMethod,
    setterPropertyLambda,
    Expression.Constant("some name"));

var lambda = Expression.Lambda<Func<SetPropertyCalls<Blog>, SetPropertyCalls<Blog>>>(setPropertyCall, parameter);

await context.Blogs.ExecuteUpdateAsync(lambda);

public class BlogContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer("Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0;Encrypt=false")
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();
}

public class Blog
{
    public int Id { get; set; }
    public string? Name { get; set; }
}

This is indeed quite complex, unfortunately. #32018 tracks making ExecuteUpdate accept multiple expression trees (i.e. for each setter) rather than a single tree as the entire parameter; this would make ExecuteUpdate behave more like what you want, and would simplify constructing dynamic ExecuteUpdate invocations.

@roji
Copy link
Member

roji commented Apr 29, 2024

@aslooni am going to close this as a usage issue, and the above should get you what you need (though it's certainly more complex than it should be).

@roji roji closed this as not planned Won't fix, can't repro, duplicate, stale Apr 29, 2024
@roji roji self-assigned this Apr 29, 2024
@roji roji added the closed-no-further-action The issue is closed and no further action is planned. label Apr 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-bulkcud closed-no-further-action The issue is closed and no further action is planned. customer-reported
Projects
None yet
Development

No branches or pull requests

3 participants