如何从Angular上传文件到ASP.NET Core Web API

14

类似的问题已经被问过,但我查看了所有这些问题以及许多有关该主题的博客文章后,仍然无法解决,请原谅我。

我正在使用Angular 8创建一个简单的博客,并且在此问题的目的下有两个部分:前端SPA和ASP.NET Core 3中的后端API。在我的前端的某个部分,我试图上传一张图像用作新创建的博客的图像。当我尝试上传图片时,后端中的结果IFormFile始终为null。以下是代码,非常感谢您的帮助!

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来自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();
  }

我的BlogController的签名看起来像这样:

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

使用以下PostBlogModel:

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

我已经实现了日志中间件进行调试。输出结果如下(我看到前端发送的是 application/json ,而不是 multipart/form-data,但我不确定为什么或如何解决...)

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"}

你尝试过在http.post中添加包含Content-Type头的httpOptions吗? - Yeheshuah
看起来你正在处理跨域服务。你的浏览器将首先发送一个HTTP选项请求并测试返回的头信息是否允许它进行真正的提交。在HTTP选项测试过程中,你的服务器无法获取文件。请参见:https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS - Anduin Xue
@Yeheshuah - 当我尝试添加Content-Type头时,似乎ASP.NET Core无法处理输入,它仍然显示来自Angular的主体{"Name":"test","TileImage":"C:\\fakepath\\DSC_0327.jpg"},并且我收到错误System.IO.InvalidDataException: Missing content-type boundary.@Anduin - 是的,这是在Docker容器中运行,但是日志显示看起来OPTIONS请求已经成功发送,并且Access-Control-Allow-Origin作为响应返回,因为我已经为此设置了CORS策略。 - Robin Zimmerman
@AngelaAmarapala 我在上面的postBlog函数中添加了以下内容: let httpOptions: any = { headers: new HttpHeaders({ "Content-Type": "multipart/form-data" }) }; this.http .post(url, blogData, httpOptions)。 可能还需要注意的是,当我指定这个时,OPTIONS请求似乎没有被执行,直接进入了POST。 - Robin Zimmerman
我已经在这里 https://dev59.com/MC0AtIcB2Jgan1znaEdq#54424925 和这里 https://dev59.com/NK3la4cB1Zd3GeqPUvBL#52034495 回答过了,因此将其标记为重复。 - alsami
显示剩余2条评论
2个回答

16

我的BlogController如下所示:

[HttpPost]

public async Task<ActionResult<Blog>> PostBlog([FromForm]PostBlogModel blogModel)
似乎您想使用表单数据传递数据。为了实现这一点,您可以参考以下代码示例。
.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>

.组件.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();
}

测试结果

输入图像描述


我是Angular的新手,你能解释一下为什么在PostBlog中没有async Task<>和.subscribe(res => { alert('Uploaded!!');});它不会抛出异常,但它不会进入方法吗? - Silny ToJa
你必须单独将FormData对象发送到后端吗?我似乎无法将FormData对象包装在另一个对象中,然后一起发送。 - Wassim Katbey
讲解得很清楚。 - Faisal

2

首先

使用ngModelformControlName绑定带有<input type="file">的元素只会捕获value属性,但实际上当我们提交表单时,我们需要files属性,因此我们可以创建一个自定义指令,将其应用于整个项目中的<input type="file">元素,这样当我们提交表单时就可以得到文件属性。

之前的情况:

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]);
    }

}

之后

enter image description here 第二步

使用 Http 上传文件时,您的数据应该使用 multipart/form-data 进行编码,以便可以通过 http post 发送文件,这就是为什么要使用 FormData 的原因。

FormData 对象将自动生成 MIME 类型为 multipart/form-data 的请求数据,现有服务器可以处理。要向数据添加文件字段,您需要使用文件对象,扩展程序可以从文件路径构造该对象。然后,可以将 FormData 对象简单地传递给 XMLHttpRequest:

Http 上传文件

所以,您的提交方法应该像这样:

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
  }

第三步

在你的postBlog方法中,你创建了一个没有任何好处的Subject,你可以直接返回http.post,然后在调用方法中使用subscribe或使用async/await来触发http调用。

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

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接