refactor: rename domain types and introduce TransactionBehavior pattern

- 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>
This commit is contained in:
Anatolii Grynchuk
2026-05-02 00:16:47 +03:00
parent 088eab0428
commit 6dcc911fc2
28 changed files with 290 additions and 251 deletions
@@ -2,6 +2,7 @@ namespace HrynCo.NotificationService.DAL.Abstract;
public interface IUnitOfWork public interface IUnitOfWork
{ {
Task SaveChangesAsync(CancellationToken cancellationToken = default);
Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default); Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
ITransaction? GetCurrentTransaction(); ITransaction? GetCurrentTransaction();
@@ -2,14 +2,14 @@ using HrynCo.NotificationService.DAL.Abstract.Entities;
namespace HrynCo.NotificationService.DAL.Abstract.Providers; namespace HrynCo.NotificationService.DAL.Abstract.Providers;
public class Provider : Entity public class EmailChannel : Entity
{ {
public required string ServiceName { get; set; } public required string ServiceName { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
public ProviderType ProviderType { get; set; } public EmailChannelType EmailChannelType { get; set; }
public required ProviderSettings Settings { get; set; } public required EmailChannelSettings Settings { get; set; }
public int? DailyLimit { get; set; } public int? DailyLimit { get; set; }
public int? MonthlyLimit { get; set; } public int? MonthlyLimit { get; set; }
public int WarnThresholdPercent { get; set; } = 90; public int WarnThresholdPercent { get; set; } = 90;
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
} }
@@ -1,13 +1,13 @@
namespace HrynCo.NotificationService.DAL.Abstract.Providers; 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 required string Host { get; set; }
public int Port { get; set; } = 587; public int Port { get; set; } = 587;
@@ -1,6 +1,7 @@
namespace HrynCo.NotificationService.DAL.Abstract.Providers; namespace HrynCo.NotificationService.DAL.Abstract.Providers;
public enum ProviderType public enum EmailChannelType
{ {
Undefined = 0,
Smtp = 1 Smtp = 1
} }
@@ -3,10 +3,10 @@ using HrynCo.NotificationService.DAL.Abstract.Entities;
namespace HrynCo.NotificationService.DAL.Abstract.Providers; namespace HrynCo.NotificationService.DAL.Abstract.Providers;
/// <summary> /// <summary>
/// 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. /// Monthly counts are derived by summing daily records within a month.
/// </summary> /// </summary>
public class ProviderUsage : Entity public class EmailChannelUsage : Entity
{ {
public Guid ProviderId { get; set; } public Guid ProviderId { get; set; }
public DateOnly Date { get; set; } public DateOnly Date { get; set; }
@@ -0,0 +1,12 @@
using HrynCo.NotificationService.DAL.Abstract.Providers;
namespace HrynCo.NotificationService.DAL.Abstract.Repositories;
public interface IEmailChannelRepository
{
Task<IReadOnlyList<EmailChannel>> GetByServiceAsync(string serviceName, CancellationToken ct = default);
Task<EmailChannel?> 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);
}
@@ -2,9 +2,9 @@ using HrynCo.NotificationService.DAL.Abstract.Providers;
namespace HrynCo.NotificationService.DAL.Abstract.Repositories; namespace HrynCo.NotificationService.DAL.Abstract.Repositories;
public interface IProviderUsageRepository public interface IEmailChannelUsageRepository
{ {
Task<int> GetDailyCountAsync(Guid providerId, DateOnly date, CancellationToken ct = default); Task<int> GetDailyCountAsync(Guid providerId, DateOnly date, CancellationToken ct = default);
Task<int> GetMonthlyCountAsync(Guid providerId, int year, int month, CancellationToken ct = default); Task<int> 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);
} }
@@ -0,0 +1,12 @@
using HrynCo.NotificationService.DAL.Abstract.Templates;
namespace HrynCo.NotificationService.DAL.Abstract.Repositories;
public interface IEmailEmailTemplateRepository
{
Task<IReadOnlyList<EmailTemplate>> GetByServiceAsync(string serviceName, CancellationToken ct = default);
Task<EmailTemplate?> 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);
}
@@ -1,12 +0,0 @@
using HrynCo.NotificationService.DAL.Abstract.Providers;
namespace HrynCo.NotificationService.DAL.Abstract.Repositories;
public interface IProviderRepository
{
Task<IReadOnlyList<Provider>> GetByServiceAsync(string serviceName, CancellationToken ct = default);
Task<Provider?> 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);
}
@@ -1,12 +0,0 @@
using HrynCo.NotificationService.DAL.Abstract.Templates;
namespace HrynCo.NotificationService.DAL.Abstract.Repositories;
public interface ITemplateRepository
{
Task<IReadOnlyList<Template>> GetByServiceAsync(string serviceName, CancellationToken ct = default);
Task<Template?> 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);
}
@@ -2,7 +2,7 @@ using HrynCo.NotificationService.DAL.Abstract.Entities;
namespace HrynCo.NotificationService.DAL.Abstract.Templates; namespace HrynCo.NotificationService.DAL.Abstract.Templates;
public class Template : Entity public class EmailTemplate : Entity
{ {
public required string ServiceName { get; set; } public required string ServiceName { get; set; }
public required string Key { 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 Subject { get; set; }
public required string HtmlBody { get; set; } public required string HtmlBody { get; set; }
public required string TextBody { get; set; } public required string TextBody { get; set; }
public IReadOnlyList<TemplateVariable> Variables { get; set; } = []; public IReadOnlyList<EmailTemplateVariable> Variables { get; set; } = [];
} }
@@ -1,6 +1,6 @@
namespace HrynCo.NotificationService.DAL.Abstract.Templates; namespace HrynCo.NotificationService.DAL.Abstract.Templates;
public record TemplateVariable public record EmailTemplateVariable
{ {
public required string Name { get; init; } public required string Name { get; init; }
public bool Required { get; init; } public bool Required { get; init; }
@@ -4,9 +4,9 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace HrynCo.NotificationService.DAL.EF.Configurations; namespace HrynCo.NotificationService.DAL.EF.Configurations;
internal class ProviderEntityConfiguration : IEntityTypeConfiguration<ProviderEntity> internal class EmailChannelEntityConfiguration : IEntityTypeConfiguration<EmailChannelEntity>
{ {
public void Configure(EntityTypeBuilder<ProviderEntity> builder) public void Configure(EntityTypeBuilder<EmailChannelEntity> builder)
{ {
builder.ToTable("providers"); builder.ToTable("providers");
@@ -22,7 +22,7 @@ internal class ProviderEntityConfiguration : IEntityTypeConfiguration<ProviderEn
builder.Property(x => x.Priority).HasColumnName("priority"); builder.Property(x => 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) builder.Property(x => x.SettingsJson)
.HasColumnName("settings") .HasColumnName("settings")
@@ -4,9 +4,9 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace HrynCo.NotificationService.DAL.EF.Configurations; namespace HrynCo.NotificationService.DAL.EF.Configurations;
internal class ProviderUsageEntityConfiguration : IEntityTypeConfiguration<ProviderUsageEntity> internal class EmailChannelUsageEntityConfiguration : IEntityTypeConfiguration<EmailChannelUsageEntity>
{ {
public void Configure(EntityTypeBuilder<ProviderUsageEntity> builder) public void Configure(EntityTypeBuilder<EmailChannelUsageEntity> builder)
{ {
builder.ToTable("provider_usage"); builder.ToTable("provider_usage");
@@ -4,9 +4,9 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace HrynCo.NotificationService.DAL.EF.Configurations; namespace HrynCo.NotificationService.DAL.EF.Configurations;
internal class TemplateEntityConfiguration : IEntityTypeConfiguration<TemplateEntity> internal class EmailEmailTemplateEntityConfiguration : IEntityTypeConfiguration<EmailTemplateEntity>
{ {
public void Configure(EntityTypeBuilder<TemplateEntity> builder) public void Configure(EntityTypeBuilder<EmailTemplateEntity> builder)
{ {
builder.ToTable("templates"); builder.ToTable("templates");
@@ -18,40 +18,28 @@ internal abstract class EfRepository<TEntity>
protected async Task AddAsync(TEntity entity, CancellationToken ct = default) protected async Task AddAsync(TEntity entity, CancellationToken ct = default)
{ {
await DbSet.AddAsync(entity, ct); await DbSet.AddAsync(entity, ct);
await DbContext.SaveChangesAsync(ct);
} }
protected async Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default) protected async Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default)
{ {
await DbSet.AddRangeAsync(entities, ct); 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); DbSet.Update(entity);
await DbContext.SaveChangesAsync(ct);
} }
protected async Task DeleteAsync(TEntity entity, CancellationToken ct = default) protected void Delete(TEntity entity)
{ {
DbSet.Remove(entity); DbSet.Remove(entity);
await DbContext.SaveChangesAsync(ct);
} }
protected async Task DeleteRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default) protected void DeleteRange(IEnumerable<TEntity> entities)
{ {
DbSet.RemoveRange(entities); DbSet.RemoveRange(entities);
await DbContext.SaveChangesAsync(ct);
} }
protected Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default) protected Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default) =>
{ DbSet.AnyAsync(predicate, ct);
return DbSet.AnyAsync(predicate, ct);
}
protected Task SaveAsync(CancellationToken ct = default)
{
return DbContext.SaveChangesAsync(ct);
}
} }
@@ -15,6 +15,11 @@ internal abstract class EfUnitOfWork<TDbContext> : IUnitOfWork
_context = context; _context = context;
} }
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
return _context.SaveChangesAsync(cancellationToken);
}
public async Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default) public async Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)
{ {
if (_currentTransaction != null) if (_currentTransaction != null)
@@ -3,15 +3,15 @@ using HrynCo.NotificationService.DAL.Abstract.Providers;
namespace HrynCo.NotificationService.DAL.EF.Entities; namespace HrynCo.NotificationService.DAL.EF.Entities;
internal class ProviderEntity : Entity internal class EmailChannelEntity : Entity
{ {
public required string ServiceName { get; set; } public required string ServiceName { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
public ProviderType ProviderType { get; set; } public EmailChannelType EmailChannelType { get; set; }
/// <summary> /// <summary>
/// Provider-specific credentials and settings stored as JSONB. /// EmailChannel-specific credentials and settings stored as JSONB.
/// Deserialized based on <see cref="ProviderType"/> in the repository. /// Deserialized based on <see cref="EmailChannelType"/> in the repository.
/// </summary> /// </summary>
public required string SettingsJson { get; set; } public required string SettingsJson { get; set; }
@@ -2,7 +2,7 @@ using HrynCo.NotificationService.DAL.Abstract.Entities;
namespace HrynCo.NotificationService.DAL.EF.Entities; namespace HrynCo.NotificationService.DAL.EF.Entities;
internal class ProviderUsageEntity : Entity internal class EmailChannelUsageEntity : Entity
{ {
public Guid ProviderId { get; set; } public Guid ProviderId { get; set; }
public DateOnly Date { get; set; } public DateOnly Date { get; set; }
@@ -2,7 +2,7 @@ using HrynCo.NotificationService.DAL.Abstract.Entities;
namespace HrynCo.NotificationService.DAL.EF.Entities; namespace HrynCo.NotificationService.DAL.EF.Entities;
internal class TemplateEntity : Entity internal class EmailTemplateEntity : Entity
{ {
public required string ServiceName { get; set; } public required string ServiceName { get; set; }
public required string Key { 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 Subject { get; set; }
public required string HtmlBody { get; set; } public required string HtmlBody { get; set; }
public required string TextBody { get; set; } public required string TextBody { get; set; }
public List<TemplateVariableData> Variables { get; set; } = []; public List<EmailTemplateVariableData> Variables { get; set; } = [];
} }
internal class TemplateVariableData internal class EmailTemplateVariableData
{ {
public required string Name { get; set; } public required string Name { get; set; }
public bool Required { get; set; } public bool Required { get; set; }
@@ -10,9 +10,9 @@ public class NotificationDbContext : DbContext
{ {
} }
internal DbSet<TemplateEntity> Templates => Set<TemplateEntity>(); internal DbSet<EmailTemplateEntity> Templates => Set<EmailTemplateEntity>();
internal DbSet<ProviderEntity> Providers => Set<ProviderEntity>(); internal DbSet<EmailChannelEntity> Providers => Set<EmailChannelEntity>();
internal DbSet<ProviderUsageEntity> ProviderUsage => Set<ProviderUsageEntity>(); internal DbSet<EmailChannelUsageEntity> EmailChannelUsage => Set<EmailChannelUsageEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@@ -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<EmailChannelEntity>, IEmailChannelRepository
{
public EmailChannelRepository(NotificationDbContext dbContext) : base(dbContext)
{
}
public async Task<IReadOnlyList<EmailChannel>> 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<EmailChannel?> 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<SmtpChannelSettings>(json)
?? throw new InvalidOperationException(
"Failed to deserialize SMTP EmailChannel settings."),
_ => throw new InvalidOperationException($"Unknown or undefined email channel type: {type}")
};
}
}
@@ -5,15 +5,15 @@ using Microsoft.EntityFrameworkCore;
namespace HrynCo.NotificationService.DAL.EF.Repositories; namespace HrynCo.NotificationService.DAL.EF.Repositories;
internal sealed class ProviderUsageRepository : EfRepository<ProviderUsageEntity>, IProviderUsageRepository internal sealed class EmailChannelUsageRepository : EfRepository<EmailChannelUsageEntity>, IEmailChannelUsageRepository
{ {
public ProviderUsageRepository(NotificationDbContext dbContext) : base(dbContext) public EmailChannelUsageRepository(NotificationDbContext dbContext) : base(dbContext)
{ {
} }
public async Task<int> GetDailyCountAsync(Guid providerId, DateOnly date, CancellationToken ct = default) public async Task<int> 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); .FirstOrDefaultAsync(x => x.ProviderId == providerId && x.Date == date, ct);
return entity?.SentCount ?? 0; return entity?.SentCount ?? 0;
@@ -28,18 +28,17 @@ internal sealed class ProviderUsageRepository : EfRepository<ProviderUsageEntity
.SumAsync(x => x.SentCount, ct); .SumAsync(x => 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. if (entity is null)
await DbContext.Database.ExecuteSqlAsync( await AddAsync(new EmailChannelUsageEntity { ProviderId = providerId, Date = date, SentCount = 1 }, ct);
$""" else
INSERT INTO provider_usage (id, provider_id, date, sent_count, created) {
VALUES ({Guid.NewGuid()}, {providerId}, {date}, 1, {now}) entity.SentCount++;
ON CONFLICT (provider_id, date) DO UPDATE SET Update(entity);
sent_count = provider_usage.sent_count + 1, }
updated = {now}
""", ct);
} }
} }
@@ -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<EmailTemplateEntity>, IEmailEmailTemplateRepository
{
public EmailTemplateRepository(NotificationDbContext dbContext) : base(dbContext)
{
}
public async Task<IReadOnlyList<EmailTemplate>> GetByServiceAsync(string serviceName, CancellationToken ct = default)
{
List<EmailTemplateEntity> entities = await DbSet
.Where(x => x.ServiceName == serviceName)
.ToListAsync(ct);
return entities.Select(MapToDomain).ToList();
}
public async Task<EmailTemplate?> 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
};
}
@@ -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<ProviderEntity>, IProviderRepository
{
public ProviderRepository(NotificationDbContext dbContext) : base(dbContext)
{
}
public async Task<IReadOnlyList<Provider>> GetByServiceAsync(string serviceName, CancellationToken ct = default)
{
List<ProviderEntity> entities = await DbSet
.Where(x => x.ServiceName == serviceName)
.OrderBy(x => x.Priority)
.ToListAsync(ct);
return entities.Select(MapToDomain).ToList();
}
public async Task<Provider?> 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<SmtpProviderSettings>(json)
?? throw new InvalidOperationException("Failed to deserialize SMTP provider settings."),
_ => throw new InvalidOperationException($"Unknown provider type: {type}")
};
}
@@ -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<TemplateEntity>, ITemplateRepository
{
public TemplateRepository(NotificationDbContext dbContext) : base(dbContext)
{
}
public async Task<IReadOnlyList<Template>> GetByServiceAsync(string serviceName, CancellationToken ct = default)
{
List<TemplateEntity> entities = await DbSet
.Where(x => x.ServiceName == serviceName)
.ToListAsync(ct);
return entities.Select(MapToDomain).ToList();
}
public async Task<Template?> 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
};
}
@@ -0,0 +1,25 @@
using HrynCo.NotificationService.DAL.Abstract;
using MediatR;
namespace HrynCo.NotificationService.Services.Behaviors;
public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IUnitOfWork _unitOfWork;
public TransactionBehavior(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
return _unitOfWork.ExecuteInTransactionAsync(async () =>
{
TResponse result = await next();
await _unitOfWork.SaveChangesAsync(cancellationToken);
return result;
});
}
}
@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="MediatR" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\HrynCo.NotificationService.DAL.Abstract\HrynCo.NotificationService.DAL.Abstract.csproj" /> <ProjectReference Include="..\HrynCo.NotificationService.DAL.Abstract\HrynCo.NotificationService.DAL.Abstract.csproj" />
</ItemGroup> </ItemGroup>