Skip to content

Commit a821029

Browse files
authored
VCST-4353: Improve Swagger Schema for File Upload using streaming (#2968)
feat: Adds UploadFileAttribute and registers UploadFileOperationFilter to describe streaming file-upload endpoints as multipart/form-data with binary file fields.
1 parent f4e41d9 commit a821029

File tree

3 files changed

+145
-0
lines changed

3 files changed

+145
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System;
2+
3+
namespace VirtoCommerce.Platform.Core.Swagger;
4+
5+
/// <summary>
6+
/// Marks an action as a <b>file upload endpoint that uses streaming</b>,
7+
/// so that Swagger / OpenAPI generators describe a <c>multipart/form-data</c>
8+
/// request body with a binary file field.
9+
/// <para>
10+
/// This attribute is intended for large-file uploads that read directly from
11+
/// <c>HttpContext.Request.Body</c> (for example, with <c>MultipartReader</c>)
12+
/// and typically use <c>[DisableFormValueModelBinding]</c>, rather than
13+
/// binding <c>IFormFile</c> parameters.
14+
/// </para>
15+
/// <para>
16+
/// It is consumed by platform-level Swagger / OpenAPI filters/transformers
17+
/// (for both Swashbuckle and <c>Microsoft.AspNetCore.OpenApi</c>) and does
18+
/// not change runtime behavior of the action itself.
19+
/// </para>
20+
/// </summary>
21+
[AttributeUsage(AttributeTargets.Method)]
22+
public sealed class UploadFileAttribute : Attribute
23+
{
24+
/// <summary>
25+
/// Logical name of the file field in the generated OpenAPI schema.
26+
/// Defaults to <c>"uploadedFile"</c>. This name is used only for
27+
/// documentation/UI (for example, Swagger UI form field name) and does
28+
/// not affect how the stream is read in the action.
29+
/// </summary>
30+
public string Name { get; set; } = "file";
31+
32+
/// <summary>
33+
/// Human‑readable description for the file field in the generated
34+
/// OpenAPI document (for example, tooltip in Swagger UI).
35+
/// </summary>
36+
public string Description { get; set; } = "Upload File";
37+
38+
/// <summary>
39+
/// OpenAPI schema type for the file property.
40+
/// For file uploads this should remain <c>"string"</c>; the corresponding
41+
/// schema formatter will set <c>format = "binary"</c> to indicate a
42+
/// streamed binary payload.
43+
/// See: https://swagger.io/docs/specification/v3_0/describing-request-body/file-upload/
44+
/// </summary>
45+
public string Type { get; set; } = "string";
46+
47+
/// <summary>
48+
/// Indicates whether the file field is required in the generated
49+
/// OpenAPI schema. Set to <c>true</c> when a file must always be
50+
/// provided in the multipart request body.
51+
/// </summary>
52+
public bool Required { get; set; } = false;
53+
54+
/// <summary>
55+
/// When <c>true</c>, describes the file field as a collection of files
56+
/// (for example, an array of <c>string</c>/<c>binary</c> items) in the
57+
/// generated OpenAPI schema, allowing multiple files to be uploaded
58+
/// under the same logical field name.
59+
/// </summary>
60+
public bool AllowMultiple { get; set; } = false;
61+
}

src/VirtoCommerce.Platform.Web/Swagger/SwaggerServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public static void AddSwagger(this IServiceCollection services, IConfiguration c
8585
c.OperationFilter<FileResponseTypeFilter>();
8686
c.OperationFilter<OptionalParametersFilter>();
8787
c.OperationFilter<ArrayInQueryParametersFilter>();
88+
c.OperationFilter<UploadFileOperationFilter>();
8889
c.OperationFilter<ModuleInfoFilter>();
8990
c.OperationFilter<OpenIDEndpointDescriptionFilter>();
9091
c.SchemaFilter<EnumSchemaFilter>();
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Microsoft.OpenApi.Models;
4+
using Swashbuckle.AspNetCore.SwaggerGen;
5+
using VirtoCommerce.Platform.Core.Swagger;
6+
7+
namespace VirtoCommerce.Platform.Web.Swagger;
8+
9+
/// <summary>
10+
/// Swashbuckle operation filter that describes file upload endpoints
11+
/// marked with <see cref="UploadFileAttribute"/> as multipart/form-data
12+
/// with a binary file field.
13+
/// </summary>
14+
public class UploadFileOperationFilter : IOperationFilter
15+
{
16+
public void Apply(OpenApiOperation operation, OperationFilterContext context)
17+
{
18+
var uploadAttribute = context.MethodInfo
19+
.GetCustomAttributes(typeof(UploadFileAttribute), inherit: true)
20+
.Cast<UploadFileAttribute>()
21+
.FirstOrDefault();
22+
23+
if (uploadAttribute == null)
24+
{
25+
return;
26+
}
27+
28+
operation.RequestBody ??= new OpenApiRequestBody();
29+
operation.RequestBody.Content ??= new Dictionary<string, OpenApiMediaType>();
30+
31+
if (!operation.RequestBody.Content.TryGetValue("multipart/form-data", out var mediaType))
32+
{
33+
mediaType = new OpenApiMediaType();
34+
operation.RequestBody.Content["multipart/form-data"] = mediaType;
35+
}
36+
37+
mediaType.Schema ??= new OpenApiSchema { Type = "object" };
38+
var schema = mediaType.Schema;
39+
40+
var filePropertyName = string.IsNullOrEmpty(uploadAttribute.Name) ? "file" : uploadAttribute.Name;
41+
42+
schema.Properties ??= new Dictionary<string, OpenApiSchema>();
43+
var fileSchema = CreateFileSchema(uploadAttribute);
44+
schema.Properties[filePropertyName] = fileSchema;
45+
46+
if (uploadAttribute.Required)
47+
{
48+
schema.Required ??= new HashSet<string>();
49+
schema.Required.Add(filePropertyName);
50+
operation.RequestBody.Required = true;
51+
}
52+
}
53+
54+
private static OpenApiSchema CreateFileSchema(UploadFileAttribute uploadAttribute)
55+
{
56+
var elementType = string.IsNullOrEmpty(uploadAttribute.Type) ? "string" : uploadAttribute.Type;
57+
58+
if (uploadAttribute.AllowMultiple)
59+
{
60+
// array of binary strings
61+
return new OpenApiSchema
62+
{
63+
Type = "array",
64+
Description = uploadAttribute.Description,
65+
Items = new OpenApiSchema
66+
{
67+
Type = elementType,
68+
Format = "binary",
69+
},
70+
};
71+
}
72+
73+
// single binary string
74+
return new OpenApiSchema
75+
{
76+
Type = elementType,
77+
Format = "binary",
78+
Description = uploadAttribute.Description,
79+
};
80+
}
81+
}
82+
83+

0 commit comments

Comments
 (0)