14

Similar questions have been asked but after looking through all of those and many blog posts on the subject I have not been able to figure this out so please forgive me.

I am creating a simple blog with (for the purposes of this question) two parts, a front end SPA in Angular 8 and a back-end API in ASP.NET Core 3. In one part of my front end I am attempting to upload an image to be used as the image for a newly created blog. When I try to upload an image, the resulting IFormFile in the backend is always coming out to null. Below is the code, any help is greatly appreciated!

new-blog.component.html:

<form [formGroup]="newBlogForm" (ngSubmit)="onSubmit(newBlogForm.value)">

    <div>
        <label for="Name">
            Blog Name
        </label>
        <input type="text" formControlName="Name">
    </div>

    <div>
        <label for="TileImage">
            Tile Image
        </label>
        <input type="file" formControlName="TileImage">
    </div>

    <button type="submit">Create Blog</button>

</form>

new-blog.component.ts:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl } from '@angular/forms';
import { BlogService } from '../blog-services/blog.service';

@Component({
  selector: 'app-new-blog',
  templateUrl: './new-blog.component.html',
  styleUrls: ['./new-blog.component.css']
})
export class NewBlogComponent implements OnInit {
  private newBlogForm: FormGroup;

  constructor(private formBuilder: FormBuilder, private blogService: BlogService) { }

  ngOnInit() {
    this.newBlogForm = this.formBuilder.group({
      Name: new FormControl(null),
      TileImage: new FormControl(null)
    });
  }

  onSubmit(blogData: FormData) {
    console.log('new blog has been submitted.', blogData);
    this.blogService.postBlog(blogData);
    this.newBlogForm.reset();
  }

}

postBlog from blog.service.ts:

  postBlog(blogData: FormData): Observable<any> {
    const postBlogSubject = new Subject();
    this.appOptions.subscribe(
      (options) => {
        const url = options.blogAPIUrl + '/Blogs';
        this.http
          .post(url, blogData)
          .subscribe(
            (blog) => {
              postBlogSubject.next(blog);
            }
          );
      }
    );
    return postBlogSubject.asObservable();
  }

The signature for my BlogController looks like this:

[HttpPost]
public async Task<ActionResult<Blog>> PostBlog([FromForm]PostBlogModel blogModel)

with the PostBlogModel as follows:

    public class PostBlogModel
    {
        public string Name { get; set; }
        public IFormFile TileImage { get; set; }
    }

