为什么来自 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.Update
和 context.SaveChanges
内会发生什么。也许请求正文在服务器上没有被正确解释,所以不是更新而是插入...
一般来说,我是 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.Update
和 context.SaveChanges
内会发生什么。也许请求正文在服务器上没有被正确解释,所以不是更新而是插入...