Issue
I'm fairly new to ASP.NET Core, Angular HttpClient and Observables, so I need some help.
I have created and published a web-based application that is not public facing which has an Angular 9 client and a Microsoft ASP.NET Core 2.2 API controller backend that uses Entity Framework Core ORM to access SQL Server. I use Microsoft ASP.NET Core Identity for authentication, authorization, Identity Model Tokens (JWT) and AntiForgery as security. NOTE: As an update on 01/14/2024, I'm no longer using Authorize or AntiForgery attributes and no longer sending a Head request before sending the actual Verb (Put, Post, Delete) Request. You can see the updates to code below in the answer.
I can login (from Angular) with credentials and retrieve my required ORM Entity data using Http Get requests for my various tables. But when I try to Post
, Put
or Delete
an entity in a table, generally only the first attempts are successful (200 status) and further attempts fail (302 status). I have to logout/login again to continue making updates. I am not sending credentials in my requests after the initial login request.
I'm including my Angular component, repository and datasource code plus my StartUp.cs
and API controller.
Note that I am only generating a JWT
as a bearer token once upon login, returning it to the client, and sending it back to API in subsequent requests for comparison. I am not decoding the bearer token for the username value at this time, but I did in the past with same result.
I think the status code 302
is telling me that my Put
request found the URL, but that I am no longer authenticated on the server at this point, and I am not sure what to do about that scenario.
Any help in pointing out what I'm missing or doing wrong would be greatly appreciated. Note: after doing more research and education on the relationship among the Angular component, the model Repository service and the model Datasource service (especially with Observables and the HttpClient), and on the API Controller on the server, especially with the use of Async, Await and the IActionResult objects that can be returned in the Http Response, I was able to eliminate the error code 302. Instead, I began receiving the HTTP error 503. The answer below gives the details on solving my http error 503 issue by adding a Session Data table and Session cookie. Thank you.
Original Angular 9 component method:
saveMember(member: Member): boolean {
this.repo.saveMember(member).subscribe(result => this.members.splice(this.members.
findIndex(m => m.memberID == member.memberID), 1, result),
err => console.log('From saveMember(): ', err),
() => console.log('From saveMember(): Completed'));
}
Original Angular 9 Repository service Observable method includes Http HEAD request:
// Repository Method: creates new member or updates existing member
saveMember(theMember: Member): Observable<Member> {
if (theMember.memberID == null || theMember.memberID == 0) {
this.dataSource.saveMemberHeader().subscribe(result => { this.showError = !result },
err => console.log('From Head() Request: ', err),
() => console.log('From Head() Request: Completed'));
return this.dataSource.saveMember(theMember).pipe(map(response => {
if (response) {
this.members.push(response);
}
return response;
}));
}
else {
this.dataSource.updateMemberHeader().subscribe(result => { this.showError = !result },
err => console.log('From Head() Request: ', err),
() => console.log('From Head() Request: Completed'));
return this.dataSource.updateMember(theMember).pipe(map(response => {
if (response) {
let index = this.members.findIndex(item => this.locator(item, response.memberID));
this.members.splice(index, 1, response);
}
return response;
}));
}
}
Original Angular 9 Datasource service:
// Datasource http calls to server
update: string = "update";
updateMember(member: Member): Observable<Member> {
return this.sendPutRequest<Member>(`${this.url}/${this.update}`, member);
}
private sendPutRequest<T>(url: string, member: Member): Observable<T> {
let myHeaders = new HttpHeaders();
if (this.authCookie.JWTauthcookie == null) {
myHeaders = myHeaders.set("Access-Key", "<secret>");
} else {
myHeaders = myHeaders.set("Authorization", "Bearer<" + this.authCookie.JWTauthcookie + ">");
}
myHeaders = myHeaders.set("Application-Names", ["ClientApp", "SPA"]);
return this.http.put<T>(url,
{
member: member
},
{
headers: myHeaders
}).pipe(map(response => {
return response; // response object is a Member entity
}))
.pipe(catchError((error: Response) =>
throwError(`An Error Occurred: ${error.statusText} (${error.status})`)
));
}
updateMemberHeader(member?: Member): Observable<any> {
return this.sendRequest<Member>("HEAD", `${this.url}/${this.update}`, member);
}
private sendRequest<T>(verb: string, url: string, body?: Member): Observable<T> {
let myHeaders = new HttpHeaders();
if (this.authCookie.JWTauthcookie == null) {
myHeaders = myHeaders.set("Access-Key", "<secret>");
} else {
myHeaders = myHeaders.set("Authorization", "Bearer<" + this.authCookie.JWTauthcookie + ">");
}
myHeaders = myHeaders.set("Application-Names", ["ClientApp", "SPA"]);
return this.http.request<T>(verb, url, {
body: body,
headers: myHeaders
}).pipe(map(response => {
return response;
})).pipe(catchError((error: Response) =>
throwError(`An Error Occurred: ${error.statusText} (${error.status})`)
));
}
Original ASP.NET Core Web Account controller:
using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
using Microsoft.AspNetCore.Mvc;
using ServerApp.Infrastructure; // contains the code for "Jwt_GenerateToken"
namespace ServerApp.Controllers
{
[ValidateAntiForgeryToken]
public class AccountController : Controller
{
private UserManager<IdentityUser> userManager;
private SignInManager<IdentityUser> signInManager;
private RoleManager<IdentityRole> roleManager;
public static string bearerJWTokenString;
private string bearerToken;
public SignInResult signInResult;
private string plainName = "";
private string plainLoginPwd = "";
public AccountController(UserManager<IdentityUser> userMgr,
SignInManager<IdentityUser> signInMgr, RoleManager<IdentityRole> roleMgr)
{
userManager = userMgr;
signInManager = signInMgr;
roleManager = roleMgr;
}
[HttpPost("/api/account/login")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Login([FromBody] LoginViewModel creds)
{
if (ModelState.IsValid && await DoLogin(creds))
{
string isAuthenticated = signInResult.Succeeded.ToString();
object myJWT = "{ " + '\n' + " " + '"' + "success" + '"' + ": " + '"' + isAuthenticated + '"' + "," + '\n' +
" " + '"' + "token" + '"' + ':' + '"' + _token + '"' + "," + '\n' +
"}";
return Ok(myJWT);
}
return BadRequest();
}
public string _token = null;
public async Task<bool> DoLogin(LoginViewModel creds)
{
plainName = creds.Name;
plainLoginPwd = creds.Password;
IdentityUser loginuser = await userManager.FindByNameAsync(plainName);
if (loginuser != null)
{
await signInManager.SignOutAsync();
signInResult =
await signInManager.PasswordSignInAsync(loginuser, plainLoginPwd, false, false);
if (signInResult.Succeeded)
{
// generates a signed Json Web Token with current user name
_token = Jwt_GenerateToken.GenerateToken(loginuser.ToString(), 90); // in Infrastructure folder
bearerJWTokenString = _token;
}
return signInResult.Succeeded;
}
return false;
}
}
public class LoginViewModel
{
[Required]
public string Name { get; set; }
[Required]
public string Password { get; set; }
}
}
Original ASP.NET Core Web API controller:
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ServerApp.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using System.ComponentModel.DataAnnotations;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using ServerApp.Infrastructure;
namespace ServerApp.Controllers {
[Route("api/members")]
[ApiController]
[Authorize]
[AutoValidateAntiforgeryToken]
public class MemberValuesController : Controller
{
private string bearerToken;
private SecurityToken validatedToken;
private JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
private ClaimsPrincipal vtoken = new ClaimsPrincipal();
private ApplicationDbContext context;
public MemberValuesController(ApplicationDbContext ctx)
{
context = ctx;
}
public class MemberModel
{
[Required]
public Member Member { get; set; } // must capitalize or creates naming convention error!
}
[HttpPut("/api/members/update")]
[Authorize(Roles = "Admin, SuperUser, User")]
public async Task<Member> Put([FromBody] MemberModel MbrModel)
{
bearerToken = Request.Headers["Authorization"];
var bearer = bearerToken.Substring(7, bearerToken.Length - 8);
vtoken = tokenHandler.ValidateToken(bearer, Jwt_GenerateToken.vParms, out validatedToken);
if (vtoken.Identity.Name.ToString() == Jwt_GenerateToken.cPrince.Name.ToString())
{
if (vtoken.Identity.IsAuthenticated == true)
{
Member UpdatedMember = new Member();
if (ModelState.IsValid)
{
UpdatedMember = await DoMemberUpdate(MbrModel);
return UpdatedMember;
}
return UpdatedMember;
}
else
{
return ViewBag;
}
}
else
{
return ViewBag;
}
}
private async Task<Member> DoMemberUpdate(MemberModel MbrModel)
{
Member member = new Member();
member = MbrModel.Member;
context.Members.Update(member);
var saved = false;
while (!saved)
{
try
{
await context.SaveChangesAsync();
saved = true;
return member;
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
if (entry.Entity is Member)
{
var proposedValues = entry.CurrentValues;
var databaseValues = entry.GetDatabaseValues();
foreach (var property in proposedValues.Properties)
{
var proposedValue = proposedValues[property];
proposedValues[property] = proposedValue; //<value to be saved>;
}
entry.OriginalValues.SetValues(databaseValues);
}
}
}
}
return member;
}
[HttpHead("/api/members/update")]
public async Task<IActionResult> GetUpdateHeader()
{
bearerToken = Request.Headers["Authorization"];
var bearer = bearerToken.Substring(7, bearerToken.Length - 8);
if (AccountController.bearerJWTokenString == bearer)
{
int MbrCount = 0;
MbrCount = await DoGetMembersHeader();
if (MbrCount > -1)
{
Response.Headers.Add("Items-total", MbrCount.ToString());
}
else
{
Response.Headers.Add("Items-total", "Bad result");
}
return Ok();
}
else
{
return BadRequest();
}
}
private async Task<int> DoGetMembersHeader()
{
return await context.Members.Include(m => m.MemberID)
.OrderBy(m => m.Last_Name + m.First_Name + m.MidInit)
.Select(m => new
{
m.MemberID,
m.Last_Name,
m.First_Name,
m.MidInit,
m.Email_Address,
m.Cell_Phone,
// ..... more properties
m.GUIDKey,
m.RowVersion
}).CountAsync();
}
}
Original Server-side Infrastructure class that generates token:
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
namespace ServerApp.Infrastructure
{
public class Jwt_GenerateToken
{
public static ClaimsIdentity cPrince;
public static TokenValidationParameters vParms = new TokenValidationParameters();
private const string Beans = "bhmYDKCEN4pGSEoJcI6t ... more ... ==";
public static string GenerateToken(string username, int expireMinutes)
{
// "expireMinutes" value along with "username" comes from "DoLogin()" method in AccountController.cs
byte[] symmetricKey = Convert.FromBase64String(Beans);
var tokenHandler = new JwtSecurityTokenHandler();
var now = DateTime.UtcNow;
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username)
}),
Expires = now.AddMinutes(Convert.ToDouble(expireMinutes)),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(symmetricKey),
SecurityAlgorithms.HmacSha256Signature)
};
var stoken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(stoken);
// creating a static public ClaimsPrincipal object to compare to JWT token in client request
// https://stackoverflow.com/questions/40281050/jwt-authentication-for-asp-net-web-api?rq=1
vParms.IssuerSigningKey = tokenDescriptor.SigningCredentials.Key;
vParms.RequireExpirationTime = true;
vParms.ValidateIssuer = false;
vParms.ValidateAudience = false;
cPrince = tokenDescriptor.Subject; // the Claims Identity object to compare to future requests
// end update
return token;
}
}
}
Original Server-side MVC StartUp.cs
:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.DependencyInjection;
using ServerApp.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Antiforgery;
namespace ServerApp
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(); // is this necessary or redundant here?
string conString_A = Configuration["TheDatabaseProd:AConnectionString"];
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(conString_A));
string conString_I = Configuration["TheDatabaseProd:IConnectionString"];
services.AddDbContext<IdentityDataContext>(options =>
options.UseSqlServer(conString_I));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<IdentityDataContext>();
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
});
services.AddMvc(options =>
{
options.Filters.Add(new ValidateAntiForgeryTokenAttribute());
});
services.AddMemoryCache(); // is this necessary or redundant?
services.AddDistributedMemoryCache();
services.AddSession();
services.AddSession(options =>
{
options.Cookie.Name = "MyApp.Session";
options.IdleTimeout = TimeSpan.FromMinutes(10);
options.Cookie.IsEssential = true;
options.Cookie.SameSite = SameSiteMode.Lax;
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
IServiceProvider services, IAntiforgery antiforgery)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
RequestPath = "",
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "./wwwroot/app"))
});
app.UseAuthentication();
app.UseSession();
app.Use(nextDelegate => context =>
{
string path = context.Request.Path.Value;
string[] directUrls = { "/appmenu", "/table", "/form", "/form/edit", "/form/create",
"/queryTable", "/oneMemberTable", "/retreatTable", "/coreTeamMemberTable" };
if (path.StartsWith("/api") || string.Equals("/", path) || directUrls.Any(url => path.StartsWith(url)))
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-REQUEST-TOKEN", tokens.RequestToken,
new CookieOptions()
{
HttpOnly = false, // HttpOnly must be false or the x-xsrf-token header is not filled in the Request
Secure = true,
IsEssential = true // was true, 2023.06.06
});
}
return nextDelegate(context);
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
Solution
Thanks to all who answered and commented on this question. In brief, I added a dbo.SessionData table and added a Session cookie and added code to send an http request to save "contact info" in the dbo.SessionData table ahead of each Request for Put, Post or Delete. I also removed the "Head" type requests.
I spent a lot of time redesigning my client-side and server-side code (component, repository and datasource on my client app and API Controller on my server app) especially where Observable method types were being used on the client. Learning when and how to subscribe to an observable method to make a request and how to pipe and map the response back to the component was very helpful.
I also simplified my security by removing the Authorize and AntiForgery attributes and code on the server. I may re-add them in the future, but for now I'm only using MS Identity and the JWT web token for authentication. All users who are authenticated are now also authorized for data access but are restricted through code logic.
Also, learning how to use server-side async methods was also crucial. The client and server code redesign eliminated the 302 error, but I still received a 503 error intermittently until I tried what my web hosting support team suggested.
They suggested I add a Session Data table.
I found how to add the "dbo.SessionData" table, cookie and code in the Adam Freeman book (Apress Publishing) "Essential Angular for ASP.NET Core MVC 3" - but the author is applying his ideas to a "Cart" model in a web store site. I substituted contact info data from entities in my data model instead of "orders" data as is used with a Shopping Cart.
The saving of this contact info into the "dbo.SessionData" table, the logging of the Session cookie, etc seems to have contributed to keeping my user authenticated after a Put, Post or Delete Request and seems to have solved the 503 Error condition. This update required adding a (TypeScript) model and repository on the client for sending "contact info" as a separate Request prior to any other normal Request and a model and controller for storing the contact info on the Server (C# MVC).
My web host support team told me that the Error 503 occurred because I was running out of memory - I assume they meant "sql-cache" as I had to install a server-side dotnet tool "dotnet-sql-cache" in order to create and use the session database table.
I tried it, and it seems to have eliminated the 503 errors.
Here is the definition for http error 503:
503 Service Unavailable
The HyperText Transfer Protocol (HTTP) 503 Service Unavailable server error response code indicates that the server is not ready to handle the request.
Common causes are a server that is down for maintenance or that is overloaded. This response should be used for temporary conditions and the Retry-After HTTP header should, if possible, contain the estimated time for the recovery of the service.
Caching-related headers that are sent along with this response should be taken care of, as a 503 status is often a temporary condition and responses shouldn't usually be cached.
This is how my StartUp.cs looks now for a production build (let me know if you want to see other client/server code for adding the "cart-like" session data.
using System;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders; // needed in Production environment
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.SpaServices.AngularCli; // needed in Development environment
using ServerApp.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
// using Microsoft.AspNetCore.Antiforgery; // removing to simplify in my private-facing ASP.NET Core 2.2 application.
namespace ServerApp
{
public class Startup
{
// NOTE: To simplify my app, I uninstalled unnecessary MailKit modules by doing the following:
//
// Note: Deleted the "bin" and "obj" subfolders in ServerApp folder, then ran ...
//
// ../ServerApp> dotnet nuget locals all --clear
//
// then, rebuilt the application - and installed the Session Data tools, then
// added the dbo.SessionData table in Development.
// I ran SQL scripts to add it to Prod database environment.
//
// From "Essential Angular for ASP.NET Core MVC 3" by Adam Freeman
// - see pages 211-212 - to install and set up a Session Database (for a Shopping Cart, etc)
//
// ../ServerApp> dotnet add package Microsoft.Extensions.Caching.SqlServer --version 2.2.0
// ../ServerApp> dotnet tool uninstall --global dotnet-sql-cache
// ../ServerApp> dotnet tool install --global dotnet-sql-cache --version 2.2.0
// ../ServerApp> dotnet sql-cache create "Server=(localdb)\MSSQLLocalDB;Database=MyDatabase" "dbo" "SessionData"
//
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.Lax;
});
string conString_A = Configuration["TheDatabaseProd:AConnectionString"];
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(conString_A));
string conString_I = Configuration["TheDatabaseProd:IConnectionString"];
services.AddDbContext<IdentityDataContext>(options =>
options.UseSqlServer(conString_I));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<IdentityDataContext>();
// New service section added here to reference the new dbo.SessionData table
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = conString_A;
options.SchemaName = "dbo";
options.TableName = "SessionData";
});
services.AddSession(options =>
{
options.Cookie.Name = "MyApp.Session";
options.IdleTimeout = TimeSpan.FromMinutes(10);
options.Cookie.HttpOnly = false;
options.Cookie.IsEssential = true;
options.Cookie.SameSite = SameSiteMode.Lax;
});
// end new "Session" configuration
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// to simplify for Core 2.2 - I'm not using AntiForgery services for now,
// so I removed references to AntiForgery services and XSRF header - here and in Controllers
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider services)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// USE THIS VERSION in PRODUCTION BUILD ONLY
// /*
app.UseStaticFiles(new StaticFileOptions
{
RequestPath = "",
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "./wwwroot/app"))
});
// */
app.UseSession();
app.UseCookiePolicy();
app.UseAuthentication();
// Here I removed the app references to Request Paths and cookie
// used in AntiForgery Cross-Site attack prevention
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
// use in DEVELOPMENT ONLY - comment out for deployment to Production
/*
app.UseSpa(spa =>
{
spa.Options.SourcePath = "../ClientApp";
spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
});
// */
}
}
}
Here is the SessionInfo data model I added to server-side Model:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace ServerApp.Models
{
public class SessionInfo
{
public string lastname { get; set; }
public string firstname { get; set; }
public string midinit { get; set; }
public string cellphone { get; set; }
public string emailaddress { get; set; }
public int memberid { get; set; }
}
}
Here is the server-side SessionValuesController class:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using ServerApp.Models;
namespace ServerApp.Controllers
{
// Web Hosting support says that the use of
// the "AddDistributedSqlServerCache()" service and database will help reduce the
// amount of memory used by the application's sub-domain on the hosting server.
// They also said that running out of memory on the server causes the server's
// dedicated application pool to shut down - while presenting an http error code 503 to
// users of the web site. For Windows Shared customers, there is a max of 300MB
// of memory allowable per domain.
[Route("/api/session")]
[ApiController]
public class SessionValuesController : Controller
{
[HttpGet("contactinfo")]
public IActionResult GetContactInfo()
{
return Ok(HttpContext.Session.GetString("contactinfo"));
}
[HttpPost("contactinfo")]
public void StoreContactInfo([FromBody] SessionInfo[] contacts)
{
var jsonData = JsonConvert.SerializeObject(contacts);
HttpContext.Session.SetString("contactinfo", jsonData);
}
}
}
In the Angular client, this is the "ContactInfo" respository and data model:
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Member } from "./member.model";
const sessionUrl = "/api/session";
type membersMetadata = {
data: Member[];
}
// Note: this was a Cart service, but I replaced "Cart" with "ContactInfo" and "cart" with "contactinfo"
// and I'm saving member contact info instead of "product" data - as in a Web Store app.
// This "service" is registered in model.module.ts. Look at pp216-240 in Essential Angular
@Injectable()
export class ContactInfo {
selections: MembersInfoSelection[] = [];
itemCount: number = 0;
constructor(private http: HttpClient) {
}
storeSessionData<T>(dataType: string, data: T) {
return this.http.post(`${sessionUrl}/${dataType}`, data)
.subscribe(response => { }); // note: the "send" does not occur until subscibed,
// so, we must subscribe, but then discard ("response => { }") the "response" when it is not needed.
}
addMemberInfo(member: Member) {
let selection = this.selections
.find(mis => mis.memberid == member.memberID);
if (selection) {
selection.quantity++;
} else {
this.selections.push(new MembersInfoSelection(this,
member.memberID,
member.last_Name, member.first_Name, member.midInit,
member.cell_Phone, member.email_Address));
}
this.update();
}
updateQuantity(memberID: number, quantity: number) {
if (quantity > 0) {
let selection = this.selections.find(mis => mis.memberid == memberID);
if (selection) {
selection.quantity = quantity;
}
} else {
let index = this.selections.findIndex(mis => mis.memberid == memberID);
if (index != -1) {
this.selections.splice(index, 1);
}
this.update();
}
}
update(storeData: boolean = true) {
this.itemCount = this.selections.map(mis => mis.quantity)
.reduce((prev, curr) => prev + curr, 0);
if (storeData) {
this.storeSessionData("contactinfo", this.selections.map(s => {
return {
memberid: s.memberid, lastname: s.lastname, firstname: s.firstname, midinit: s.midinit,
cellphone: s.cellphone, emailaddress: s.emailaddress, quantity: s.quantity
}
}));
}
} // end of "update()" method
}
export class MembersInfoSelection {
constructor(public contactinfo: ContactInfo,
public memberid?: number,
public lastname?: string,
public firstname?: string,
public midinit?: string,
public cellphone?: string,
public emailaddress?: string,
public quantityValue?: number) { }
get quantity() {
return this.quantityValue;
}
set quantity(newQuantity: number) {
this.quantityValue = newQuantity;
this.contactinfo.update();
}
}
Here is part of the Angular client's member.datasource.ts which sends the "member" post, put, delete requests and now performs the "contactinfo" session posts through dependency injection:
import { Injectable, Inject, InjectionToken } from "@angular/core";
import { HttpClient, HttpHeaders, HttpResponse, HttpParams } from "@angular/common/http";
import { Observable, throwError } from "rxjs";
import { Member } from "./member.model";
import { Injectable, Inject, InjectionToken } from "@angular/core";
import { HttpClient, HttpHeaders, HttpResponse, HttpParams } from "@angular/common/http";
import { Observable, throwError } from "rxjs";
import { Member } from "./member.model";
import { catchError, map } from "rxjs/operators";
// for Session Data
import { ContactInfo } from "./contactinfo.model";
export const REST_URL = new InjectionToken("rest_url");
@Injectable()
export class MemberDataSource {
constructor(private cInfo: ContactInfo, public authCookie: DatasourcesService,
private http: HttpClient, @Inject(REST_URL) private url: string) {
}
getData(): Observable<Member[]> {
return this.sendMultiRequest<Member[]>("GET", this.url);
}
getMember(id: number): Observable<Member> {
return this.sendMultiRequest<Member>("GET", `${this.url}/${id}`);
}
create: string = "create";
saveMember(member: Member): Observable<Member> {
// add to Session Data
this.cInfo.addMemberInfo(member); // note - this action stores a binary value
// as the cached entry in the "value" column of the dbo.SessionData table - with an expiration
// of 10 minutes after creation date/time. At this time, I cannot read and convert the binary
// data in order to store it in the permanent database.
return this.sendPostRequest<Member>(`${this.url}/${this.create}`, member);
}
... more code ... omitted ...
Here is the revised Member Repository Save method:
// Repository: to create (post) new members or update (put) existing members
saveMember(theMember: Member): Observable<Member> {
if (theMember.memberID == null || theMember.memberID == 0) {
// here we .pipe and map the Response to return the new member as an Observable<Member>
return this.dataSource.saveMember(theMember).pipe(
map(response => {
if (response) {
this.members.push(response);
}
return response;
},
catchError(e => {
return of(false);
})));
}
else {
// make this .pipe and map the updated existing member as an Observable<Member> after splicing the response into
// the existing member[] array
return this.dataSource.updateMember(theMember).pipe(
map(response => {
if (response) {
let index = this.members.findIndex(item => this.locator(item, response.memberID));
this.members.splice(index, 1, response);
}
return response;
},
catchError(e => {
return of(false);
})));
}
}
Here is the revised server-side MemberValuesController (Put only):
using Microsoft.AspNetCore.Mvc;
using ServerApp.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using ServerApp.Infrastructure;
namespace ServerApp.Controllers {
[Route("api/members")]
[ApiController]
public class MemberValuesController : Controller {
private string bearerToken;
Jwt_GenerateToken objTokens = new Jwt_GenerateToken();
private ApplicationDbContext context;
public MemberValuesController(ApplicationDbContext ctx)
{
context = ctx;
}
// this is the Request's [FromBody] model which must be capitalized!
public class MemberModel
{
[Required]
public Member Member { get; set; }
}
// I updated the return type to <IActionResult> to include the JWT object and return the updated
// Member in response as item1, item2.
[HttpPut("/api/members/update")]
public async Task<IActionResult> Put([FromBody] MemberModel MbrModel)
{
bearerToken = Request.Headers["Authorization"];
var bearer = bearerToken.Substring(7, bearerToken.Length - 8);
if (objTokens.ValidateCurrentToken(bearer) == true)
{
Member UpdatedMember = new Member();
UpdatedMember = await DoMemberUpdate(MbrModel);
object myJWT = "true"; // was "_token;"
return Ok((myJWT, UpdatedMember)); // returns an object plus the Member as the HTTP Response object's response.item1 & response.item2
}
return BadRequest();
}
private async Task<Member> DoMemberUpdate(MemberModel MbrModel)
{
Member member = new Member();
member = MbrModel.Member;
context.Members.Update(member);
var saved = false;
while (!saved)
{
try
{
// Attempt to save changes to the database
await context.SaveChangesAsync();
saved = true;
return member;
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
if (entry.Entity is Member)
{
var proposedValues = entry.CurrentValues;
var databaseValues = entry.GetDatabaseValues();
foreach (var property in proposedValues.Properties)
{
var proposedValue = proposedValues[property];
// TODO: decide which value should be written to database - using Proposed Values
proposedValues[property] = proposedValue; //<value to be saved>;
}
// Refresh original values to bypass next concurrency check
entry.OriginalValues.SetValues(databaseValues);
}
}
}
}
return member;
}
Answered By - Cwinds
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.