【问题标题】:ASP.NET Core and formdata binding with file and json propertyASP.NET Core 和 formdata 与文件和 json 属性的绑定
【发布时间】:2017-07-10 16:55:12
【问题描述】:

我有以下型号:

public class MyJson
{
 public string Test{get;set;}
}

public class Dto
{
 public IFormFile MyFile {get;set;}
 public MyJson MyJson {get;set;}
}

在客户端,我想发送一个文件和一个 json。所以我用以下键在 formData 中发送它:

var formData = new FormData();
formData["myFile"] = file//here is my file
formData["myJson"] = obj; // object to be serialized to json.

我的动作是这样的:

public void MyAction(Dto dto) // or with [FromForm], doesn't really matter
{
  //dto.MyJson is null here
  //dto.myFile is set correctly.
}

如果我将dto.MyJson 更改为字符串,那么它可以正常工作,但是我必须在操作中手动将其反序列化到我的对象中。将它作为字符串的第二个问题是我无法使用 swagger UI 来正确处理它,因为它会要求我输入 json 字符串而不是对象,无论如何将它作为字符串听起来并不正确。有没有一种本地方法可以在动作参数中正确处理 json 和文件,而不是使用 Request.Form 手动解析它?

【问题讨论】:

  • IFormFile 是在从序列化数据解析过程中无法实例化的东西。
  • @Maxim IFormFile 是 ASP.Net Core 的一部分,它由 asp.net 核心机制正确处理。这里的问题是 json 参数。
  • 好的,我明白了。也许原因是 2 种不同的序列化格式......所以它尝试使用表单数据格式化程序反序列化 JSON 属性。是否可以仅使用 json 作为解决方法?
  • @Maxim 实际上我没有尝试在 FormData 中只发送 json,但我怀疑它也会失败。我不确定 ASP.NET 核心是否对 formdata 中的 json 对象具有本机支持。我会试一试,不幸的是明天。
  • 它的 [FromForm] 是多部分表单发送所需的,它的内容类型为 x-www-form-urlencoded 或类似,并且它不会反序列化 json(即 application/json)。您可能想要为该内容类型执行自己的反序列化程序,或者只是在您的操作中手动执行。如果您的 json 不是太复杂,您可以通过将其展平为原始类型来解决问题,因此它仅包含 IFromFile 和原始类型属性。如果这对您不起作用,请手动反序列化。

标签: c# asp.net json


【解决方案1】:

这可以使用自定义模型绑定器来完成:

public class FormDataJsonBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if(bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));

        // Fetch the value of the argument by name and set it to the model state
        string fieldName = bindingContext.FieldName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(fieldName);
        if(valueProviderResult == ValueProviderResult.None) return Task.CompletedTask;
        else bindingContext.ModelState.SetModelValue(fieldName, valueProviderResult);

        // Do nothing if the value is null or empty
        string value = valueProviderResult.FirstValue;
        if(string.IsNullOrEmpty(value)) return Task.CompletedTask;

        try
        {
            // Deserialize the provided value and set the binding result
            object result = JsonConvert.DeserializeObject(value, bindingContext.ModelType);
            bindingContext.Result = ModelBindingResult.Success(result);
        }
        catch(JsonException)
        {
            bindingContext.Result = ModelBindingResult.Failed();
        }

        return Task.CompletedTask;
    }
}

然后您可以在 DTO 类中使用 ModelBinder 属性来指示应该使用此绑定器来绑定 MyJson 属性:

public class Dto
{
    public IFormFile MyFile {get;set;}

    [ModelBinder(BinderType = typeof(FormDataJsonBinder))]
    public MyJson MyJson {get;set;}
}

请注意,您还需要在客户端中正确序列化您的 JSON 数据:

const formData = new FormData();
formData.append(`myFile`, file);
formData.append('myJson', JSON.stringify(obj));

上面的代码可以工作,但你也可以更进一步,定义一个自定义属性和一个自定义IModelBinderProvider,这样你就不需要每次都使用更冗长的ModelBinder属性。 .请注意,我为此重新使用了现有的 [FromForm] 属性,但您也可以定义自己的属性来代替。

public class FormDataJsonBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if(context == null) throw new ArgumentNullException(nameof(context));

        // Do not use this provider for binding simple values
        if(!context.Metadata.IsComplexType) return null;

        // Do not use this provider if the binding target is not a property
        var propName = context.Metadata.PropertyName;
        var propInfo = context.Metadata.ContainerType?.GetProperty(propName);
        if(propName == null || propInfo == null) return null;

        // Do not use this provider if the target property type implements IFormFile
        if(propInfo.PropertyType.IsAssignableFrom(typeof(IFormFile))) return null;

        // Do not use this provider if this property does not have the FromForm attribute
        if(!propInfo.GetCustomAttributes(typeof(FromForm), false).Any()) return null;

        // All criteria met; use the FormDataJsonBinder
        return new FormDataJsonBinder();
    }
}

您需要将此模型绑定器提供程序添加到您的启动配置中,然后才会被拾取:

services.AddMvc(options =>
{
    // add custom model binders to beginning of collection
    options.ModelBinderProviders.Insert(0, new FormDataJsonBinderProvider())
});

那么你的 DTO 可以简单一点:

public class Dto
{
    public IFormFile MyFile {get;set;}

    [FromForm]
    public MyJson MyJson {get;set;}
}

您可以在 ASP.NET Core 文档中阅读有关自定义模型绑定的更多信息:https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding

【讨论】:

  • 您好,感谢您的帮助,当我们有一个要绑定的数组时,它是如何工作的?
  • @CedricArnould 我没有用数组测试过,但用法应该和对象一样。你遇到问题了吗?
  • 我意识到问题来自于绑定一个抽象类数组,而抽象类才是真正的问题。我想我找到了一种方法,目前仍在努力。感谢您的回答
  • 是的,我和 OP 有同样的问题,而且这个(精彩的)解决方案不适用于抽象类,因为它提供了 Model bound complex types must not be abstract or value types and must have a parameterless constructor. 所以我想我正在寻找一个额外的步骤来根据 JSON 中定义的类型,将其转换为从抽象类扩展的类。哦,在我的例子中,它是抽象类的集合......
  • @bertuslakkis 我相信您无需太多修改即可使用此解决方案来反序列化为抽象类。不过,您需要做的是指导 Json.NET 如何选择要反序列化的具体类。一种选择是SerializeTypeNameHandling。在 Stack Overflow 上还可以找到其他选项,例如 this one
猜你喜欢
  • 2018-02-10
  • 2022-06-11
  • 2020-01-08
  • 1970-01-01
  • 1970-01-01
  • 2021-10-28
  • 1970-01-01
  • 1970-01-01
  • 2021-05-09
相关资源
最近更新 更多