Создание универсального селектора/установщика свойств на C#, если вы знаете тип только во время выполнения

Я работаю с API, которому нужны выражения в качестве параметров для идентификации/изменения свойств объекта. Это отлично работает, если я знаю тип во время компиляции. Например. для API требуется Expression<Func<T, object>>

и я могу использовать его, используя выражение типа x => x.Id

Но в общем мире у меня есть объект obj, с которым я могу работать, и я знаю имя свойства. Как мне построить экспресс Expression<Func<T, object>>?

Аналогично, API, с которым я имею дело, также нуждается в выражении для установки заданного свойства объекта в заданное значение.

API выглядит следующим образом и представляет собой метод экземпляра:

void Patch<T, TProperty>(string id, Expression<Func<T, TProperty>> fieldPath, TProperty value)

когда я знаю Т и как выглядит объект, я могу

class MyClass { internal string Id {get; set;} }
Patch<MyClass, string>("some_id", x => x.Id, "someValue");

(при общем определении Patch

Patch<T, TProperty>("some_id", x => x.Id, someValue);

где x имеет значение T, а someValue имеет значение TProperty)

Но если я не знаю T и TProperty во время компиляции (но могу определить их во время выполнения), мне нужно сформулировать правильное выражение.

Учитывая API, с которыми я работаю, я не могу использовать PropertyInfo.GetValue/SetValue (для этого у меня есть решение)

Можете показать реализацию Patch? Как он использует fieldPath для установки свойства? Кроме того, знаете ли вы T и TProperty во время выполнения? то есть можете ли вы получить объект Type, представляющий эти типы?

Sweeper 10.07.2024 11:04

Я обновил вопрос, добавив полностью рабочий образец патча, и пояснил, что я знаю типы во время выполнения (или, скорее, могу их определить, что сводится к одному и тому же).

Stephan Steiner 10.07.2024 11:09

То, что вы задаете, почти то же самое, что и EF Core ExecuteUpdate. Обратите внимание, что он не использует необработанное выражение ` x => x.Id`, он использует setters => setters.SetProperty(b => b.IsVisible, false), выражение, в котором компилятор знает, что такое свойство. setters сам ничего не выполняет, он используется для создания выражений

Panagiotis Kanavos 10.07.2024 12:01

Вы можете проверить исходный код ExecuteUpdate и код SetPropertyCalls на Github.

Panagiotis Kanavos 10.07.2024 12:05

Просто для ясности: для тех, кто разрабатывает решение, является ли метод Patch статическим или экземплярным? Какому типу или реальному объекту оно принадлежит?

Rubidium 37 10.07.2024 13:07

Patch — это метод экземпляра объекта, который является частью стороннего SDK. Я обновлю вопрос соответственно.

Stephan Steiner 10.07.2024 14:06

@PanagiotisKanavos, вы действительно попали в цель - я начал с EfCore ExecuteUpdate, где с большой помощью создал хороший метод расширения. Но я не очень хорошо знаком с выражениями, поэтому мне было трудно собрать вещи воедино.

Stephan Steiner 11.07.2024 18:00

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

Jeremy Lakeman 12.07.2024 04:46

@JeremyLakeman, это я уже сделал, но идентификатор — это всего лишь ключ, обычно я исправляю любые другие атрибуты, которые не являются общими. Думайте об этом как о REST PUT, который содержит только те свойства, которые вы хотите изменить — эти реквизиты различны для каждого запроса (и запрос может быть на различных контроллерах, обслуживающих разные объекты). Это в основном мой вариант использования.

Stephan Steiner 12.07.2024 11:16
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
9
106
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Прежде всего: Вы утверждаете, что сигнатура метода — void Patch<T, U>(string id, Expression<Func<T, TProperty>> fieldPath, TProperty value), где T и TProperty — значимые параметры, но U — это параметр, на который нет ссылок: я предполагаю, что U должно быть TProperty.

