为什么来自 Angular 9 服务的 HTTP PUT 会创建一个新的数据库实体而不是更新原始实体? - 无法读取 属性 'id' of null & 并发错误

Why does HTTP PUT from Angular 9 service CREATE a new DB entity instead of UPDATING original? - cannot read property 'id' of null & concurrency error

一般来说,我是 Angular SPA 和 MVC 的新手,我目前正在 Visual Studio 2017 年使用 EF Core ORM(模型)和一个 MVC Core 2.2 Web 应用程序项目工作Angular9前端。该项目的目的是学习 Angular/AspNetCore MVC/EF Core 以替换使用 WCF 和 EF 6 的 .NET 4.5 Winforms 应用程序。我正在使用的源代码示例来自三本不同的书同一作者,我正在尝试学习如何将所有部分组合在一起。

我的测试应用程序有一个 MVC API 控制器,它从我的 Angular 服务接收 HTTP 请求。使用我的 Angular 模板,我可以从我的本地 MSSQL 数据库中获取一个或所有项目,并且我可以毫无问题地创建和删除项目。但是,当我尝试更新现有项目时,我的结果是使用更新后的数据创建了另一个项目,而原始项目仍然存在并带有旧数据。

当我编辑 ID 2010 并更改名称和价格时,我得到一个新创建的国际象棋项目 - 而不是更新的 #2010。

这是我的 Angular 服务:

(other imports above)
import { Product } from "./product.model";

export const REST_URL = new InjectionToken("rest_url");

@Injectable()
export class RestDataSource {

  constructor(private http: HttpClient, @Inject(REST_URL) private url: string) { }

  getData(): Observable<Product[]> {
        return this.sendRequest<Product[]>("GET", this.url);
  }

  saveProduct(product: Product): Observable<Product> {
        return this.sendRequest<Product>("POST", this.url, product);
  }

  updateProduct(product: Product): Observable<Product> {
    return this.sendRequest<Product>("PUT", this.url, product);
  }

  deleteProduct(id: number): Observable<Product> {
    return this.sendRequest<Product>("DELETE", `${this.url}/${id}`);
  }

  private sendRequest<T>(verb: string, url: string, body?: Product)
    : Observable<T> {
    return this.http.request<T>(verb, url, {
          body: body,
          headers: new HttpHeaders({
            "Access-Key": "<secret>",
            "Application-Name": "exampleApp"
            })
        });
    }
  }

这是我的Angular模型特征模块:

 import { NgModule } from "@angular/core";
 import { Model } from "./repository.model";
 import { HttpClientModule, HttpClientJsonpModule } from "@angular/common/http";
 import { RestDataSource, REST_URL } from "./rest.datasource";

 @NgModule({
   imports: [HttpClientModule, HttpClientJsonpModule],
   providers: [Model, RestDataSource,
       { provide: REST_URL, useValue: `http://${location.hostname}:51194/api/products` }]
})
export class ModelModule { }

这是我的Angular9 repository.model.ts:

  import { Injectable } from "@angular/core";
  import { Product } from "./product.model";
  import { Observable } from "rxjs";
  import { RestDataSource } from "./rest.datasource";
  
  @Injectable()
  export class Model {
    private products: Product[] = new Array<Product>();
    private locator = (p: Product, id: number) => p.id == id;
  
      constructor(private dataSource: RestDataSource) {
        this.dataSource.getData().subscribe(data => this.products = data);
      }

    //removed GET, DELETE methods that are working

        //this method below is supposed to CREATE (POST) if Product has no ID 
        //and UPDATE (PUT) if there is a Product ID, but the update is not working

      saveProduct(product: Product) {
        if (product.id == 0 || product.id == null) {
           this.dataSource.saveProduct(product).subscribe(p => this.products.push(p));
        } else {
          this.dataSource.updateProduct(product).subscribe(p => {
            const index = this.products.findIndex(item => this.locator(item, p.id));
            this.products.splice(index, 1, p);
          });
      }
    }
  }

带有Chrome F12的HTTP PUT请求方法图片:

