Sealed qualifying public classes Migrated database changes
This commit is contained in:
@@ -7,7 +7,7 @@ public abstract class EventBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.CreateVersion7();
|
||||
|
||||
public DateTimeOffset EnqueueAt { get; set; } = SouthAfricanTimeZone.UtcNow();
|
||||
public DateTimeOffset EnqueueAt { get; set; } = (DateTimeOffset)SouthAfricanTimeZone.UtcNow();
|
||||
|
||||
public string CorrelationId { get; set; } = Guid.CreateVersion7().ToString();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.Email.Configuration;
|
||||
|
||||
public class Account
|
||||
public sealed class Account
|
||||
{
|
||||
public string? Username { get; set; }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.Email.Configuration;
|
||||
|
||||
public class SmtpSettings
|
||||
public sealed class SmtpSettings
|
||||
{
|
||||
public Account? Credentials { get; set; }
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ using LiteCharms.Features.Email.Models;
|
||||
|
||||
namespace LiteCharms.Features.Email;
|
||||
|
||||
public class EmailService(IOptions<SmtpSettings> options) : IDisposable
|
||||
public sealed class EmailService(IOptions<SmtpSettings> options) : IDisposable
|
||||
{
|
||||
private readonly SmtpSettings settings = options.Value;
|
||||
private readonly SmtpClient client = new();
|
||||
@@ -16,6 +16,7 @@ public class EmailService(IOptions<SmtpSettings> options) : IDisposable
|
||||
public async ValueTask<Result<Response>> SendEmailAsync(Message message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = EmailTelemetry.Source.StartActivity("Email Send");
|
||||
|
||||
activity?.SetTag("email.recipient", message.Recipient?.Address);
|
||||
|
||||
try
|
||||
@@ -27,21 +28,7 @@ public class EmailService(IOptions<SmtpSettings> options) : IDisposable
|
||||
return Result.Fail<Response>("Smtp service is disconnected.");
|
||||
}
|
||||
|
||||
var email = new MimeMessage();
|
||||
email.From.Add(new MailboxAddress(message.Sender!.Name, message.Sender.Address!));
|
||||
email.To.Add(new MailboxAddress(message.Recipient!.Name, message.Recipient!.Address!));
|
||||
email.Subject = message.Subject!;
|
||||
|
||||
var bodyBuilder = new BodyBuilder();
|
||||
|
||||
if (message.Body!.Properties.HasAttachments)
|
||||
foreach (var attachment in message.Body?.Attachments!)
|
||||
bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken);
|
||||
|
||||
if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message;
|
||||
if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message;
|
||||
|
||||
email.Body = bodyBuilder.ToMessageBody();
|
||||
var email = ConstructEmail(message, cancellationToken);
|
||||
|
||||
var response = await client.SendAsync(email, cancellationToken);
|
||||
|
||||
@@ -69,21 +56,9 @@ public class EmailService(IOptions<SmtpSettings> options) : IDisposable
|
||||
|
||||
await DisconnectAsync(cancellationToken);
|
||||
|
||||
if (response.Contains("421"))
|
||||
{
|
||||
Status = EmailStatuses.TooManyConnections;
|
||||
var failCheckResult = HandleNegativeResponse(response);
|
||||
|
||||
return Result.Fail<Response>(response);
|
||||
}
|
||||
|
||||
if (response.Contains("451"))
|
||||
{
|
||||
Status = EmailStatuses.ConnectionAborted;
|
||||
|
||||
return Result.Fail<Response>(response);
|
||||
}
|
||||
|
||||
EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } });
|
||||
if (failCheckResult.IsFailed) return failCheckResult;
|
||||
|
||||
Status = EmailStatuses.Disconnected;
|
||||
|
||||
@@ -100,6 +75,48 @@ public class EmailService(IOptions<SmtpSettings> options) : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private static MimeMessage ConstructEmail(Message message, CancellationToken cancellationToken)
|
||||
{
|
||||
var email = new MimeMessage();
|
||||
email.From.Add(new MailboxAddress(message.Sender!.Name, message.Sender.Address!));
|
||||
email.To.Add(new MailboxAddress(message.Recipient!.Name, message.Recipient!.Address!));
|
||||
email.Subject = message.Subject!;
|
||||
|
||||
var bodyBuilder = new BodyBuilder();
|
||||
|
||||
if (message.Body!.Properties.HasAttachments)
|
||||
foreach (var attachment in message.Body?.Attachments!)
|
||||
bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken);
|
||||
|
||||
if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message;
|
||||
if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message;
|
||||
|
||||
email.Body = bodyBuilder.ToMessageBody();
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
private Result<Response> HandleNegativeResponse(string response)
|
||||
{
|
||||
if (response.Contains("421", StringComparison.Ordinal))
|
||||
{
|
||||
Status = EmailStatuses.TooManyConnections;
|
||||
|
||||
return Result.Fail<Response>(response);
|
||||
}
|
||||
|
||||
if (response.Contains("451", StringComparison.Ordinal))
|
||||
{
|
||||
Status = EmailStatuses.ConnectionAborted;
|
||||
|
||||
return Result.Fail<Response>(response);
|
||||
}
|
||||
|
||||
EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } });
|
||||
|
||||
return Result.Fail<Response>(response);
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Response>> ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = EmailTelemetry.Source.StartActivity("Email Connect");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.Email.Models;
|
||||
|
||||
public class Attachment
|
||||
public sealed class Attachment
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.Email.Models;
|
||||
|
||||
public class Body : IDisposable
|
||||
public sealed class Body : IDisposable
|
||||
{
|
||||
public string? Message { get; set; }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.Email.Models;
|
||||
|
||||
public class BodyProperties
|
||||
public sealed class BodyProperties
|
||||
{
|
||||
public bool IsHtml { get; set; }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.Email.Models;
|
||||
|
||||
public class Message : IDisposable
|
||||
public sealed class Message : IDisposable
|
||||
{
|
||||
public Party? Sender { get; set; }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.Email.Models;
|
||||
|
||||
public class Party
|
||||
public sealed class Party
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.Email.Models;
|
||||
|
||||
public class Response
|
||||
public sealed class Response
|
||||
{
|
||||
public int Code { get; set; }
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
public static class Hash
|
||||
{
|
||||
public static Func<string?, string?> StringToSha256Hash = (input) =>
|
||||
public static readonly Func<string?, string?> StringToSha256Hash = (input) =>
|
||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!)));
|
||||
|
||||
public static Func<Stream, string?> StreamToSha256Hash = (stream) =>
|
||||
public static readonly Func<Stream, string?> StreamToSha256Hash = (stream) =>
|
||||
Convert.ToHexString(SHA256.HashData(stream));
|
||||
|
||||
public static Func<byte[], string?> BytesToSha256Hash = (bytes) =>
|
||||
public static readonly Func<byte[], string?> BytesToSha256Hash = (bytes) =>
|
||||
Convert.ToHexString(SHA256.HashData(bytes));
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ public static class Quartz
|
||||
config.UseDefaultThreadPool(options => options.MaxConcurrency = 1);
|
||||
config.UseTimeZoneConverter();
|
||||
|
||||
config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString());
|
||||
config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
config.UsePersistentStore(storage =>
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ public static class Timezones
|
||||
? new DateTimeOffset(sourceDateAdjusted.Ticks, SouthAfricanTimeZone.BaseUtcOffset).LocaliseDateTimeOffset(SouthAfricanTimeZone.BaseUtcOffset)
|
||||
: new DateTimeOffset(sourceDateAdjusted.Ticks, timezone!.BaseUtcOffset).LocaliseDateTimeOffset(timezone.BaseUtcOffset);
|
||||
|
||||
return DateTimeOffset.Parse(localised!);
|
||||
return DateTimeOffset.Parse(localised!, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static DateTime UtcNow(this TimeZoneInfo timezone) => ToDateTimeWithTimeZone(DateTime.Now, timezone).UtcDateTime;
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
|
||||
<!-- Quartz Scheduler-->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="3.0.96">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="OpenTelemetry" Version="1.15.3" />
|
||||
<PackageReference Include="Quartz" Version="3.18.1" />
|
||||
<PackageReference Include="Quartz.Plugins" Version="3.18.1" />
|
||||
@@ -142,6 +146,7 @@
|
||||
|
||||
<!-- Shared Usings -->
|
||||
<ItemGroup>
|
||||
<Using Include="System.Globalization" />
|
||||
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||
<Using Include="Microsoft.Extensions.Hosting" />
|
||||
<Using Include="System.Text" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.Models;
|
||||
|
||||
public class DateRange
|
||||
public sealed class DateRange
|
||||
{
|
||||
public DateOnly From { get; set; }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.Models;
|
||||
|
||||
public class PageReference
|
||||
public sealed class PageReference
|
||||
{
|
||||
public string? Tag { get; set; }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.Models;
|
||||
|
||||
public class ProductFilter
|
||||
public sealed class ProductFilter
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.Models;
|
||||
|
||||
public class ProductMetadata
|
||||
public sealed class ProductMetadata
|
||||
{
|
||||
public string? Manufacturer { get; set; }
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
namespace LiteCharms.Features.Models;
|
||||
|
||||
public class SocialMedia
|
||||
public sealed class SocialMedia
|
||||
{
|
||||
public SocialMediaTypes Type { get; set; }
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using LiteCharms.Features.Quartz.Abstractions;
|
||||
|
||||
namespace LiteCharms.Features.Quartz;
|
||||
|
||||
public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestrator
|
||||
public sealed class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestrator
|
||||
{
|
||||
public async Task SendAsync<TNotification>(TNotification notification, CancellationToken cancellationToken = default)
|
||||
where TNotification : IEvent
|
||||
@@ -11,7 +11,7 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra
|
||||
var chainedJobGroup = "onetime-jobs";
|
||||
|
||||
var scheduler = await schedulerFactory.GetScheduler(cancellationToken);
|
||||
var jobKey = new JobKey($"{notification.Name.ToLower()}-{notification.CorrelationId.ToLower()}", chainedJobGroup);
|
||||
var jobKey = new JobKey($"{notification.Name.ToLower(CultureInfo.InvariantCulture)}-{notification.CorrelationId.ToLower(CultureInfo.InvariantCulture)}", chainedJobGroup);
|
||||
var triggerKey = new TriggerKey($"{jobKey.Name}-trigger", chainedJobGroup);
|
||||
|
||||
var job = JobBuilder.Create<MediatorJob<TNotification>>()
|
||||
@@ -35,7 +35,7 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra
|
||||
var chainedJobGroup = "scheduled-jobs";
|
||||
|
||||
var scheduler = await schedulerFactory.GetScheduler(cancellationToken);
|
||||
var jobKey = new JobKey($"{notification.Name.ToLower()}", chainedJobGroup);
|
||||
var jobKey = new JobKey($"{notification.Name.ToLower(CultureInfo.InvariantCulture)}", chainedJobGroup);
|
||||
var triggerKey = new TriggerKey($"{jobKey.Name}-trigger", chainedJobGroup);
|
||||
|
||||
var job = JobBuilder.Create<MediatorJob<TNotification>>()
|
||||
@@ -53,7 +53,7 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra
|
||||
.WithDescription($"Scheduled via Main Job at {now:g}")
|
||||
.WithCronSchedule(cronExpression, cron => cron
|
||||
.WithMisfireHandlingInstructionIgnoreMisfires())
|
||||
.StartAt(now)
|
||||
.StartAt((DateTimeOffset)now)
|
||||
.Build();
|
||||
|
||||
await scheduler.AddJob(job, replace: true, cancellationToken);
|
||||
|
||||
@@ -4,7 +4,7 @@ using LiteCharms.Features.Mediator;
|
||||
namespace LiteCharms.Features.Quartz;
|
||||
|
||||
[DisallowConcurrentExecution]
|
||||
public class MediatorJob<TNotification>(IMediator mediator) : IJob where TNotification : IEvent
|
||||
public sealed class MediatorJob<TNotification>(IMediator mediator) : IJob where TNotification : IEvent
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.Quartz;
|
||||
|
||||
public class RetryJobListener : IJobListener
|
||||
public sealed class RetryJobListener : IJobListener
|
||||
{
|
||||
public string Name => "RetryJobListener";
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
||||
if(string.IsNullOrWhiteSpace(fileHash))
|
||||
return Result.Fail<string>("Failed to compute file hash.");
|
||||
|
||||
var fileKey = $"{fileHash.ToLower()}{Path.GetExtension(fileName)}";
|
||||
var fileKey = $"{fileHash.ToLower(CultureInfo.InvariantCulture)}{Path.GetExtension(fileName)}";
|
||||
|
||||
var putRequest = new PutObjectRequest
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using static LiteCharms.Features.S3.Constants;
|
||||
|
||||
namespace LiteCharms.Features.S3;
|
||||
|
||||
public class BookshopInvoicesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopInvoicesBucketName)] IAmazonS3 amazonS3) :
|
||||
public sealed class BookshopInvoicesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopInvoicesBucketName)] IAmazonS3 amazonS3) :
|
||||
S3ServiceBase(amazonS3), IS3Service
|
||||
{
|
||||
protected override string BucketName => configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:BucketName").Value ?? "";
|
||||
|
||||
@@ -3,7 +3,7 @@ using static LiteCharms.Features.S3.Constants;
|
||||
|
||||
namespace LiteCharms.Features.S3;
|
||||
|
||||
public class BookshopQuotesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopQuotesBucketName)] IAmazonS3 amazonS3) :
|
||||
public sealed class BookshopQuotesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopQuotesBucketName)] IAmazonS3 amazonS3) :
|
||||
S3ServiceBase(amazonS3), IS3Service
|
||||
{
|
||||
protected override string BucketName => configuration.GetSection($"{BookshopQuotesS3SettingsSection}:BucketName").Value ?? "";
|
||||
|
||||
@@ -3,7 +3,7 @@ using static LiteCharms.Features.S3.Constants;
|
||||
|
||||
namespace LiteCharms.Features.S3;
|
||||
|
||||
public class BookshopS3Service(IConfiguration configuration, [FromKeyedServices(BookshopBucketName)] IAmazonS3 amazonS3) :
|
||||
public sealed class BookshopS3Service(IConfiguration configuration, [FromKeyedServices(BookshopBucketName)] IAmazonS3 amazonS3) :
|
||||
S3ServiceBase(amazonS3), IS3Service
|
||||
{
|
||||
protected override string BucketName => configuration.GetSection($"{BookshopS3SettingsSection}:BucketName").Value ?? "";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.S3.Configuration;
|
||||
|
||||
public class S3Settings
|
||||
public sealed class S3Settings
|
||||
{
|
||||
public string? ServiceUrl { get; set; }
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ using LiteCharms.Features.ServiceBus.Queues;
|
||||
|
||||
namespace LiteCharms.Features.ServiceBus;
|
||||
|
||||
public class EmailServiceBus(EmailQueue messages) : IEventBus
|
||||
public sealed class EmailServiceBus(EmailQueue messages) : IEventBus
|
||||
{
|
||||
public async Task<Result> PublishAsync<TEvent>(TEvent notification, CancellationToken cancellationToken = default)
|
||||
where TEvent : class, IEvent
|
||||
|
||||
@@ -3,7 +3,7 @@ using LiteCharms.Features.ServiceBus.Queues;
|
||||
|
||||
namespace LiteCharms.Features.ServiceBus.Exchanges;
|
||||
|
||||
public class EmailExchange(EmailQueue messages, ILogger<EmailExchange> logger, IPublisher mediator) : BackgroundService
|
||||
public sealed class EmailExchange(EmailQueue messages, ILogger<EmailExchange> logger, IPublisher mediator) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace LiteCharms.Features.ServiceBus.Exchanges;
|
||||
|
||||
public class GeneralExchange(GeneralQueue messages) : BackgroundService
|
||||
public sealed class GeneralExchange(GeneralQueue messages) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace LiteCharms.Features.ServiceBus.Exchanges;
|
||||
|
||||
public class SalesExchange(SalesQueue messages) : BackgroundService
|
||||
public sealed class SalesExchange(SalesQueue messages) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using LiteCharms.Features.ServiceBus.Queues;
|
||||
|
||||
namespace LiteCharms.Features.ServiceBus;
|
||||
|
||||
public class GeneralServiceBus(GeneralQueue messages) : IEventBus
|
||||
public sealed class GeneralServiceBus(GeneralQueue messages) : IEventBus
|
||||
{
|
||||
public async Task<Result> PublishAsync<TEvent>(TEvent notification, CancellationToken cancellationToken = default)
|
||||
where TEvent : class, IEvent
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace LiteCharms.Features.ServiceBus.Queues;
|
||||
|
||||
public class EmailQueue : EventBusQueueBase, IEventBusQueue;
|
||||
public sealed class EmailQueue : EventBusQueueBase, IEventBusQueue;
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace LiteCharms.Features.ServiceBus.Queues;
|
||||
|
||||
public class GeneralQueue : EventBusQueueBase, IEventBusQueue;
|
||||
public sealed class GeneralQueue : EventBusQueueBase, IEventBusQueue;
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace LiteCharms.Features.ServiceBus.Queues;
|
||||
|
||||
public class SalesQueue : EventBusQueueBase, IEventBusQueue;
|
||||
public sealed class SalesQueue : EventBusQueueBase, IEventBusQueue;
|
||||
|
||||
@@ -4,7 +4,7 @@ using LiteCharms.Features.ServiceBus.Queues;
|
||||
|
||||
namespace LiteCharms.Features.ServiceBus;
|
||||
|
||||
public class SalesServiceBus(SalesQueue messages) : IEventBus
|
||||
public sealed class SalesServiceBus(SalesQueue messages) : IEventBus
|
||||
{
|
||||
public async Task<Result> PublishAsync<TEvent>(TEvent notification, CancellationToken cancellationToken = default)
|
||||
where TEvent : class, IEvent
|
||||
|
||||
Reference in New Issue
Block a user