Пользовательская модель Binder AspNet Core 2.2 для сложных вложенных свойств - PullRequest
0 голосов
/ 16 апреля 2019

У меня есть клиент Angular и я создаю запрос POST с этим телом:

{ "Имя": "пример", "Валюта": "EUR"}

Я использую протокол Odata, и мой контроллер:

    [HttpPost, ODataRoute("Templates")]
    public IActionResult Insert([FromBody] Template value)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);

        value.Id = Guid.NewGuid();

        _context.Templates.Add(value);
        _context.SaveChanges();
        return Created(value);
    }

с шаблоном:

public class Template
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public Currency Currency { get; set; }
}

и валюта:

[Serializable]
public class Currency : StringEnumeration<Currency>
{
    public static Currency EUR = new Currency("EUR", "EUR");
    public static Currency USD = new Currency("USD", "USD");

    Currency() { }
    Currency(string code, string description) : base(code, description) { }
}

Currency - это особый класс, потому что он имеет частные конструкторы, и по этой причине я не могу создать новый экземпляр Currency. Я хочу использовать один из существующих экземпляров (EUR или USD).

(StringEnumeration поддерживает метод Parse и TryParse и возвращает правильный экземпляр)

Стандартная конфигурация:

    public void ConfigureServices(IServiceCollection services)
    {
        services.ConfigureCors();

        services.AddOData();

        services.ConfigureIISIntegration();

        services.AddMvc()                
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

        services.AddDbContext<GpContext>(option => option
            .UseSqlServer(Configuration.GetConnectionString(GpConnection)));
    }

Моя проблема, когда клиент вызывает POST на http://localhost:4200/template с телом: {"Name": "example", "Currency": "EUR"}

Модель Bindel не может конвертировать "EUR" в экземпляре Currency.EUR, поэтому я хочу предоставить кое-что, чтобы помочь связующему модели создать шаблон с свойством Currency с экземпляром Currency.EUR

Это сгенерированная ошибка: При попытке прочитать значение свойства «Валюта» был обнаружен узел «PrimitiveValue» с ненулевым значением; однако ожидался узел 'StartArray', узел 'StartObject' или узел 'PrimitiveValue' с нулевым значением.

В моем проекте у меня есть много классов со свойством Currency внутри.

Я пытался использовать IModelBinder для класса Template, и он работает, но я не хочу писать modelBinder для любого свойства Currency.

Я пытался с JsonConverter, но у меня это не работает (может, что-то не так)

Мой ожидаемый результат - экземпляр шаблона со следующими значениями:

Id = defaluf(Guid)
Name = "example"
Currency = Currency.EUR

Ответы [ 2 ]

0 голосов
/ 16 апреля 2019

Я пробую эту реализацию, и у меня та же ошибка.

Я устанавливаю точки останова в CurrencyModelBinder и в CurrencyModelBinderProvider

Точка останова на поставщике подшивки моделей

Проблема в сравнении: context.Metadata.ModelType = "Шаблон" и CurrencyModelBinder вызывается только для валюты.

Я решил с этим решением:

  1. Получить RawValue из запроса тела
  2. Десериализация с помощью JsonConverter

    [HttpPost, ODataRoute("Templates")]
    public IActionResult Insert([FromBody] object value)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);
    
        var template = JsonConvert.DeserializeObject<Template>(value.ToString());
        template.Id = Guid.NewGuid();
    
        _context.Templates.Add(template);
        _context.SaveChanges();
        return Created(value);
    }
    

Класс Валюты теперь

[Serializable]
[JsonConverter(typeof(CurrencyJsonConverter))]
public class Currency : StringEnumeration<Currency>
{
    public static Currency CHF = new Currency("CHF", "CHF");
    public static Currency EUR = new Currency("EUR", "EUR");
    public static Currency USD = new Currency("USD", "USD");

    Currency() { }
    Currency(string code, string description) : base(code, description) { }
}

и JsonConverter

public class CurrencyJsonConverter : JsonConverter
{
    public override bool CanWrite => true;

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Currency);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null) return null;
        var value = reader.Value as string;
        return Currency.Parse(value);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value is Currency currency)
            serializer.Serialize(writer, currency.Code);
    }
}

Я не понимаю, почему связующий элемент по умолчанию не использует десериализацию Json.

Я жду вашего доброго ответа.

0 голосов
/ 16 апреля 2019

Если у вас уже есть работающее связующее модель для вашего типа Currency, то вы можете просто внедрить IModelBinderProvider, который автоматически предоставляет связыватель модели всякий раз, когда MVC необходимо связать с типом Currency:

public class CurrencyModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType == typeof(Currency))
            return new BinderTypeModelBinder(typeof(CurrencyModelBinder));
        return null;
    }
}

Затем вам нужно зарегистрировать это в ConfigureServices вашего * стартапа:

services.AddMvc(options =>
{
    options.ModelBinderProviders.Insert(0, new CurrencyModelBinderProvider());
});

И затем все элементы Currency будут автоматически связаны, используя ваш CurrencyModelBinder без необходимости использованияАтрибут [ModelBinder] везде.

Это также описано в разделе «Образец пользовательского связующего устройства» документации.


Просто для полноты возможной реализациииз CurrencyModelBinder:

public class CurrencyModelBinder : IModelBinder
{
    private static readonly Currency[] _currencies = new Currency[]
    {
        Currency.EUR,
        Currency.USD,
    };

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelName = bindingContext.ModelName;
        var providerResult = bindingContext.ValueProvider.GetValue(modelName);
        if (providerResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        var value = providerResult.FirstValue;
        if (string.IsNullOrEmpty(value))
        {
            return Task.CompletedTask;
        }

        var currency = _currencies
            .FirstOrDefault(c => c.Code.Equals(value, StringComparison.OrdinalIgnoreCase));

        if (currency != null)
            bindingContext.Result = ModelBindingResult.Success(currency);
        else
            bindingContext.ModelState.TryAddModelError(modelName, "Unknown currency");

        return Task.CompletedTask;
    }
}
...