Создайте строго типизированный прокси, который может отслеживать изменения в именах свойств, а не в значениях, когда одно свойство установлено на другое - PullRequest
2 голосов
/ 02 ноября 2019

Настройка:

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)

Предположим, это контракт трекераused:

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

Вопрос: Я хотел бы каким-то образом проксировать класс Data, чтобы он все еще мог использоваться в коде интерфейса (например, Runner - это целая кучакода бизнес-логики, который использует Data), включая строгую типизацию, и он может отслеживать его изменения без изменения кода (например, есть много мест, таких как «data.A = data.B»).

Есть ли способ сделать это, не прибегая к, я полагаю, какой-то магии, связанной с генерацией IL?

Уже исследовано / опробовано:

  • PostSharp перехватчики / Castle.DynamicProxy с перехватчиками - только они не могут помочь. Максимум, что я могу из этого получить - это иметь значение data.B внутри перехватчика сеттера, но не nameof(data.B).
  • Службы компилятора - не нашли здесь ничего подходящего - получить имя вызывающего не получаетсяЭто действительно помогает.
  • Генерация рутинного кода - что-то вроде прокси, унаследованного от DynamicObject или использующего Relfection.Emit (вероятно, TypeBuilder) - Я теряю наборы.

Текущее решение:

Используйте реализацию Tracker вышеупомянутого контракта и передайте его каждой функции в будущем. Затем вместо записи data.A = data.B используйте метод tracker.SetFrom(x => x.A, x => x.B) - трекер содержит экземпляр Data, и это работает. НО в реальной кодовой базе легко что-то пропустить, и это просто делает его менее читабельным.

1 Ответ

0 голосов
/ 02 ноября 2019

Это самое близкое решение, которое я придумал. Это не идеально, так как мне все еще нужно изменить все контракты / методы в клиентском коде, чтобы использовать новую модель данных, но по крайней мере вся логика осталась прежней.

Так что я открыт для других ответов.

Вот обновленная модель 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, заполняя имена свойств.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...