如何在Entity Framework Core ExecuteUpdate中有条件地设置属性?

huangapple go评论74阅读模式
英文:

How to conditionally SetProperty with Entity Framework Core ExecuteUpdate?

问题

在使用.ExecuteUpdate时,我看不到如何有条件地链接.SetProperty子句:

[HttpPatch("{id}")]
public async Task<IActionResult> UpdateEntityAsync(int id, Entity entity)
{
    var entitiesUpdated = await _context.Entities
        .Where(e => e.Id == id)
        .ExecuteUpdateAsync(s => s
            // 如何基于entity.Property1和entity.Property2有条件地链接SetProperty?
            .SetProperty(e => e.Property1, entity.Property1)
            .SetProperty(e => e.Property2, entity.Property2)
    );

    return entitiesUpdated == 1 ? NoContent() : NotFound();
}
英文:

When querying a database with EF Core, it's easy to conditionally add .Where clauses to a query before executing the query, e.g.:

[HttpGet]
public async Task<List<Entity>> GetEntitiesAsync(string? property1, string? property2)
{
    var query = _context.Entities.AsNoTracking();
    if (property1 != null)
    {
        query = query.Where(e => e.Property1.Contains(property1));
    }
    if (property2 != null)
    {
        query = query.Where(e => e.Property2.Contains(property2));
    }
    return await query.ToListAsync();
}

However, when using .ExecuteUpdate, I can't see how you would conditionally chain .SetProperty clauses:

[HttpPatch("{id}")]
public async Task<IActionResult> UpdateEntityAsync(int id, Entity entity)
{
    var entitiesUpdated = await _context.Entities
        .Where(e => e.Id == id)
        .ExecuteUpdateAsync(s => s
            // How to conditionally chain SetProperty based on
            // if entity.Property1 and entity.Property2 are null?
            .SetProperty(e => e.Property1, entity.Property1)
            .SetProperty(e => e.Property2, entity.Property2)
    );

    return entitiesUpdated == 1 ? NoContent() : NotFound();
}

You can't use if statements inside the lambda. It needs to be a single expression that evaluates to a SetPropertyCalls<T>. Maybe you could manually create an expression tree, but wouldn't you need to build it on top of the parameter passed into the lambda? Is there an easy way I'm not seeing?

答案1

得分: 4

SetProperty 允许传递表达式来计算值。要根据条件动态组合 SetProperty 调用,可以使用 三元条件运算符

var entitiesUpdated = await _context.Entities
    .Where(e => e.Id == id)
    .ExecuteUpdateAsync(s => s
         .SetProperty(e => e.Property1, e => entity.Property1 != null
              ? entity.Property1
              : e.Property1)
         .SetProperty(e => e.Property2, e => entity.Property2 != null
              ? entity.Property2
              : e.Property2));

虽然这会生成类似于 SQL 的代码 "PropertyX" = "e"."PropertyX",用于源为 null 的情况。

更具挑战性但更“正确”的方法是执行一些 表达式树操作

Expression<Func<SetPropertyCalls<TEntity>, SetPropertyCalls<TEntity>>> AppendSetProperty<TEntity>(
	Expression<Func<SetPropertyCalls<TEntity>, SetPropertyCalls<TEntity>>> left,
	Expression<Func<SetPropertyCalls<TEntity>, SetPropertyCalls<TEntity>>> right)
{
	var replace = new ReplacingExpressionVisitor(right.Parameters, new []{left.Body});
	var combined = replace.Visit(right.Body);
	return Expression.Lambda<Func<SetPropertyCalls<TEntity>, SetPropertyCalls<TEntity>>>(combined, left.Parameters);
}

Expression<Func<SetPropertyCalls<Author>, SetPropertyCalls<Author>>> set = 
     calls => calls;

if (entity.Property1 is not null)
{
	set = AppendSetProperty(set,
          s => s.SetProperty(e => e.Property1, entity.Property1));
}