Теперь, я думаю, вам нужен метод расширения для вызова метода Patch, указывающий цель PropertyInfo или указав ее имя вместе с объявляющим типом. Эти две альтернативы — минимум для построения ожидаемого Expression<T, TProperty>.

Взгляните на следующий код:

Вероятное исходное определение класса (при условии, что описанный метод Patch исходит из интерфейса):

public interface IPatcher
{
    void Patch<T, TProperty>(string id, Expression<Func<T, TProperty>> fieldPath, TProperty value);
}
public class PatcherClass : IPatcher
{
    public void Patch<T, TProperty>(string id, Expression<Func<T, TProperty>> fieldPath, TProperty value)
    {
    }
}

Класс расширения

public static partial class IPatcherExtensions
{
    // Represents a callback to a convenient compiled method
    private delegate void PatchCallback(IPatcher self, string id, object? value);

    // Creates an instance of PatchCallback
    private static PatchCallback CreatePatchCallback(PropertyInfo info)
    {
        var parSelf = Expression.Parameter(typeof(IPatcher), "self");
        var parId = Expression.Parameter(typeof(string), "id");
        var parValue = Expression.Parameter(typeof(object), "value");

        var parTarget = Expression.Parameter(info.DeclaringType, "target");
        var exprGetter = Expression.Lambda(typeof(Func<,>).MakeGenericType(info.DeclaringType, info.PropertyType), Expression.Property(parTarget, info), parTarget);

        var exprValue = Expression.Convert(parValue, info.PropertyType);
        var body = Expression.Call(parSelf, nameof(IPatcher.Patch), new Type[] { info.DeclaringType, info.PropertyType }, parId, exprGetter, exprValue);

        var lambda = Expression.Lambda<PatchCallback>(body, parSelf, parId, parValue);

        return lambda.Compile();
    }

    // Caches PatchCallback instances for specific PropertyInfos
    private static readonly ConcurrentDictionary<PropertyInfo, PatchCallback> CacheForPropertyInfo = new();
    public static void Patch(this IPatcher self, string id, PropertyInfo info, object? value)
    {
        var callback = CacheForPropertyInfo.GetOrAdd(info, CreatePatchCallback);
        callback(self, id, value);
    }

    // Caches PatchCallback instances for pairs of Type/property names
    private static readonly ConcurrentDictionary<(Type TargetType, string PropertyName), PatchCallback> CacheForTypeAndPropertyName = new();
    public static void Patch(this IPatcher self, string id, Type targetType, string propertyName, object? value)
    {
        var callback = CacheForTypeAndPropertyName.GetOrAdd((targetType, propertyName), Factory);
        callback(self, id, value);
        static PatchCallback Factory((Type TargetType, string PropertyName) key)
        {
            var (targetType, propertyName) = key;
            var info = targetType.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
            Debug.Assert(info is not null);
            return CreatePatchCallback(info);
        }
    }
}

Пример использования:

IPatcher patcher = new PatcherClass();

// original
patcher.Patch<MyClass, string>("some_id", x => x.Id, "someValue");

// with type and property name
var declaringType = typeof(MyClass);
var propertyName = nameof(MyClass.Id);
patcher.Patch("some_id", declaringType, propertyName, "someValue");

// with property info
var propertyInfo = typeof(MyClass).GetProperty(nameof(MyClass.Id), BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
patcher.Patch("some_id", propertyInfo, "someValue");

Обратите внимание, что здесь нет никаких проверок работоспособности или обнаружения ошибок: их следует реализовать для перехвата любых недопустимых пар PropertyInfo или Тип/имя.

Вы правы, вы должны быть TProperty. Я отредактировал вопрос соответствующим образом.

Stephan Steiner 10.07.2024 12:18

Если исходный метод Patch является статическим, а не методом экземпляра интерфейса или класса, приведенный выше код можно адаптировать, удалив любую ссылку на объекты IPatcher и в случае изменения инструкции Expression.Call для правильного вызова метода (статический или пример).

Rubidium 37 10.07.2024 13:04

Другие вопросы по теме