Added ControllerExtensions to be used instead of HtmlViewRenderer for net8

Added HttpException class for missing HttpException for net8

Wrapped HttpContext.Session, GetJsonResult, IsAjaxRequest and GetUserIdentityName in controllers for net8

Added AuthenticationService to test Fab2ApprovalMKLink code for net8

Compile conditionally flags to debug in dotnet core
This commit is contained in:
2025-05-19 13:29:54 -07:00
parent 8bae94de96
commit 83789cdd91
89 changed files with 3939 additions and 1455 deletions

View File

@ -36,14 +36,36 @@ mklink /J "L:\DevOps\Mesa_FI\MesaFabApproval\Fab2ApprovalMKLink\Jobs" "L:\DevOps
mklink /J "L:\DevOps\Mesa_FI\MesaFabApproval\Fab2ApprovalMKLink\JobSchedules" "L:\DevOps\Mesa_FI\MesaFabApproval\Fab2ApprovalSystem\JobSchedules"
```
```bash 1734015544321 = 638696123443210000 = Thu Dec 12 2024 07:59:03 GMT-0700 (Mountain Standard Time)
mklink /J ".vscode\.UserSecrets" "%AppData%\Microsoft\UserSecrets\f2da5035-aba9-4676-9f8d-d6689f84663d"
mklink /J "DMO" "..\Fab2ApprovalSystem\DMO"
mklink /J "Jobs" "..\Fab2ApprovalSystem\Jobs"
mklink /J "JobSchedules" "..\Fab2ApprovalSystem\JobSchedules"
mklink /J "Misc" "..\Fab2ApprovalSystem\Misc"
mklink /J "Models" "..\Fab2ApprovalSystem\Models"
mklink /J "PdfGenerator" "..\Fab2ApprovalSystem\PdfGenerator"
mklink /J "Utilities" "..\Fab2ApprovalSystem\Utilities"
mklink /J "ViewModels" "..\Fab2ApprovalSystem\ViewModels"
```bash 1747242128286 = 638828389282860000 = 2025-2.Spring = Wed May 14 2025 10:02:07 GMT-0700 (Mountain Standard Time)
mklink /J "Fab2ApprovalMKLink\.vscode\.UserSecrets" "%AppData%\Microsoft\UserSecrets\f2da5035-aba9-4676-9f8d-d6689f84663d"
mklink /J "Fab2ApprovalMKLink\Controllers" "Fab2ApprovalSystem\Controllers"
mklink /J "Fab2ApprovalMKLink\DMO" "Fab2ApprovalSystem\DMO"
mklink /J "Fab2ApprovalMKLink\Jobs" "Fab2ApprovalSystem\Jobs"
mklink /J "Fab2ApprovalMKLink\JobSchedules" "Fab2ApprovalSystem\JobSchedules"
mklink /J "Fab2ApprovalMKLink\Misc" "Fab2ApprovalSystem\Misc"
mklink /J "Fab2ApprovalMKLink\Models" "Fab2ApprovalSystem\Models"
mklink /J "Fab2ApprovalMKLink\PdfGenerator" "Fab2ApprovalSystem\PdfGenerator"
mklink /J "Fab2ApprovalMKLink\Utilities" "Fab2ApprovalSystem\Utilities"
mklink /J "Fab2ApprovalMKLink\ViewModels" "Fab2ApprovalSystem\ViewModels"
mklink /J "Fab2ApprovalMKLink\ViewModels" "Fab2ApprovalSystem\ViewModels"
```
```bash 1747249935803 = 638828467358030000 = 2025-2.Spring = Wed May 14 2025 12:12:15 GMT-0700 (Mountain Standard Time)
mkdir "Fab2ApprovalMKLink\Views"
mklink /J "Fab2ApprovalMKLink\Views\Account" "Fab2ApprovalSystem\Views\Account"
mklink /J "Fab2ApprovalMKLink\Views\Admin" "Fab2ApprovalSystem\Views\Admin"
mklink /J "Fab2ApprovalMKLink\Views\Audit" "Fab2ApprovalSystem\Views\Audit"
mklink /J "Fab2ApprovalMKLink\Views\ChangeControl" "Fab2ApprovalSystem\Views\ChangeControl"
mklink /J "Fab2ApprovalMKLink\Views\CorrectiveAction" "Fab2ApprovalSystem\Views\CorrectiveAction"
mklink /J "Fab2ApprovalMKLink\Views\ECN" "Fab2ApprovalSystem\Views\ECN"
mklink /J "Fab2ApprovalMKLink\Views\Home" "Fab2ApprovalSystem\Views\Home"
mklink /J "Fab2ApprovalMKLink\Views\LotDisposition" "Fab2ApprovalSystem\Views\LotDisposition"
mklink /J "Fab2ApprovalMKLink\Views\LotTraveler" "Fab2ApprovalSystem\Views\LotTraveler"
mklink /J "Fab2ApprovalMKLink\Views\Manager" "Fab2ApprovalSystem\Views\Manager"
mklink /J "Fab2ApprovalMKLink\Views\MRB" "Fab2ApprovalSystem\Views\MRB"
mklink /J "Fab2ApprovalMKLink\Views\PartsRequest" "Fab2ApprovalSystem\Views\PartsRequest"
mklink /J "Fab2ApprovalMKLink\Views\Reports" "Fab2ApprovalSystem\Views\Reports"
mklink /J "Fab2ApprovalMKLink\Views\Shared" "Fab2ApprovalSystem\Views\Shared"
mklink /J "Fab2ApprovalMKLink\Views\Training" "Fab2ApprovalSystem\Views\Training"
mklink /J "Fab2ApprovalMKLink\Views\Workflow" "Fab2ApprovalSystem\Views\Workflow"
```