这是我的 MVC API 控制器:

    using Microsoft.AspNetCore.Mvc;
    using Core22MvcNg9.Models;

    namespace Core22MvcNg9.Controllers {

    [Route("api/products")]
    public class ProductValuesController : Controller {
      private IWebServiceRepository repository;

      public ProductValuesController(IWebServiceRepository repo) 
          => repository = repo;
  
      [HttpGet("{id}")]
      public object GetProduct(int id) {
          return repository.GetProduct(id) ?? NotFound();
      }
  
      [HttpGet]
      public object Products() { 
          return repository.GetProducts(); 
      }
  
      [HttpPost]
      public Product StoreProduct([FromBody] Product product) {
          return repository.StoreProduct(product);
      }
  
      [HttpPut]
      public void UpdateProduct([FromBody] Product product) {
          repository.UpdateProduct(product);
      }
  
      [HttpDelete("{id}")]
      public void DeleteProduct(int id) {
          repository.DeleteProduct(id);
      }
   }
  }

这里是MVC模型Web服务库(接口):

  namespace Core22MvcNg9.Models {

     public interface IWebServiceRepository {
  
       object GetProduct(int id);

       object GetProducts();

       Product StoreProduct(Product product);

       void UpdateProduct(Product product);

       void DeleteProduct(int id);
      }
  }

这是 MVC Web 服务模型存储库实现 Class:

  using System.Linq;
  using Microsoft.EntityFrameworkCore;
  
  namespace Core22MvcNg9.Models {
  
  public class WebServiceRepository : IWebServiceRepository {
       private ApplicationDbContext context;
  
       public WebServiceRepository(ApplicationDbContext ctx) => context = ctx;
  
       public object GetProduct(int id)
       {
           return context.Products.Include(p => p.Category)
               .Select(p => new {
                   Id = p.ProductID,
                   Name = p.Name,
                   Category = p.Category,
                   Price = p.Price
               })
               .FirstOrDefault(p => p.Id == id);
       }
       
       public object GetProducts() {
           return context.Products.Include(p => p.Category)
               .OrderBy(p => p.ProductID)
               .Select(p => new {
                   Id = p.ProductID,
                   Name = p.Name,
                   Category = p.Category,
                   Price = p.Price
                });
       }
  
       public Product StoreProduct(Product product) {
           context.Products.Add(product);
           context.SaveChanges();
           return product;
       }
  
       public void UpdateProduct(Product product) {
           context.Products.Update(product);
           context.SaveChanges();
       }
  
       public void DeleteProduct(int id) {
           context.Products.Remove(new Product { ProductID = id });
           context.SaveChanges();
       }
    }
  }

这是 MVC Startup.cs:

  (other using statements above)    
  using Microsoft.AspNetCore.SpaServices.AngularCli;
  using Microsoft.Extensions.DependencyInjection;
  using Core22MvcNg9.Models;

  namespace Core22MvcNg9
  {
      public class Startup
      {
          public Startup(IConfiguration configuration)
          {
              Configuration = configuration;
          }

          public IConfiguration Configuration { get; }

          // Use this method to add services to the container.
          public void ConfigureServices(IServiceCollection services)
          {
              services.AddDbContext<ApplicationDbContext>(options => 
                  options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]));
              services.AddTransient<IProductRepository, EFProductRepository>();
              services.AddTransient<IWebServiceRepository, WebServiceRepository>();
              services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        
              // In production, the Angular files will be served from this directory
              services.AddSpaStaticFiles(configuration =>
              {
                  configuration.RootPath = "exampleApp/dist"; 
              });
          }

          // Use this method to configure the HTTP request pipeline.
          public void Configure(IApplicationBuilder app, IHostingEnvironment env)
          {
              if (env.IsDevelopment())
              {
                  app.UseDeveloperExceptionPage();
                  app.UseStatusCodePages();
              }
              else
              {
                  app.UseExceptionHandler("/Error");
              }

              app.UseStaticFiles();
              app.UseSpaStaticFiles();
        
              SeedData.EnsurePopulated(app);

              app.UseMvc(routes =>
              {
                  routes.MapRoute(
                      name: "pagination",
                      template: "Products/Page{productPage}",
                      defaults: new { Controller = "Product", action = "List" });
              });

              app.UseSpa(spa =>
              {

                  spa.Options.SourcePath = "exampleApp"; 

                  if (env.IsDevelopment())
                  {
                      spa.UseAngularCliServer(npmScript: "start");
                  }
              });
          }
      }
  }

你能帮忙吗?感谢你耐心看完很长的文章post,如果你还需要什么,请告诉我。

根据@derpirscher 评论的指导:

