生成一个强类型代理,当一个 属性 被设置为另一个时,它可以跟踪 属性 名称而不是值的变化
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.Run
中 data.A = data.B
我需要以某种方式记录 "A" 被设置为 "B" (字面上 属性 名称) 然后在下一行 data.A = 1
我需要记录 "A" 被设置为常量并说忘记它。
约束:
- 将一个属性设置为另一个(如A=B)需要记录时
- 当将 属性 设置为任何其他值(例如 A = 1 或 A = B * 2)时,需要忘记此更改(例如只记住 A)
假设这是正在使用的跟踪器合约:
void RecordChange(string setterName, string getterName);
void UnTrackChange(string setterName);
问题:
我想以某种方式代理 Data
class 所以它仍然可以在界面代码中使用(例如 Runner
- 是一大堆使用 [=17= 的业务逻辑代码]) 包括强类型,它可以在不修改代码的情况下跟踪它的变化(例如,有很多地方像 'data.A = data.B')。
有没有什么方法可以不求助于一些涉及 IL 生成的魔法?
已经investigated/tried:
- PostSharp interceptors/Castle.DynamicProxy 与拦截器 - 仅靠这些无济于事。我能从中得到的最多是在 setter 拦截器中有一个
data.B
的值,而不是 nameof(data.B)
.
- 编译器服务 - 在这里没有找到任何合适的东西 - 获取调用者的名字并没有真正的帮助。
- Runtine 代码生成 - 类似于从 DynamicObject 继承的代理或使用 Relfection.Emit(可能是 TypeBuilder) - 我输了字。
当前解决方案:
使用上述合约的 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
并填写 属性 名称。
设置:
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.Run
中 data.A = data.B
我需要以某种方式记录 "A" 被设置为 "B" (字面上 属性 名称) 然后在下一行 data.A = 1
我需要记录 "A" 被设置为常量并说忘记它。
约束:
- 将一个属性设置为另一个(如A=B)需要记录时
- 当将 属性 设置为任何其他值(例如 A = 1 或 A = B * 2)时,需要忘记此更改(例如只记住 A)
假设这是正在使用的跟踪器合约:
void RecordChange(string setterName, string getterName);
void UnTrackChange(string setterName);
问题:
我想以某种方式代理 Data
class 所以它仍然可以在界面代码中使用(例如 Runner
- 是一大堆使用 [=17= 的业务逻辑代码]) 包括强类型,它可以在不修改代码的情况下跟踪它的变化(例如,有很多地方像 'data.A = data.B')。
有没有什么方法可以不求助于一些涉及 IL 生成的魔法?
已经investigated/tried:
- PostSharp interceptors/Castle.DynamicProxy 与拦截器 - 仅靠这些无济于事。我能从中得到的最多是在 setter 拦截器中有一个
data.B
的值,而不是nameof(data.B)
. - 编译器服务 - 在这里没有找到任何合适的东西 - 获取调用者的名字并没有真正的帮助。
- Runtine 代码生成 - 类似于从 DynamicObject 继承的代理或使用 Relfection.Emit(可能是 TypeBuilder) - 我输了字。
当前解决方案:
使用上述合约的 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
并填写 属性 名称。