0

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.

Angular Template 1

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: 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
          });
      }
    }
5
  • PUT requests don't randomly change to become POST requests. Set a breakpoint on your server to check if the correct endpoint is hit. And also check the implementation of context.Products.Update and context.SaveChanges Commented Jul 28, 2021 at 6:41
  • @derpirscher - thanks for the tip, very helpful; I set a breakpoint in code on the UpdateProduct(Product product) method in my MVC WebServiceRepository, The ProductID value is 0 at this point. The name and price are the updated values, however. Now investigating why the ProductID was changed to 0 when it should come from the message body - which still showed as 2010 in Payload. And you're right, final http method still shows as PUT, not POST. Commented Jul 28, 2021 at 14:55
  • @derpirscher - thanks for the help, I have reworded my question - so if you can let me know how to upgrade your question to an Answer, I would be happy to. Thanks again. Commented Jul 28, 2021 at 16:52
  • Glad I could help. I'll phrase my comment as answer ... Commented Jul 28, 2021 at 18:19
  • I have added 2 updates to my original post, Update 1 addresses the "cannot read property 'id' of null" error, and Update 2 addresses part of the concurrency issue, while not completely resolving the issue of updates occurring from multiple users. But it is a start at being able to read and use the rowVersion property to notify the client's user of a concurrency issue. Commented Aug 25, 2021 at 21:47

1 Answer 1

2

PUT request don't randomly change to become POST requests. And also your screenshot of the browser dev-tools show, that the request is indeed a PUT request.

The error may be at the server. Set a breakpoint and check what happens when you hit your PUT endpoint and what happens within context.Products.Update and context.SaveChanges. Maybe the request body isn't interpreted correctly at the server and so instead of an update an insert is done ...

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.