Я пытаюсь реализовать один ModelBinder
для всех моих DTO:
public class MyModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext) {
var queryDto = bindingContext.ModelType.GetConstructors()[0].Invoke([]);
// fill properties via Reflection
bindingContext.Result = ModelBindingResult.Success(queryDto);
return Task.CompletedTask;
}
}
Это пример DTO:
public class Dto {
public int Id { get; set; }
public string Name { get; set; }
}
Теперь, если я попытаюсь установить конечную точку следующим образом:
app.MapGet("/get-dto", ([FromQuery] [ModelBinder(typeof(MyModelBinder))] Dto dto) => {
return CalculateResultSomehow(dot);
});
Компилятор выдает мне ошибку:
ошибка ASP0020: параметр dto типа Dto должен определить метод bool TryParse(string, IFormatProvider, out Dto) или реализовать IParsable.
Если я удалю атрибут [FromQuery], лямбда выдаст предупреждение:
ModelBinderAttribute не следует указывать для параметра MapGet Delegate.
И код прерывается во время выполнения с исключением:
При обработке запроса произошло необработанное исключение. InvalidOperationException: тело было выведено, но метод не позволяет выводить параметры тела... Вы имели в виду зарегистрировать параметр(ы) «Body (Inferred)» как Службу или применить атрибут [FromServices] или [FromBody]?
Теперь, поскольку я реализую логику синтаксического анализа, основанную на Reflection, я не хочу реализовывать статику TryParse()
для каждого отдельного DTO моего приложения (у меня их 100...). А мне не следует: ModelBinder
у меня уже есть.
Действие контроллера прекрасно работает с использованием той же системы:
[ApiController]
public class MyController
{
[HttpGet("/get-dto")]
public Dto GetDto([FromQuery] [ModelBinder(typeof(MyModelBinder))] Dto dto) {
return dto;
}
}
Я потерялся здесь. Что мне не хватает? Почему это не работает для минимальных API?
Пожалуйста, ознакомьтесь с обновлением в ответе.
Привет @Massimiliano Kraus, ASP.NET Core поддерживает [AsParameters]
привязку сложной модели вместо вложенной модели. По вашему требованию меня смущает, почему вы связываете вложенную модель с телом или с формой? Я думаю, что длина запроса будет ограничена браузером или веб-сервером, а также небезопасно, чтобы конфиденциальная информация могла быть раскрыта.
В чем разница между «сложной моделью» и «вложенной моделью»? Вызов, извлекающий данные, должен использовать метод GET, поэтому вам следует передавать параметры в пути и в запросе, поскольку вы не можете использовать тело. [FromQuery]
считывает параметры из строки запроса. [AsParameters]
не работает, если мой DTO имеет вложенные классы (например: PaginationDto имеет фильтры свойств, которые представляют собой список FilterDto, и каждый FilterDto имеет PropertyName и PropertyValue...)
Я потерялся здесь. Что мне не хватает? Почему это не работает для минимальных API?
Потому что минимальные API не поддерживают «обычные» привязки моделей. Привязки моделей являются частью «полной» структуры (что можно косвенно заключить из пространства имен ModelBinderAttribute — Microsoft.AspNetCore.Mvc
).
Сведения о привязке, поддерживаемой минимальными API, см. в документе Привязка параметров в приложениях с минимальным API.
поскольку я реализую логику синтаксического анализа, основанную на Reflection
Не видя строк запроса и логики отражения, трудно сказать, но обычно такая логика вам не нужна. Но повторюсь – трудно сказать.
Я не хочу реализовывать статический TryParse()
В качестве обходного пути для минимальных API вы можете создать некоторый класс-оболочку, например MinimalApiBinder<T>
, и один раз реализовать в нем логику привязки. Примеры см. в разделе Как настроить NewtonsoftJson с минимальным API в .NET 6.0.
УПД
Из комментария:
Я не хочу писать <если строка запроса имеет ключ «имя», установите свойство Name; если у него есть «код», установите свойство Code ecc. и т.д.>
Это должно работать из коробки. Например, с атрибутом AsParameters (см. ранее связанные документы):
app.MapGet("/get-dto", ([AsParameters] Dto dto) => dto);
Будет правильно привязан из строки запроса /get-dto?name=test&id=1
.
[AsParameters]
работает, но только если DTO «плоский», т. е. имеет только свойства первого уровня. Если у него есть вложенные свойства, выдается исключение: «System.InvalidOperationException: тело было выведено, но метод не допускает выводимые параметры тела».... Я не знаю, смогу ли я решить эту проблему с помощью атрибутов во вложенных полях DTO, но я не хочу, чтобы мне приходилось повсюду отмечать свои DTO атрибутами. Это должно работать из коробки. Я оцениваю другие предложенные вами решения.
@MassimilianoKraus «DTO является «плоским», т.е. имеет свойства только первого уровня». - это описано в связанных документах: «AsParametersAttribute
обеспечивает простую привязку параметров к типам, а не сложную или рекурсивную привязку модели». «Это должно работать из коробки» — это зависит от того, что вы подразумеваете под «должно». Согласно текущей документации/реализации - этого не должно быть, минимальные API не зря названы так. Если вам нужны «продвинутые» сценарии привязки — я бы рекомендовал придерживаться контроллеров.
@MassimilianoKraus Также одна из ссылок в связанном ответе о Newtonsoft указывает на способ повторного использования связующих моделей MVC - ModelBinderOfT @github
«Не должно быть, минимальные API не зря названы так». ... ну... функция ModelBinder
уже реализована, не думаю, что было бы так сложно включить ее и в минимальные API... Возможно, есть более серьезная проблема: я не могу найти четкая официальная спецификация того, как должна быть структурирована строка запроса, чтобы иметь вложенные свойства. Похоже, что строки запроса никогда не рассматривались как нечто большее, чем простой список пар ключ-значение. Microsoft принимает множество разных форматов (с []
, с индексами, с повторяющимися ключами...), но какой из них является официальным?
@MassimilianoKraus «Я не думаю, что было бы так сложно включить его в минимальные API» - насколько я понимаю, одной из основных целей внедрения минимальных API было значительное повышение производительности, поэтому они были почти полностью переписаны. .
Я нашел решение, подходящее для моего случая, но +1 за вашу поддержку и полезную информацию!
Поскольку у меня уже был динамически сгенерированный набор конечных точек, основанный на нескольких общих универсальных методах, я объявил строку запроса как простой строковый параметр и отправил преобразование «строка запроса => типизированный dto» после начала метода, вот так :
// Simplified version:
public static Delegate ConfigureEndpoint<TQueryStringDto>()
{
return async ([FromQuery] string query /* other params omitted */) => {
var dto = ConvertToDto<TQueryStringDto>(query);
// Do something general, valid for every endpoint,
// like sending the dto to IMediator
};
}
ConvertToDto
— это общий метод, который я бы использовал в раннем промежуточном программном обеспечении или в ModelBinder
, но я также могу использовать его здесь, немного ниже цепочки обработки запроса.
"почему"? По той же причине все используют Reflection: чтобы написать преобразование/синтаксический анализ/алгоритм один раз. Я не хочу писать <если строка запроса имеет ключ «имя», установите свойство Name; если у него есть «код», установите свойство Code ecc. ecc.> для каждого свойства каждого DTO — это кошмар. Я хочу сделать это через Reflection для одного класса: <ключ foreach в строке запроса, если у DTO есть свойство с таким же именем, попробуйте установить его значение>.