Skip to content

Expose response ContentTypes in api-definition and use them in proxies#25639

Merged
sumeyyeKurtulus merged 16 commits into
devfrom
maliming/proxy-content-types
Jun 18, 2026
Merged

Expose response ContentTypes in api-definition and use them in proxies#25639
sumeyyeKurtulus merged 16 commits into
devfrom
maliming/proxy-content-types

Conversation

@maliming

@maliming maliming commented Jun 16, 2026

Copy link
Copy Markdown
Member

Expose response ContentTypes and IsRemoteStream flag in api-definition so C# / jQuery / Angular proxies can echo back the actual declared media type instead of collapsing everything to application/json + text/plain. Also unwraps ABP error envelopes in RestService for text and blob response modes. The jQuery generator and Angular schematic now forward DTO uploads containing IRemoteStreamContent as multipart FormData instead of JSON.

How to use

Server side — register the upload DTOs in FormBodyBindingIgnoredTypes (same pattern Volo.CmsKit / Volo.Blogging / file management already follow, documented here):

Configure<AbpAspNetCoreMvcOptions>(options =>
{
    options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(UploadFileDto));
    options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(UploadFilesDto));
});

Application service:

public interface IFileAppService : IApplicationService
{
    Task<string> UploadFileAsync(UploadFileDto input);
    Task<string> UploadFilesAsync(UploadFilesDto input);
}

public class UploadFileDto
{
    public string Name { get; set; }
    public IRemoteStreamContent File { get; set; }
}

public class UploadFilesDto
{
    public IEnumerable<IRemoteStreamContent> Files { get; set; }
}

Client side — jQuery:

var fd = new FormData();
fd.append('Name', 'logo');
fd.append('File', fileInput.files[0], 'logo.png');
myApp.file.uploadFile(fd).done(result => ...);

Client side — Angular:

const fd = new FormData();
fd.append('Name', 'logo');
fd.append('File', fileInput.files[0], 'logo.png');
this.fileService.uploadFile(fd).subscribe(result => ...);

Client side — C# (dynamic + static proxy identical):

await using var stream = File.OpenRead("logo.png");
await _fileAppService.UploadFileAsync(new UploadFileDto
{
    Name = "logo",
    File = new RemoteStreamContent(stream, "logo.png", "image/png"),
});

Generated proxy before / after

jQuery — before (what @martingagne reported in #8581: the input argument is silently dropped, no body):

my.project.app.uploadFile = function(input, ajaxParams) {
  return abp.ajax($.extend(true, {
    url: abp.appPath + 'api/app/file/upload-file' + abp.utils.buildQueryString([{ name: 'name', value: input.name }]) + '',
    type: 'POST'
  }, ajaxParams));
};

jQuery — after (FormData forwarded as multipart body):

my.project.app.uploadFile = function(input, ajaxParams) {
  return abp.ajax($.extend(true, {
    url: abp.appPath + 'api/app/file/upload-file',
    type: 'POST',
    data: input,
    processData: false,
    contentType: false
  }, { dataType: 'json', headers: { Accept: 'application/json' } }, ajaxParams));
};

Angular schematic — before (upload arg typed as the DTO; input.name access fails at runtime because the caller has to pass a real DTO with a stream property the proxy then ignores):

uploadFile = (input: UploadFileDto, config?: Partial<Rest.Config>) =>
  this.restService.request<any, string>({
    method: 'POST',
    url: '/api/app/file/upload-file',
    params: { name: input.name },
  }, { apiName: this.apiName, ...config });

Angular schematic — after (arg collapses to FormData, body forwards it directly, query-string fields originating from the upload arg are dropped to keep the rendered code TS-safe):

uploadFile = (input: FormData, config?: Partial<Rest.Config>) =>
  this.restService.request<any, string>({
    method: 'POST',
    url: '/api/app/file/upload-file',
    body: input,
  }, { apiName: this.apiName, ...config });

Test coverage

Framework C# unit (Volo.Abp.Http.Client.Tests + Volo.Abp.Http.Tests):

  • ClientProxyRequestPayloadBuilder multipart serialization: direct IRemoteStreamContent, DTO with stream property, DTO with IEnumerable<>, nested DTO (Child.File), 3-level nested (Inner.Child.File), UTF-8 RFC 5987 filename, polymorphic / generic / record / inherited DTO, DateTime / enum / nullable struct fields, mixed property + collection, Body binding wins over Form, null FormFile skip, Path + Form + FormFile mix, multiple actions on the same builder, stream pass-through (no implicit buffering)
  • JQueryProxyScriptGenerator: application/json / +json / text/csv / application/pdf / charset variants echo, IRemoteStreamContent skips dataType override, multipart emits data + processData: false + contentType: false, custom subclass narrowed scope, multiple direct FormFile known limitation

Angular schematic vitest (@abp/ng.schematics):

  • Body mapper: response type → Accept / responseType resolution across json / text / blob / +json / xml / custom MIME, 6 multipart DTO shapes, ModelBinding field dropped from query params when sharing the upload arg, multiple direct FormFile limitation
  • Signature mapper: FormData collapse on upload arg, Path arg keeps primitive type, multiple direct FormFile each become FormData independently, non-upload action signature unchanged
  • Method mapper: signature + body wired together for upload actions, query + ModelBinding + FormFile mix
  • Template render: real lodash template + ts.createProgram semantic check (incl. FormData signature compilation), vm sandbox runtime forwarding the FormData arg to restService.request

End-to-end against a local demo project (MVC + AppService + Angular):

  • 18 controller endpoints + 8 AppService methods covering all 5 IRemoteStreamContent shapes from Make generate-proxy support IRemoteStreamContent. #8581, plus PUT upload / PUT + Path / IFormFile direct param / UTF-8 filename / 5 MB stream / api-version + upload / cancellation / byte[] documented limitation / anti-forgery / multi-tenant / OpenIddict password-grant authorized upload
  • 107 wire-level bash assertions over the controller routes + generated Abp/ServiceProxyScript content
  • C# ClientDemoService 45 cases × DYNAMIC + STATIC proxy modes (90 total)
  • jQuery dynamic proxy run from a browser page against single / many / nested / direct / path-mixed / query-mixed / validation-error endpoints
  • Real ng build of the Angular schematic output through the existing dev-app nx project, exercising the rendered service in the real Angular compiler pipeline

Closes #23732
Closes #8581

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.03448% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.42%. Comparing base (59e2a99) to head (68b6b0a).
⚠️ Report is 32 commits behind head on dev.

Files with missing lines Patch % Lines
.../Abp/Http/Client/ClientProxying/ClientProxyBase.cs 86.44% 5 Missing and 3 partials ⚠️
...ng/Generators/JQuery/JQueryProxyScriptGenerator.cs 91.30% 2 Missing and 2 partials ⚠️
...tCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs 94.11% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev   #25639      +/-   ##
==========================================
+ Coverage   49.36%   49.42%   +0.06%     
==========================================
  Files        3687     3687              
  Lines      124432   124550     +118     
  Branches     9507     9534      +27     
==========================================
+ Hits        61421    61557     +136     
+ Misses      61171    61149      -22     
- Partials     1840     1844       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sumeyyeKurtulus sumeyyeKurtulus merged commit 9a793c7 into dev Jun 18, 2026
4 of 6 checks passed
@sumeyyeKurtulus sumeyyeKurtulus deleted the maliming/proxy-content-types branch June 18, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Angular proxy generator ignoring [Produces] attribute Make generate-proxy support IRemoteStreamContent.

3 participants