我在 API 控制器方法 UpdateProduct([FromBody] Product product) 的 MVC 代码中设置了一个断点。

此方法将 product.ProductID 值显示为 0,因此该方法未在消息正文中找到 [FromBody] 属性暗示的“ProductID”。

这让我想起 Angular 数据模型使用“id”作为产品的标识,而不是 ProductID - 我已经在 MVC 代码和模型中更改了它,包括数据上下文。

因此,我将 MVC Model/Repositories 和控制器中的数据上下文更改回产品标识的“Id”,使用 dotnet 迁移删除并重建数据库,现在更新正在运行,与“id”从 Angular 服务到 MVC API Controller using [From Body] 中的“Id”。

我的 Angular 9 html/component 仍然需要解决 "cannot read 属性 'id' of null" 问题和并发问题,但我很高兴 MVC 更新现在可以正常工作。

更新 1:编辑以解决“无法读取 属性 'id' of null”错误:我不得不更改“saveProduct()” repository.model.ts 文件中的方法到以下 - 使新的 and/or 更新“产品”在模板中正确显示。 请参阅下面的更新 2

    saveProduct(product: Product) {
      if (product.id == null || product.id == 0) {
        this.dataSource.saveProduct(product)  // sends HttpPost to MVC/API
          .subscribe(product => this.products.push(product)); // adds new product to array[] listing & template including the ID 
      } else {
        this.dataSource.updateProduct(product)  // sends HttpPut changes to MVC/API
          .subscribe(p => {
            this.products.splice(this.products.   
               findIndex(p => p.id == product.id), 1, product); // updates the array[] listing & template
          });
      }
    }

在我的原始代码中,变量“p”在 Post/Put 之后为 null,这导致 “无法读取 属性 'id' of null”。 =109=] 错误,所以我更正了代码,将当前“产品”添加到“POST”之后的本地产品数组,并在“PUT”之后拼接更新的产品。没有更多错误,模板现在显示 return.

上更新和添加的产品

更新 2:对于并发错误,更改了 Return 类型的 HTTP POST 和 PUT: 我将服务器上 Http POST 方法的 return 类型从“int”更改为“Product”,现在 return 是 http.post 中的新“产品”实体包括响应正文(基于我有限的知识)以及 ID 和 rowVersion。此更改在 MVC API 控制器中,在 MVC 模型的 Web 服务及其实现中 class。

在repository.model.ts中,我编辑了saveProduct(product: Product)方法: 我不得不将 Http POST 的订阅从:

更改为
      .subscribe(p => this.products.push(p => product));

对此:

      .subscribe(product => this.products.push(product));

为了让新插入的实体附加到 products[] 列表,并将其“ID”值填充到 html table 的 ID 列中169=]交易。如果您需要乐观并发,这也会带回初始 rowVersion - 无需将 byte[] 转换为 Uint8Array。可能不是最好的政策,但它适合我的需要。

HTTP PUT 更新修改:

来自:

      .subscribe(p => {
            this.products.splice(this.products.   
               findIndex(p => p.id == product.id), 1, product);

至:

      .subscribe(product => {
            this.products.splice(this.products.   
               findIndex(p => p.id == product.id), 1, product);

对 HTTP PUT 事务的更改需要在服务器上更改 return 类型 API 控制器更新方法和 WebService 接口以及 class 从“void”return 类型到“Product”类型的实现,每个以前的“void”方法现在 returning “product”。

repository.model.ts 中“saveProduct()”方法的最终版本:

    saveProduct(product: Product) {
      if (product.id == null || product.id == 0) {
        this.dataSource.saveProduct(product)  // sends HttpPost to MVC/API
          .subscribe(product => this.products.push(product)); // adds new product to array[] listing & template including the ID 
      } else {
        this.dataSource.updateProduct(product)  // sends HttpPut changes to MVC/API
          .subscribe(product => {
            this.products.splice(this.products.   
               findIndex(p => p.id == product.id), 1, product); // updates the array[] listing & template with latest product and rowVersion
          });
      }
    }

PUT 请求不要随机更改为 POST 请求。还有你的浏览器开发工具截图显示,请求确实是一个 PUT 请求。

错误可能出在服务器上。设置一个断点并检查当您到达 PUT 端点时会发生什么,以及在 context.Products.Updatecontext.SaveChanges 内会发生什么。也许请求正文在服务器上没有被正确解释,所以不是更新而是插入...