AspNet Core 2.2的自定义模型绑定器,用于复杂嵌套属性。

3

我有一个Angular客户端,并使用以下内容创建了POST请求:

{"Name":"example","Currency":"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的新实例。我想使用现有实例之一(如EUR或USD)。

(StringEnumeration支持Parse和TryParse方法,并返回正确的实例)

标准配置:

Original Answer翻译成"最初的回答"

    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)));
    }

我的问题是当客户端在http://localhost:4200/template上调用POST时,使用{"Name":"example","Currency":"EUR"}作为body,此时Model Binder无法将Currency中的"EUR"转换为实例Currency.EUR。因此,我想提供一些帮助来帮助Model Binder创建具有Currency属性和Currency.EUR实例的模板。
以下是生成的错误: 尝试读取属性“货币”的值时找到了一个具有非空值的“PrimitiveValue”节点;但是,期望出现“StartArray”节点、“StartObject”节点或具有空值的“PrimitiveValue”节点。
我的项目中有许多包含Currency属性的类。
我尝试在Template类上使用IModelBinder,它可以工作,但我不想为每个Currency属性编写一个modelBinder。
我尝试过JsonConverter,但对我来说它不起作用(可能是某些问题)。
我的预期结果是一个具有以下值的Template实例:
Id = defaluf(Guid)
Name = "example"
Currency = Currency.EUR
2个回答

1
如果您已经实现了一个可以正常工作的模型绑定器,用于您的 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;
    }
}

5
由于货币是模型的嵌套属性,所以这样做行不通。该模型属于“Template”类型,因此“if (context.Metadata.ModelType == typeof(Currency))”将永远不会成立。 - undefined
是的,它会这样做。ComplexTypeModelBinder将尝试获取其属性的绑定器,因此条件将变为真。 - undefined

1
我尝试了这个实现,但是出现了同样的错误。
我在CurrencyModelBinder和CurrencyModelBinderProvider中设置了断点。 在模型绑定器提供程序上设置断点 问题出在比较上: context.Metadata.ModelType = "Template" 而CurrencyModelBinder仅适用于货币。
我通过以下解决方法解决了这个问题:
  1. Get RawValue from Body Request
  2. Deserialize with 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反序列化。
我仍在等待您的回复。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接