Created Windows Service

This commit is contained in:
Chase Tucker 2024-04-03 12:05:44 -07:00
parent 35690df362
commit 794b616293
14 changed files with 247 additions and 38 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
.vs
bin
obj
TestResults
TestResults
Properties

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
@ -8,6 +8,9 @@
<ServerGarbageCollection>true</ServerGarbageCollection>
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
<Configurations>Debug;Release;Staging</Configurations>
<OutputType>exe</OutputType>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -26,12 +29,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac.Extras.Quartz" Version="10.0.0" />
<PackageReference Include="Dapper" Version="2.1.28" />
<PackageReference Include="Dapper.Contrib" Version="2.0.78" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="NLog" Version="5.2.8" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.8" />
<PackageReference Include="Quartz" Version="3.8.1" />

View File

@ -4,5 +4,6 @@
<Controller_SelectedScaffolderID>ApiControllerEmptyScaffolder</Controller_SelectedScaffolderID>
<Controller_SelectedScaffolderCategoryPath>root/Common/Api</Controller_SelectedScaffolderCategoryPath>
<NameOfLastUsedPublishProfile>C:\Users\tuckerc\FabApprovalWorkerService\FabApprovalWorkerService\Properties\PublishProfiles\Staging.pubxml</NameOfLastUsedPublishProfile>
<_LastSelectedProfileId>C:\Users\tuckerc\FabApprovalWorkerService\FabApprovalWorkerService\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
</PropertyGroup>
</Project>

View File

@ -1,10 +1,14 @@
namespace FabApprovalWorkerService.Models;
using Dapper.Contrib.Extensions;
namespace FabApprovalWorkerService.Models;
[Table("ECN")]
public class ECN {
[Key]
public required int ECNNumber { get; set; }
public bool IsTECN { get; set; } = false;
public DateTime ExpirationDate { get; set; }
public DateTime? ExtensionDate { get; set; }
public DateTime ExtensionDate { get; set; } = DateTime.MinValue;
public required int OriginatorID { get; set; }
public required string Title { get; set; }
}

View File

@ -0,0 +1,12 @@
using Dapper.Contrib.Extensions;
namespace FabApprovalWorkerService.Models;
[Table("TrainingAssignment")]
public class TrainingAssignment {
[Key]
public int ID { get; set; }
public int TrainingID { get; set; }
public bool Status { get; set; } = false;
public bool Deleted { get; set; } = false;
public DateTime DeletedDate { get; set; }
}

View File

