生成一个强类型代理,当一个 属性 被设置为另一个时,它可以跟踪 属性 名称而不是值的变化

Generate a strongly-typed proxy that can track changes on property names not values when one property is set to another

设置:

public class Data
{
    public int A { get; set; }
    public int B { get; set; }
}

public class Runner
{
    public static void Run(Data data)
    {
        data.A = data.B;
        data.A = 1;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var data = new Data() { A = 1, B = 2 };
        Runner.Run(data);
    }
}

问题: 我需要在此处为​​ 属性 名称而不是值实施更改跟踪。在第一行的 Runner.Rundata.A = data.B 我需要以某种方式记录 "A" 被设置为 "B" (字面上 属性 名称) 然后在下一行 data.A = 1 我需要记录 "A" 被设置为常量并说忘记它。

约束:

假设这是正在使用的跟踪器合约:

void RecordChange(string setterName, string getterName);
void UnTrackChange(string setterName);

问题: 我想以某种方式代理 Data class 所以它仍然可以在界面代码中使用(例如 Runner - 是一大堆使用 [=17= 的业务逻辑代码]) 包括强类型,它可以在不修改代码的情况下跟踪它的变化(例如,有很多地方像 'data.A = data.B')。

有没有什么方法可以不求助于一些涉及 IL 生成的魔法?

已经investigated/tried:

当前解决方案:

使用上述合约的 Tracker 实现,并将其传递到后续的每个函数中。然后不用写 data.A = data.B 使用方法 tracker.SetFrom(x => x.A, x => x.B) - 跟踪器持有一个 Data 实例,所以这有效。但是在真实的代码库中,很容易遗漏一些东西,这只会降低可读性。

这是我想出的最接近的解决方案。它并不完美,因为我仍然需要修改客户端代码中的所有 contracts/methods 以使用新的数据模型,但至少所有逻辑都保持不变。

所以我愿意接受其他答案。

这是更新后的 Data 型号:

public readonly struct NamedProperty<TValue>
{
    public NamedProperty(string name, TValue value)
    {
        Name = name;
        Value = value;
    }

    public string Name { get; }
    public TValue Value { get; }

    public static implicit operator TValue (NamedProperty<TValue> obj)
        => obj.Value;

    public static implicit operator NamedProperty<TValue>(TValue value)
        => new NamedProperty<TValue>(null, value);
}

public interface ISelfTracker<T> 
    where T : class, ISelfTracker<T>
{
    Tracker<T> Tracker { get; set; }
}

public class NamedData : ISelfTracker<NamedData>
{
    public virtual NamedProperty<int> A { get; set; }
    public virtual NamedProperty<int> B { get; set; }

    public Tracker<NamedData> Tracker { get; set; }
}

基本上我复制粘贴了原始 Data 模型,但更改了它的所有属性以了解它们的名称。

然后是跟踪器本身:

public class Tracker<T> 
    where T : class, ISelfTracker<T>
{
    public T Instance { get; }
    public T Proxy { get; }

    public Tracker(T instance)
    {
        Instance = instance;
        Proxy = new ProxyGenerator().CreateClassProxyWithTarget<T>(Instance, new TrackingNamedProxyInterceptor<T>(this));
        Proxy.Tracker = this;
    }

    public void RecordChange(string setterName, string getterName)
    {
    }

    public void UnTrackChange(string setterName)
    {
    }
}

Castle.DynamicProxy 的拦截器:

public class TrackingNamedProxyInterceptor<T> : IInterceptor
    where T : class, ISelfTracker<T>
{
    private const string SetterPrefix = "set_";
    private const string GetterPrefix = "get_";

    private readonly Tracker<T> _tracker;

    public TrackingNamedProxyInterceptor(Tracker<T> proxy)
    {
        _tracker = proxy;
    }

    public void Intercept(IInvocation invocation)
    {
        if (IsSetMethod(invocation.Method))
        {
            string propertyName = GetPropertyName(invocation.Method);
            dynamic value = invocation.Arguments[0];

            var propertyType = value.GetType();
            if (IsOfGenericType(propertyType, typeof(NamedProperty<>)))
            {
                if (value.Name == null)
                {
                    _tracker.UnTrackChange(propertyName);
                }
                else
                {
                    _tracker.RecordChange(propertyName, value.Name);
                }

                var args = new[] { propertyName, value.Value };
                invocation.Arguments[0] = Activator.CreateInstance(propertyType, args);
            }
        }

        invocation.Proceed();
    }

    private string GetPropertyName(MethodInfo method)
        => method.Name.Replace(SetterPrefix, string.Empty).Replace(GetterPrefix, string.Empty);

    private bool IsSetMethod(MethodInfo method)
        => method.IsSpecialName && method.Name.StartsWith(SetterPrefix);

    private bool IsOfGenericType(Type type, Type openGenericType)
        => type.IsGenericType && type.GetGenericTypeDefinition() == openGenericType;
}

以及修改后的入口点:

static void Main(string[] args)
{
    var data = new Data() { A = 1, B = 2 };
    NamedData namedData = Map(data);
    var proxy = new Tracker<NamedData>(namedData).Proxy;
    Runner.Run(proxy);

    Console.ReadLine();
}

Map() 函数实际上将 Data 映射到 NamedData 并填写 属性 名称。