if (entity.Property2 is not null)
{
	set = AppendSetProperty(set,
          s => s.SetProperty(e => e.Property2, entity.Property2));
}
英文:

SetProperty allows passing expression to calculate the value. To dynamically combine SetProperty calls based on condition you can use ternary conditional operator :

var entitiesUpdated = await _context.Entities
    .Where(e =&gt; e.Id == id)
    .ExecuteUpdateAsync(s =&gt; s
         .SetProperty(e =&gt; e.Property1, e =&gt; entity.Property1 != null
              ? entity.Property1
              : e.Property1)
         .SetProperty(e =&gt; e.Property2, , e =&gt; entity.Property2 != null
              ? entity.Property2
              : e.Property2));

Though this will generate SQL like &quot;PropertyX&quot; = &quot;e&quot;.&quot;PropertyX&quot; for cases when the source is null.

A bit harder but more "correct" approach is to perform some expression trees manipulation:

Expression&lt;Func&lt;SetPropertyCalls&lt;TEntity&gt;, SetPropertyCalls&lt;TEntity&gt;&gt;&gt; AppendSetProperty&lt;TEntity&gt;(
	Expression&lt;Func&lt;SetPropertyCalls&lt;TEntity&gt;, SetPropertyCalls&lt;TEntity&gt;&gt;&gt; left,
	Expression&lt;Func&lt;SetPropertyCalls&lt;TEntity&gt;, SetPropertyCalls&lt;TEntity&gt;&gt;&gt; right)
{
	var replace = new ReplacingExpressionVisitor(right.Parameters, new []{left.Body});
	var combined = replace.Visit(right.Body);
	return Expression.Lambda&lt;Func&lt;SetPropertyCalls&lt;TEntity&gt;, SetPropertyCalls&lt;TEntity&gt;&gt;&gt;(combined, left.Parameters);
}

Expression&lt;Func&lt;SetPropertyCalls&lt;Author&gt;, SetPropertyCalls&lt;Author&gt;&gt;&gt; set = 
     calls =&gt; calls;

if (entity.Property1 is not null)
{
	set = AppendSetProperty(set,
          s =&gt; s.SetProperty(e =&gt; e.Property1, entity.Property1));
}

if (entity.Property2 is not null)
{
	set = AppendSetProperty(set,
          s =&gt; s.SetProperty(e =&gt; e.Property2, entity.Property2));
}

答案2

得分: 1

这是我写的一个简单的辅助程序,它组合了表达式树并启用了这些情景,同时允许你保持相同的 API 结构,不同于 Guru Stron 的回答:

用法:

int affectedCount = await dbContext.Books
    .Where(b => b.Id == someBookId)
    .ExecutePatchUpdateAsync(b =>
    {
        if (someCondition)
            b.SetProperty(b => b.Foo, "Foo");
        else
            b.SetProperty(b => b.Bar, "Bar");
    });

实现:

public static class QueryableExtension
{
    public static Task<int> ExecutePatchUpdateAsync<TSource>(
        this IQueryable<TSource> source,
        Action<SetPropertyBuilder<TSource>> setPropertyBuilder,
        CancellationToken ct = default
    )
    {
        var builder = new SetPropertyBuilder<TSource>();
        setPropertyBuilder.Invoke(builder);
        return source.ExecuteUpdateAsync(builder.SetPropertyCalls, ct);
    }

    public static int ExecutePatchUpdate<TSource>(
        this IQueryable<TSource> source,
        Action<SetPropertyBuilder<TSource>> setPropertyBuilder
    )
    {
        var builder = new SetPropertyBuilder<TSource>();
        setPropertyBuilder.Invoke(builder);
        return source.ExecuteUpdate(builder.SetPropertyCalls);
    }
}

public class SetPropertyBuilder<TSource>
{
    public Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>> SetPropertyCalls { get; private set; } = b => b;

