8000 Help with C# Client IFormFile · Issue #4518 · RicoSuter/NSwag · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Help with C# Client IFormFile #4518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
kingua opened this issue Sep 14, 2023 · 2 comments
Open

Help with C# Client IFormFile #4518

kingua opened this issue Sep 14, 2023 · 2 comments

Comments

@kingua
Copy link
kingua commented Sep 14, 2023

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,
      "config
8000
urationClass": 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);
        });
    }
@kingua
Copy link
Author
kingua commented Sep 20, 2023

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.

@janseris
Copy link

Did that force you to use base64?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants
0