Я работаю с 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
(для этого у меня есть решение)
Я обновил вопрос, добавив полностью рабочий образец патча, и пояснил, что я знаю типы во время выполнения (или, скорее, могу их определить, что сводится к одному и тому же).
То, что вы задаете, почти то же самое, что и EF Core ExecuteUpdate. Обратите внимание, что он не использует необработанное выражение ` x => x.Id`, он использует setters => setters.SetProperty(b => b.IsVisible, false)
, выражение, в котором компилятор знает, что такое свойство. setters
сам ничего не выполняет, он используется для создания выражений
Вы можете проверить исходный код ExecuteUpdate и код SetPropertyCalls на Github.
Просто для ясности: для тех, кто разрабатывает решение, является ли метод Patch статическим или экземплярным? Какому типу или реальному объекту оно принадлежит?
Patch — это метод экземпляра объекта, который является частью стороннего SDK. Я обновлю вопрос соответственно.
@PanagiotisKanavos, вы действительно попали в цель - я начал с EfCore ExecuteUpdate
, где с большой помощью создал хороший метод расширения. Но я не очень хорошо знаком с выражениями, поэтому мне было трудно собрать вещи воедино.
Если вы всегда обращаетесь к .Id
, вы можете определить, что все ваши классы реализуют один и тот же интерфейс.
@JeremyLakeman, это я уже сделал, но идентификатор — это всего лишь ключ, обычно я исправляю любые другие атрибуты, которые не являются общими. Думайте об этом как о REST PUT, который содержит только те свойства, которые вы хотите изменить — эти реквизиты различны для каждого запроса (и запрос может быть на различных контроллерах, обслуживающих разные объекты). Это в основном мой вариант использования.
Прежде всего:
Вы утверждаете, что сигнатура метода — 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. Я отредактировал вопрос соответствующим образом.
Если исходный метод Patch является статическим, а не методом экземпляра интерфейса или класса, приведенный выше код можно адаптировать, удалив любую ссылку на объекты IPatcher и в случае изменения инструкции Expression.Call для правильного вызова метода (статический или пример).
Можете показать реализацию
Patch
? Как он используетfieldPath
для установки свойства? Кроме того, знаете ли выT
иTProperty
во время выполнения? то есть можете ли вы получить объектType
, представляющий эти типы?