Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/VirtoCommerce.Platform.Core/Swagger/UploadFileAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;

namespace VirtoCommerce.Platform.Core.Swagger;

/// <summary>
/// Marks an action as a <b>file upload endpoint that uses streaming</b>,
/// so that Swagger / OpenAPI generators describe a <c>multipart/form-data</c>
/// request body with a binary file field.
/// <para>
/// This attribute is intended for large-file uploads that read directly from
/// <c>HttpContext.Request.Body</c> (for example, with <c>MultipartReader</c>)
/// and typically use <c>[DisableFormValueModelBinding]</c>, rather than
/// binding <c>IFormFile</c> parameters.
/// </para>
/// <para>
/// It is consumed by platform-level Swagger / OpenAPI filters/transformers
/// (for both Swashbuckle and <c>Microsoft.AspNetCore.OpenApi</c>) and does
/// not change runtime behavior of the action itself.
/// </para>
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class UploadFileAttribute : Attribute
{
/// <summary>
/// Logical name of the file field in the generated OpenAPI schema.
/// Defaults to <c>"uploadedFile"</c>. This name is used only for
/// documentation/UI (for example, Swagger UI form field name) and does
/// not affect how the stream is read in the action.
/// </summary>
public string Name { get; set; } = "file";

/// <summary>
/// Human‑readable description for the file field in the generated
/// OpenAPI document (for example, tooltip in Swagger UI).
/// </summary>
public string Description { get; set; } = "Upload File";

/// <summary>
/// OpenAPI schema type for the file property.
/// For file uploads this should remain <c>"string"</c>; the corresponding
/// schema formatter will set <c>format = "binary"</c> to indicate a
/// streamed binary payload.
/// See: https://swagger.io/docs/specification/v3_0/describing-request-body/file-upload/
/// </summary>
public string Type { get; set; } = "string";

/// <summary>
/// Indicates whether the file field is required in the generated
/// OpenAPI schema. Set to <c>true</c> when a file must always be
/// provided in the multipart request body.
/// </summary>
public bool Required { get; set; } = false;

/// <summary>
/// When <c>true</c>, describes the file field as a collection of files
/// (for example, an array of <c>string</c>/<c>binary</c> items) in the
/// generated OpenAPI schema, allowing multiple files to be uploaded
/// under the same logical field name.
/// </summary>
public bool AllowMultiple { get; set; } = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public static void AddSwagger(this IServiceCollection services, IConfiguration c
c.OperationFilter<FileResponseTypeFilter>();
c.OperationFilter<OptionalParametersFilter>();
c.OperationFilter<ArrayInQueryParametersFilter>();
c.OperationFilter<UploadFileOperationFilter>();
c.OperationFilter<SecurityRequirementsOperationFilter>();
c.OperationFilter<ModuleInfoFilter>();
c.OperationFilter<OpenIDEndpointDescriptionFilter>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using VirtoCommerce.Platform.Core.Swagger;

namespace VirtoCommerce.Platform.Web.Swagger;

/// <summary>
/// Swashbuckle operation filter that describes file upload endpoints
/// marked with <see cref="UploadFileAttribute"/> as multipart/form-data
/// with a binary file field.
/// </summary>
public class UploadFileOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var uploadAttribute = context.MethodInfo
.GetCustomAttributes(typeof(UploadFileAttribute), inherit: true)
.Cast<UploadFileAttribute>()
.FirstOrDefault();

if (uploadAttribute == null)
{
return;
}

operation.RequestBody ??= new OpenApiRequestBody();
operation.RequestBody.Content ??= new Dictionary<string, OpenApiMediaType>();

if (!operation.RequestBody.Content.TryGetValue("multipart/form-data", out var mediaType))
{
mediaType = new OpenApiMediaType();
operation.RequestBody.Content["multipart/form-data"] = mediaType;
}

mediaType.Schema ??= new OpenApiSchema { Type = "object" };
var schema = mediaType.Schema;

var filePropertyName = string.IsNullOrEmpty(uploadAttribute.Name) ? "file" : uploadAttribute.Name;

schema.Properties ??= new Dictionary<string, OpenApiSchema>();
var fileSchema = CreateFileSchema(uploadAttribute);
schema.Properties[filePropertyName] = fileSchema;

if (uploadAttribute.Required)
{
schema.Required ??= new HashSet<string>();
schema.Required.Add(filePropertyName);
operation.RequestBody.Required = true;
}
}

private static OpenApiSchema CreateFileSchema(UploadFileAttribute uploadAttribute)
{
var elementType = string.IsNullOrEmpty(uploadAttribute.Type) ? "string" : uploadAttribute.Type;

if (uploadAttribute.AllowMultiple)
{
// array of binary strings
return new OpenApiSchema
{
Type = "array",
Description = uploadAttribute.Description,
Items = new OpenApiSchema
{
Type = elementType,
Format = "binary",
},
};
}

// single binary string
return new OpenApiSchema
{
Type = elementType,
Format = "binary",
Description = uploadAttribute.Description,
};
}
}


Loading