In ABP 9.3, when an application service method accepts an input DTO that contains IRemoteStreamContent (including multiple files, e.g., IEnumerable<IRemoteStreamContent>), the Angular proxy generator (abp generate-proxy -t ng) generates a proxy method that posts a JSON body (body: request) instead of a multipart/form-data request (FormData). This makes the generated proxy method unusable for uploads, since IRemoteStreamContent represents a file and cannot be sent as JSON.
This appears inconsistent with the ABP documentation for v9.3, which explicitly describes DTO usage and indicates that IRemoteStreamContent is compatible with Auto API Controller and Dynamic C# HTTP Proxy systems, and that DTO types must be added to FormBodyBindingIgnoredTypes for DTO scenarios. Reference: ABP 9.3 docs – Working with Streams.
9 Answer(s)
-
0
- Solution
In ABP 9.3.x the Angular proxy generator only knows how to switch to
FormDatawhen the service method parameter itself isIRemoteStreamContent/RemoteStreamContent(or a similar direct stream parameter).
It does not detectIRemoteStreamContent(or collections likeIEnumerable<IRemoteStreamContent>) inside a DTO, therefore it generates:body: request // JSON bodyinstead of a
FormDataupload. That’s a limitation of the current proxy generator – your observation is correct.Because of this, there are only two practical options in 9.3.6:
Option A – Change the backend signature to use direct stream parameters
If you want to keep using generated proxies, the easiest way is to avoid wrapping streams in a DTO for this method.
Backend app service:
public class MyUploadAppService : ApplicationService { // Single file + extra data public async Task UploadAsync( IRemoteStreamContent file, string someExtraField, int anotherField) { using var stream = file.GetStream(); // handle file + fields … } // Multiple files public async Task UploadManyAsync( IEnumerable<IRemoteStreamContent> files, string someExtraField) { foreach (var f in files) { using var stream = f.GetStream(); // handle each file… } } }Then run:
abp generate-proxy -t ngThe Angular proxy for
UploadAsyncwill look like:upload = (file: FormData, someExtraField: string, anotherField: number, config?: Partial<Rest.Config>) => this.restService.request<any, void>({ method: 'POST', url: '/api/app/my-upload/upload', body: file, params: { someExtraField, anotherField }, }, { apiName: this.apiName, ...config });And you use it like:
const form = new FormData(); form.append('file', inputFile); // `file` must match param name this.myUploadService.upload(form, 'value', 123).subscribe();For multiple files, the generated method will take
FormDataas well; you append multiple file entries (e.g.form.append('files', file)for each).This keeps everything within the auto API + proxy system and avoids the DTO-with-stream case the generator cannot handle.
Option B – Keep your DTO, but bypass the generated proxy for that method
If you really need a DTO like:
public class MyUploadDto { public string SomeField { get; set; } public IEnumerable<IRemoteStreamContent> Files { get; set; } }you must:
- Configure the server-side model binding for that DTO properly:
Configure<AbpAspNetCoreMvcOptions>(options => { options.FormBodyBindingIgnoredTypes.Add(typeof(MyUploadDto)); });and bind
IRemoteStreamContentfrom form fields (ABP handles this when you use form-data correctly).- Do not use the generated Angular proxy for this method. Instead, write a small Angular service that posts
FormDatamanually:
import { Injectable } from '@angular/core'; import { RestService } from '@abp/ng.core'; @Injectable({ providedIn: 'root' }) export class MyUploadClient { constructor(private restService: RestService) {} upload(dto: { someField: string; files: File[] }) { const formData = new FormData(); formData.append('someField', dto.someField); dto.files.forEach(file => { formData.append('files', file); // 'files' matches DTO property name }); return this.restService.request<any, void>( { method: 'POST', url: '/api/app/my-upload/upload', body: formData, // FormData, not JSON }, { apiName: 'Default' }, ); } }ABP support has explicitly recommended not using the generated proxy for file upload scenarios when it doesn’t produce
FormData, and using a customFormData+RestService/HttpClientcall instead.
In summary:
- What you see (JSON body for DTO with
IRemoteStreamContent) is a current limitation of the Angular proxy generator in 9.3.x. - To make upload work:
- Either change the app service signature to use direct
IRemoteStreamContentparameters (Option A, fully supported by generator), or - Keep your DTO and bypass the generated proxy with a custom
FormDatarequest (Option B).
- Either change the app service signature to use direct
- Sources
- ABP docs – Application Services, Working with Streams (IRemoteStreamContent):
https://abp.io/docs/9.3/framework/architecture/domain-driven-design/application-services?_redirected=B8ABF606AA1BDF5C629883DF1061649A - Community article – Upload/Download files with Angular &
IRemoteStreamContent(shows generatedFormDataproxies for direct parameters):
https://abp.io/community/articles/que8cdr8 - Support Q&A – File-management API from Angular, recommendation not to use generated proxy for upload and to use
FormDatamanually:
https://abp.io/support/questions/5731
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
-
0
AI response doesn't solve my problem. In my case I have multiple files and I would like to have something like that:
public class TestUploadInput { public Guid RecordId { get; set; } public TestUploadInputDocument[] Documents { get; set; } = []; } public class TestUploadInputDocument { public Guid DocumentId { get; set; } public IEnumerable<IRemoteStreamContent> Files { get; set; } = []; } // Application service method. Tried with and without [FromForm]. public void Upload([FromForm]TestUploadInput input)...Generated proxy is:
uploadByInput = (input: TestUploadInput, config?: Partial<Rest.Config>) => this.restService.request<any, void>({ method: 'POST', url: '/api/app/ownership-change-flow/upload', }, { apiName: this.apiName,...config }); // When application service method has no [FromForm] attribute uploadByInput = (input: TestUploadInput, config?: Partial<Rest.Config>) => this.restService.request<any, void>({ method: 'POST', url: '/api/app/ownership-change-flow/upload', params: { recordId: input.recordId, documents: input.documents }, }, { apiName: this.apiName,...config });I believe it should be possible to generate a function which accepts
FormDatainstead:uploadByInput = (input: FormData, config?: Partial<Rest.Config>) => this.restService.request<any, void>( { method: 'POST', url: '/api/app/ownership-change-flow/upload', body: input, }, { apiName: this.apiName, ...config }, );Method above works well, backend accepts it. I understand I can build a custom service as workaround, but that defeats the purpose of using ABP’s proxy generator.
IRemoteStreamContent(including DTO/multiple files) is documented as supported, yet the Angular proxy still generates unusable methods. -
0
- Solution
Your expectation is reasonable, but with ABP 9.3.x what you want is not supported by the Angular proxy generator.
More precisely:
IRemoteStreamContentitself is supported by:- ASP.NET Core model binding (server side),
- Auto API controller,
- C# dynamic HTTP proxies.
However, the Angular proxy generator only switches to
FormDatawhen:- The controller/app-service parameter is directly
IRemoteStreamContent/RemoteStreamContent, or - A simple collection (e.g.
IEnumerable<IRemoteStreamContent>) is a top-level parameter.
- The controller/app-service parameter is directly
It does not inspect complex DTO graphs like:
public class TestUploadInput { public Guid RecordId { get; set; } public TestUploadInputDocument[] Documents { get; set; } = []; } public class TestUploadInputDocument { public Guid DocumentId { get; set; } public IEnumerable<IRemoteStreamContent> Files { get; set; } = []; } public void Upload([FromForm] TestUploadInput input) ...and therefore always generates JSON (or query params) for
TestUploadInput, notFormData.
There is no configuration switch in 9.3 to tell the Angular proxy generator “for this DTO, useFormData”.So what you see:
uploadByInput = (input: TestUploadInput, ...) => this.restService.request<any, void>({ method: 'POST', url: '/api/app/ownership-change-flow/upload', body: input, // or params: { ... } }, ...);is exactly the current limitation, not a misconfiguration on your side.
Given that, the only fully working approaches in 9.3.6 are:
A. Keep your DTO on the server, use a custom Angular client (recommended for your current design)
You can continue to use your DTO structure exactly as-is on the backend, and just bypass the generated proxy for this method only.
- Keep your app service like:
public class OwnershipChangeFlowAppService : ApplicationService { public void Upload([FromForm] TestUploadInput input) { // input.RecordId // input.Documents[i].DocumentId // input.Documents[i].Files (IEnumerable<IRemoteStreamContent>) } }(And ensure you configure MVC so this DTO isn’t JSON-bound, e.g. with
FormBodyBindingIgnoredTypesif needed, as described in the docs.)- In Angular, write a small wrapper over
RestServicethat sends theFormDatathe way the backend expects:
import { Injectable } from '@angular/core'; import { RestService } from '@abp/ng.core'; @Injectable({ providedIn: 'root' }) export class OwnershipChangeFlowUploadClient { constructor(private restService: RestService) {} upload(input: { recordId: string; documents: { documentId: string; files: File[] }[]; }) { const form = new FormData(); form.append('recordId', input.recordId); input.documents.forEach((doc, docIndex) => { form.append(`documents[${docIndex}].documentId`, doc.documentId); doc.files.forEach(file => { // Name must match DTO structure that ASP.NET Core can bind to. form.append(`documents[${docIndex}].files`, file); }); }); return this.restService.request<any, void>( { method: 'POST', url: '/api/app/ownership-change-flow/upload', body: form, }, { apiName: 'Default' }, ); } }ABP support has already confirmed in a previous ticket that for such file upload scenarios you should not use the generated Angular proxy when it does not produce
FormData, and instead sendFormDatamanually withRestService/HttpClient.
B. Change the backend signature so the generator can use
FormDataIf you want to rely strictly on the generated Angular proxy, you must change your method into a shape the generator understands, e.g. flattening out the file parameters so they are top-level
IRemoteStreamContent/IEnumerable<IRemoteStreamContent>and using a simpler DTO for the rest of the data.For example (conceptually):
public class TestUploadFlatInput { public Guid RecordId { get; set; } public Guid[] DocumentIds { get; set; } = []; } public Task UploadAsync( [FromForm] TestUploadFlatInput input, IEnumerable<IRemoteStreamContent> files) { // correlate input.DocumentIds[i] with files.ElementAt(i) yourself }For such signatures the Angular proxy generator can generate a method that takes
FormDataasbody. But with nestedIRemoteStreamContentinside DTOs/child DTOs, it cannot.
Direct answer to your request
I believe it should be possible to generate a function which accepts
FormDatainstead…From a design perspective: yes, it could be implemented in the generator.
In ABP 9.3.6 however, this is not implemented, and there is no extension point to change the Angular proxy generator’s behavior for this specific method or DTO.So currently:
- Your backend code with nested DTOs +
IRemoteStreamContentis valid and supported on the server side. - It is also compatible with C# HTTP proxies.
- The Angular proxy generator does not support this pattern and will continue to produce “unusable” JSON-based methods for it.
- The only robust solutions today are:
- Custom Angular
FormDataclient for that method (A), or - Adjusted backend method signature (B).
- Custom Angular
If this is blocking and you want first-class generator support for nested DTO uploads, the next step would be to open an issue on ABP’s GitHub repo or via the ABP support portal so the team can treat it as a feature/bug for a future version.
- Sources
Working with Streams (
IRemoteStreamContent, DTO +FormBodyBindingIgnoredTypes):
https://abp.io/docs/9.3/framework/architecture/domain-driven-design/application-services#working-with-streamsABP support ticket where the team recommends not using the generated Angular proxy for file uploads and instead sending
FormDatamanually:
https://abp.io/support/questions/5731
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
-
0
ABP support ticket where the team recommends not using the generated Angular proxy for file uploads and instead sending FormData manually: https://abp.io/support/questions/5731
Ticket is 2 years old, problem is still here...
-
0
Hello,
We will be checking this and releasing a fix for this. You can check the process here:
- Related issue: https://github.com/abpframework/abp/issues/24588
- Releases: https://github.com/abpframework/abp/releases
I can also share a sample project containing a fixed version if you prefer.
Thank you for your cooperation.
-
0
Hi,
I can also share a sample project containing a fixed version if you prefer.
Yes, that would be great if it's possible.
Thanks for response!
-
0
You can reach the project via this link. It contains a separate structure for managing different upload types.
You can also let us know if you need further assistance.
-
0
I can't access it, I've sent a request to get it.
-
0
I have granted the access. You should be able to reach now.