Open Closed

Angular proxy generator does not generate FormData for DTOs containing IRemoteStreamContent (single + multiple files) #10289


User avatar
0
papusa created

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)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    1. Solution

    In ABP 9.3.x the Angular proxy generator only knows how to switch to FormData when the service method parameter itself is IRemoteStreamContent / RemoteStreamContent (or a similar direct stream parameter).
    It does not detect IRemoteStreamContent (or collections like IEnumerable<IRemoteStreamContent>) inside a DTO, therefore it generates:

    body: request // JSON body
    

    instead of a FormData upload. 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 ng
    

    The Angular proxy for UploadAsync will 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 FormData as 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:

    1. Configure the server-side model binding for that DTO properly:
    Configure<AbpAspNetCoreMvcOptions>(options =>
       {
           options.FormBodyBindingIgnoredTypes.Add(typeof(MyUploadDto));
       });
    

    and bind IRemoteStreamContent from form fields (ABP handles this when you use form-data correctly).

    1. Do not use the generated Angular proxy for this method. Instead, write a small Angular service that posts FormData manually:
    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 custom FormData+RestService/HttpClient call 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 IRemoteStreamContent parameters (Option A, fully supported by generator), or
      • Keep your DTO and bypass the generated proxy with a custom FormData request (Option B).
    1. 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 generated FormData proxies 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 FormData manually:
      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.

  • User Avatar
    0
    papusa created

    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 FormData instead:

    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.

  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    1. Solution

    Your expectation is reasonable, but with ABP 9.3.x what you want is not supported by the Angular proxy generator.

    More precisely:

    • IRemoteStreamContent itself 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 FormData when:

      • The controller/app-service parameter is directly IRemoteStreamContent / RemoteStreamContent, or
      • A simple collection (e.g. IEnumerable<IRemoteStreamContent>) is a top-level parameter.
    • 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, not FormData.
    There is no configuration switch in 9.3 to tell the Angular proxy generator “for this DTO, use FormData”.

    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.

    1. 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 FormBodyBindingIgnoredTypes if needed, as described in the docs.)

    1. In Angular, write a small wrapper over RestService that sends the FormData the 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 send FormData manually with RestService/HttpClient.


    B. Change the backend signature so the generator can use FormData

    If 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 FormData as body. But with nested IRemoteStreamContent inside DTOs/child DTOs, it cannot.


    Direct answer to your request

    I believe it should be possible to generate a function which accepts FormData instead…

    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 + IRemoteStreamContent is 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 FormData client for that method (A), or
      • Adjusted backend method signature (B).

    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.

    1. Sources
    • Working with Streams (IRemoteStreamContent, DTO + FormBodyBindingIgnoredTypes):
      https://abp.io/docs/9.3/framework/architecture/domain-driven-design/application-services#working-with-streams

    • 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


    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.

  • User Avatar
    0
    papusa created

    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...

  • User Avatar
    0
    sumeyye.kurtulus created
    Support Team Angular Expert

    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.

  • User Avatar
    0
    papusa created

    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!

  • User Avatar
    0
    sumeyye.kurtulus created
    Support Team Angular Expert

    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.

  • User Avatar
    0
    papusa created

    I can't access it, I've sent a request to get it.

  • User Avatar
    0
    sumeyye.kurtulus created
    Support Team Angular Expert

    I have granted the access. You should be able to reach now.

Learn More, Pay Less
33% OFF
All Trainings!
Get Your Deal
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.2.0-preview. Updated on January 09, 2026, 07:22
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.