#c# #asp.net-core #validation #.net-core #asp.net-core-webapi
#c# #asp.net-core #проверка #.net-core #asp.net-core-webapi
Вопрос:
Я пытаюсь найти «лучший» способ проверить согласованность между данными в URL-адресе запроса и данными в теле запроса.
Например, допустим, у нас есть эта конечная точка PUT:
https://host:port/foo/bar/{carId}
где я могу передать объект Car как тело:
class Car {
string CarId { get; set; }
string Brand { get; set; }
int MaxSpeed { get; set; }
...
}
Теперь в моем контроллере у меня есть что-то вроде этого:
[HttpPut("foo/bar/{carId}")]
public async Task updateCar([FromRoute] string carId, [FromBody] Car car) {
...
}
И я хочу быть уверен, что carId
в маршруте соответствует CarId
свойству в теле.
Каков «лучший» способ добиться этого? Конечно, я мог бы просто проверить с if
помощью контроллера в теле, но поскольку это задача проверки (или, по крайней мере, я так думаю), я хотел бы иметь эту логику в моем уровне проверки.
Пока что личные идеи
Хорошо, вопрос исчерпан, здесь я просто изложу некоторые идеи, которые я пробовал или хочу попробовать.
У меня есть пользовательский фильтр действий для проверки проверки, и я пытаюсь поиграть с ним, чтобы посмотреть, смогу ли я что-то там сделать, или добавить другой пользовательский фильтр действий только там, где я хочу проверить согласованность, но это не выглядит многообещающим.
На данный момент я видел, что в фильтре действий у меня есть доступ к параметрам метода контроллера через context.ActionArguments
свойство, но я не знаю, как проверить, были ли эти аргументы «связанными аргументами» (а именно имели некоторый [FromXXX]
атрибут). Если бы я мог это сделать, возможно, я мог бы проверить, есть ли аргументы с тем же именем (или со свойством с тем же именем), а затем сравнить их значения. Но это кажется очень громоздким и непоследовательным.
Я читал о пользовательских связующих, но я все еще царапаю поверхность (я надеюсь узнать что-то еще в ближайшие несколько часов): могут ли они быть возможным решением?
Комментарии:
1. Лучший способ — не ставить carId в маршрут. Просто из любопытства, зачем вам нужно ставить carId дважды?
2. @Serge Потому что один из них является частью модели автомобиля, а другой — частью маршрута для однозначной идентификации автомобиля. Так что, ИМХО, оба должны присутствовать. Я тоже кое -что читал об этом.
3. Спасибо за ответ и за ссылку. Но эта статья выглядит очень странно для меня. Это было написано человеком, который никогда не участвовал ни в одном серьезном проекте, кроме «Hello world». ИМХО, вам следует забыть об этой статье, это несерьезно.
4. @Serge Спасибо за разъяснение! Итак, как мне это сделать? Должен ли я удалить дублирующуюся информацию из тела или из маршрута? Или я должен просто «перезаписать» информацию в теле с информацией в маршруте? Также можете ли вы указать мне какой-нибудь ресурс для изучения этих дизайнерских материалов? Я также пытался прочитать сообщение MS о дизайне REST API, но, похоже, об этом не говорится.
5. Вы не можете удалить frombody, поскольку вам нужен не только carId, поэтому просто удалите «[FromRoute] string carId» и создайте свой маршрут [Route(«~/ foo / bar»)]. Я не думаю, что вам также нужно Put или Delete, например, если это не ваш проект scholl. Я всегда использую только Get или Post и не вижу никакого смысла добавлять эти слова к route. Не слушайте, если кто-то рассказывает вам о принципах Rest, это годится только для учебника. У реального контроллера может быть около сотни действий, и вы, конечно, не сможете просто использовать 4 метода для реализации этих действий. Ваши действия должны иметь осмысленные имена.
Ответ №1:
Вы должны сделать это так:
[AttributeUsage(AttributeTargets.Property)]
public class VerifyWithRouteParams : Attribute
{
public string ParamName
{
get
{
return paramName;
}
}
private readonly string paramName;
public VerifyWithRouteParams(string paramName)
{
this.paramName = paramName;
}
}
Предположим, у вас есть модель автомобиля, которая передается через тело:
public class Car
{
[VerifyWithRouteParams("carId")]
public string CarId { set; get; }
public string AnotherParam {set; get;}
}
У вас должен быть фильтр по умолчанию:
public class RouteBodyVerificationActionFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
// do something before the action executes
if (context.ActionArguments != null amp;amp; context.ActionArguments.Count > 0)
{
foreach (var arg in context.ActionArguments)
{
if (arg.Value == null) continue;
bool isThereAnyObjectInArgumentsWithVerificationAttribute = arg.Value
.GetType()
.GetProperties()
.Any(
x => x.GetCustomAttributes(typeof(VerifyWithRouteParams), false).Any()
);
if (isThereAnyObjectInArgumentsWithVerificationAttribute)
{
foreach (var prop in arg.Value.GetType().GetProperties())
{
var verificationAttr = prop.GetCustomAttributes(typeof(VerifyWithRouteParams), false).FirstOrDefault() as VerifyWithRouteParams;
if (null == verificationAttr) continue;
string routeArgumentName = verificationAttr.ParamName;
context.ActionArguments.TryGetValue(routeArgumentName, out var routeArgumentValue);
if (null == routeArgumentValue)
{
context.ModelState.AddModelError("invalid argument value", routeArgumentName);
}
if (routeArgumentValue?.Equals(prop.GetValue(arg.Value)) != true)
{
context.ModelState.AddModelError("invalid argument value", routeArgumentName);
}
}
}
}
}
}
}
Затем вы должны добавить фильтр по умолчанию в автозагрузке:
services.AddControllers(cfg =>
{
cfg.Filters.Add<RouteBodyVerificationActionFilter>();
});
Затем вы можете проверить это с помощью ошибок проверки модели в контроллере:
if (!ModelState.IsValid)
{
return Content("invalid model");
}
Комментарии:
1. Большое вам спасибо за этот ответ! Я попробую сделать это сегодня днем. Мой единственный вопрос: я могу использовать этот фильтр только в определенных конечных точках, а не во всех, верно? Мне просто нужно добавить фильтр действий декларативным способом перед конечной точкой, на которой я хочу, чтобы он был активен?
2. Да конечно дорогая