From acc0d44c7c530a801dd4c2982c96432fc18eb8e5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Thu, 7 May 2026 16:08:47 +0200 Subject: [PATCH] Added abstractions Internal service bus support enabled Added quartz support Reconfigured extensions --- LiteCharms.Abstractions/Constants.cs | 10 +++ LiteCharms.Abstractions/EventBusQueueBase.cs | 10 +++ LiteCharms.Abstractions/IEvent.cs | 12 ++++ LiteCharms.Abstractions/IEventBus.cs | 7 ++ LiteCharms.Abstractions/IEventBusQueue.cs | 8 +++ LiteCharms.Abstractions/IJobOrchestrator.cs | 10 +++ .../LiteCharms.Abstractions.csproj | 40 +++++++++++ LiteCharms.Abstractions/Timezones.cs | 27 ++++++++ LiteCharms.Extensions/Email.cs | 13 ++++ LiteCharms.Extensions/HealthChecks.cs | 34 ++++++++++ .../LiteCharms.Extensions.csproj | 33 +++++++--- .../{Services.cs => Monitoring.cs} | 38 +---------- LiteCharms.Extensions/Postgres.cs | 14 ++++ LiteCharms.Extensions/Quartz.cs | 64 ++++++++++++++++++ LiteCharms.Extensions/ServiceBus.cs | 26 ++++++++ .../HealthChecks/QuartzHealthCheck.cs | 23 +++++++ .../LiteCharms.Infrastructure.csproj | 23 ++++++- .../Quartz/JobOrchestrator.cs | 66 +++++++++++++++++++ .../Quartz/MediatorJob.cs | 21 ++++++ .../Quartz/RetryJobListener.cs | 18 +++++ .../ServiceBus/EmailServiceBus.cs | 22 +++++++ .../ServiceBus/Exchanges/EmailExchange.cs | 12 ++++ .../ServiceBus/Exchanges/GeneralExchange.cs | 12 ++++ .../ServiceBus/Exchanges/SalesExchange.cs | 12 ++++ .../ServiceBus/GeneralServiceBus.cs | 22 +++++++ .../ServiceBus/Queues/EmailQueue.cs | 5 ++ .../ServiceBus/Queues/GeneralQueue.cs | 5 ++ .../ServiceBus/Queues/SalesQueue.cs | 5 ++ .../ServiceBus/SalesServiceBus.cs | 22 +++++++ LiteCharms.Models/LiteCharms.Models.csproj | 2 +- LiteCharmsShared.slnx | 1 + 31 files changed, 570 insertions(+), 47 deletions(-) create mode 100644 LiteCharms.Abstractions/Constants.cs create mode 100644 LiteCharms.Abstractions/EventBusQueueBase.cs create mode 100644 LiteCharms.Abstractions/IEvent.cs create mode 100644 LiteCharms.Abstractions/IEventBus.cs create mode 100644 LiteCharms.Abstractions/IEventBusQueue.cs create mode 100644 LiteCharms.Abstractions/IJobOrchestrator.cs create mode 100644 LiteCharms.Abstractions/LiteCharms.Abstractions.csproj create mode 100644 LiteCharms.Abstractions/Timezones.cs create mode 100644 LiteCharms.Extensions/Email.cs create mode 100644 LiteCharms.Extensions/HealthChecks.cs rename LiteCharms.Extensions/{Services.cs => Monitoring.cs} (58%) create mode 100644 LiteCharms.Extensions/Postgres.cs create mode 100644 LiteCharms.Extensions/Quartz.cs create mode 100644 LiteCharms.Extensions/ServiceBus.cs create mode 100644 LiteCharms.Infrastructure/HealthChecks/QuartzHealthCheck.cs create mode 100644 LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs create mode 100644 LiteCharms.Infrastructure/Quartz/MediatorJob.cs create mode 100644 LiteCharms.Infrastructure/Quartz/RetryJobListener.cs create mode 100644 LiteCharms.Infrastructure/ServiceBus/EmailServiceBus.cs create mode 100644 LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs create mode 100644 LiteCharms.Infrastructure/ServiceBus/Exchanges/GeneralExchange.cs create mode 100644 LiteCharms.Infrastructure/ServiceBus/Exchanges/SalesExchange.cs create mode 100644 LiteCharms.Infrastructure/ServiceBus/GeneralServiceBus.cs create mode 100644 LiteCharms.Infrastructure/ServiceBus/Queues/EmailQueue.cs create mode 100644 LiteCharms.Infrastructure/ServiceBus/Queues/GeneralQueue.cs create mode 100644 LiteCharms.Infrastructure/ServiceBus/Queues/SalesQueue.cs create mode 100644 LiteCharms.Infrastructure/ServiceBus/SalesServiceBus.cs diff --git a/LiteCharms.Abstractions/Constants.cs b/LiteCharms.Abstractions/Constants.cs new file mode 100644 index 0000000..6f533d2 --- /dev/null +++ b/LiteCharms.Abstractions/Constants.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Abstractions; + +public static class Constants +{ + public const int QueueBounds = 100000; + + public const string EmailServiceBus = nameof(EmailServiceBus); + public const string GeneralServiceBus = nameof(GeneralServiceBus); + public const string SalesServiceBus = nameof(SalesServiceBus); +} diff --git a/LiteCharms.Abstractions/EventBusQueueBase.cs b/LiteCharms.Abstractions/EventBusQueueBase.cs new file mode 100644 index 0000000..29f7265 --- /dev/null +++ b/LiteCharms.Abstractions/EventBusQueueBase.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Abstractions; + +public abstract class EventBusQueueBase +{ + protected readonly Channel channel = Channel.CreateBounded(Constants.QueueBounds); + + public ChannelWriter Outgoing => channel.Writer; + + public ChannelReader Incoming => channel.Reader; +} diff --git a/LiteCharms.Abstractions/IEvent.cs b/LiteCharms.Abstractions/IEvent.cs new file mode 100644 index 0000000..08bc091 --- /dev/null +++ b/LiteCharms.Abstractions/IEvent.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Abstractions; + +public interface IEvent : INotification +{ + Guid Id { get; set; } + + string Name { get; set; } + + DateTimeOffset EnqueueAt { get; set; } + + string CorrelationId { get; set; } +} diff --git a/LiteCharms.Abstractions/IEventBus.cs b/LiteCharms.Abstractions/IEventBus.cs new file mode 100644 index 0000000..f40d372 --- /dev/null +++ b/LiteCharms.Abstractions/IEventBus.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Abstractions; + +public interface IEventBus +{ + Task PublishAsync(TEvent notification, CancellationToken cancellationToken = default) + where TEvent : class, IEvent; +} diff --git a/LiteCharms.Abstractions/IEventBusQueue.cs b/LiteCharms.Abstractions/IEventBusQueue.cs new file mode 100644 index 0000000..98750b4 --- /dev/null +++ b/LiteCharms.Abstractions/IEventBusQueue.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Abstractions; + +public interface IEventBusQueue +{ + ChannelWriter Outgoing { get; } + + ChannelReader Incoming { get; } +} diff --git a/LiteCharms.Abstractions/IJobOrchestrator.cs b/LiteCharms.Abstractions/IJobOrchestrator.cs new file mode 100644 index 0000000..d98c155 --- /dev/null +++ b/LiteCharms.Abstractions/IJobOrchestrator.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Abstractions; + +public interface IJobOrchestrator +{ + Task SendAsync(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : IEvent; + + Task ScheduleAsync(TNotification notification, string cronExpression, CancellationToken cancellationToken = default) + where TNotification : IEvent; +} diff --git a/LiteCharms.Abstractions/LiteCharms.Abstractions.csproj b/LiteCharms.Abstractions/LiteCharms.Abstractions.csproj new file mode 100644 index 0000000..020ff44 --- /dev/null +++ b/LiteCharms.Abstractions/LiteCharms.Abstractions.csproj @@ -0,0 +1,40 @@ + + + + net10.0 + enable + enable + True + ..\LiteCharms.snk + + + + + LiteCharms.Abstractions + 1.0.0 + Khwezi Mngoma + Lite Charms (PTY) Ltd + Shared abstractions for Lite Charms applications. + https://gitea.khongisa.co.za/litecharms/components + https://gitea.khongisa.co.za/litecharms/components.git + git + LICENSE + utility;dotnet + icon.png + + + + + + + + + + + + + + + + + diff --git a/LiteCharms.Abstractions/Timezones.cs b/LiteCharms.Abstractions/Timezones.cs new file mode 100644 index 0000000..5457130 --- /dev/null +++ b/LiteCharms.Abstractions/Timezones.cs @@ -0,0 +1,27 @@ +namespace LiteCharms.Abstractions; + +public static class Timezones +{ + public static TimeZoneInfo SouthAfricanTimeZone => TimeZoneInfo.FindSystemTimeZoneById("South Africa Standard Time"); + + public static string? LocaliseDateTime(this DateTime dateTime, TimeSpan offset) => offset.Hours > 0 + ? $"{dateTime:yyyy-MM-ddTHH:mm:ss.fff}+{offset.Hours:00}:{offset.Minutes:00}" + : $"{dateTime:yyyy-MM-ddTHH:mm:ss.fff}{offset.Hours:00}:{offset.Minutes:00}"; + + public static string? LocaliseDateTimeOffset(this DateTimeOffset dateTime, TimeSpan offset) => LocaliseDateTime(dateTime.DateTime, offset); + + public static DateTimeOffset ToDateTimeWithTimeZone(this DateTime source, TimeZoneInfo? timezone = null) + { + DateTime sourceDateAdjusted = source.Kind != DateTimeKind.Utc + ? new(source.Ticks, DateTimeKind.Utc) + : source; + + var localised = timezone is null + ? new DateTimeOffset(sourceDateAdjusted.Ticks, SouthAfricanTimeZone.BaseUtcOffset).LocaliseDateTimeOffset(SouthAfricanTimeZone.BaseUtcOffset) + : new DateTimeOffset(sourceDateAdjusted.Ticks, timezone!.BaseUtcOffset).LocaliseDateTimeOffset(timezone.BaseUtcOffset); + + return DateTimeOffset.Parse(localised!); + } + + public static DateTimeOffset UtcNow(this TimeZoneInfo timezone) => ToDateTimeWithTimeZone(DateTime.Now, timezone); +} diff --git a/LiteCharms.Extensions/Email.cs b/LiteCharms.Extensions/Email.cs new file mode 100644 index 0000000..e081f4e --- /dev/null +++ b/LiteCharms.Extensions/Email.cs @@ -0,0 +1,13 @@ +using LiteCharms.Models.Configuraton.Email; + +namespace LiteCharms.Extensions; + +public static class Email +{ + public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("Email")); + + return services; + } +} diff --git a/LiteCharms.Extensions/HealthChecks.cs b/LiteCharms.Extensions/HealthChecks.cs new file mode 100644 index 0000000..01bb1ae --- /dev/null +++ b/LiteCharms.Extensions/HealthChecks.cs @@ -0,0 +1,34 @@ +using LiteCharms.Infrastructure.HealthChecks; + +namespace LiteCharms.Extensions; + +public static class HealthChecks +{ + public static IServiceCollection AddQuartzHealtchCheck(this IServiceCollection services) + { + services.AddHealthChecks().AddCheck("Quartz"); + + return services; + } + + public static IServiceCollection AddPostgresHealtchCheck(this IServiceCollection services) + { + services.AddHealthChecks().AddCheck("PostgreSQL"); + + return services; + } + + public static IServiceCollection AddHealthChecksSupport(this IServiceCollection services, IConfiguration configuration) + { + services.AddHealthChecks() + .AddCheck("Self", () => HealthCheckResult.Healthy()); + + //services.AddHealthChecksUI(setup => + //{ + // setup.AddHealthCheckEndpoint("Lead Generator", $"{configuration["ASPNETCORE_URLS"]}/health"); + // setup.SetEvaluationTimeInSeconds(15); + //}).AddInMemoryStorage(databaseName: "healthuidb"); + + return services; + } +} diff --git a/LiteCharms.Extensions/LiteCharms.Extensions.csproj b/LiteCharms.Extensions/LiteCharms.Extensions.csproj index 6d540f9..0baf9ff 100644 --- a/LiteCharms.Extensions/LiteCharms.Extensions.csproj +++ b/LiteCharms.Extensions/LiteCharms.Extensions.csproj @@ -6,8 +6,19 @@ enable True ..\LiteCharms.snk + true + + + $(NoWarn);MA0004 + + $(NoWarn);AD0001 + true + $(NoWarn);IL2080;IL2065;IL2075;IL2087;IL2057;IL2060;IL2070;IL2067;IL2072;IL2026;IL2104 + $(NoWarn);IL2110;IL2111 + + LiteCharms.Extensions @@ -28,14 +39,6 @@ - - - - - - - - @@ -44,7 +47,8 @@ - + + @@ -58,6 +62,7 @@ + @@ -88,6 +93,16 @@ + + + + + + + + + + diff --git a/LiteCharms.Extensions/Services.cs b/LiteCharms.Extensions/Monitoring.cs similarity index 58% rename from LiteCharms.Extensions/Services.cs rename to LiteCharms.Extensions/Monitoring.cs index 61c9dad..a5b52da 100644 --- a/LiteCharms.Extensions/Services.cs +++ b/LiteCharms.Extensions/Monitoring.cs @@ -1,41 +1,7 @@ -using LiteCharms.Infrastructure.Database; -using LiteCharms.Infrastructure.HealthChecks; -using LiteCharms.Models.Configuraton.Email; +namespace LiteCharms.Extensions; -namespace LiteCharms.Extensions; - -public static class Services +public static class Monitoring { - public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration) - { - services.Configure(configuration.GetSection("Email")); - - return services; - } - - public static IServiceCollection AddHealthChecksSupport(this IServiceCollection services, IConfiguration configuration) - { - services.AddHealthChecks() - .AddCheck("Self", () => HealthCheckResult.Healthy()) - .AddCheck("PostgreSQL"); - - //services.AddHealthChecksUI(setup => - //{ - // setup.AddHealthCheckEndpoint("Lead Generator", $"{configuration["ASPNETCORE_URLS"]}/health"); - // setup.SetEvaluationTimeInSeconds(15); - //}).AddInMemoryStorage(databaseName: "healthuidb"); - - return services; - } - - public static IServiceCollection AddLeadGeneratorDatabase(this IServiceCollection services, IConfiguration configuration) - { - services.AddPooledDbContextFactory(options => - options.UseNpgsql(configuration.GetConnectionString("PostgresLeadGenerator"))); - - return services; - } - public static WebApplicationBuilder AddMonitoring(this WebApplicationBuilder builder) { var serviceName = builder.Configuration.GetValue("Monitoring:ServiceName") ?? "LiteCharms"; diff --git a/LiteCharms.Extensions/Postgres.cs b/LiteCharms.Extensions/Postgres.cs new file mode 100644 index 0000000..d46bdaa --- /dev/null +++ b/LiteCharms.Extensions/Postgres.cs @@ -0,0 +1,14 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Extensions; + +public static class Postgres +{ + public static IServiceCollection AddLeadGeneratorDatabase(this IServiceCollection services, IConfiguration configuration) + { + services.AddPooledDbContextFactory(options => + options.UseNpgsql(configuration.GetConnectionString("PostgresLeadGenerator"))); + + return services; + } +} diff --git a/LiteCharms.Extensions/Quartz.cs b/LiteCharms.Extensions/Quartz.cs new file mode 100644 index 0000000..471f92d --- /dev/null +++ b/LiteCharms.Extensions/Quartz.cs @@ -0,0 +1,64 @@ +using LiteCharms.Abstractions; +using LiteCharms.Infrastructure.Quartz; + +namespace LiteCharms.Extensions; + +public static class Quartz +{ + private const string databaseConfigName = "PostgresScheduler"; + + public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, string schedulerId, IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString(databaseConfigName); + + services.ConfigureCommon(); + + services.AddQuartz(config => + { + config.SchedulerName = schedulerName; + config.SchedulerId = schedulerId; + config.InterruptJobsOnShutdown = true; + config.InterruptJobsOnShutdownWithWait = true; + config.MaxBatchSize = 5; + + config.UseSimpleTypeLoader(); + config.UseDefaultThreadPool(options => options.MaxConcurrency = 1); + config.UseTimeZoneConverter(); + + config.UsePersistentStore(storage => + { + storage.PerformSchemaValidation = false; + + storage.UseSystemTextJsonSerializer(); + storage.SetProperty("quartz.jobStore.clustered", "true"); + storage.SetProperty("quartz.jobStore.tablePrefix", "quartz_"); + + storage.UsePostgres(connectionString!); + storage.UseClustering(cluster => + { + cluster.CheckinInterval = TimeSpan.FromSeconds(30); + cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(2); + }); + }); + }); + + return services; + } + + private static IServiceCollection ConfigureCommon(this IServiceCollection services) + { + services.Configure(options => + { + options.Scheduling.IgnoreDuplicates = true; + options.Scheduling.OverWriteExistingData = true; + options["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins"; + options["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins"; + }); + + services.AddTransient(); + services.AddTransient(); + services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true); + + return services; + } +} diff --git a/LiteCharms.Extensions/ServiceBus.cs b/LiteCharms.Extensions/ServiceBus.cs new file mode 100644 index 0000000..7ffc0c2 --- /dev/null +++ b/LiteCharms.Extensions/ServiceBus.cs @@ -0,0 +1,26 @@ +using LiteCharms.Abstractions; +using LiteCharms.Infrastructure.ServiceBus; +using LiteCharms.Infrastructure.ServiceBus.Exchanges; + +namespace LiteCharms.Extensions; + +public static class ServiceBus +{ + public static IServiceCollection AddGeneralServiceBus(this IServiceCollection services) => services + .AddSingleton() + .AddHostedService() + .AddKeyedTransient(Constants.GeneralServiceBus) + .AddMemoryCache(); + + public static IServiceCollection AddEmailServiceBus(this IServiceCollection services) => services + .AddSingleton() + .AddHostedService() + .AddKeyedTransient(Constants.EmailServiceBus) + .AddMemoryCache(); + + public static IServiceCollection AddSalesServiceBus(this IServiceCollection services) => services + .AddSingleton() + .AddHostedService() + .AddKeyedTransient(Constants.SalesServiceBus) + .AddMemoryCache(); +} diff --git a/LiteCharms.Infrastructure/HealthChecks/QuartzHealthCheck.cs b/LiteCharms.Infrastructure/HealthChecks/QuartzHealthCheck.cs new file mode 100644 index 0000000..d1a8bd0 --- /dev/null +++ b/LiteCharms.Infrastructure/HealthChecks/QuartzHealthCheck.cs @@ -0,0 +1,23 @@ +namespace LiteCharms.Infrastructure.HealthChecks; + +public class QuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck +{ + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var scheduler = await schedulerFactory.GetScheduler(cancellationToken); + + if (!scheduler.IsStarted) + return HealthCheckResult.Unhealthy("Quartz scheduler is not running"); + + await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken); + + return HealthCheckResult.Healthy("Quartz scheduler is ready"); + } + catch (SchedulerException) + { + return HealthCheckResult.Unhealthy("Quartz scheduler cannot connect to the store"); + } + } +} diff --git a/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj b/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj index 9da74f8..4ecd523 100644 --- a/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj +++ b/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj @@ -28,6 +28,19 @@ + + + + + + + + + + + + + @@ -70,10 +83,18 @@ - + + + + + + + + + PreserveNewest diff --git a/LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs b/LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs new file mode 100644 index 0000000..c03f3c6 --- /dev/null +++ b/LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs @@ -0,0 +1,66 @@ +using LiteCharms.Abstractions; +using static LiteCharms.Abstractions.Timezones; + +namespace LiteCharms.Infrastructure.Quartz; + +public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestrator +{ + public async Task SendAsync(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : IEvent + { + var chainedJobGroup = "onetime-jobs"; + + var scheduler = await schedulerFactory.GetScheduler(cancellationToken); + var jobKey = new JobKey($"{notification.Name.ToLower()}-{notification.CorrelationId.ToLower()}", chainedJobGroup); + var triggerKey = new TriggerKey($"{jobKey.Name}-trigger", chainedJobGroup); + + var job = JobBuilder.Create>() + .WithIdentity(jobKey) + .WithDescription($"Correlation ID: {notification.CorrelationId}") + .UsingJobData(new JobDataMap { ["Payload"] = JsonSerializer.Serialize(notification) }) + .DisallowConcurrentExecution() + .Build(); + + var trigger = global::Quartz.TriggerBuilder.Create() + .WithIdentity(triggerKey) + .StartNow() + .Build(); + + await scheduler.ScheduleJob(job, new List { trigger }.AsReadOnly(), replace: true, cancellationToken); + } + + public async Task ScheduleAsync(TNotification notification, string cronExpression, CancellationToken cancellationToken = default) + where TNotification : IEvent + { + var chainedJobGroup = "scheduled-jobs"; + + var scheduler = await schedulerFactory.GetScheduler(cancellationToken); + var jobKey = new JobKey($"{notification.Name.ToLower()}-{notification.CorrelationId.ToLower()}", chainedJobGroup); + var triggerKey = new TriggerKey($"{jobKey.Name}-trigger", chainedJobGroup); + + var job = JobBuilder.Create>() + .WithIdentity(jobKey) + .WithDescription($"Correlation ID: {notification.CorrelationId}") + .UsingJobData(new JobDataMap { ["Payload"] = JsonSerializer.Serialize(notification) }) + .DisallowConcurrentExecution() + .StoreDurably() + .Build(); + + var now = SouthAfricanTimeZone.UtcNow(); + + var trigger = global::Quartz.TriggerBuilder.Create() + .WithIdentity(triggerKey) + .WithDescription($"Scheduled via Main Job at {now:g}") + .WithCronSchedule(cronExpression, cron => cron.InTimeZone(SouthAfricanTimeZone) + .WithMisfireHandlingInstructionFireAndProceed()) + .StartAt(now) + .Build(); + + await scheduler.AddJob(job, replace: true, cancellationToken); + + if (await scheduler.CheckExists(triggerKey, cancellationToken)) + await scheduler.RescheduleJob(triggerKey, trigger, cancellationToken); + else + await scheduler.ScheduleJob(job, new List { trigger }.AsReadOnly(), replace: true, cancellationToken); + } +} diff --git a/LiteCharms.Infrastructure/Quartz/MediatorJob.cs b/LiteCharms.Infrastructure/Quartz/MediatorJob.cs new file mode 100644 index 0000000..8cf9c75 --- /dev/null +++ b/LiteCharms.Infrastructure/Quartz/MediatorJob.cs @@ -0,0 +1,21 @@ +using LiteCharms.Abstractions; + +namespace LiteCharms.Infrastructure.Quartz; + +[DisallowConcurrentExecution] +public class MediatorJob(IMediator mediator) : IJob where TNotification : IEvent +{ + public async Task Execute(IJobExecutionContext context) + { + var data = context.MergedJobDataMap["Payload"] as string; + + if (string.IsNullOrWhiteSpace(data)) return; + + var notification = JsonSerializer.Deserialize(data); + + if(notification is null) return; + + if(notification is TNotification) + await mediator.Publish(notification, context.CancellationToken); + } +} diff --git a/LiteCharms.Infrastructure/Quartz/RetryJobListener.cs b/LiteCharms.Infrastructure/Quartz/RetryJobListener.cs new file mode 100644 index 0000000..afe84e7 --- /dev/null +++ b/LiteCharms.Infrastructure/Quartz/RetryJobListener.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Infrastructure.Quartz; + +public class RetryJobListener : IJobListener +{ + public string Name => "RetryJobListener"; + + public int RetryCount { get; set; } = 3; + + public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default) => Task.CompletedTask; + + public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default) => Task.CompletedTask; + + public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default) + { + if (jobException is not null && context.RefireCount < RetryCount) + jobException.RefireImmediately = true; + } +} diff --git a/LiteCharms.Infrastructure/ServiceBus/EmailServiceBus.cs b/LiteCharms.Infrastructure/ServiceBus/EmailServiceBus.cs new file mode 100644 index 0000000..5205283 --- /dev/null +++ b/LiteCharms.Infrastructure/ServiceBus/EmailServiceBus.cs @@ -0,0 +1,22 @@ +using LiteCharms.Abstractions; +using LiteCharms.Infrastructure.ServiceBus.Queues; + +namespace LiteCharms.Infrastructure.ServiceBus; + +public class EmailServiceBus(EmailQueue messages) : IEventBus +{ + public async Task PublishAsync(TEvent notification, CancellationToken cancellationToken = default) + where TEvent : class, IEvent + { + try + { + await messages.Outgoing.WriteAsync(notification, cancellationToken); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs b/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs new file mode 100644 index 0000000..52928fd --- /dev/null +++ b/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs @@ -0,0 +1,12 @@ +using LiteCharms.Infrastructure.ServiceBus.Queues; + +namespace LiteCharms.Infrastructure.ServiceBus.Exchanges; + +public class EmailExchange(EmailQueue messages) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if(messages.Incoming.CanCount) + await Task.Delay(1000, stoppingToken); + } +} diff --git a/LiteCharms.Infrastructure/ServiceBus/Exchanges/GeneralExchange.cs b/LiteCharms.Infrastructure/ServiceBus/Exchanges/GeneralExchange.cs new file mode 100644 index 0000000..f21d566 --- /dev/null +++ b/LiteCharms.Infrastructure/ServiceBus/Exchanges/GeneralExchange.cs @@ -0,0 +1,12 @@ +using LiteCharms.Infrastructure.ServiceBus.Queues; + +namespace LiteCharms.Infrastructure.ServiceBus.Exchanges; + +public class GeneralExchange(GeneralQueue messages) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (messages.Incoming.CanCount) + await Task.Delay(1000, stoppingToken); + } +} diff --git a/LiteCharms.Infrastructure/ServiceBus/Exchanges/SalesExchange.cs b/LiteCharms.Infrastructure/ServiceBus/Exchanges/SalesExchange.cs new file mode 100644 index 0000000..6288734 --- /dev/null +++ b/LiteCharms.Infrastructure/ServiceBus/Exchanges/SalesExchange.cs @@ -0,0 +1,12 @@ +using LiteCharms.Infrastructure.ServiceBus.Queues; + +namespace LiteCharms.Infrastructure.ServiceBus.Exchanges; + +public class SalesExchange(SalesQueue messages) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (messages.Incoming.CanCount) + await Task.Delay(1000, stoppingToken); + } +} diff --git a/LiteCharms.Infrastructure/ServiceBus/GeneralServiceBus.cs b/LiteCharms.Infrastructure/ServiceBus/GeneralServiceBus.cs new file mode 100644 index 0000000..28d6088 --- /dev/null +++ b/LiteCharms.Infrastructure/ServiceBus/GeneralServiceBus.cs @@ -0,0 +1,22 @@ +using LiteCharms.Abstractions; +using LiteCharms.Infrastructure.ServiceBus.Queues; + +namespace LiteCharms.Infrastructure.ServiceBus; + +public class GeneralServiceBus(GeneralQueue messages) : IEventBus +{ + public async Task PublishAsync(TEvent notification, CancellationToken cancellationToken = default) + where TEvent : class, IEvent + { + try + { + await messages.Outgoing.WriteAsync(notification, cancellationToken); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Infrastructure/ServiceBus/Queues/EmailQueue.cs b/LiteCharms.Infrastructure/ServiceBus/Queues/EmailQueue.cs new file mode 100644 index 0000000..24b161e --- /dev/null +++ b/LiteCharms.Infrastructure/ServiceBus/Queues/EmailQueue.cs @@ -0,0 +1,5 @@ +using LiteCharms.Abstractions; + +namespace LiteCharms.Infrastructure.ServiceBus.Queues; + +public class EmailQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Infrastructure/ServiceBus/Queues/GeneralQueue.cs b/LiteCharms.Infrastructure/ServiceBus/Queues/GeneralQueue.cs new file mode 100644 index 0000000..f7024f6 --- /dev/null +++ b/LiteCharms.Infrastructure/ServiceBus/Queues/GeneralQueue.cs @@ -0,0 +1,5 @@ +using LiteCharms.Abstractions; + +namespace LiteCharms.Infrastructure.ServiceBus.Queues; + +public class GeneralQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Infrastructure/ServiceBus/Queues/SalesQueue.cs b/LiteCharms.Infrastructure/ServiceBus/Queues/SalesQueue.cs new file mode 100644 index 0000000..714ac11 --- /dev/null +++ b/LiteCharms.Infrastructure/ServiceBus/Queues/SalesQueue.cs @@ -0,0 +1,5 @@ +using LiteCharms.Abstractions; + +namespace LiteCharms.Infrastructure.ServiceBus.Queues; + +public class SalesQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Infrastructure/ServiceBus/SalesServiceBus.cs b/LiteCharms.Infrastructure/ServiceBus/SalesServiceBus.cs new file mode 100644 index 0000000..bb3412c --- /dev/null +++ b/LiteCharms.Infrastructure/ServiceBus/SalesServiceBus.cs @@ -0,0 +1,22 @@ +using LiteCharms.Abstractions; +using LiteCharms.Infrastructure.ServiceBus.Queues; + +namespace LiteCharms.Infrastructure.ServiceBus; + +public class SalesServiceBus(SalesQueue messages) : IEventBus +{ + public async Task PublishAsync(TEvent notification, CancellationToken cancellationToken = default) + where TEvent : class, IEvent + { + try + { + await messages.Outgoing.WriteAsync(notification, cancellationToken); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Models/LiteCharms.Models.csproj b/LiteCharms.Models/LiteCharms.Models.csproj index cd19a1f..ad248a6 100644 --- a/LiteCharms.Models/LiteCharms.Models.csproj +++ b/LiteCharms.Models/LiteCharms.Models.csproj @@ -4,8 +4,8 @@ net10.0 enable enable - ..\LiteCharms.snk True + ..\LiteCharms.snk diff --git a/LiteCharmsShared.slnx b/LiteCharmsShared.slnx index 10ae06d..a08c005 100644 --- a/LiteCharmsShared.slnx +++ b/LiteCharmsShared.slnx @@ -1,4 +1,5 @@ +