@ -8,7 +8,7 @@ using Quartz;
using System.Net.Mail;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.SetMinimumLevel(LogLevel.Trace);
@ -36,11 +36,7 @@ builder.Services.AddQuartz(q => {
q.AddTrigger(opts => opts
.ForJob(pendingOOOStatusJob)
.WithIdentity("Pending OOO status trigger")
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(10)
.RepeatForever()
)
.StartNow()
.WithCronSchedule(CronScheduleBuilder.DailyAtHourAndMinute(0, 0))
);
JobKey expiredOOOStatusJob = new JobKey("Expired OOO status job");
@ -50,6 +46,16 @@ builder.Services.AddQuartz(q => {
q.AddTrigger(opts => opts
.ForJob(expiredOOOStatusJob)
.WithIdentity("Expired OOO status trigger")
.WithCronSchedule(CronScheduleBuilder.DailyAtHourAndMinute(0, 0))
);
JobKey expiringTECNJob = new JobKey("Expiring TECN job");
q.AddJob<ExpiringTECNWorker>(opts => opts
.WithIdentity(expiringTECNJob)
);
q.AddTrigger(opts => opts
.ForJob(expiringTECNJob)
.WithIdentity("Expiring TECN trigger")
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(10)
.RepeatForever()
@ -62,6 +68,11 @@ builder.Services.AddQuartzHostedService(opt => {
opt.WaitForJobsToComplete = true;
});
WebApplication app = builder.Build();
builder.Services.AddWindowsService(options => {
options.ServiceName = "Fab Approval Worker Service";
});
builder.Services.AddHostedService<WindowsService>();
IHost app = builder.Build();
app.Run();

View File

@ -32,13 +32,13 @@ public class ECNService : IECNService {
queryBuilder.Append($"ExpirationDate between '{yesterday}' ");
queryBuilder.Append($"and '{today}'");
IEnumerable<ECN> expiredTecns = (await _dalService.QueryAsync<ECN>(queryBuilder.ToString()))
.Where(e => e.ExtensionDate is null || e.ExtensionDate < DateTime.Now)
.ToList();
IEnumerable<ECN> expiredTecns = (await _dalService.QueryAsync<ECN>(queryBuilder.ToString()));
_logger.LogInformation($"Found {expiredTecns.Count()} expired TECNs");
IEnumerable<ECN> expiredTecnsNotExtended = expiredTecns
.Where(e => e.ExtensionDate < DateTime.Now)
.ToList();
return expiredTecns;
return expiredTecnsNotExtended;
} catch (Exception ex) {
StringBuilder errMsgBuilder = new();
errMsgBuilder.Append("An exception occurred when attempting to get all TECNs expired in the last day. ");
@ -56,18 +56,18 @@ public class ECNService : IECNService {
string fiveDaysFromToday = DateTime.Now.AddDays(5).ToString("yyyy-MM-dd HH:mm:ss");
StringBuilder queryBuilder = new StringBuilder();
queryBuilder.Append("select ECNNumber, IsTECN, ExpirationDate, ExtensionDate, OriginatorID, Title ");
queryBuilder.Append($"from ECN where IsTECN = 1 and ");
queryBuilder.Append("select * from ECN ");
queryBuilder.Append($"where IsTECN = 1 and ");
queryBuilder.Append($"ExpirationDate between '{today}' ");
queryBuilder.Append($"and '{fiveDaysFromToday}'");
queryBuilder.Append($"and '{fiveDaysFromToday}';");
IEnumerable<ECN> expiringTecns = (await _dalService.QueryAsync<ECN>(queryBuilder.ToString()))
.Where(e => e.ExtensionDate is null || e.ExtensionDate <= DateTime.Now.AddDays(5))
IEnumerable<ECN> expiringTecns = (await _dalService.QueryAsync<ECN>(queryBuilder.ToString()));
IEnumerable<ECN> expiringTecnsNotExtended = expiringTecns
.Where(e => e.ExtensionDate <= DateTime.Now.AddDays(5))
.ToList();
_logger.LogInformation($"Found {expiringTecns.Count()} expiring TECNs");
return expiringTecns;
return expiringTecnsNotExtended;
} catch (Exception ex) {
StringBuilder errMsgBuilder = new();
errMsgBuilder.Append("An exception occurred when attempting to get all TECNs expiring in the next five days. ");

View File

@ -0,0 +1,102 @@
using FabApprovalWorkerService.Models;
using System.Text;
namespace FabApprovalWorkerService.Services;
public interface ITrainingService {
Task<IEnumerable<int>> GetTrainingIdsForECN(int ecnNumber);
Task DeleteTrainingAssignment(int trainingId);
Task<IEnumerable<int>> GetTrainingAssignmentIdsForTraining(int trainingId);
Task DeleteDocAssignment(int trainingAssignmentId);
}
public class TrainingService : ITrainingService {
private ILogger<TrainingService> _logger;
private IDalService _dalService;
public TrainingService(ILogger<TrainingService> logger, IDalService dalService) {
_logger = logger ??
throw new ArgumentNullException("ILogger not injected");
_dalService = dalService ??
throw new ArgumentNullException("IDalService not injected");
}
public async Task DeleteDocAssignment(int trainingAssignmentId) {
if (trainingAssignmentId <= 0) throw new ArgumentException($"Invalid training assignment id: {trainingAssignmentId}");
try {
_logger.LogInformation($"Attempting to delete training doc assignments for training assignment {trainingAssignmentId}");
StringBuilder queryBuilder = new();
queryBuilder.Append($"update TrainingDocAcks set Deleted = 1, ");
queryBuilder.Append($"DeletedDate = {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} ");
queryBuilder.Append($"where TrainingAssignmentID = {trainingAssignmentId} and Reviewed = 0;");
await _dalService.ExecuteAsync(queryBuilder.ToString());
} catch (Exception ex) {
StringBuilder errMsgBuilder = new();
errMsgBuilder.Append($"An exception occurred when attempting to delete training doc assignment ");
errMsgBuilder.Append($"{trainingAssignmentId}. Exception: {ex.Message}");
_logger.LogError(errMsgBuilder.ToString());
throw;
}
}
public async Task DeleteTrainingAssignment(int trainingId) {
if (trainingId <= 0) throw new ArgumentException($"Invalid training id: {trainingId}");
try {
_logger.LogInformation($"Attempting to delete training assignment {trainingId}");
StringBuilder queryBuilder = new();
queryBuilder.Append($"update TrainingAssignments set Deleted = 1, ");
queryBuilder.Append($"DeletedDate = {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} ");
queryBuilder.Append($"where TrainingID = {trainingId} and status = 0;");
await _dalService.ExecuteAsync(queryBuilder.ToString());
} catch (Exception ex) {
StringBuilder errMsgBuilder = new();
errMsgBuilder.Append($"An exception occurred when attempting to delete training assignment ");
errMsgBuilder.Append($"{trainingId}. Exception: {ex.Message}");
_logger.LogError(errMsgBuilder.ToString());
throw;
}
}
public async Task<IEnumerable<int>> GetTrainingAssignmentIdsForTraining(int trainingId) {
if (trainingId <= 0) throw new ArgumentException($"Invalid trainingID: {trainingId}");
try {
_logger.LogInformation($"Attempting to get training assignment ids for training id {trainingId}");
string sql = $"select ID from TrainingAssignments where TrainingID = {trainingId};";
return await _dalService.QueryAsync<int>(sql);
} catch (Exception ex) {
StringBuilder errMsgBuilder = new();
errMsgBuilder.Append($"An exception occurred when attempting to get training assignment ids ");
errMsgBuilder.Append($"for training id {trainingId}. Exception: {ex.Message}");
_logger.LogError(errMsgBuilder.ToString());
throw;
}
}
public async Task<IEnumerable<int>> GetTrainingIdsForECN(int ecnNumber) {
if (ecnNumber <= 0) throw new ArgumentException($"Invalid ecnNumber: {ecnNumber}");
try {
_logger.LogInformation($"Attempting to get training ids for ecn {ecnNumber}");
string sql = $"select TrainingID from Training where ECN = {ecnNumber};";
return await _dalService.QueryAsync<int>(sql);
} catch (Exception ex) {
StringBuilder errMsgBuilder = new();
errMsgBuilder.Append($"An exception occurred when attempting to get training ids ");
errMsgBuilder.Append($"for ECN {ecnNumber}. Exception: {ex.Message}");
_logger.LogError(errMsgBuilder.ToString());
throw;
}
}
}

View File

@ -0,0 +1,40 @@
using FabApprovalWorkerService.Models;
namespace FabApprovalWorkerService.Services;
public class WindowsService : BackgroundService {
private readonly ILogger<WindowsService> _logger;
private readonly IMonInWorkerClient _monInClient;
public WindowsService(ILogger<WindowsService> logger,
IServiceProvider serviceProvider) {
_logger = logger ??
throw new ArgumentNullException("ILogger not injected");
using (IServiceScope scope = serviceProvider.CreateScope()) {
_monInClient = scope.ServiceProvider.GetService<IMonInWorkerClient>() ??
throw new ArgumentNullException("IMonInWorkerClient not injected");
}
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
_logger.LogInformation("Starting Windows service");
try {
while (!stoppingToken.IsCancellationRequested) {
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
_monInClient.PostStatus("WindowsService", StatusValue.Ok);
}
} catch (OperationCanceledException) {
_logger.LogError("The Windows service has been stopped");
_monInClient.PostStatus("WindowsService", StatusValue.Critical);
} catch (Exception ex) {
_logger.LogError($"An exception occurred when running Windows Service. Exception: {ex.Message}");
_monInClient.PostStatus("WindowsService", StatusValue.Critical);
Environment.Exit(1);
}
}
}

View File

@ -1,17 +1,16 @@
drop table if exists ECN;
create table ECN (
ECNID integer primary key,
ECNNumber integer not null,
ECNNumber integer primary key not null,
IsTECN integer default 0,
ExpirationDate text not null,
ExpirationDate text,
ExtensionDate text,
OriginatorID integer not null,
Title text not null
);
insert into ECN (ECNNumber, IsTECN, ExpirationDate, ExtensionDate, OriginatorID, Title)
values (1, 0, '2024-03-30 00:00:00', '', 1, 'title1'),
(2, 1, '2024-04-01 00:00:00', '', 6, 'title2'),
(3, 1, '2024-06-01 00:00:00', '', 4, 'title3'),
(4, 1, '2024-04-01 00:00:00', '2024-06-01 00:00:00', 3, 'title4')
values (1, 0, '2024-04-06 00:00:00', null, 1, 'title1'),
(2, 1, '2024-04-04 00:00:00', null, 6, 'title2'),
(3, 1, '2024-06-01 00:00:00', null, 4, 'title3'),
(4, 1, '2024-04-03 00:00:00', '2024-06-01 00:00:00', 3, 'title4')

View File

@ -1,9 +1,9 @@
drop table if exists TECNNotificationUsers;
drop table if exists TECNNotificationsUsers;
create table TECNNotificationUsers (
create table TECNNotificationsUsers (
Id integer primary key,
UserId integer not null
);
insert into TECNNotificationUsers (UserId)
insert into TECNNotificationsUsers (UserId)
values (1), (2), (3);

View File

@ -43,6 +43,8 @@ public class ExpiringTECNWorker : IJob {
IEnumerable<ECN> expiringTECNs = await _ecnService.GetExpiringTECNs();
_logger.LogInformation($"There are {expiringTECNs.Count()} TECNs expiring in the next 5 days");
foreach (ECN eCN in expiringTECNs) {
string recipientEmail = await _userService.GetUserEmail(eCN.OriginatorID);
MailAddress recipientAddress = new MailAddress(recipientEmail);
@ -55,9 +57,8 @@ public class ExpiringTECNWorker : IJob {
}
StringBuilder bodyBuilder = new();
bodyBuilder.Append($"Good day, TECN# {eCN.ECNNumber} will be expiring in ");
bodyBuilder.Append($"{(eCN.ExpirationDate - DateTime.Now).Days} ");
bodyBuilder.Append($"on {eCN.ExpirationDate.ToString("MMMM dd, yyyy")}. ");
bodyBuilder.Append($"Good day, TECN# {eCN.ECNNumber} will be expire on ");
bodyBuilder.Append($"{eCN.ExpirationDate.ToString("MMMM dd, yyyy")}. ");
bodyBuilder.Append($"<br /> Review TECN <a href='https://mesaapproval.mes.com/ECN/Edit?IssueID={eCN.ECNNumber}'> ");
bodyBuilder.Append("here </a>");

View File

@ -112,6 +112,9 @@ internal class SmtpServiceTests {
Assert.True(await _smtpService.SendEmail(ADDRESS_LIST, ADDRESS_LIST, "subject", "body"));
_mockSmtpClient.Verify(s => s.Send(It.IsAny<MailMessage>()));
string? env = Environment.GetEnvironmentVariable("FabApprovalEnvironmentName");
if (env is not null && !env.ToLower().Equals("development"))
_mockSmtpClient.Verify(s => s.Send(It.IsAny<MailMessage>()));
}
}

View File

@ -0,0 +1,29 @@
using FabApprovalWorkerService.Services;
using Microsoft.Extensions.Logging;
using Moq;
namespace FabApprovalWorkerServiceTests;
public class TrainingServiceTests {
private Mock<ILogger<TrainingService>> _mockLogger;
private Mock<IDalService> _mockDalService;
private TrainingService _trainingService;
[SetUp]
public void Setup() {
_mockLogger = new Mock<ILogger<TrainingService>>();
_mockDalService = new Mock<IDalService>();
}
[Test]
public void TrainingServiceWithNullLoggerShouldThrowException() {
Assert.Throws<ArgumentNullException>(() => new TrainingService(null, _mockDalService.Object));
}
[Test]
public void TrainingServiceWithNullDalServiceShouldThrowException() {
Assert.Throws<ArgumentNullException>(() => new TrainingService(_mockLogger.Object, null));
}
}