From 6dcc911fc2ade0b0d1d3e0e7c920f5d2aa96d103 Mon Sep 17 00:00:00 2001 From: Anatolii Grynchuk Date: Sat, 2 May 2026 00:16:47 +0300 Subject: [PATCH] refactor: rename domain types and introduce TransactionBehavior pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename Template -> EmailTemplate, Provider -> EmailChannel, ProviderSettings -> EmailChannelSettings, ProviderType -> EmailChannelType, ProviderUsage -> EmailChannelUsage throughout all layers - Add Undefined = 0 to EmailChannelType enum for safe default handling - Remove SaveChangesAsync from EfRepository methods — repositories now only stage changes - Add SaveChangesAsync to IUnitOfWork and EfUnitOfWork - Add TransactionBehavior MediatR pipeline: wraps every handler in a transaction, saves and commits on success, rolls back on exception - Add MediatR package reference to Services project Ref: IT-628 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../IUnitOfWork.cs | 1 + .../{Provider.cs => EmailChannel.cs} | 8 +- ...derSettings.cs => EmailChannelSettings.cs} | 8 +- .../{ProviderType.cs => EmailChannelType.cs} | 3 +- ...{ProviderUsage.cs => EmailChannelUsage.cs} | 4 +- .../Repositories/IEmailChannelRepository.cs | 12 +++ ...ory.cs => IEmailChannelUsageRepository.cs} | 4 +- .../Repositories/IEmailTemplateRepository.cs | 12 +++ .../Repositories/IProviderRepository.cs | 12 --- .../Repositories/ITemplateRepository.cs | 12 --- .../{Template.cs => EmailTemplate.cs} | 4 +- .../Templates/TemplateVariable.cs | 2 +- ....cs => EmailChannelEntityConfiguration.cs} | 6 +- ...> EmailChannelUsageEntityConfiguration.cs} | 4 +- ...cs => EmailTemplateEntityConfiguration.cs} | 4 +- .../Core/EfRepository.cs | 22 +--- .../Core/EfUnitOfWork.cs | 5 + ...roviderEntity.cs => EmailChannelEntity.cs} | 8 +- ...geEntity.cs => EmailChannelUsageEntity.cs} | 2 +- ...mplateEntity.cs => EmailTemplateEntity.cs} | 6 +- .../NotificationDbContext.cs | 6 +- .../Repositories/EmailChannelRepository.cs | 100 ++++++++++++++++++ ...tory.cs => EmailChannelUsageRepository.cs} | 29 +++-- .../Repositories/EmailTemplateRepository.cs | 77 ++++++++++++++ .../Repositories/ProviderRepository.cs | 85 --------------- .../Repositories/TemplateRepository.cs | 76 ------------- .../Behaviors/TransactionBehavior.cs | 25 +++++ ...HrynCo.NotificationService.Services.csproj | 4 + 28 files changed, 290 insertions(+), 251 deletions(-) rename HrynCo.NotificationService.DAL.Abstract/Providers/{Provider.cs => EmailChannel.cs} (71%) rename HrynCo.NotificationService.DAL.Abstract/Providers/{ProviderSettings.cs => EmailChannelSettings.cs} (67%) rename HrynCo.NotificationService.DAL.Abstract/Providers/{ProviderType.cs => EmailChannelType.cs} (62%) rename HrynCo.NotificationService.DAL.Abstract/Providers/{ProviderUsage.cs => EmailChannelUsage.cs} (76%) create mode 100644 HrynCo.NotificationService.DAL.Abstract/Repositories/IEmailChannelRepository.cs rename HrynCo.NotificationService.DAL.Abstract/Repositories/{IProviderUsageRepository.cs => IEmailChannelUsageRepository.cs} (70%) create mode 100644 HrynCo.NotificationService.DAL.Abstract/Repositories/IEmailTemplateRepository.cs delete mode 100644 HrynCo.NotificationService.DAL.Abstract/Repositories/IProviderRepository.cs delete mode 100644 HrynCo.NotificationService.DAL.Abstract/Repositories/ITemplateRepository.cs rename HrynCo.NotificationService.DAL.Abstract/Templates/{Template.cs => EmailTemplate.cs} (78%) rename HrynCo.NotificationService.DAL.EF/Configurations/{ProviderEntityConfiguration.cs => EmailChannelEntityConfiguration.cs} (83%) rename HrynCo.NotificationService.DAL.EF/Configurations/{ProviderUsageEntityConfiguration.cs => EmailChannelUsageEntityConfiguration.cs} (80%) rename HrynCo.NotificationService.DAL.EF/Configurations/{TemplateEntityConfiguration.cs => EmailTemplateEntityConfiguration.cs} (90%) rename HrynCo.NotificationService.DAL.EF/Entities/{ProviderEntity.cs => EmailChannelEntity.cs} (67%) rename HrynCo.NotificationService.DAL.EF/Entities/{ProviderUsageEntity.cs => EmailChannelUsageEntity.cs} (83%) rename HrynCo.NotificationService.DAL.EF/Entities/{TemplateEntity.cs => EmailTemplateEntity.cs} (76%) create mode 100644 HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelRepository.cs rename HrynCo.NotificationService.DAL.EF/Repositories/{ProviderUsageRepository.cs => EmailChannelUsageRepository.cs} (50%) create mode 100644 HrynCo.NotificationService.DAL.EF/Repositories/EmailTemplateRepository.cs delete mode 100644 HrynCo.NotificationService.DAL.EF/Repositories/ProviderRepository.cs delete mode 100644 HrynCo.NotificationService.DAL.EF/Repositories/TemplateRepository.cs create mode 100644 HrynCo.NotificationService.Services/Behaviors/TransactionBehavior.cs diff --git a/HrynCo.NotificationService.DAL.Abstract/IUnitOfWork.cs b/HrynCo.NotificationService.DAL.Abstract/IUnitOfWork.cs index 50b54a5..3e1bf64 100644 --- a/HrynCo.NotificationService.DAL.Abstract/IUnitOfWork.cs +++ b/HrynCo.NotificationService.DAL.Abstract/IUnitOfWork.cs @@ -2,6 +2,7 @@ namespace HrynCo.NotificationService.DAL.Abstract; public interface IUnitOfWork { + Task SaveChangesAsync(CancellationToken cancellationToken = default); Task BeginTransactionAsync(CancellationToken cancellationToken = default); ITransaction? GetCurrentTransaction(); diff --git a/HrynCo.NotificationService.DAL.Abstract/Providers/Provider.cs b/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannel.cs similarity index 71% rename from HrynCo.NotificationService.DAL.Abstract/Providers/Provider.cs rename to HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannel.cs index a6fae1c..b288734 100644 --- a/HrynCo.NotificationService.DAL.Abstract/Providers/Provider.cs +++ b/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannel.cs @@ -2,14 +2,14 @@ using HrynCo.NotificationService.DAL.Abstract.Entities; namespace HrynCo.NotificationService.DAL.Abstract.Providers; -public class Provider : Entity +public class EmailChannel : Entity { public required string ServiceName { get; set; } public int Priority { get; set; } - public ProviderType ProviderType { get; set; } - public required ProviderSettings Settings { get; set; } + public EmailChannelType EmailChannelType { get; set; } + public required EmailChannelSettings Settings { get; set; } public int? DailyLimit { get; set; } public int? MonthlyLimit { get; set; } public int WarnThresholdPercent { get; set; } = 90; public bool IsActive { get; set; } = true; -} +} \ No newline at end of file diff --git a/HrynCo.NotificationService.DAL.Abstract/Providers/ProviderSettings.cs b/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannelSettings.cs similarity index 67% rename from HrynCo.NotificationService.DAL.Abstract/Providers/ProviderSettings.cs rename to HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannelSettings.cs index 0420950..4cca19b 100644 --- a/HrynCo.NotificationService.DAL.Abstract/Providers/ProviderSettings.cs +++ b/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannelSettings.cs @@ -1,13 +1,13 @@ namespace HrynCo.NotificationService.DAL.Abstract.Providers; -public abstract class ProviderSettings +public abstract class EmailChannelSettings { - public abstract ProviderType ProviderType { get; } + public abstract EmailChannelType EmailChannelType { get; } } -public class SmtpProviderSettings : ProviderSettings +public class SmtpChannelSettings : EmailChannelSettings { - public override ProviderType ProviderType => ProviderType.Smtp; + public override EmailChannelType EmailChannelType => EmailChannelType.Smtp; public required string Host { get; set; } public int Port { get; set; } = 587; diff --git a/HrynCo.NotificationService.DAL.Abstract/Providers/ProviderType.cs b/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannelType.cs similarity index 62% rename from HrynCo.NotificationService.DAL.Abstract/Providers/ProviderType.cs rename to HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannelType.cs index fd7ffc8..cf33f8b 100644 --- a/HrynCo.NotificationService.DAL.Abstract/Providers/ProviderType.cs +++ b/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannelType.cs @@ -1,6 +1,7 @@ namespace HrynCo.NotificationService.DAL.Abstract.Providers; -public enum ProviderType +public enum EmailChannelType { + Undefined = 0, Smtp = 1 } diff --git a/HrynCo.NotificationService.DAL.Abstract/Providers/ProviderUsage.cs b/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannelUsage.cs similarity index 76% rename from HrynCo.NotificationService.DAL.Abstract/Providers/ProviderUsage.cs rename to HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannelUsage.cs index a4dac2a..336f284 100644 --- a/HrynCo.NotificationService.DAL.Abstract/Providers/ProviderUsage.cs +++ b/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannelUsage.cs @@ -3,10 +3,10 @@ using HrynCo.NotificationService.DAL.Abstract.Entities; namespace HrynCo.NotificationService.DAL.Abstract.Providers; /// -/// Tracks email send counts per provider per calendar day. +/// Tracks email send counts per EmailChannel per calendar day. /// Monthly counts are derived by summing daily records within a month. /// -public class ProviderUsage : Entity +public class EmailChannelUsage : Entity { public Guid ProviderId { get; set; } public DateOnly Date { get; set; } diff --git a/HrynCo.NotificationService.DAL.Abstract/Repositories/IEmailChannelRepository.cs b/HrynCo.NotificationService.DAL.Abstract/Repositories/IEmailChannelRepository.cs new file mode 100644 index 0000000..e1bfdc6 --- /dev/null +++ b/HrynCo.NotificationService.DAL.Abstract/Repositories/IEmailChannelRepository.cs @@ -0,0 +1,12 @@ +using HrynCo.NotificationService.DAL.Abstract.Providers; + +namespace HrynCo.NotificationService.DAL.Abstract.Repositories; + +public interface IEmailChannelRepository +{ + Task> GetByServiceAsync(string serviceName, CancellationToken ct = default); + Task GetByIdAsync(Guid id, CancellationToken ct = default); + Task AddAsync(EmailChannel channel, CancellationToken ct = default); + Task UpdateAsync(EmailChannel channel, CancellationToken ct = default); + Task DeleteAsync(EmailChannel channel, CancellationToken ct = default); +} diff --git a/HrynCo.NotificationService.DAL.Abstract/Repositories/IProviderUsageRepository.cs b/HrynCo.NotificationService.DAL.Abstract/Repositories/IEmailChannelUsageRepository.cs similarity index 70% rename from HrynCo.NotificationService.DAL.Abstract/Repositories/IProviderUsageRepository.cs rename to HrynCo.NotificationService.DAL.Abstract/Repositories/IEmailChannelUsageRepository.cs index 4fb6da7..e7c8bdf 100644 --- a/HrynCo.NotificationService.DAL.Abstract/Repositories/IProviderUsageRepository.cs +++ b/HrynCo.NotificationService.DAL.Abstract/Repositories/IEmailChannelUsageRepository.cs @@ -2,9 +2,9 @@ using HrynCo.NotificationService.DAL.Abstract.Providers; namespace HrynCo.NotificationService.DAL.Abstract.Repositories; -public interface IProviderUsageRepository +public interface IEmailChannelUsageRepository { Task GetDailyCountAsync(Guid providerId, DateOnly date, CancellationToken ct = default); Task GetMonthlyCountAsync(Guid providerId, int year, int month, CancellationToken ct = default); - Task IncrementAsync(Guid providerId, DateOnly date, CancellationToken ct = default); + Task IncrementUsageAsync(Guid providerId, DateOnly date, CancellationToken ct = default); } diff --git a/HrynCo.NotificationService.DAL.Abstract/Repositories/IEmailTemplateRepository.cs b/HrynCo.NotificationService.DAL.Abstract/Repositories/IEmailTemplateRepository.cs new file mode 100644 index 0000000..43bf89d --- /dev/null +++ b/HrynCo.NotificationService.DAL.Abstract/Repositories/IEmailTemplateRepository.cs @@ -0,0 +1,12 @@ +using HrynCo.NotificationService.DAL.Abstract.Templates; + +namespace HrynCo.NotificationService.DAL.Abstract.Repositories; + +public interface IEmailEmailTemplateRepository +{ + Task> GetByServiceAsync(string serviceName, CancellationToken ct = default); + Task GetAsync(string serviceName, string key, string languageCode, CancellationToken ct = default); + Task AddAsync(EmailTemplate EmailTemplate, CancellationToken ct = default); + Task UpdateAsync(EmailTemplate EmailTemplate, CancellationToken ct = default); + Task DeleteAsync(EmailTemplate EmailTemplate, CancellationToken ct = default); +} diff --git a/HrynCo.NotificationService.DAL.Abstract/Repositories/IProviderRepository.cs b/HrynCo.NotificationService.DAL.Abstract/Repositories/IProviderRepository.cs deleted file mode 100644 index 49f6bd2..0000000 --- a/HrynCo.NotificationService.DAL.Abstract/Repositories/IProviderRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -using HrynCo.NotificationService.DAL.Abstract.Providers; - -namespace HrynCo.NotificationService.DAL.Abstract.Repositories; - -public interface IProviderRepository -{ - Task> GetByServiceAsync(string serviceName, CancellationToken ct = default); - Task GetByIdAsync(Guid id, CancellationToken ct = default); - Task AddAsync(Provider provider, CancellationToken ct = default); - Task UpdateAsync(Provider provider, CancellationToken ct = default); - Task DeleteAsync(Provider provider, CancellationToken ct = default); -} diff --git a/HrynCo.NotificationService.DAL.Abstract/Repositories/ITemplateRepository.cs b/HrynCo.NotificationService.DAL.Abstract/Repositories/ITemplateRepository.cs deleted file mode 100644 index 0838809..0000000 --- a/HrynCo.NotificationService.DAL.Abstract/Repositories/ITemplateRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -using HrynCo.NotificationService.DAL.Abstract.Templates; - -namespace HrynCo.NotificationService.DAL.Abstract.Repositories; - -public interface ITemplateRepository -{ - Task> GetByServiceAsync(string serviceName, CancellationToken ct = default); - Task GetAsync(string serviceName, string key, string languageCode, CancellationToken ct = default); - Task AddAsync(Template template, CancellationToken ct = default); - Task UpdateAsync(Template template, CancellationToken ct = default); - Task DeleteAsync(Template template, CancellationToken ct = default); -} diff --git a/HrynCo.NotificationService.DAL.Abstract/Templates/Template.cs b/HrynCo.NotificationService.DAL.Abstract/Templates/EmailTemplate.cs similarity index 78% rename from HrynCo.NotificationService.DAL.Abstract/Templates/Template.cs rename to HrynCo.NotificationService.DAL.Abstract/Templates/EmailTemplate.cs index c19b407..d436382 100644 --- a/HrynCo.NotificationService.DAL.Abstract/Templates/Template.cs +++ b/HrynCo.NotificationService.DAL.Abstract/Templates/EmailTemplate.cs @@ -2,7 +2,7 @@ using HrynCo.NotificationService.DAL.Abstract.Entities; namespace HrynCo.NotificationService.DAL.Abstract.Templates; -public class Template : Entity +public class EmailTemplate : Entity { public required string ServiceName { get; set; } public required string Key { get; set; } @@ -10,5 +10,5 @@ public class Template : Entity public required string Subject { get; set; } public required string HtmlBody { get; set; } public required string TextBody { get; set; } - public IReadOnlyList Variables { get; set; } = []; + public IReadOnlyList Variables { get; set; } = []; } diff --git a/HrynCo.NotificationService.DAL.Abstract/Templates/TemplateVariable.cs b/HrynCo.NotificationService.DAL.Abstract/Templates/TemplateVariable.cs index 74c7413..3180ad5 100644 --- a/HrynCo.NotificationService.DAL.Abstract/Templates/TemplateVariable.cs +++ b/HrynCo.NotificationService.DAL.Abstract/Templates/TemplateVariable.cs @@ -1,6 +1,6 @@ namespace HrynCo.NotificationService.DAL.Abstract.Templates; -public record TemplateVariable +public record EmailTemplateVariable { public required string Name { get; init; } public bool Required { get; init; } diff --git a/HrynCo.NotificationService.DAL.EF/Configurations/ProviderEntityConfiguration.cs b/HrynCo.NotificationService.DAL.EF/Configurations/EmailChannelEntityConfiguration.cs similarity index 83% rename from HrynCo.NotificationService.DAL.EF/Configurations/ProviderEntityConfiguration.cs rename to HrynCo.NotificationService.DAL.EF/Configurations/EmailChannelEntityConfiguration.cs index 4d584c9..2fc3d58 100644 --- a/HrynCo.NotificationService.DAL.EF/Configurations/ProviderEntityConfiguration.cs +++ b/HrynCo.NotificationService.DAL.EF/Configurations/EmailChannelEntityConfiguration.cs @@ -4,9 +4,9 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace HrynCo.NotificationService.DAL.EF.Configurations; -internal class ProviderEntityConfiguration : IEntityTypeConfiguration +internal class EmailChannelEntityConfiguration : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { builder.ToTable("providers"); @@ -22,7 +22,7 @@ internal class ProviderEntityConfiguration : IEntityTypeConfiguration x.Priority).HasColumnName("priority"); - builder.Property(x => x.ProviderType).HasColumnName("provider_type"); + builder.Property(x => x.EmailChannelType).HasColumnName("provider_type"); builder.Property(x => x.SettingsJson) .HasColumnName("settings") diff --git a/HrynCo.NotificationService.DAL.EF/Configurations/ProviderUsageEntityConfiguration.cs b/HrynCo.NotificationService.DAL.EF/Configurations/EmailChannelUsageEntityConfiguration.cs similarity index 80% rename from HrynCo.NotificationService.DAL.EF/Configurations/ProviderUsageEntityConfiguration.cs rename to HrynCo.NotificationService.DAL.EF/Configurations/EmailChannelUsageEntityConfiguration.cs index b2ac7ff..9823d93 100644 --- a/HrynCo.NotificationService.DAL.EF/Configurations/ProviderUsageEntityConfiguration.cs +++ b/HrynCo.NotificationService.DAL.EF/Configurations/EmailChannelUsageEntityConfiguration.cs @@ -4,9 +4,9 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace HrynCo.NotificationService.DAL.EF.Configurations; -internal class ProviderUsageEntityConfiguration : IEntityTypeConfiguration +internal class EmailChannelUsageEntityConfiguration : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { builder.ToTable("provider_usage"); diff --git a/HrynCo.NotificationService.DAL.EF/Configurations/TemplateEntityConfiguration.cs b/HrynCo.NotificationService.DAL.EF/Configurations/EmailTemplateEntityConfiguration.cs similarity index 90% rename from HrynCo.NotificationService.DAL.EF/Configurations/TemplateEntityConfiguration.cs rename to HrynCo.NotificationService.DAL.EF/Configurations/EmailTemplateEntityConfiguration.cs index ed49681..efb6fa9 100644 --- a/HrynCo.NotificationService.DAL.EF/Configurations/TemplateEntityConfiguration.cs +++ b/HrynCo.NotificationService.DAL.EF/Configurations/EmailTemplateEntityConfiguration.cs @@ -4,9 +4,9 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace HrynCo.NotificationService.DAL.EF.Configurations; -internal class TemplateEntityConfiguration : IEntityTypeConfiguration +internal class EmailEmailTemplateEntityConfiguration : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { builder.ToTable("templates"); diff --git a/HrynCo.NotificationService.DAL.EF/Core/EfRepository.cs b/HrynCo.NotificationService.DAL.EF/Core/EfRepository.cs index 6d93260..2cd5661 100644 --- a/HrynCo.NotificationService.DAL.EF/Core/EfRepository.cs +++ b/HrynCo.NotificationService.DAL.EF/Core/EfRepository.cs @@ -18,40 +18,28 @@ internal abstract class EfRepository protected async Task AddAsync(TEntity entity, CancellationToken ct = default) { await DbSet.AddAsync(entity, ct); - await DbContext.SaveChangesAsync(ct); } protected async Task AddRangeAsync(IEnumerable entities, CancellationToken ct = default) { await DbSet.AddRangeAsync(entities, ct); - await DbContext.SaveChangesAsync(ct); } - protected async Task UpdateAsync(TEntity entity, CancellationToken ct = default) + protected void Update(TEntity entity) { DbSet.Update(entity); - await DbContext.SaveChangesAsync(ct); } - protected async Task DeleteAsync(TEntity entity, CancellationToken ct = default) + protected void Delete(TEntity entity) { DbSet.Remove(entity); - await DbContext.SaveChangesAsync(ct); } - protected async Task DeleteRangeAsync(IEnumerable entities, CancellationToken ct = default) + protected void DeleteRange(IEnumerable entities) { DbSet.RemoveRange(entities); - await DbContext.SaveChangesAsync(ct); } - protected Task ExistsAsync(Expression> predicate, CancellationToken ct = default) - { - return DbSet.AnyAsync(predicate, ct); - } - - protected Task SaveAsync(CancellationToken ct = default) - { - return DbContext.SaveChangesAsync(ct); - } + protected Task ExistsAsync(Expression> predicate, CancellationToken ct = default) => + DbSet.AnyAsync(predicate, ct); } \ No newline at end of file diff --git a/HrynCo.NotificationService.DAL.EF/Core/EfUnitOfWork.cs b/HrynCo.NotificationService.DAL.EF/Core/EfUnitOfWork.cs index 892b323..0d45441 100644 --- a/HrynCo.NotificationService.DAL.EF/Core/EfUnitOfWork.cs +++ b/HrynCo.NotificationService.DAL.EF/Core/EfUnitOfWork.cs @@ -15,6 +15,11 @@ internal abstract class EfUnitOfWork : IUnitOfWork _context = context; } + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return _context.SaveChangesAsync(cancellationToken); + } + public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) { if (_currentTransaction != null) diff --git a/HrynCo.NotificationService.DAL.EF/Entities/ProviderEntity.cs b/HrynCo.NotificationService.DAL.EF/Entities/EmailChannelEntity.cs similarity index 67% rename from HrynCo.NotificationService.DAL.EF/Entities/ProviderEntity.cs rename to HrynCo.NotificationService.DAL.EF/Entities/EmailChannelEntity.cs index fb6259a..577544b 100644 --- a/HrynCo.NotificationService.DAL.EF/Entities/ProviderEntity.cs +++ b/HrynCo.NotificationService.DAL.EF/Entities/EmailChannelEntity.cs @@ -3,15 +3,15 @@ using HrynCo.NotificationService.DAL.Abstract.Providers; namespace HrynCo.NotificationService.DAL.EF.Entities; -internal class ProviderEntity : Entity +internal class EmailChannelEntity : Entity { public required string ServiceName { get; set; } public int Priority { get; set; } - public ProviderType ProviderType { get; set; } + public EmailChannelType EmailChannelType { get; set; } /// - /// Provider-specific credentials and settings stored as JSONB. - /// Deserialized based on in the repository. + /// EmailChannel-specific credentials and settings stored as JSONB. + /// Deserialized based on in the repository. /// public required string SettingsJson { get; set; } diff --git a/HrynCo.NotificationService.DAL.EF/Entities/ProviderUsageEntity.cs b/HrynCo.NotificationService.DAL.EF/Entities/EmailChannelUsageEntity.cs similarity index 83% rename from HrynCo.NotificationService.DAL.EF/Entities/ProviderUsageEntity.cs rename to HrynCo.NotificationService.DAL.EF/Entities/EmailChannelUsageEntity.cs index 20bb6b9..71fe531 100644 --- a/HrynCo.NotificationService.DAL.EF/Entities/ProviderUsageEntity.cs +++ b/HrynCo.NotificationService.DAL.EF/Entities/EmailChannelUsageEntity.cs @@ -2,7 +2,7 @@ using HrynCo.NotificationService.DAL.Abstract.Entities; namespace HrynCo.NotificationService.DAL.EF.Entities; -internal class ProviderUsageEntity : Entity +internal class EmailChannelUsageEntity : Entity { public Guid ProviderId { get; set; } public DateOnly Date { get; set; } diff --git a/HrynCo.NotificationService.DAL.EF/Entities/TemplateEntity.cs b/HrynCo.NotificationService.DAL.EF/Entities/EmailTemplateEntity.cs similarity index 76% rename from HrynCo.NotificationService.DAL.EF/Entities/TemplateEntity.cs rename to HrynCo.NotificationService.DAL.EF/Entities/EmailTemplateEntity.cs index 844d6a6..f22f3d6 100644 --- a/HrynCo.NotificationService.DAL.EF/Entities/TemplateEntity.cs +++ b/HrynCo.NotificationService.DAL.EF/Entities/EmailTemplateEntity.cs @@ -2,7 +2,7 @@ using HrynCo.NotificationService.DAL.Abstract.Entities; namespace HrynCo.NotificationService.DAL.EF.Entities; -internal class TemplateEntity : Entity +internal class EmailTemplateEntity : Entity { public required string ServiceName { get; set; } public required string Key { get; set; } @@ -10,10 +10,10 @@ internal class TemplateEntity : Entity public required string Subject { get; set; } public required string HtmlBody { get; set; } public required string TextBody { get; set; } - public List Variables { get; set; } = []; + public List Variables { get; set; } = []; } -internal class TemplateVariableData +internal class EmailTemplateVariableData { public required string Name { get; set; } public bool Required { get; set; } diff --git a/HrynCo.NotificationService.DAL.EF/NotificationDbContext.cs b/HrynCo.NotificationService.DAL.EF/NotificationDbContext.cs index 2ff4c34..bcf61e3 100644 --- a/HrynCo.NotificationService.DAL.EF/NotificationDbContext.cs +++ b/HrynCo.NotificationService.DAL.EF/NotificationDbContext.cs @@ -10,9 +10,9 @@ public class NotificationDbContext : DbContext { } - internal DbSet Templates => Set(); - internal DbSet Providers => Set(); - internal DbSet ProviderUsage => Set(); + internal DbSet Templates => Set(); + internal DbSet Providers => Set(); + internal DbSet EmailChannelUsage => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelRepository.cs b/HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelRepository.cs new file mode 100644 index 0000000..1fd914a --- /dev/null +++ b/HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelRepository.cs @@ -0,0 +1,100 @@ +using System.Text.Json; +using HrynCo.NotificationService.DAL.Abstract.Providers; +using HrynCo.NotificationService.DAL.Abstract.Repositories; +using HrynCo.NotificationService.DAL.EF.Core; +using HrynCo.NotificationService.DAL.EF.Entities; +using Microsoft.EntityFrameworkCore; + +namespace HrynCo.NotificationService.DAL.EF.Repositories; + +internal sealed class EmailChannelRepository : EfRepository, IEmailChannelRepository +{ + public EmailChannelRepository(NotificationDbContext dbContext) : base(dbContext) + { + } + + public async Task> GetByServiceAsync(string serviceName, CancellationToken ct = default) + { + var entities = await DbSet + .Where(x => x.ServiceName == serviceName) + .OrderBy(x => x.Priority) + .ToListAsync(ct); + + return entities.Select(MapToDomain).ToList(); + } + + public async Task GetByIdAsync(Guid id, CancellationToken ct = default) + { + EmailChannelEntity? entity = await DbSet.FindAsync([id], ct); + return entity is null ? null : MapToDomain(entity); + } + + public Task AddAsync(EmailChannel channel, CancellationToken ct = default) + { + return base.AddAsync(MapToEntity(channel), ct); + } + + public Task UpdateAsync(EmailChannel channel, CancellationToken ct = default) + { + EmailChannelEntity entity = MapToEntity(channel); + entity.Updated = DateTimeOffset.UtcNow; + Update(entity); + return Task.CompletedTask; + } + + public async Task DeleteAsync(EmailChannel channel, CancellationToken ct = default) + { + EmailChannelEntity? entity = await DbSet.FindAsync([channel.Id], ct); + if (entity is not null) + { + Delete(entity); + } + } + + private static EmailChannel MapToDomain(EmailChannelEntity e) + { + return new EmailChannel + { + Id = e.Id, + ServiceName = e.ServiceName, + Priority = e.Priority, + EmailChannelType = e.EmailChannelType, + Settings = DeserializeSettings(e.EmailChannelType, e.SettingsJson), + DailyLimit = e.DailyLimit, + MonthlyLimit = e.MonthlyLimit, + WarnThresholdPercent = e.WarnThresholdPercent, + IsActive = e.IsActive, + Created = e.Created, + Updated = e.Updated + }; + } + + private static EmailChannelEntity MapToEntity(EmailChannel p) + { + return new EmailChannelEntity + { + Id = p.Id, + ServiceName = p.ServiceName, + Priority = p.Priority, + EmailChannelType = p.EmailChannelType, + SettingsJson = JsonSerializer.Serialize(p.Settings), + DailyLimit = p.DailyLimit, + MonthlyLimit = p.MonthlyLimit, + WarnThresholdPercent = p.WarnThresholdPercent, + IsActive = p.IsActive, + Created = p.Created, + Updated = p.Updated + }; + } + + private static EmailChannelSettings DeserializeSettings(EmailChannelType type, string json) + { + return type switch + { + EmailChannelType.Smtp => JsonSerializer.Deserialize(json) + ?? throw new InvalidOperationException( + "Failed to deserialize SMTP EmailChannel settings."), + _ => throw new InvalidOperationException($"Unknown or undefined email channel type: {type}") + }; + } +} \ No newline at end of file diff --git a/HrynCo.NotificationService.DAL.EF/Repositories/ProviderUsageRepository.cs b/HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelUsageRepository.cs similarity index 50% rename from HrynCo.NotificationService.DAL.EF/Repositories/ProviderUsageRepository.cs rename to HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelUsageRepository.cs index 8f7261d..0a79344 100644 --- a/HrynCo.NotificationService.DAL.EF/Repositories/ProviderUsageRepository.cs +++ b/HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelUsageRepository.cs @@ -5,15 +5,15 @@ using Microsoft.EntityFrameworkCore; namespace HrynCo.NotificationService.DAL.EF.Repositories; -internal sealed class ProviderUsageRepository : EfRepository, IProviderUsageRepository +internal sealed class EmailChannelUsageRepository : EfRepository, IEmailChannelUsageRepository { - public ProviderUsageRepository(NotificationDbContext dbContext) : base(dbContext) + public EmailChannelUsageRepository(NotificationDbContext dbContext) : base(dbContext) { } public async Task GetDailyCountAsync(Guid providerId, DateOnly date, CancellationToken ct = default) { - ProviderUsageEntity? entity = await DbSet + EmailChannelUsageEntity? entity = await DbSet .FirstOrDefaultAsync(x => x.ProviderId == providerId && x.Date == date, ct); return entity?.SentCount ?? 0; @@ -28,18 +28,17 @@ internal sealed class ProviderUsageRepository : EfRepository x.SentCount, ct); } - public async Task IncrementAsync(Guid providerId, DateOnly date, CancellationToken ct = default) + public async Task IncrementUsageAsync(Guid providerId, DateOnly date, CancellationToken ct = default) { - DateTimeOffset now = DateTimeOffset.UtcNow; + EmailChannelUsageEntity? entity = await DbSet + .FirstOrDefaultAsync(x => x.ProviderId == providerId && x.Date == date, ct); - // Atomic upsert: insert with count=1 or increment existing count. - await DbContext.Database.ExecuteSqlAsync( - $""" - INSERT INTO provider_usage (id, provider_id, date, sent_count, created) - VALUES ({Guid.NewGuid()}, {providerId}, {date}, 1, {now}) - ON CONFLICT (provider_id, date) DO UPDATE SET - sent_count = provider_usage.sent_count + 1, - updated = {now} - """, ct); + if (entity is null) + await AddAsync(new EmailChannelUsageEntity { ProviderId = providerId, Date = date, SentCount = 1 }, ct); + else + { + entity.SentCount++; + Update(entity); + } } -} +} \ No newline at end of file diff --git a/HrynCo.NotificationService.DAL.EF/Repositories/EmailTemplateRepository.cs b/HrynCo.NotificationService.DAL.EF/Repositories/EmailTemplateRepository.cs new file mode 100644 index 0000000..81199b7 --- /dev/null +++ b/HrynCo.NotificationService.DAL.EF/Repositories/EmailTemplateRepository.cs @@ -0,0 +1,77 @@ +using HrynCo.NotificationService.DAL.Abstract.Repositories; +using HrynCo.NotificationService.DAL.Abstract.Templates; +using HrynCo.NotificationService.DAL.EF.Core; +using HrynCo.NotificationService.DAL.EF.Entities; +using Microsoft.EntityFrameworkCore; + +namespace HrynCo.NotificationService.DAL.EF.Repositories; + +internal sealed class EmailTemplateRepository : EfRepository, IEmailEmailTemplateRepository +{ + public EmailTemplateRepository(NotificationDbContext dbContext) : base(dbContext) + { + } + + public async Task> GetByServiceAsync(string serviceName, CancellationToken ct = default) + { + List entities = await DbSet + .Where(x => x.ServiceName == serviceName) + .ToListAsync(ct); + + return entities.Select(MapToDomain).ToList(); + } + + public async Task GetAsync(string serviceName, string key, string languageCode, CancellationToken ct = default) + { + EmailTemplateEntity? entity = await DbSet.FirstOrDefaultAsync( + x => x.ServiceName == serviceName && x.Key == key && x.LanguageCode == languageCode, ct); + + return entity is null ? null : MapToDomain(entity); + } + + public Task AddAsync(EmailTemplate EmailTemplate, CancellationToken ct = default) => + base.AddAsync(MapToEntity(EmailTemplate), ct); + + public Task UpdateAsync(EmailTemplate EmailTemplate, CancellationToken ct = default) + { + EmailTemplateEntity entity = MapToEntity(EmailTemplate); + entity.Updated = DateTimeOffset.UtcNow; + Update(entity); + return Task.CompletedTask; + } + + public async Task DeleteAsync(EmailTemplate EmailTemplate, CancellationToken ct = default) + { + EmailTemplateEntity? entity = await DbSet.FindAsync([EmailTemplate.Id], ct); + if (entity is not null) + Delete(entity); + } + + private static EmailTemplate MapToDomain(EmailTemplateEntity e) => new() + { + Id = e.Id, + ServiceName = e.ServiceName, + Key = e.Key, + LanguageCode = e.LanguageCode, + Subject = e.Subject, + HtmlBody = e.HtmlBody, + TextBody = e.TextBody, + Variables = e.Variables.Select(v => new EmailTemplateVariable { Name = v.Name, Required = v.Required }).ToList(), + Created = e.Created, + Updated = e.Updated + }; + + private static EmailTemplateEntity MapToEntity(EmailTemplate t) => new() + { + Id = t.Id, + ServiceName = t.ServiceName, + Key = t.Key, + LanguageCode = t.LanguageCode, + Subject = t.Subject, + HtmlBody = t.HtmlBody, + TextBody = t.TextBody, + Variables = t.Variables.Select(v => new EmailTemplateVariableData { Name = v.Name, Required = v.Required }).ToList(), + Created = t.Created, + Updated = t.Updated + }; +} \ No newline at end of file diff --git a/HrynCo.NotificationService.DAL.EF/Repositories/ProviderRepository.cs b/HrynCo.NotificationService.DAL.EF/Repositories/ProviderRepository.cs deleted file mode 100644 index 597646d..0000000 --- a/HrynCo.NotificationService.DAL.EF/Repositories/ProviderRepository.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Text.Json; -using HrynCo.NotificationService.DAL.Abstract.Providers; -using HrynCo.NotificationService.DAL.Abstract.Repositories; -using HrynCo.NotificationService.DAL.EF.Core; -using HrynCo.NotificationService.DAL.EF.Entities; -using Microsoft.EntityFrameworkCore; - -namespace HrynCo.NotificationService.DAL.EF.Repositories; - -internal sealed class ProviderRepository : EfRepository, IProviderRepository -{ - public ProviderRepository(NotificationDbContext dbContext) : base(dbContext) - { - } - - public async Task> GetByServiceAsync(string serviceName, CancellationToken ct = default) - { - List entities = await DbSet - .Where(x => x.ServiceName == serviceName) - .OrderBy(x => x.Priority) - .ToListAsync(ct); - - return entities.Select(MapToDomain).ToList(); - } - - public async Task GetByIdAsync(Guid id, CancellationToken ct = default) - { - ProviderEntity? entity = await DbSet.FindAsync([id], ct); - return entity is null ? null : MapToDomain(entity); - } - - public Task AddAsync(Provider provider, CancellationToken ct = default) => - base.AddAsync(MapToEntity(provider), ct); - - public Task UpdateAsync(Provider provider, CancellationToken ct = default) - { - ProviderEntity entity = MapToEntity(provider); - entity.Updated = DateTimeOffset.UtcNow; - return base.UpdateAsync(entity, ct); - } - - public async Task DeleteAsync(Provider provider, CancellationToken ct = default) - { - ProviderEntity? entity = await DbSet.FindAsync([provider.Id], ct); - if (entity is not null) - await base.DeleteAsync(entity, ct); - } - - private static Provider MapToDomain(ProviderEntity e) => new() - { - Id = e.Id, - ServiceName = e.ServiceName, - Priority = e.Priority, - ProviderType = e.ProviderType, - Settings = DeserializeSettings(e.ProviderType, e.SettingsJson), - DailyLimit = e.DailyLimit, - MonthlyLimit = e.MonthlyLimit, - WarnThresholdPercent = e.WarnThresholdPercent, - IsActive = e.IsActive, - Created = e.Created, - Updated = e.Updated - }; - - private static ProviderEntity MapToEntity(Provider p) => new() - { - Id = p.Id, - ServiceName = p.ServiceName, - Priority = p.Priority, - ProviderType = p.ProviderType, - SettingsJson = JsonSerializer.Serialize(p.Settings), - DailyLimit = p.DailyLimit, - MonthlyLimit = p.MonthlyLimit, - WarnThresholdPercent = p.WarnThresholdPercent, - IsActive = p.IsActive, - Created = p.Created, - Updated = p.Updated - }; - - private static ProviderSettings DeserializeSettings(ProviderType type, string json) => type switch - { - ProviderType.Smtp => JsonSerializer.Deserialize(json) - ?? throw new InvalidOperationException("Failed to deserialize SMTP provider settings."), - _ => throw new InvalidOperationException($"Unknown provider type: {type}") - }; -} diff --git a/HrynCo.NotificationService.DAL.EF/Repositories/TemplateRepository.cs b/HrynCo.NotificationService.DAL.EF/Repositories/TemplateRepository.cs deleted file mode 100644 index eb33797..0000000 --- a/HrynCo.NotificationService.DAL.EF/Repositories/TemplateRepository.cs +++ /dev/null @@ -1,76 +0,0 @@ -using HrynCo.NotificationService.DAL.Abstract.Repositories; -using HrynCo.NotificationService.DAL.Abstract.Templates; -using HrynCo.NotificationService.DAL.EF.Core; -using HrynCo.NotificationService.DAL.EF.Entities; -using Microsoft.EntityFrameworkCore; - -namespace HrynCo.NotificationService.DAL.EF.Repositories; - -internal sealed class TemplateRepository : EfRepository, ITemplateRepository -{ - public TemplateRepository(NotificationDbContext dbContext) : base(dbContext) - { - } - - public async Task> GetByServiceAsync(string serviceName, CancellationToken ct = default) - { - List entities = await DbSet - .Where(x => x.ServiceName == serviceName) - .ToListAsync(ct); - - return entities.Select(MapToDomain).ToList(); - } - - public async Task GetAsync(string serviceName, string key, string languageCode, CancellationToken ct = default) - { - TemplateEntity? entity = await DbSet.FirstOrDefaultAsync( - x => x.ServiceName == serviceName && x.Key == key && x.LanguageCode == languageCode, ct); - - return entity is null ? null : MapToDomain(entity); - } - - public Task AddAsync(Template template, CancellationToken ct = default) => - base.AddAsync(MapToEntity(template), ct); - - public Task UpdateAsync(Template template, CancellationToken ct = default) - { - TemplateEntity entity = MapToEntity(template); - entity.Updated = DateTimeOffset.UtcNow; - return base.UpdateAsync(entity, ct); - } - - public async Task DeleteAsync(Template template, CancellationToken ct = default) - { - TemplateEntity? entity = await DbSet.FindAsync([template.Id], ct); - if (entity is not null) - await base.DeleteAsync(entity, ct); - } - - private static Template MapToDomain(TemplateEntity e) => new() - { - Id = e.Id, - ServiceName = e.ServiceName, - Key = e.Key, - LanguageCode = e.LanguageCode, - Subject = e.Subject, - HtmlBody = e.HtmlBody, - TextBody = e.TextBody, - Variables = e.Variables.Select(v => new TemplateVariable { Name = v.Name, Required = v.Required }).ToList(), - Created = e.Created, - Updated = e.Updated - }; - - private static TemplateEntity MapToEntity(Template t) => new() - { - Id = t.Id, - ServiceName = t.ServiceName, - Key = t.Key, - LanguageCode = t.LanguageCode, - Subject = t.Subject, - HtmlBody = t.HtmlBody, - TextBody = t.TextBody, - Variables = t.Variables.Select(v => new TemplateVariableData { Name = v.Name, Required = v.Required }).ToList(), - Created = t.Created, - Updated = t.Updated - }; -} diff --git a/HrynCo.NotificationService.Services/Behaviors/TransactionBehavior.cs b/HrynCo.NotificationService.Services/Behaviors/TransactionBehavior.cs new file mode 100644 index 0000000..dcccbba --- /dev/null +++ b/HrynCo.NotificationService.Services/Behaviors/TransactionBehavior.cs @@ -0,0 +1,25 @@ +using HrynCo.NotificationService.DAL.Abstract; +using MediatR; + +namespace HrynCo.NotificationService.Services.Behaviors; + +public class TransactionBehavior : IPipelineBehavior + where TRequest : notnull +{ + private readonly IUnitOfWork _unitOfWork; + + public TransactionBehavior(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + return _unitOfWork.ExecuteInTransactionAsync(async () => + { + TResponse result = await next(); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return result; + }); + } +} diff --git a/HrynCo.NotificationService.Services/HrynCo.NotificationService.Services.csproj b/HrynCo.NotificationService.Services/HrynCo.NotificationService.Services.csproj index 8011d57..e7978bb 100644 --- a/HrynCo.NotificationService.Services/HrynCo.NotificationService.Services.csproj +++ b/HrynCo.NotificationService.Services/HrynCo.NotificationService.Services.csproj @@ -1,5 +1,9 @@  + + + +