    public SetPropertyBuilder<TSource> SetProperty<TProperty>(
        Expression<Func<TSource, TProperty>> propertyExpression,
        TProperty value
    ) => SetProperty(propertyExpression, _ => value);

    public SetPropertyBuilder<TSource> SetProperty<TProperty>(
        Expression<Func<TSource, TProperty>> propertyExpression,
        Expression<Func<TSource, TProperty>> valueExpression
    )
    {
        SetPropertyCalls = SetPropertyCalls.Update(
            body: Expression.Call(
                instance: SetPropertyCalls.Body,
                methodName: nameof(SetPropertyCalls<TSource>.SetProperty),
                typeArguments: new[] { typeof(TProperty) },
                arguments: new Expression[] {
                    propertyExpression,
                    valueExpression
                }
            ),
            parameters: SetPropertyCalls.Parameters
        );

        return this;
    }
}
英文:

Here's a simple helper I wrote that composes the expression tree and enables these kinds of scenarios — while allowing you the same API shape, unlike Guru Stron's answer:

Usage:

int affectedCount = await dbContext.Books
    .Where(b =&gt; b.Id == someBookId)
    .ExecutePatchUpdateAsync(b =&gt;
    {
        if (someCondition)
            b.SetProperty(b =&gt; b.Foo, &quot;Foo&quot;);
        else
            b.SetProperty(b =&gt; b.Bar, &quot;Bar&quot;);
    });

The implementation:

public static class QueryableExtension
{
	public static Task&lt;int&gt; ExecutePatchUpdateAsync&lt;TSource&gt;(
		this IQueryable&lt;TSource&gt; source,
		Action&lt;SetPropertyBuilder&lt;TSource&gt;&gt; setPropertyBuilder,
		CancellationToken ct = default
	)
	{
		var builder = new SetPropertyBuilder&lt;TSource&gt;();
		setPropertyBuilder.Invoke(builder);
		return source.ExecuteUpdateAsync(builder.SetPropertyCalls, ct);
	}

	public static int ExecutePatchUpdate&lt;TSource&gt;(
		this IQueryable&lt;TSource&gt; source,
		Action&lt;SetPropertyBuilder&lt;TSource&gt;&gt; setPropertyBuilder,
	)
	{
		var builder = new SetPropertyBuilder&lt;TSource&gt;();
		setPropertyBuilder.Invoke(builder);
		return source.ExecuteUpdate(builder.SetPropertyCalls);
	}
}

public class SetPropertyBuilder&lt;TSource&gt;
{
	public Expression&lt;Func&lt;SetPropertyCalls&lt;TSource&gt;, SetPropertyCalls&lt;TSource&gt;&gt;&gt; SetPropertyCalls { get; private set; } = b =&gt; b;

	public SetPropertyBuilder&lt;TSource&gt; SetProperty&lt;TProperty&gt;(
		Expression&lt;Func&lt;TSource, TProperty&gt;&gt; propertyExpression,
		TProperty value
	) =&gt; SetProperty(propertyExpression, _ =&gt; value);

	public SetPropertyBuilder&lt;TSource&gt; SetProperty&lt;TProperty&gt;(
		Expression&lt;Func&lt;TSource, TProperty&gt;&gt; propertyExpression,
		Expression&lt;Func&lt;TSource, TProperty&gt;&gt; valueExpression
	)
	{
		SetPropertyCalls = SetPropertyCalls.Update(
			body: Expression.Call(
				instance: SetPropertyCalls.Body,
				methodName: nameof(SetPropertyCalls&lt;TSource&gt;.SetProperty),
				typeArguments: new[] { typeof(TProperty) },
				arguments: new Expression[] {
					propertyExpression,
					valueExpression
				}
			),
			parameters: SetPropertyCalls.Parameters
		);

		return this;
	}
}

huangapple
  • 本文由 发表于 2023年4月4日 06:07:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/75924095.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定