Display и DisplayName не работают для Web API
У меня простой веб АПИ проект. Все на .NET Core 3.1. Для маппинга json на view модели использую newtonsoft json. Я покажу, как я это в Startup.cs подключаю:
services.AddControllers().AddNewtonsoftJson();
и пакет Microsoft.AspNetCore.Mvc.NewtonsoftJson.
И еще у меня вот такая view модель:
public class AdvertiserViewModel
{
[Required(ErrorMessage = "Empty inner id!!!!!!")]
[JsonProperty("inner_id")]
[DisplayName("inner_id")]
public int ? InnerId { get; set; }
[Required(ErrorMessage = "Empty name!!!!!")]
[JsonProperty("name")]
[DisplayName("name")]
public string Name { get; set; }
}
И вот такой контроллер:
[Route("/api/advertisers/create")]
[HttpPost]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<AdvertiserViewModel>> Create([FromBody] AdvertiserViewModel viewModel)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var advertiser = new Advertiser
{
Name = viewModel.Name,
AdvertiserRed2Id = viewModel.InnerId.Value
};
await _dbContext.Advertisers.AddAsync(advertiser);
await _dbContext.SaveChangesAsync();
return Ok();
}
И вот такой реквест:
{
"name": "hello!!!",
"inner_id": null
}
Получаю в качестве респонса
{
"InnerId": [
"Empty inner id!!!!!!"
]
}
Но это же не логично! Мой клиент ничего не знает о InnerId. Он знает лишь о inner_id.
Я нашел вот такой код фильтра, чтоб исправить это:
public class DisplayNameValidationFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.ModelState.ErrorCount > 0)
{
var modelType = context.ActionDescriptor.Parameters
.FirstOrDefault(p =>
p.BindingInfo.BindingSource.Id.Equals("Body", StringComparison.InvariantCultureIgnoreCase) ||
p.BindingInfo.BindingSource.Id.Equals("Custom", StringComparison.InvariantCultureIgnoreCase)
)
?.ParameterType; //Get model type
var expandoObj = new ExpandoObject();
var expandoObjCollection =
(ICollection<KeyValuePair<String, Object>>)
expandoObj; //Cannot convert IEnumrable to ExpandoObject
var dictionary = context.ModelState.ToDictionary(k => k.Key, v => v.Value)
.Where(v => v.Value.ValidationState == ModelValidationState.Invalid)
.ToDictionary(
k =>
{
if (modelType != null)
{
var property = modelType.GetProperties().FirstOrDefault(p =>
p.Name.Equals(k.Key, StringComparison.InvariantCultureIgnoreCase));
if (property != null)
{
//Try to get the attribute
var displayName = property.GetCustomAttributes(typeof(DisplayNameAttribute), true)
.Cast<DisplayNameAttribute>().SingleOrDefault()?.DisplayName;
return displayName ?? property.Name;
}
}
return k.Key; //Nothing found, return original vaidation key
},
v => v.Value.Errors.Select(e => e.ErrorMessage).ToList() as Object); //Box String collection
foreach (var keyValuePair in dictionary)
{
expandoObjCollection.Add(keyValuePair);
}
dynamic eoDynamic = expandoObj;
context.Result = new BadRequestObjectResult(eoDynamic);
}
base.OnActionExecuting(context);
}
}
Использование этого чуда:
[DisplayNameValidationFilter]
MyControllerClass : ControllerBase {...}
Но ведь это же просто костыль, верно? Я вынужден таскать это из солюшена в солюшен. Неужели нет какого-то способа из коробки? Я пробовал Display, DisplayName, JsonPropertyName (это вроде уже из стандартного пакета Json Text). Ничего не работает. Я не могу понять, почему это является проблемой? Кто-нибудь, подскажите пожалуйста стандартный способ это сделать!
Обновление
попробовал установить глобально стратегию для сериализации json
services.AddControllers(
).ConfigureApiBehaviorOptions(options =>
{
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
// options.SuppressMapClientErrors = true;
}).AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver.ResolveContract(typeof(DefaultContractResolver));
options.SerializerSettings.Converters.Add(new StringEnumConverter(new SnakeCaseNamingStrategy()));
});
Нет эффекта(
Ответы (2 шт):
Вам надо смотреть в сторону указания стратегии наименования полей, что-то типа:
services.AddControllers().AddNewtonsoftJson(options =>
{
options.SerializerSettings.Converters.Add(new StringEnumConverter(new CamelCaseNamingStrategy()));
});
И вообще перестаньте указывать JsonProperty на свойствах (как минимум - на совпадающих с политикой).
Этих стратегий в json.net множество, подробнее см. в документации.
Примеры на en so: https://stackoverflow.com/q/34070459/5752652
Судя по всему вам нужна стратегия SnakeCase
var model = new AdvertiserViewModel { InnerId = 123, Name = "something"};
var serializerSettings = new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
},
};
var json = JsonConvert.SerializeObject(model, serializerSettings);
json.Dump();
var deserialized = JsonConvert.DeserializeObject<AdvertiserViewModel>(json, serializerSettings);
deserialized.Dump();
Итак, кажется я это решил! Пользователь @A K♦ был прав, надо задать стратегию сериализации. И тут у нас всплывают следующие варианты:
- Использовать старый добрый NetonsoftJson;
- Использовать новый Text.Json (с .NET Core 3.0 появился)
В моем случае последний вариант не работает, так как не поддерживает нативную реализацию snake_case. В интернете с пол клика гуглится стат о том, как сделать свою. Вот. Данная реализация ругалась у меня на некоторые варианты JSON. Изучать я это не стал и решил использовать старый добрый проверенный ньютонсофт.
Итак, вот рабочая конфигурация
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
{
ProcessDictionaryKeys = true,
OverrideSpecifiedNames = true,
ProcessExtensionDataNames = true
}
};
})