View File

@ -0,0 +1,74 @@
#if NET8
using System;
using System.IO;
using System.Threading.Tasks;
using Fab2ApprovalSystem.PdfGenerator;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
namespace Fab2ApprovalSystem.Extensions;
public static class ControllerExtensions {
public static ActionResult GetBinaryContentResult<TModel>(this Controller controller, string viewName, string contentType, TModel model) {
string pageTitle = string.Empty;
string htmlText = RenderViewToString(controller, viewName, model);
StandardPdfRenderer standardPdfRenderer = new();
// Let the html be rendered into a PDF document through iTextSharp.
byte[] buffer = standardPdfRenderer.Render(htmlText, pageTitle);
// Return the PDF as a binary stream to the client.
return new BinaryContentResult(buffer, contentType);
}
public static string RenderViewToString<TModel>(this Controller controller, string viewName, TModel model) {
if (string.IsNullOrEmpty(viewName))
viewName = controller.ControllerContext.ActionDescriptor.ActionName;
controller.ViewData.Model = model;
using (StringWriter writer = new()) {
try {
CompositeViewEngine compositeViewEngine = controller.HttpContext.RequestServices.GetRequiredService(typeof(ICompositeViewEngine)) as CompositeViewEngine;
if (compositeViewEngine is null || compositeViewEngine.ViewEngines.Count == 0) { }
ViewEngineResult viewResult = null;
if (viewName.EndsWith(".cshtml"))
viewResult = compositeViewEngine.GetView(viewName, viewName, false);
else
viewResult = compositeViewEngine.FindView(controller.ControllerContext, viewName, false);
if (!viewResult.Success)
return $"A view with the name '{viewName}' could not be found";
ViewContext viewContext = new(
controller.ControllerContext,
viewResult.View,
controller.ViewData,
controller.TempData,
writer,
new HtmlHelperOptions()
);
Task task = viewResult.View.RenderAsync(viewContext);
task.Wait();
return writer.GetStringBuilder().ToString();
} catch (Exception ex) {
return $"Failed - {ex.Message}";
}
}
}
}
#endif

View File