I have implemented logging middleware to try to debug. The output is as follows (I see that for some reason the front-end is sending application/json rather than multipart/form-data but I'm not sure why or how to fix...)

blogapi_1  | info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
blogapi_1  |       Request finished in 170.16740000000001ms 500
blogapi_1  | info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
blogapi_1  |       Request starting HTTP/1.1 OPTIONS http://localhost:5432/api/v1/Blogs
blogapi_1  | dbug: BlogAPI.Middleware.RequestResponseLoggingMiddleware[0]
blogapi_1  |       HTTP Request: Headers:
blogapi_1  |            key: Connection, values: keep-alive
blogapi_1  |            key: Accept, values: */*
blogapi_1  |            key: Accept-Encoding, values: gzip, deflate, br
blogapi_1  |            key: Accept-Language, values: en-US,en-IN;q=0.9,en;q=0.8,en-GB;q=0.7
blogapi_1  |            key: Host, values: localhost:5432
blogapi_1  |            key: Referer, values: http://localhost:5431/blog/new-blog
blogapi_1  |            key: User-Agent, values: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
blogapi_1  |            key: Origin, values: http://localhost:5431
blogapi_1  |            key: Access-Control-Request-Method, values: POST
blogapi_1  |            key: Access-Control-Request-Headers, values: content-type
blogapi_1  |            key: Sec-Fetch-Site, values: same-site
blogapi_1  |            key: Sec-Fetch-Mode, values: cors
blogapi_1  |
blogapi_1  |        type:
blogapi_1  |        scheme: http
blogapi_1  |        host+path: localhost:5432/api/v1/Blogs
blogapi_1  |        queryString:
blogapi_1  |        body: 
blogapi_1  | info: Microsoft.AspNetCore.Cors.Infrastructure.CorsService[4]
blogapi_1  |       CORS policy execution successful.
blogapi_1  | dbug: BlogAPI.Middleware.RequestResponseLoggingMiddleware[0]
blogapi_1  |       HTTP Response: Headers:
blogapi_1  |            key: Access-Control-Allow-Headers, values: Content-Type
blogapi_1  |            key: Access-Control-Allow-Origin, values: http://localhost:5431
blogapi_1  |
blogapi_1  |        statusCode: 204
blogapi_1  |        responseBody: 
blogapi_1  | info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
blogapi_1  |       Request finished in 58.5088ms 204
blogapi_1  | info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
blogapi_1  |       Request starting HTTP/1.1 POST http://localhost:5432/api/v1/Blogs application/json 56
blogapi_1  | dbug: BlogAPI.Middleware.RequestResponseLoggingMiddleware[0]
blogapi_1  |       HTTP Request: Headers:
blogapi_1  |            key: Connection, values: keep-alive
blogapi_1  |            key: Content-Type, values: application/json
blogapi_1  |            key: Accept, values: application/json, text/plain, */*
blogapi_1  |            key: Accept-Encoding, values: gzip, deflate, br
blogapi_1  |            key: Accept-Language, values: en-US,en-IN;q=0.9,en;q=0.8,en-GB;q=0.7
blogapi_1  |            key: Host, values: localhost:5432
blogapi_1  |            key: Referer, values: http://localhost:5431/blog/new-blog
blogapi_1  |            key: User-Agent, values: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
blogapi_1  |            key: Origin, values: http://localhost:5431
blogapi_1  |            key: Content-Length, values: 56
blogapi_1  |            key: Sec-Fetch-Site, values: same-site
blogapi_1  |            key: Sec-Fetch-Mode, values: cors
blogapi_1  |       
blogapi_1  |        type: application/json
blogapi_1  |        scheme: http
blogapi_1  |        host+path: localhost:5432/api/v1/Blogs
blogapi_1  |        queryString:
blogapi_1  |        body: {"Name":"test","TileImage":"C:\\fakepath\\DSC_0327.jpg"}
7
  • Did you try to add httpOptions with included Content-Type header to http.post? Commented Dec 16, 2019 at 1:44
  • Seems you are working on a cross-domain service. Your browser will first send an HTTP options request and test if the returned header allows it to do the real submit. During the HTTP Options test process your server can't get the file. Please see: developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS Commented Dec 16, 2019 at 3:07
  • @Yeheshuah - When I tried adding the Content-Type header it seems ASP.NET core can't process the input, it still shows body from the Angular with {"Name":"test","TileImage":"C:\\fakepath\\DSC_0327.jpg"} and I get error System.IO.InvalidDataException: Missing content-type boundary. @Anduin - Yes, this is running in a Docker container, but as the logs show it looks like the OPTIONS request is being mad successfully with Access-Control-Allow-Origin being returned in the response as I have set up a CORS policy for this. Commented Dec 16, 2019 at 3:35
  • How did you set the HttpOptions in your post request? Commented Dec 16, 2019 at 3:44
  • @AngelaAmarapala I added the following to postBlog function above: let httpOptions: any = { headers: new HttpHeaders({ "Content-Type": "multipart/form-data" }) }; this.http .post(url, blogData, httpOptions). It may also be important to add that when I specified this, the OPTIONS request did not seem to be made, it just went directly to the POST. Commented Dec 16, 2019 at 4:39

2 Answers 2

16

my BlogController looks like this:

[HttpPost]

public async Task<ActionResult<Blog>> PostBlog([FromForm]PostBlogModel blogModel)

It seems that you'd like to pass data using form-data, to achieve it, you can refer to the following code sample.

.component.html

<form [formGroup]="newBlogForm" (ngSubmit)="onSubmit(newBlogForm.value)">

  <div>
      <label for="Name">
          Blog Name
      </label>
      <input type="text" formControlName="Name">
  </div>

  <div>
      <label for="TileImage">
          Tile Image
      </label>
      <input type="file" formControlName="TileImage" (change)="onSelectFile($event)" >
  </div>

  <button type="submit">Create Blog</button>

</form>

.component.ts

selectedFile: File = null;
private newBlogForm: FormGroup;
constructor(private http: HttpClient) { }

ngOnInit() {
  this.newBlogForm = new FormGroup({
    Name: new FormControl(null),
    TileImage: new FormControl(null)
  });
}

onSelectFile(fileInput: any) {
  this.selectedFile = <File>fileInput.target.files[0];
}

onSubmit(data) {
  
  const formData = new FormData();
  formData.append('Name', data.Name);
  formData.append('TileImage', this.selectedFile);

  this.http.post('your_url_here', formData)
  .subscribe(res => {

    alert('Uploaded!!');
  });

  this.newBlogForm.reset();
}

Test Result

enter image description here

Sign up to request clarification or add additional context in comments.

3 Comments

I am new in angular, can you explein me why dont it work without async Task<> in PostBlog and .subscribe(res => { alert('Uploaded!!');}); it dont throw exception but it does not enter to method.
Do you have to send the FormData object alone to the backend? I can't seem to wrap the FormData object in another object, and send that object altogether
well explained.
2

First

<input type="file"> binding with angular using ngModel or formControlName will only catch the value property but actually when we submit form we need the files property so we can create custom directive that will apply to all the project <input type="file"> elements so when we submit the form we got the file property

Before

enter image description here

import { Directive, forwardRef, HostListener, ElementRef, Renderer2 } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';


@Directive({
    selector : `input[type=file][formControlName], 
    input[type=file][formControl],
    input[type=file][ngModel]`,
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: FileValueAccessorDirective,
            multi: true
        }
    ]
})
export class FileValueAccessorDirective implements ControlValueAccessor {

    constructor(private elementRef: ElementRef, private render: Renderer2) {

    }

    // Function to call when the file changes.
    onChange = (file: any) => {}


    //fire when the form value changed programmaticly
    writeValue(value: any): void {

    }

    //fire only one time to register on change event
    registerOnChange = (fn: any) => { this.onChange = fn; }


    //fire only one time to register on touched event
    registerOnTouched = (fn: any) => { }


    //Disable the input
    setDisabledState?(isDisabled: boolean): void {

    }

    //listen to change event
    @HostListener('change', ['$event.target.files'])
    handleChange(file) {
        this.onChange(file[0]);
    }

}

After

enter image description here Second

To upload file using Http your data shoud be encoded using multipart/form-data that allows files to be send over http post so that why FormData is used,

A FormData object will automatically generate request data with MIME type multipart/form-data that existing servers can process. To add a file field to the data you use a File object that an extension can construct from file path. The FormData object can then simply be passed to XMLHttpRequest:

Http Upload Files

so your submit method should be like

onSubmit() {
      let formData: FormData = new FormData();
        Object.keys(this.newBlogForm.value).forEach(key => {
            formData.append(key, this.newBlogForm.value[key])
        });
    //pass formData to your service
  }

Thrid

In your postBlog method you are creating Subject without any benefit , you can just return http.post then in the caller method use specifiy whether you subscribe or use async/await to make the http call fire

onSubmit() {
   .....
    this.postBlog(formData).subscribe(
        result => { }
    );
}

async onSubmit() {
   .....
    let res = await this.postBlog(formData).toPromise();
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.