NSwag Generated C# Client with IFormFile

407 Views Asked by At

I am using NSwag to generate a C# client from a dotnet 7 Web API that is consumed by a Blazor WASM application. I am having trouble getting a Post with an IFormFile to upload an attachment with the client.

Here is my API Controller method. Note that it takes an IFormFile parameter.

[MapToApiVersion(Constants.ApiVersions.V1)]
[HttpPost]
[ProducesResponseType(typeof(BlobContentInfo), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(IEnumerable<ValidationError>), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<BlobContentInfo>> Post(
    [FromRoute] int projectId,
    [FromRoute] int equipmentId,
    [FromForm] string? description,
    [FromForm] IFormFile file,
    [FromForm] AttachmentType? attachmentType = null)
{
    return await TryGetAsync<ActionResult<BlobContentInfo>>(async _ =>
    {
        var user = await GetUser();

        var equipment = await Mediator.Send(new GetEquipment { EquipmentId = equipmentId, ProjectId = projectId, User = user });
        if (equipment == null)
            return NotFound();

        var attachment = new AttachmentUpload
        {
            EquipmentId = equipmentId,
            UploadedOn = DateTimeOffset.UtcNow,
            AttachmentType = attachmentType,
            Description = description,
            FormFile = file
        };

        var result = await _validator.ValidateAsync(attachment);

        if (!result.IsValid)
            return BadRequest(result.ToValidationErrors());

        var uploaded = await Mediator.Send(new UploadAttachment
        {
            ProjectId = projectId,
            AttachmentUpload = attachment,
            User = user
        });

        return CreatedAtAction(nameof(Get), new { projectId, equipmentId }, uploaded);
    }, (ex, _) => Error<BlobContentInfo>(ex));
}

Here is my Service Method which calls the generated API client method.

  1. Note that is splits the IFormFile into parameters which make up its constituent parts. Is there a way to avoid this?

  2. Note that the IHeaderDictionary expected by the generated client is of type Api.Client.IHeaderDictionary instead of Microsoft.AspNetCore.Http.IHeaderDictionary. Is there a way to make the generated client use Microsoft.AspNetCore.Http.IHeaderDictionary instead? I've tried an extension method to convert, but this doesn't work as there's no concrete Api.Client.IHeaderDictionary. I've also looked at the nswag.json, but I don't see anything in there that could accomplish this.

    public async Task<ServiceResult<BlobContentInfo>> UploadEquipmentAttachment(
        int projectId,
        int equipmentId,
        AttachmentUpload attachment,
        CancellationToken cancellationToken = default)
    {
        return await TryGetAsync(async _ =>
        {
            var response =
                await ApiClient.V1ProjectsEquipmentAttachmentsPostAsync(
                    projectId,
                    equipmentId,
                    attachment.Description,
                    attachment.FormFile.ContentType,
                    attachment.FormFile.ContentDisposition,
                    attachment.FormFile.Headers, // This fails as the NSwag client is expecting Api.Client.IHeaderDictionary instead of Microsoft.AspNetCore.Http.IHeaderDictionary
                    attachment.FormFile.Length,
                    attachment.FormFile.Name,
                    attachment.FormFile.FileName,
                    attachment.AttachmentType,
                    cancellationToken);
    
            return new ServiceResult<BlobContentInfo>
            {
                Status = OperationResult.Success,
                HttpResponseCode = (HttpStatusCode)response.StatusCode,
                Data = response.Result
            };
        }, (ex, _) => Task.FromResult(HandleClientApiException<BlobContentInfo>(ex)));
    }
    

Here is my nswag.json configuration:

{
  "runtime": "Net70",
  "defaultVariables": null,
  "documentGenerator": {
    "aspNetCoreToOpenApi": {
      "project": "$(MSBuildProjectFullPath)",
      "msBuildProjectExtensionsPath": null,
      "configuration": "$(Configuration)",
      "runtime": "",
      "targetFramework": "",
      "noBuild": true,
      "msBuildOutputPath": null,
      "verbose": true,
      "workingDirectory": null,
      "requireParametersWithoutDefault": false,
      "apiGroupNames": null,
      "defaultPropertyNameHandling": "Default",
      "defaultReferenceTypeNullHandling": "Null",
      "defaultDictionaryValueReferenceTypeNullHandling": "NotNull",
      "defaultResponseReferenceTypeNullHandling": "NotNull",
      "generateOriginalParameterNames": true,
      "defaultEnumHandling": "Integer",
      "flattenInheritanceHierarchy": false,
      "generateKnownTypes": true,
      "generateEnumMappingDescription": false,
      "generateXmlObjects": false,
      "generateAbstractProperties": false,
      "generateAbstractSchemas": true,
      "ignoreObsoleteProperties": false,
      "allowReferencesWithProperties": false,
      "useXmlDocumentation": true,
      "resolveExternalXmlDocumentation": true,
      "excludedTypeNames": [],
      "serviceHost": null,
      "serviceBasePath": null,
      "serviceSchemes": [],
      "infoTitle": "My Title",
      "infoDescription": null,
      "infoVersion": "1.0.0",
      "documentTemplate": null,
      "documentProcessorTypes": [],
      "operationProcessorTypes": [],
      "typeNameGeneratorType": null,
      "schemaNameGeneratorType": null,
      "contractResolverType": null,
      "serializerSettingsType": null,
      "useDocumentProvider": true,
      "documentName": "v1",
      "aspNetCoreEnvironment": null,
      "createWebHostBuilderMethod": null,
      "startupType": null,
      "allowNullableBodyParameters": true,
      "useHttpAttributeNameAsOperationId": false,
      "output": null,
      "outputType": "Swagger2",
      "newLineBehavior": "Auto",
      "assemblyPaths": [],
      "assemblyConfig": null,
      "referencePaths": [],
      "useNuGetCache": false
    }
  },
  "codeGenerators": {
    "openApiToCSharpClient": {
      "clientBaseClass": null,
      "configurationClass": null,
      "generateClientClasses": true,
      "generateClientInterfaces": true,
      "clientBaseInterface": null,
      "injectHttpClient": true,
      "disposeHttpClient": true,
      "protectedMethods": [],
      "generateExceptionClasses": true,
      "exceptionClass": "ApiException",
      "wrapDtoExceptions": true,
      "useHttpClientCreationMethod": false,
      "httpClientType": "System.Net.Http.HttpClient",
      "useHttpRequestMessageCreationMethod": false,
      "useBaseUrl": true,
      "generateBaseUrlProperty": true,
      "generateSyncMethods": false,
      "generatePrepareRequestAndProcessResponseAsAsyncMethods": false,
      "exposeJsonSerializerSettings": false,
      "clientClassAccessModifier": "public",
      "typeAccessModifier": "public",
      "generateContractsOutput": false,
      "contractsNamespace": null,
      "contractsOutputFilePath": null,
      "parameterDateTimeFormat": "s",
      "parameterDateFormat": "yyyy-MM-dd",
      "generateUpdateJsonSerializerSettingsMethod": true,
      "useRequestAndResponseSerializationSettings": false,
      "serializeTypeInformation": false,
      "queryNullValue": "",
      "className": "$(ClientClassNamePrefix)Client",
      "operationGenerationMode": "SingleClientFromPathSegments",
      "additionalNamespaceUsages": [],
      "additionalContractNamespaceUsages": [],
      "generateOptionalParameters": true,
      "generateJsonMethods": false,
      "enforceFlagEnums": false,
      "parameterArrayType": "System.Collections.Generic.IEnumerable",
      "parameterDictionaryType": "System.Collections.Generic.IDictionary",
      "responseArrayType": "System.Collections.Generic.ICollection",
      "responseDictionaryType": "System.Collections.Generic.IDictionary",
      "wrapResponses": true,
      "wrapResponseMethods": [],
      "generateResponseClasses": true,
      "responseClass": "ApiResponse",
      "namespace": "$(ClientNamespace)",
      "requiredPropertiesMustBeDefined": true,
      "dateType": "System.DateTimeOffset",
      "jsonConverters": null,
      "anyType": "object",
      "dateTimeType": "System.DateTimeOffset",
      "timeType": "System.TimeSpan",
      "timeSpanType": "System.TimeSpan",
      "arrayType": "System.Collections.Generic.ICollection",
      "arrayInstanceType": "System.Collections.ObjectModel.Collection",
      "dictionaryType": "System.Collections.Generic.IDictionary",
      "dictionaryInstanceType": "System.Collections.Generic.Dictionary",
      "arrayBaseType": "System.Collections.ObjectModel.Collection",
      "dictionaryBaseType": "System.Collections.Generic.Dictionary",
      "classStyle": "Poco",
      "jsonLibrary": "NewtonsoftJson",
      "generateDefaultValues": true,
      "generateDataAnnotations": true,
      "excludedTypeNames": [],
      "excludedParameterNames": [],
      "handleReferences": false,
      "generateImmutableArrayProperties": false,
      "generateImmutableDictionaryProperties": false,
      "jsonSerializerSettingsTransformationMethod": null,
      "inlineNamedArrays": false,
      "inlineNamedDictionaries": false,
      "inlineNamedTuples": true,
      "inlineNamedAny": false,
      "generateDtoTypes": true,
      "generateOptionalPropertiesAsNullable": false,
      "generateNullableReferenceTypes": false,
      "templateDirectory": null,
      "typeNameGeneratorType": null,
      "propertyNameGeneratorType": null,
      "enumNameGeneratorType": null,
      "serviceHost": null,
      "serviceSchemes": null,
      "newLineBehavior": "Auto",
      "output": "$(ClientOutDir)"
    }
  }
}

Here is where I wire-up the client from Program.cs

private static void AddTmsCoreApiClient(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddOptions<TmsCoreApiClientSettings>()
            .Bind(configuration.GetSection(TmsCoreApiClientSettings.ConfigSectionName));
        var tmsCoreApiClientSettings = new TmsCoreApiClientSettings();
        configuration.GetSection(TmsCoreApiClientSettings.ConfigSectionName).Bind(tmsCoreApiClientSettings);

        services.AddHttpClient(TmsApiClientName, client =>
                client.BaseAddress = new Uri(tmsCoreApiClientSettings.BaseUri))
            .AddHttpMessageHandler(sp => sp.GetRequiredService<AuthorizationMessageHandler>()
                .ConfigureHandler(
                    authorizedUrls: new[] { tmsCoreApiClientSettings.BaseUri },
                    scopes: tmsCoreApiClientSettings.Scopes
                ));

        services.AddScoped<ICoreApiClient, CoreApiClient>(provider =>
        {
            var httpClient = provider.GetRequiredService<IHttpClientFactory>().CreateClient(TmsApiClientName);
            return new CoreApiClient(tmsCoreApiClientSettings.BaseUri, httpClient);
        });
    }
1

There are 1 best solutions below

0
kingua On

Not an answer, but the only way I could get this to work was by changing the API to accept a byte[] instead of an IFormFile and then converting the byte[] to an IFormFile in the API before sending to Azure Blob storage.