I am new to Angular SPA and MVC in general, and I am currently working in Visual Studio 2017 on a MVC Core 2.2 Web Application project with EF Core ORM (the Model) and an Angular 9 front-end. The the purpose of this project is to learn Angular/AspNetCore MVC/EF Core to replace a .NET 4.5 Winforms application that uses WCF and EF 6. The source code examples I'm working with are from three different books by the same author, and I am trying to learn how to put all the pieces together.
My test application has an MVC API Controller that receives HTTP requests from my Angular service. Using my Angular Template, I can GET one or all items from my local MSSQL database, and I can CREATE and DELETE items with no problems. But, when I try to UPDATE an existing item, my result is the creation of another item with the updated data, and the original item still exists with the old data.
When I Edit ID 2010 and change the name and price, I get a newly created Chess item - instead of an updated #2010.
Here's my Angular service:
(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"
})
});
}
}
Here is my Angular model feature module:
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 { }
Here is my Angular 9 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);
});
}
}
}
Picture of HTTP PUT Request Method with Chrome F12:

Here is my MVC API Controller:
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);
}
}
}
Here is the MVC Model Web Service Repository (interface):
namespace Core22MvcNg9.Models {
public interface IWebServiceRepository {
object GetProduct(int id);
object GetProducts();
Product StoreProduct(Product product);
void UpdateProduct(Product product);
void DeleteProduct(int id);
}
}
Here is the MVC Web Service Model Repository Implementation 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();
}
}
}
This is the 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");
}
});
}
}
}
Can you help? Thanks for your patience in reading a long post, let me know if you need any thing else.
On guidance from @derpirscher comments:
I set a breakpoint in MVC code on my API Controller method UpdateProduct([FromBody] Product product).
This method showed the product.ProductID value as 0, so the method was not finding the "ProductID" in the message body as the [FromBody] attribute implies.
This reminded me that the Angular data model uses "id" as the identity of the product, not ProductID - which I had changed in MVC code and model, including data context.
So, I changed the data context in the MVC Model/Repositories and Controllers back to "Id" for the product identity, dropped and rebuilt the database using dotnet migration, and the Update is now working, matching the "id" from Angular service to the "Id" in MVC API Controller using [From Body].
My Angular 9 html/component still needs work to address a "cannot read property 'id' of null" issue and concurrency issue, but I'm glad the MVC Update is now working.
Update 1: Edited to address the "cannot read property 'id' of null" error: I had to change the "saveProduct()" method in the repository.model.ts file to the following - to get the new and/or updated "product" to display correctly in the template. See Update 2 below
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
});
}
}
In my original code, variable "p" was null after the Post/Put, and that caused the "cannot read property 'id' of null" error, so I corrected the code to add the current "product" to the local Product array after the "POST" and to splice in the updated product after the "PUT". No more error, and the template now displays the updated and added products on return.
Update 2: For Concurrency Error, changed Return type of HTTP POST and PUT: I changed the return type on the Http POST method at server from "int" to "Product", which now returns the new "product" entity in the http.post Response body (based on my limited knowledge) and the ID and rowVersion are included. This change is in the MVC API Controller, and in the MVC Model's Web Service and its implementation class.
In repository.model.ts, I edited the saveProduct(product: Product) method: I had to change the subcription of the Http POST from:
.subscribe(p => this.products.push(p => product));
to this:
.subscribe(product => this.products.push(product));
in order to get the newly inserted entity appended to the products[] list, and to populate its "ID" value into the html table's ID column after an Http POST transaction. This also brings back the initial rowVersion - without having to convert a byte[] to Uint8Array - if you need optimistic concurrency. Probably not the best policy, but it works for my needs.
Modification for the HTTP PUT update:
from:
.subscribe(p => {
this.products.splice(this.products.
findIndex(p => p.id == product.id), 1, product);
to:
.subscribe(product => {
this.products.splice(this.products.
findIndex(p => p.id == product.id), 1, product);
This change to the HTTP PUT transaction requires changes at the server to the return type of the API Controller update method and WebService interface and class implementation from a "void" return type to a "Product" type, with each previously "void" method now returning "product".
Final version of "saveProduct()" method in repository.model.ts:
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
});
}
}

PUTrequests don't randomly change to becomePOSTrequests. Set a breakpoint on your server to check if the correct endpoint is hit. And also check the implementation ofcontext.Products.Updateandcontext.SaveChanges