JasperFx / wolverine

Supercharged .NET server side development!
https://wolverinefx.net
MIT License
1.17k stars 126 forks source link

Using `IFormFile` in both an endpoint and its middleware causes code generation error #926

Closed forrestab closed 2 weeks ago

forrestab commented 3 weeks ago

Describe the bug Running into an issue creating a file upload endpoint with file validation middleware. If both the endpoint and middleware have an IFormFile parameter the code generation fails with the following error.

CS0128: A local variable or function named 'file' is already defined in this scope

To Reproduce From a simple web api project I have the following endpoint and middleware classes:

public static class FileUploadEndpoint
{
    [Middleware(
        typeof(FileLengthValidationMiddleware), 
        typeof(FileExtensionValidationMiddleware)
    )]
    [WolverinePost("/api/files/upload")]
    public static async Task<Ok> HandleAsync(IFormFile file)
    {
        // todo, generate filename, write mapping to table
        // todo, create sideeffect to write file with new filename

        return TypedResults.Ok(); // return new filename
    }
}

public static class FileLengthValidationMiddleware
{
    public static void Before(IFormFile file, IOptions<FileUploadOptions> options)
    {
        // todo, return ProblemDetail if validation fails
    }
}

public static class FileExtensionValidationMiddleware
{
    public static void Before(IFormFile file, IOptions<FileUploadOptions> options)
    {
        // todo, return ProblemDetail if validation fails
    }
}

Just in case its relavent, this is my wolverine configuration (nothing special):

// ConfigureServices
builder.Host.UseWolverine(options =>
{
    options.Durability.Mode = DurabilityMode.MediatorOnly;
    options.OptimizeArtifactWorkflow();
});

// ConfigurePipeline
app.MapWolverineEndpoints();

Expected behavior Be able to use the IFormFile request parameter in both middleware leading up to and in the endpoint.

Additional context For the above repro classes this is the code that is generated (seeing it returned with a 500 when the error is thrown):

// START: POST_api_files_upload
public class POST_api_files_upload : Wolverine.Http.HttpHandler
{
    private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
    private readonly Microsoft.Extensions.Options.IOptions<FileUpload.Server.FileUploadOptions> _options;

    public POST_api_files_upload(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Microsoft.Extensions.Options.IOptions<FileUpload.Server.FileUploadOptions> options) : base(wolverineHttpOptions)
    {
        _wolverineHttpOptions = wolverineHttpOptions;
        _options = options;
    }

    public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)
    {
        // Retrieve header value from the request
        var file = ReadSingleFormFileValue(httpContext);
        FileUpload.Server.Endpoints.FileLengthValidationMiddleware.Before(file, _options);
        FileUpload.Server.Endpoints.FileExtensionValidationMiddleware.Before(file, _options);
        // Retrieve header value from the request
        var file = ReadSingleFormFileValue(httpContext);
        // Retrieve header value from the request
        var file = ReadSingleFormFileValue(httpContext);

        // The actual HTTP request handler execution
        var ok = await FileUpload.Server.Endpoints.FileUploadEndpoint.HandleAsync(file).ConfigureAwait(false);

        await ok.ExecuteAsync(httpContext).ConfigureAwait(false);
    }

}

// END: POST_api_files_upload

If I name each parameter in the middleware and endpoint something different (ie. file1, etc), the generated code looks like this:

// START: POST_api_files_upload
public class POST_api_files_upload : Wolverine.Http.HttpHandler
{
    private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
    private readonly Microsoft.Extensions.Options.IOptions<FileUpload.Server.FileUploadOptions> _options;

    public POST_api_files_upload(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Microsoft.Extensions.Options.IOptions<FileUpload.Server.FileUploadOptions> options) : base(wolverineHttpOptions)
    {
        _wolverineHttpOptions = wolverineHttpOptions;
        _options = options;
    }

    public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)
    {
        // Retrieve header value from the request
        var file2 = ReadSingleFormFileValue(httpContext);
        FileUpload.Server.Endpoints.FileLengthValidationMiddleware.Before(file2, _options);
        // Retrieve header value from the request
        var file3 = ReadSingleFormFileValue(httpContext);
        FileUpload.Server.Endpoints.FileExtensionValidationMiddleware.Before(file3, _options);
        // Retrieve header value from the request
        var file1 = ReadSingleFormFileValue(httpContext);

        // The actual HTTP request handler execution
        var ok = await FileUpload.Server.Endpoints.FileUploadEndpoint.HandleAsync(file1).ConfigureAwait(false);

        await ok.ExecuteAsync(httpContext).ConfigureAwait(false);
    }

}

// END: POST_api_files_upload

And with the parameter names different swagger looks like this:

swagger

StellaAlexis commented 1 week ago

@jeremydmiller noticed having a before handler with the same parameter name for an IFormFile works, but breaks the swagger generation. Made a PR to hopefully fix that.