Search code examples
asp.net-web-apiswaggeropenapi

Controller method that accepts file along with a DTO object


I'm trying to create a controller method that capable to accept a file and a DTO in the same request. I need to follow this exact structure for the proper OpenAPI generation with nested types. (similar to this https://swagger.io/docs/specification/describing-request-body/multipart-requests/).

I simplified the example, however I hope the idea is clear. In the real project "SimpleDTO" class contains multiple properties of different types including enums and user types)

public class SimpleDTO
{
     public string Property1 {get;set;}

     public string Property2 {get;set;}
     ...
}

public class RequestWrapper 
{
    public IFormFile File {get;set;}

    public SimpleDTO? SimpleDto {get;set;}
}

[Post]
public async Task<IActionResult> TestMethod([FromForm]RequestWrapper dto)
{
    ...
    return Ok();
}

Then I tested this code with the Postman and got the status 400. If I replace the "SimpleDto" property type with something simple (e.g. int) it works.

I tried to apply custom model binders to the "dto" controller method parameter and to the SimpleDTO class with no result. I suppose I did it the wrong way.

I expect to get the following OpenAPI specification (and the client generated from the specification should work with the method mentioned above):

requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties: 
                file:
                    type: string
                    format: binary
                SimpleDto:
                  $ref: '#/components/schemas/SimpleDTO'
      responses:
        '200':
          description: Success

I have an alternative solution, which is a bit tricky and I would like to avoid it if possible. The idea is to use "SimpleDTO" as a method parameter and get file from the request body, then add file info to the OpenAPI specification using OperationFiltres.


Solution

  • So, I added custom IOperationFilter for the OpenApi and got rid of the RequestWrapper class.

    public class SimpleDTO
    {
        public string Property1 {get;set;}
    
        public string Property2 {get;set;}
    }
    

    Controller method:

    [Post]
    public async Task<IActionResult> TestMethod(IFormFile file, [FromForm]SimpleDTO dto)
    {
       ...
       return Ok();
    }
    

    Here, I partially replace generation of OpenApi specification with my own implementation using IOperationFilter:

    public class OpenApiSpecificationFilter : IOperationFilter
    {
       public void Apply(OpenApiOperation operation, OperationFilterContext context)
       {
           var formParameters = context.MethodInfo.GetParameters()
            .Where(p => p.GetCustomAttributes(typeof(FromFormAttribute), false).Length > 0
                        || p.ParameterType == typeof(IFormFile)
                        || p.ParameterType == typeof(IFormFileCollection)
                        || p.ParameterType == typeof(List<IFormFile>));
        
        if (!formParameters.Any())
            return;
        
        operation.RequestBody.Content.Clear();
        var schema = new OpenApiSchema();
        
        foreach (var p in formParameters)
        {
            if (p.ParameterType == typeof(IFormFile))
            {
                schema.Properties.Add(p.Name, new OpenApiSchema()
                {
                    Type = "string",
                    Format = "binary"
                });
            }
            else if (p.ParameterType == typeof(IFormCollection) || p.ParameterType == typeof(List<IFormFile>))
            {
    
                schema.Properties.Add(p.Name, new OpenApiSchema()
                {
                    Type = "array",
                    Items = new OpenApiSchema()
                    {
                        Type = "string",
                        Format = "binary"
                    }
                    
                });
            }
            else
            {
                var reference = context.SchemaGenerator.GenerateSchema(p.ParameterType, context.SchemaRepository);
                schema.Properties.Add(p.Name, reference);
            }
        }
        
        operation.RequestBody.Content.Add("multipart/form-data", new OpenApiMediaType()
        {
            Schema = schema
        });
    }
    }
    

    Program.cs:

    builder.Services.AddSwaggerGen(options =>
    {
       options.OperationFilter<OpenApiSpecificationFilter>();
    }
    

    Test request:

    curl --location 'https://localhost:[port]/testMethod' \
    --form 'file=@"testFile.jpeg"' \
    --form 'dto="{
        \"Property1\": \"test value 1\",
        \"Property2\": \"test value 2\",
      }"'