具有 FromForm 绑定(bind)到 IFormFile 属性的 C# 集成测试 Controller

标签 c# asp.net-core multipartform-data

我正在使用:

  • Visual Studio 2017 专业版
  • dotnet 核心 SDK 2.2.102
  • XUnit 2.4.1

  • 我正在尝试做的

    集成测试接受表单数据的 API Controller 方法。

    设置
  • 我的 Controller 中使用 [FromForm] 接受 CommandObject 的 API 路由属性
  • CommandObject 的属性之一是 List<IFormFile> 类型。 , 这旨在管理请求中的任何文件
  • 当我从 Postman 手动测试 Controller 方法时,它按预期工作。

  • 问题

    文件未绑定(bind)到 List<IFormFile>属性(property)。其他一切都按预期工作,但文件没有。这是我第一次使用多部分表单数据,所以不知道该尝试什么。

    当我调试测试时,您可以看到除了 Documents 之外的一切正常。属性(注意,这与下面的代码不匹配 100%,因为我不得不混淆一些东西)

    enter image description here

    我看过的东西

    有很多与多部分表单数据相关的东西,我尝试过的一些解决方案是:
  • This StackOverflow post
  • Another StackOverflow post
  • A custom class

  • MyIntegrationTest.cs

    我的集成测试设置背后有很多代码。如果我把它都贴在这里,我认为它不会很有帮助。最重要的信息是变量 serverMicrosoft.AspNetCore.TestHost.TestServer 类型

    [Fact]
    async Task Post_ItemAsync_HappyPath_ReturnsOKStatusCode()
    {
        var fileDir = @"C:/path/to/files";
        var fileNames = new string[] { "test.docx", "test.txt" };
    
        using (var server = CreateTestServer())
        {
            // Arrange
            var formData = new MultipartFormDataContent()
            {
                { new StringContent("Test Title"), "Title" },
                { new StringContent("Test Description"), "Description" },
                { new StringContent("String_1"), "AListOfStrings" },
                { new StringContent("String_2"), "AListOfStrings" },
                { new StringContent("3"), "NumberOfThings" }
            };
    
            foreach (var fileName in fileNames)
            {
                var document = File.ReadAllBytes($"{fileDir}/{fileName}");
                formData.Add(new ByteArrayContent(document), "file", fileName);
            }
    
            string formDataBoundary = String.Format("----------{0:N}", Guid.NewGuid());
            string contentType = "multipart/form-data; boundary=" + formDataBoundary;
    
            var request = new HttpRequestMessage(HttpMethod.Post, "api/v1/item")
            {
                Headers =
                {
                    { HttpRequestHeader.ContentType.ToString(), contentType }
                },
                Content = formData
            };
    
            // Act
            var response = await server.CreateClient().SendAsync(request);
    
            // Assert
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    
            // Cleanup
            ...
        }
    }
    

    MyController.cs

    [HttpPost]
    ProducesResponseType((int)HttpStatusCode.OK)]
    [ProducesResponseType((int)HttpStatusCode.BadRequest)]
    public async Task<IActionResult> CreateItemAsync([FromForm]CreateItemCommand command)
    {
        bool commandResult = false;
    
        commandResult = await _mediator.Send(command);
    
        if (!commandResult)
        {
            return BadRequest();
        }
    
        return Ok();
    }
    

    CreateItemCommand.cs

    [DataContract]
    public class CreateItemCommand
        :IRequest<bool>
    {
        [DataMember]
        public string Title { get; set; }
    
        [DataMember]
        public string Description { get; set; }
    
        [DataMember]
        public HashSet<string> AListOfThings { get; set; }
    
        [DataMember]
        public int NumberOfThings { get; set; }
    
        [DataMember]
        public List<IFormFile> Documents { get; private set; }
    
        public CreateITemCommand()
        {
            AListOfThings = new HashSet<string>();
        }
    
        public CreateItemCommand(string title, string description, HashSet<string> aListOfThings, int NumberOfThings, List<IFormFile> documents)
            : this()
        {
            Title = title;
            Description = description;
            AListOfStrings = aListOfStrings;
            NumberOfThings = numberOfThings;
            Documents = documents;
        }
    }
    

    最佳答案

    表单数据边界应添加到 MultipartFormDataContent初始化时,文件的名称需要与模型的所需属性相匹配。

    //...
    
    // Arrange
    string formDataBoundary = String.Format("----------{0:N}", Guid.NewGuid());
    
    var formData = new MultipartFormDataContent(formDataBoundary) { //<---- NOTE HERE
        { new StringContent("Test Title"), "Title" },
        { new StringContent("Test Description"), "Description" },
        { new StringContent("String_1"), "AListOfStrings" },
        { new StringContent("String_2"), "AListOfStrings" },
        { new StringContent("3"), "NumberOfThings" }
    };
    
    foreach (var fileName in fileNames) {
        var document = File.ReadAllBytes($"{fileDir}/{fileName}");
        formData.Add(new ByteArrayContent(document), "Documents", fileName); //<-- NOTE HERE
    }
    
    // Act
    var response = await server.CreateClient().PostAsync("api/v1/item", formData);
    
    //...
    

    型号Documents属性需要设置为公共(public),以便模型绑定(bind)器可以在解析表单数据时填充它。

    关于具有 FromForm 绑定(bind)到 IFormFile 属性的 C# 集成测试 Controller ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56722267/

    相关文章:

    c# - 为什么需要在 C# 中使用 DispId 注释?

    C# 8 理解 await 使用语法

    asp.net-core - 什么是 "DbContextOptions` 1"?

    asp.net-mvc - 我可以在不是 Controller 的类中使用 IStringLocalizer

    go - 如何使用 Golang net/http 服务器接收上传的文件?

    c# - 在 ASP.NET 中重命名并发上传的文件

    c# - 带有添加新元素选项的下拉菜单

    c# - 通过事件处理程序发送参数?

    c# - 依赖注入(inject)如何与中间件一起工作?

    javascript - NextJS中如何读取FormData