c# SetIfChanged-Method with 表达式

c# SetIfChanged-Method with Expressions

我必须经常遵循 C# 代码:

public void UpdateDB(Model.ResultContext db)
{
    Model.Planning.Part dbPart = db.Parts.Where(/*someClause*/).FirstOrDefault();

    //The awkward part
    if (dbPart.Number != Number)
    {
        dbPart.Number = Number;
    }

    if (dbPart.NumberShort != NumberShort)
    {
        dbPart.NumberShort = NumberShort;
    }

    if (dbPart.Designation != Designation)
    {
        dbPart.Designation = Designation;
    }
}

检查每个字段并将其包装在if != then set中显然有点尴尬 是的,需要进行检查,否则数据库会将所有内容视为已更改的列。

要设置的字段是auto-Properties:

public class Part 
{
    [MaxLength(36), MinLength(1)]
    public string Number { get; set; } = null!;

    [MaxLength(80)]
    public string Designation { get; set; } = null!;
}

而且我不想为每个字段都写一个明确的setter,这当然可以在设置之前进行检查。 所以我想到的是一些方法“SetIfChanged”,这样调用它可以使代码更易读,更少 error-prone:

//Options 
dbPart.SetIfChanged(dbPart.Number, this.Number);
dbPart.SetIfChanged(dbPart.Number = this.Number);
dbPart.SetIfChanged(Number, this.Number);

我认为表达式或 lambda 可以实现类似的功能,但老实说...我坚持使用声明和调用此类方法的语法

有人能帮帮我吗?

好吧,如果您真的需要检查(假设最后您想知道是否有任何更改),您可以使用反射并循环遍历属性。但在你的情况下不需要检查。

以这个为例:

  if (dbPart.Number != Number)
    {
        dbPart.Number = Number;
    }

true) 如果值不同则设置新值

false)表示新值和旧值相同,重新设置也无妨

如果你想知道最后有没有什么变化:

bool changed = false;
var type = dbPart.GetType();
foreach(var (PropertyInfo)pi in type.GetProperties()
{
    if(pi.GetValue(dbPart) != newValue)  
    {
         changed = true;
         pi.SetValue(dbPart, newValue);
    }
}

或者您可以这样做:

bool changed = dbPart.Number != Number || dbPart.Designation != Designation;
dbPart.Number = Number;
dbPart.Designation = Designation;

不幸的是,C# 缺少很多东西来帮助您解决这个问题(例如 属性 refs 或引用对象的扩展方法),但是您可以使用反射来帮助解决这个问题。但是,它可能会很慢。

对于采用 lambda 的方法,您可以编写一个 set 方法:

public static void SetIfDifferent<T>(Expression<Func<T>> getterFnE, T newVal) {
    var me = (MemberExpression)getterFnE.Body;
    var target = me.Expression;
    var targetLambda = Expression.Lambda(target);

    var prop = me.Member;
    var oldVal = getterFnE.Compile().Invoke();
    if ((oldVal == null && newVal != null) || !oldVal.Equals(newVal)) {
        var obj = targetLambda.Compile().DynamicInvoke();
        prop.SetValue(obj, newVal);
    }
}

这将像这样使用:

SetIfDifferent(() => dbPart.Number, Number);
SetIfDifferent(() => dbPart.NumberShort, NumberShort);
SetIfDifferent(() => dbPart.Designation, Designation);

这会很慢,因为需要编译 Expression 树并使用 DynamicInvoke。加快速度的一种方法是传入 setter 和 getter lambda,但这会导致与原始代码一样多的重复。

如果您愿意传递 属性 的对象和名称,您可以使用:

public static T GetValue<T>(this MemberInfo member, object srcObject) => (T)member.GetValue(srcObject);

public static void SetIfDifferent2<TObj, TField>(this TObj obj, string fieldName, TField newVal) {
    var prop = typeof(TObj).GetProperty(fieldName);
    var oldVal = prop.GetValue<TField>(fieldName);
    if ((oldVal == null && newVal != null) || !oldVal.Equals(newVal))
        prop.SetValue(obj, newVal);
}

你可以像这样使用:

    dbPart.SetIfDifferent2(nameof(dbPart.Number), Number);
    dbPart.SetIfDifferent2(nameof(dbPart.NumberShort), NumberShort);
    dbPart.SetIfDifferent2(nameof(dbPart.Designation), Designation);

不幸的是,它需要重复 dbPart,除非您愿意只输入字段名称(例如 "Number"),但如果字段更改,这将导致运行时错误。 您也可以缓存 PropertyInfo 而不是使用 GetProperty 查找它,但这通常非常快,缓存可能不值得。