@ -30,6 +30,7 @@
<PackageReference Include="EntityFramework" Version="6.5.1" />
<PackageReference Include="ExcelDataReader" Version="3.7.0" />
<PackageReference Include="jQuery" Version="3.7.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.10" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.2" />

View File

@ -1,15 +1,23 @@
using System;
using System.Diagnostics;
using System.Text;
using Fab2ApprovalSystem.Misc;
using Fab2ApprovalSystem.Models;
using Fab2ApprovalSystem.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.WindowsServices;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
namespace Fab2ApprovalMKLink;
@ -24,12 +32,36 @@ public class Program {
throw new Exception("Company name must have a value!");
if (string.IsNullOrEmpty(appSettings.WorkingDirectoryName))
throw new Exception("Working directory name must have a value!");
GlobalVars.AppSettings = appSettings;
GlobalVars.AttachmentUrl = appSettings.AttachmentUrl is null ? string.Empty : appSettings.AttachmentUrl;
GlobalVars.CA_BlankFormsLocation = appSettings.CABlankFormsLocation;
GlobalVars.DBConnection = appSettings.DBConnection;
GlobalVars.DB_CONNECTION_STRING = appSettings.DBConnectionString;
GlobalVars.hostURL = appSettings.HostURL;
GlobalVars.IS_INFINEON_DOMAIN = appSettings.IsInfineonDomain;
GlobalVars.MesaTemplateFiles = appSettings.MesaTemplateFiles;
GlobalVars.NDriveURL = appSettings.NDriveURL;
GlobalVars.SENDER_EMAIL = appSettings.SenderEmail;
GlobalVars.USER_ID = appSettings.UserId;
GlobalVars.USER_ISADMIN = appSettings.UserIsAdmin;
GlobalVars.WSR_URL = appSettings.WSR_URL;
try {
_ = webApplicationBuilder.Services.Configure<ApiBehaviorOptions>(options => options.SuppressModelStateInvalidFilter = true);
_ = webApplicationBuilder.Services.AddControllers();
_ = webApplicationBuilder.Services.AddControllersWithViews();
_ = webApplicationBuilder.Services.AddDistributedMemoryCache();
_ = webApplicationBuilder.Services.AddHttpClient();
_ = webApplicationBuilder.Services.AddMemoryCache();
_ = webApplicationBuilder.Services.AddSingleton(_ => appSettings);
_ = webApplicationBuilder.Services.AddSingleton<ICompositeViewEngine, CompositeViewEngine>();
// _ = webApplicationBuilder.Services.AddTransient<IViewRenderingService, ViewRenderingService>();
// _ = webApplicationBuilder.Services.AddScoped<IViewRenderService, ViewRenderService>();
// _ = webApplicationBuilder.Services.AddSingleton<ITempDataProvider, ITempDataProvider>();
_ = webApplicationBuilder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
_ = webApplicationBuilder.Services.AddScoped<IAuthenticationService, AuthenticationService>();
_ = webApplicationBuilder.Services.AddScoped<IDalService, DalService>();
_ = webApplicationBuilder.Services.AddScoped<IDbConnectionService, DbConnectionService>();
_ = webApplicationBuilder.Services.AddScoped<IUserService, UserService>();
_ = webApplicationBuilder.Services.AddSwaggerGen();
_ = webApplicationBuilder.Services.AddSession(sessionOptions => {
sessionOptions.IdleTimeout = TimeSpan.FromSeconds(2000);
@ -37,6 +69,29 @@ public class Program {
sessionOptions.Cookie.IsEssential = true;
}
);
_ = webApplicationBuilder.Services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options => {
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuerSigningKey = true,
ValidIssuer = appSettings.JwtIssuer,
ValidateAudience = false,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appSettings.JwtKey)),
ClockSkew = TimeSpan.Zero
};
});
_ = webApplicationBuilder.Services.AddAuthorization(options => {
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});
if (WindowsServiceHelpers.IsWindowsService()) {
_ = webApplicationBuilder.Services.AddSingleton<IHostLifetime, WindowsServiceLifetime>();
_ = webApplicationBuilder.Logging.AddEventLog(settings => {
@ -65,6 +120,8 @@ public class Program {
}
_ = webApplication.UseSession();
_ = webApplication.MapControllers();
_ = webApplication.UseAuthentication();
_ = webApplication.UseAuthorization();
logger.LogInformation("Starting Web Application");
webApplication.Run();
return 0;

View File

@ -0,0 +1,266 @@
using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Authentication;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using Fab2ApprovalSystem.Models;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
namespace Fab2ApprovalSystem.Services;
public interface IAuthenticationService {
public Task<LoginResult> AuthenticateUser(AuthAttempt login);
public Task<LoginResult> AttemptLocalUserAuth(WindowsIdentity identity);
public AuthTokens GenerateAuthTokens(AuthAttempt authAttempt, IEnumerable<string> roles);
public Task<LoginResult> RefreshAuthTokens(AuthAttempt authAttempt);
}
public class AuthenticationService : IAuthenticationService {
private readonly ILogger<AuthenticationService> _logger;
private readonly IMemoryCache _cache;
private readonly IUserService _userService;
private readonly string? _jwtIssuer;
private readonly string? _jwtAudience;
private readonly string? _jwtKey;
public AuthenticationService(ILogger<AuthenticationService> logger, IMemoryCache cache, IUserService userService, AppSettings appSettings) {
_logger = logger ?? throw new ArgumentNullException("ILogger not injected");
_cache = cache ?? throw new ArgumentNullException("IMemoryCache not injected");
_userService = userService ?? throw new ArgumentNullException("IUserService not injected");
_jwtKey = appSettings.JwtKey;
_jwtIssuer = appSettings.JwtIssuer;
_jwtAudience = appSettings.JwtAudience;
}
public async Task<LoginResult> AuthenticateUser(AuthAttempt login) {
try {
_logger.LogInformation("Attempting to authenticate user");
if (login is null)
throw new ArgumentNullException("Login cannot be null");
string domain = "infineon.com";
using (PrincipalContext pc = new(ContextType.Domain, domain)) {
bool isValid = pc.ValidateCredentials(login.LoginID, login.Password);
if (isValid) {
User? user = _cache.Get<User>($"user{login.LoginID}");
if (user is null) {
user = await _userService.GetUserByLoginId(login.LoginID);
_cache.Set($"user{login.LoginID}", user, DateTimeOffset.Now.AddDays(1));
}
List<string> roles = new();
if (user.IsManager)
roles.Add("manager");
if (user.IsAdmin)
roles.Add("admin");
AuthTokens tokens = GenerateAuthTokens(login, roles);
return new LoginResult {
IsAuthenticated = true,
AuthTokens = tokens,
User = user
};
} else {
return new LoginResult() {
IsAuthenticated = false,
AuthTokens = new() {
JwtToken = "",
RefreshToken = ""
},
User = null
};
}
}
} catch (Exception ex) {
_logger.LogError($"An exception occurred when attempting to authenticate user. Exception: {ex.Message}");
throw;
}
}
public async Task<LoginResult> AttemptLocalUserAuth(WindowsIdentity identity) {
try {
_logger.LogInformation("Attempting to authenticate local Windows system user");
if (identity is null)
throw new ArgumentNullException("WindowsIdentity cannot be null");
User user = await _userService.GetUserByLoginId(identity.Name);
List<string> roles = new();
if (user.IsManager)
roles.Add("manager");
if (user.IsAdmin)
roles.Add("admin");
AuthAttempt authAttempt = new() {
LoginID = user.LoginID,
};
AuthTokens tokens = GenerateAuthTokens(authAttempt, roles);
return new LoginResult {
IsAuthenticated = true,
AuthTokens = tokens,
User = user
};
} catch (Exception ex) {
_logger.LogError($"Unable to authenticate local Windows system user, because {ex.Message}");
throw;
}
}
public AuthTokens GenerateAuthTokens(AuthAttempt authAttempt, IEnumerable<string> roles) {
try {
_logger.LogInformation("Attempting to generate JWT");
if (authAttempt is null)
throw new ArgumentNullException("AuthAttempt cannot be null");
if (string.IsNullOrWhiteSpace(authAttempt.LoginID))
throw new ArgumentException("UserName cannot be null or empty");
if (roles is null)
throw new ArgumentNullException("roles cannot be null");
byte[] key = Encoding.ASCII.GetBytes(_jwtKey);
List<Claim> claims = new() {
new Claim(nameof(authAttempt.LoginID), authAttempt.LoginID)
};
foreach (string role in roles) {
claims.Add(new Claim(ClaimTypes.Role, role));
}
ClaimsIdentity identity = new(claims);
SecurityTokenDescriptor tokenDescriptor = new() {
Issuer = _jwtIssuer,
Audience = _jwtAudience,
Subject = identity,
NotBefore = DateTime.Now,
Expires = DateTime.Now.AddHours(8),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
JwtSecurityTokenHandler tokenHandler = new();
JwtSecurityToken token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor);
string jwt = tokenHandler.WriteToken(token);
string refreshToken = GenerateRefreshToken();
List<string>? refreshTokensForUser = _cache.Get<List<string>>(authAttempt.LoginID);
refreshTokensForUser ??= new List<string>();
if (refreshTokensForUser.Count > 9)
refreshTokensForUser.RemoveRange(9, refreshTokensForUser.Count - 9);
refreshTokensForUser.Insert(0, refreshToken);
_cache.Set(authAttempt.LoginID, refreshTokensForUser, DateTimeOffset.Now.AddHours(4));
return new AuthTokens {
JwtToken = jwt,
RefreshToken = refreshToken
};
} catch (Exception ex) {
_logger.LogError($"An exception occurred when attempting to generate JWT. Exception: {ex.Message}");
throw;
}
}
public async Task<LoginResult> RefreshAuthTokens(AuthAttempt authAttempt) {
try {
_logger.LogInformation("Attempting to refresh auth tokens");
if (authAttempt is null)
throw new ArgumentNullException("AuthAttempt cannot be null");
if (authAttempt.AuthTokens is null)
throw new ArgumentNullException("AuthTokens cannot be null");
bool refreshTokenIsValid = IsRefreshTokenValid(authAttempt.LoginID, authAttempt.AuthTokens.RefreshToken);
if (refreshTokenIsValid) {
User? user = _cache.Get<User>($"user{authAttempt.LoginID}");
if (user is null) {
user = await _userService.GetUserByLoginId(authAttempt.LoginID);
_cache.Set($"user{authAttempt.LoginID}", user, DateTimeOffset.Now.AddDays(1));
}
List<string> roles = new();
if (user.IsManager)
roles.Add("manager");
if (user.IsAdmin)
roles.Add("admin");
AuthTokens refreshedTokens = GenerateAuthTokens(authAttempt, roles);
LoginResult loginResult = new() {
IsAuthenticated = true,
AuthTokens = refreshedTokens,
User = user
};
return loginResult;
} else {
throw new AuthenticationException("Invalid refresh token");
}
} catch (Exception ex) {
_logger.LogError($"An exception occurred when attempting to refresh auth tokens. Exception: {ex.Message}");
throw;
}
}
private string GenerateRefreshToken() {
byte[] randomNumber = new byte[32];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) {
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}
private bool IsRefreshTokenValid(string loginId, string refreshToken) {
try {
_logger.LogInformation("Attempting to determine if refresh token is valid");
if (string.IsNullOrWhiteSpace(loginId))
throw new ArgumentNullException("LoginID cannot be null or empty");
if (string.IsNullOrWhiteSpace(refreshToken))
throw new ArgumentNullException("Refresh token cannot be null or empty");
List<string>? cachedRefreshTokensForUser = _cache.Get<List<string>>(loginId);
if (cachedRefreshTokensForUser is null || !cachedRefreshTokensForUser.Contains(refreshToken)) {
_logger.LogInformation($"Could not find cached refresh tokens for user {loginId}");
return false;
}
return true;
} catch (Exception ex) {
_logger.LogError($"An exception occurred when attempting to validate refresh token. Exception: {ex.Message}");
throw;
}
}
}

View File

@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.Threading.Tasks;
using Dapper;
using Microsoft.Extensions.Logging;
namespace Fab2ApprovalSystem.Services;
public interface IDalService {
Task<IEnumerable<T>> QueryAsync<T>(string sql);
Task<IEnumerable<T>> QueryAsync<T>(string sql, object parameters);
Task<int> ExecuteAsync(string sql);
Task<int> ExecuteAsync<T>(string sql, T parameters);
}
public class DalService : IDalService {
private static readonly int RETRIES = 3;
private static readonly int BACKOFF_SECONDS_INTERVAL = 30;
private readonly ILogger<DalService> _logger;
private readonly IDbConnectionService _dbConnectionService;
public DalService(IDbConnectionService dbConnectionService, ILogger<DalService> logger) {
_dbConnectionService = dbConnectionService ??
throw new ArgumentNullException("IDbConnectionService not injected");
_logger = logger ??
throw new ArgumentNullException("ILogger not injected");
}
public async Task<IEnumerable<T>> QueryAsync<T>(string sql) {
if (sql is null) throw new ArgumentNullException("sql cannot be null");
int remainingRetries = RETRIES;
bool queryWasSuccessful = false;
Exception exception = null;
IEnumerable<T> result = new List<T>();
while (!queryWasSuccessful && remainingRetries > 0) {
int backoffSeconds = (RETRIES - remainingRetries--) * BACKOFF_SECONDS_INTERVAL;
Task.Delay(backoffSeconds * 1000).Wait();
try {
_logger.LogInformation($"Attempting to perform query with {sql}. Remaining retries: {remainingRetries}");
using (IDbConnection conn = _dbConnectionService.GetConnection()) {
result = await conn.QueryAsync<T>(sql);
}
queryWasSuccessful = true;
} catch (Exception ex) {
_logger.LogError($"An exception occurred while attempting to perform a query. Exception: {ex.Message}");
exception = ex;
}
}
if (!queryWasSuccessful && exception is not null) {
throw exception;
}
return result;
}
public async Task<IEnumerable<T>> QueryAsync<T>(string sql, object parameters) {
if (sql is null) throw new ArgumentNullException("sql cannot be null");
if (parameters is null) throw new ArgumentNullException("parameters cannot be null");
StringBuilder logBuilder = new();
int remainingRetries = RETRIES;
bool queryWasSuccessful = false;
Exception exception = null;
IEnumerable<T> result = new List<T>();
while (!queryWasSuccessful && remainingRetries > 0) {
int backoffSeconds = (RETRIES - remainingRetries--) * BACKOFF_SECONDS_INTERVAL;
Task.Delay(backoffSeconds * 1000).Wait();
try {
logBuilder.Clear();
logBuilder.Append($"Attempting to perform query with {sql} ");
logBuilder.Append($"and parameters {parameters.ToString()}. ");
logBuilder.Append($"Remaining retries: {remainingRetries}");
_logger.LogInformation(logBuilder.ToString());
using (IDbConnection conn = _dbConnectionService.GetConnection()) {
result = await conn.QueryAsync<T>(sql, parameters);
}
queryWasSuccessful = true;
} catch (Exception ex) {
_logger.LogError($"An exception occurred while attempting to perform a query. Exception: {ex.Message}");
exception = ex;
}
}
if (!queryWasSuccessful && exception is not null) {
throw exception;
}
return result;
}
public async Task<int> ExecuteAsync(string sql) {
if (sql is null) throw new ArgumentNullException("sql cannot be null");
int remainingRetries = RETRIES;
bool queryWasSuccessful = false;
Exception exception = null;
int rowsAffected = 0;
while (!queryWasSuccessful && remainingRetries > 0) {
int backoffSeconds = (RETRIES - remainingRetries--) * BACKOFF_SECONDS_INTERVAL;
Task.Delay(backoffSeconds * 1000).Wait();
try {
_logger.LogInformation($"Attempting to execute {sql}. Remaining retries: {remainingRetries}");
using (IDbConnection conn = _dbConnectionService.GetConnection()) {
rowsAffected = await conn.ExecuteAsync(sql);
}
queryWasSuccessful = true;
} catch (Exception ex) {
_logger.LogError($"An exception occurred while attempting to execute a query. Exception: {ex.Message}");
exception = ex;
}
}
if (!queryWasSuccessful && exception is not null) {
throw exception;
}
return rowsAffected;
}
public async Task<int> ExecuteAsync<T>(string sql, T parameters) {
if (sql is null) throw new ArgumentNullException("sql cannot be null");
int remainingRetries = RETRIES;
bool queryWasSuccessful = false;
Exception exception = null;
int rowsAffected = 0;
while (!queryWasSuccessful && remainingRetries > 0) {
int backoffSeconds = (RETRIES - remainingRetries--) * BACKOFF_SECONDS_INTERVAL;
Task.Delay(backoffSeconds * 1000).Wait();
try {
_logger.LogInformation($"Attempting to execute {sql} with parameters. Remaining retries: {remainingRetries}");
using (IDbConnection conn = _dbConnectionService.GetConnection()) {
rowsAffected = await conn.ExecuteAsync(sql, parameters);
}
queryWasSuccessful = true;
} catch (Exception ex) {
_logger.LogError($"An exception occurred while attempting to execute a query. Exception: {ex.Message}");
exception = ex;
}
}
if (!queryWasSuccessful && exception is not null) {
throw exception;
}
return rowsAffected;
}
}

View File

@ -0,0 +1,22 @@
using System.Data;
using Fab2ApprovalSystem.Models;
using Microsoft.Data.SqlClient;
namespace Fab2ApprovalSystem.Services;
public interface IDbConnectionService {
IDbConnection GetConnection();
}
public class DbConnectionService : IDbConnectionService {
private readonly string _dbConnectionString;
public DbConnectionService(AppSettings appSettings) {
_dbConnectionString = appSettings.DBConnectionString;
}
public IDbConnection GetConnection() =>
new SqlConnection(_dbConnectionString);
}

View File

@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Fab2ApprovalSystem.Models;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Fab2ApprovalSystem.Services;
public interface IUserService {
Task<IEnumerable<User>> GetAllActiveUsers();
Task<User> GetUserByLoginId(string loginId);
Task<User> GetUserByUserId(int userId);
Task<IEnumerable<int>> GetApproverUserIdsBySubRoleCategoryItem(string item);
}
public class UserService : IUserService {
private readonly ILogger<UserService> _logger;
private readonly IDalService _dalService;
private readonly IMemoryCache _cache;
public UserService(ILogger<UserService> logger, IDalService dalService, IMemoryCache cache) {
_logger = logger ??
throw new ArgumentNullException("ILogger not injected");
_dalService = dalService ??
throw new ArgumentNullException("IDalService not injected");
_cache = cache ??
throw new ArgumentNullException("IMemoryCache not injected");
}
public async Task<IEnumerable<User>> GetAllActiveUsers() {
try {
_logger.LogInformation("Attempting to get all active users");
IEnumerable<User>? allActiveUsers = _cache.Get<IEnumerable<User>>("allActiveUsers");
if (allActiveUsers is null) {
string sql = "select * from Users where IsActive = 1";
allActiveUsers = (await _dalService.QueryAsync<User>(sql)).ToList();
_cache.Set("allActiveUsers", allActiveUsers, DateTimeOffset.Now.AddHours(1));
}
if (allActiveUsers is null || allActiveUsers.Count() == 0) {
throw new Exception("No users found");
}
return allActiveUsers;
} catch (Exception ex) {
string errMsg = $"An exception occurred when attempting to get all users. Exception: {ex.Message}";
_logger.LogError(errMsg);
throw;
}
}
public async Task<User> GetUserByLoginId(string loginId) {
try {
_logger.LogInformation("Attempting to get user by LoginId");
if (string.IsNullOrWhiteSpace(loginId))
throw new ArgumentException("LoginId cannot be null or empty");
User? user = _cache.Get<User>($"userByLoginId{loginId}");
user ??= _cache.Get<IEnumerable<User>>("allActiveUsers")?.FirstOrDefault(u => u.LoginID == loginId);
if (user is null) {
string sql = $"select * from Users where LoginID = '{loginId}';";
user = (await _dalService.QueryAsync<User>(sql)).FirstOrDefault();
_cache.Set($"userByLoginId{loginId}", user, DateTimeOffset.Now.AddHours(1));
}
if (user is null) throw new Exception($"No user found with LoginID {loginId}");
return user;
} catch (Exception ex) {
string errMsg = $"An exception occurred when attempting to get user for LoginID {loginId}. Exception: {ex.Message}";
_logger.LogError(errMsg);
throw;
}
}
public async Task<User> GetUserByUserId(int userId) {
try {
_logger.LogInformation("Attempting to get user by user ID");
if (userId <= 0) throw new ArgumentException($"{userId} is not a valid user ID");
User? user = _cache.Get<User>($"userByUserId{userId}");
user ??= _cache.Get<IEnumerable<User>>("allActiveUsers")?.FirstOrDefault(u => u.UserID == userId);
if (user is null) {
string sql = $"select * from Users where UserID = '{userId}';";
user = (await _dalService.QueryAsync<User>(sql)).FirstOrDefault();
_cache.Set($"userByUserId{userId}", user, DateTimeOffset.Now.AddHours(1));
}
if (user is null) throw new Exception($"No user found with UserID {userId}");
return user;
} catch (Exception ex) {
string errMsg = $"An exception occurred when attempting to get user for UserID {userId}. Exception: {ex.Message}";
_logger.LogError(errMsg);
throw;
}
}
public async Task<IEnumerable<int>> GetApproverUserIdsBySubRoleCategoryItem(string item) {
try {
_logger.LogInformation("Attempting to get approver user IDs");
if (string.IsNullOrWhiteSpace(item)) throw new ArgumentException("SubRoleCategoryItem cannot be null or empty");
IEnumerable<int>? userIds = _cache.Get<IEnumerable<int>>($"approverUserIdsBySubRollCategory{item}");
if (userIds is null) {
StringBuilder queryBuilder = new();
queryBuilder.Append("select us.UserID ");
queryBuilder.Append("from SubRole as sr ");
queryBuilder.Append("join UserSubRole as us on sr.SubRoleID=us.SubRoleID ");
queryBuilder.Append("join SubRoleCategory as sc on sr.SubRoleCategoryID=sc.SubRoleCategoryID ");
queryBuilder.Append($"where sc.SubRoleCategoryItem='{item}'");
userIds = (await _dalService.QueryAsync<int>(queryBuilder.ToString())).ToList();
_cache.Set($"approverUserIdsBySubRollCategory{item}", userIds, DateTimeOffset.Now.AddHours(1));
}
if (userIds is null || userIds.Count() == 0) {
throw new Exception($"No users found for SubRoleCategoryItem {item}");
}
return userIds;
} catch (Exception ex) {
string errMsg = $"An exception occurred when attempting to get approver user IDs. Exception: {ex.Message}";
_logger.LogError(errMsg);
throw;
}
}
}

View File

@ -0,0 +1,8 @@
@using Fab2ApprovalSystem.DMO
@using Fab2ApprovalSystem.JobSchedules
@using Fab2ApprovalSystem.Misc
@using Fab2ApprovalSystem.Models
@using Fab2ApprovalSystem.PdfGenerator
@using Fab2ApprovalSystem.Utilities
@using Fab2ApprovalSystem.ViewModels
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -0,0 +1,3 @@
@{
Layout = null;
}