diff --git a/src/Blogger.Application/Comments/MakeComment/MakeCommentCommandHandler.cs b/src/Blogger.Application/Comments/MakeComment/MakeCommentCommandHandler.cs index 1003a9b..98b950f 100644 --- a/src/Blogger.Application/Comments/MakeComment/MakeCommentCommandHandler.cs +++ b/src/Blogger.Application/Comments/MakeComment/MakeCommentCommandHandler.cs @@ -28,7 +28,8 @@ public async Task Handle(MakeCommentCommand request, comment.RaiseMakeCommentEvent(); await _commentRepository.CreateAsync(comment, cancellationToken); - await _commentRepository.SaveChangesAsync(cancellationToken); + + await _commentRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); diff --git a/src/Blogger.BuildingBlocks/Domain/IRepository.cs b/src/Blogger.BuildingBlocks/Domain/IRepository.cs new file mode 100644 index 0000000..9c8d44e --- /dev/null +++ b/src/Blogger.BuildingBlocks/Domain/IRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blogger.BuildingBlocks.Domain +{ + public interface IRepository where T : IAggregateRoot + { + IUnitOfWork UnitOfWork { get; } + } +} diff --git a/src/Blogger.BuildingBlocks/Domain/IUnitOfWork.cs b/src/Blogger.BuildingBlocks/Domain/IUnitOfWork.cs new file mode 100644 index 0000000..c60b689 --- /dev/null +++ b/src/Blogger.BuildingBlocks/Domain/IUnitOfWork.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blogger.BuildingBlocks.Domain +{ + public interface IUnitOfWork: IDisposable + { + Task SaveEntitiesAsync(CancellationToken cancellationToken = default); + } +} diff --git a/src/Blogger.Domain/ArticleAggregate/IArticleRepository.cs b/src/Blogger.Domain/ArticleAggregate/IArticleRepository.cs index 89fdbb2..95f234c 100644 --- a/src/Blogger.Domain/ArticleAggregate/IArticleRepository.cs +++ b/src/Blogger.Domain/ArticleAggregate/IArticleRepository.cs @@ -2,7 +2,7 @@ namespace Blogger.Domain.ArticleAggregate; -public interface IArticleRepository +public interface IArticleRepository:IRepository
{ Task HasIdAsync(ArticleId articleId, CancellationToken cancellationToken); diff --git a/src/Blogger.Domain/CommentAggregate/ICommentRepository.cs b/src/Blogger.Domain/CommentAggregate/ICommentRepository.cs index 99c3aa4..706f2c1 100644 --- a/src/Blogger.Domain/CommentAggregate/ICommentRepository.cs +++ b/src/Blogger.Domain/CommentAggregate/ICommentRepository.cs @@ -2,7 +2,7 @@ using Blogger.Domain.ArticleAggregate; namespace Blogger.Domain.CommentAggregate; -public interface ICommentRepository +public interface ICommentRepository:IRepository { Task GetCommentByApproveLinkAsync(string link, CancellationToken cancellationToken); Task> GetApprovedArticleCommentsAsync(ArticleId articleId, CancellationToken cancellationToken); diff --git a/src/Blogger.Domain/SubscriberAggregate/ISubscriberRepository.cs b/src/Blogger.Domain/SubscriberAggregate/ISubscriberRepository.cs index 2e57c25..d7d8a46 100644 --- a/src/Blogger.Domain/SubscriberAggregate/ISubscriberRepository.cs +++ b/src/Blogger.Domain/SubscriberAggregate/ISubscriberRepository.cs @@ -1,7 +1,7 @@  namespace Blogger.Domain.SubscriberAggregate; -public interface ISubscriberRepository +public interface ISubscriberRepository:IRepository { Task CreateAsync(Subscriber subscriber, CancellationToken cancellationToken); Task FindByIdAsync(SubscriberId subscriberId); diff --git a/src/Blogger.Infrastructure/DependencyInjection.cs b/src/Blogger.Infrastructure/DependencyInjection.cs index 749efe6..a044185 100644 --- a/src/Blogger.Infrastructure/DependencyInjection.cs +++ b/src/Blogger.Infrastructure/DependencyInjection.cs @@ -7,6 +7,13 @@ public static IServiceCollection ConfigureInfrastructureLayer(this IServiceColle { services.Configure(configuration.GetSection(nameof(EmailSettings))); + var application = typeof(IAssemblyMarker); + + services.AddMediatR(configure => + { + configure.RegisterServicesFromAssembly(application.Assembly); + }); + services.AddDbContext(options => { options.UseSqlServer(configuration.GetConnectionString(BloggerDbContextSchema.DefaultConnectionStringName)); diff --git a/src/Blogger.Infrastructure/Persistence/BloggerDbContext.cs b/src/Blogger.Infrastructure/Persistence/BloggerDbContext.cs index d19f02f..5a07d3e 100644 --- a/src/Blogger.Infrastructure/Persistence/BloggerDbContext.cs +++ b/src/Blogger.Infrastructure/Persistence/BloggerDbContext.cs @@ -1,26 +1,63 @@ -using Blogger.Domain.ArticleAggregate; -using Blogger.Domain.CommentAggregate; -using Blogger.Domain.SubscriberAggregate; -using Microsoft.EntityFrameworkCore; + +using System.Data; +using Blogger.BuildingBlocks.Domain; +using MediatR; +using Microsoft.EntityFrameworkCore.Storage; namespace Blogger.Infrastructure.Persistence; -public class BloggerDbContext : DbContext +public class BloggerDbContext : DbContext, IUnitOfWork { + private readonly IMediator? _mediator; + private IDbContextTransaction? _currentTransaction; + public BloggerDbContext(DbContextOptions dbContextOptions) - : base(dbContextOptions) + : base(dbContextOptions) { } + + public BloggerDbContext(DbContextOptions dbContextOptions, IMediator mediator/*, IDbContextTransaction dbContextTransaction*/) : base(dbContextOptions) { - + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + } + public IDbContextTransaction GetCurrentTransaction() => _currentTransaction!; + public bool HasActiveTransaction => _currentTransaction != null; + public DbSet
Articles => Set
(); public DbSet Comments => Set(); public DbSet Subscribers => Set(); + public async Task SaveEntitiesAsync(CancellationToken cancellationToken = default) + { + await _mediator!.DispatcherEventAsync(this); + await base.SaveChangesAsync(cancellationToken); + return true; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) + { + optionsBuilder.UseSqlServer("data source=.;initial catalog=HealthDB;TrustServerCertificate=True;Trusted_Connection=True;"); + } + + base.OnConfiguring(optionsBuilder); + + } + + public async Task BeginTransactionAsync() + { + if (_currentTransaction != null) return null; + + _currentTransaction = await Database.BeginTransactionAsync(IsolationLevel.ReadCommitted); + + return _currentTransaction; + } + protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema(BloggerDbContextSchema.DefaultSchema); var infrastructureAssembly = typeof(IAssemblyMarker).Assembly; - modelBuilder.ApplyConfigurationsFromAssembly(infrastructureAssembly); + modelBuilder.ApplyConfigurationsFromAssembly(infrastructureAssembly); } } diff --git a/src/Blogger.Infrastructure/Persistence/BloggerDbContextFactory.cs b/src/Blogger.Infrastructure/Persistence/BloggerDbContextFactory.cs index 1efa4df..59fd620 100644 --- a/src/Blogger.Infrastructure/Persistence/BloggerDbContextFactory.cs +++ b/src/Blogger.Infrastructure/Persistence/BloggerDbContextFactory.cs @@ -3,11 +3,15 @@ namespace Blogger.Infrastructure.Persistence; public class BloggerDbContextFactory : IDesignTimeDbContextFactory { - public BloggerDbContext CreateDbContext(string[] args) + public BloggerDbContextFactory(IServiceProvider serviceProvider) { - var optionBuilder = new DbContextOptionsBuilder(); - optionBuilder.UseSqlServer("data source=.;initial catalog=thisisnabi.blogger;TrustServerCertificate=True;Trusted_Connection=True;"); + ServiceProvider = serviceProvider; + } + + public IServiceProvider ServiceProvider { get; } - return new BloggerDbContext(optionBuilder.Options); + public BloggerDbContext CreateDbContext(string[] args) + { + return ServiceProvider.GetRequiredService(); } } diff --git a/src/Blogger.Infrastructure/Persistence/MediatorExtension.cs b/src/Blogger.Infrastructure/Persistence/MediatorExtension.cs new file mode 100644 index 0000000..9423d2b --- /dev/null +++ b/src/Blogger.Infrastructure/Persistence/MediatorExtension.cs @@ -0,0 +1,28 @@ + + +using System.Security.Cryptography; + +using Blogger.BuildingBlocks.Domain; +using MediatR; + +namespace Blogger.Infrastructure.Persistence; + +public static class MediatorExtension +{ + public static async Task DispatcherEventAsync(this IMediator mediator, BloggerDbContext bloggerDbContext) + { + var domainEntities = bloggerDbContext.ChangeTracker + .Entries>() + .Where(x => x.Entity.Events != null && x.Entity.Events.Count != 0); + + var domainEvents = domainEntities + .SelectMany(x => x.Entity.Events) + .ToList(); + + domainEntities.ToList() + .ForEach(entity => entity.Entity.ClearEvents()); + + foreach (var domainEvent in domainEvents) + await mediator.Publish(domainEvent); + } +} diff --git a/src/Blogger.Infrastructure/Persistence/Repositories/ArticleRepository.cs b/src/Blogger.Infrastructure/Persistence/Repositories/ArticleRepository.cs index 1c43bb2..31a29e2 100644 --- a/src/Blogger.Infrastructure/Persistence/Repositories/ArticleRepository.cs +++ b/src/Blogger.Infrastructure/Persistence/Repositories/ArticleRepository.cs @@ -1,11 +1,14 @@ using System.Linq; +using Blogger.BuildingBlocks.Domain; using Blogger.Domain.ArticleAggregate.Models; namespace Blogger.Infrastructure.Persistence.Repositories; public class ArticleRepository(BloggerDbContext bloggerDbContext) : IArticleRepository { + public IUnitOfWork UnitOfWork => bloggerDbContext; + public Task HasIdAsync(ArticleId articleId, CancellationToken cancellationToken) => bloggerDbContext.Articles.AnyAsync(x => x.Id == articleId, cancellationToken); diff --git a/src/Blogger.Infrastructure/Persistence/Repositories/CommentRepository.cs b/src/Blogger.Infrastructure/Persistence/Repositories/CommentRepository.cs index 5ac2454..5e051e2 100644 --- a/src/Blogger.Infrastructure/Persistence/Repositories/CommentRepository.cs +++ b/src/Blogger.Infrastructure/Persistence/Repositories/CommentRepository.cs @@ -1,7 +1,11 @@ - namespace Blogger.Infrastructure.Persistence.Repositories; +using Blogger.BuildingBlocks.Domain; + +namespace Blogger.Infrastructure.Persistence.Repositories; public class CommentRepository(BloggerDbContext bloggerDbContext) : ICommentRepository { + public IUnitOfWork UnitOfWork => bloggerDbContext; + public Task GetCommentByApproveLinkAsync(string link, CancellationToken cancellationToken) { return bloggerDbContext.Comments.FirstOrDefaultAsync(x => x.ApproveLink.ApproveId == link, cancellationToken); diff --git a/src/Blogger.Infrastructure/Persistence/Repositories/SubscriberRepository.cs b/src/Blogger.Infrastructure/Persistence/Repositories/SubscriberRepository.cs index 0ca396b..13a1fbf 100644 --- a/src/Blogger.Infrastructure/Persistence/Repositories/SubscriberRepository.cs +++ b/src/Blogger.Infrastructure/Persistence/Repositories/SubscriberRepository.cs @@ -1,7 +1,9 @@ -namespace Blogger.Infrastructure.Persistence.Repositories; +using Blogger.BuildingBlocks.Domain; + +namespace Blogger.Infrastructure.Persistence.Repositories; public class SubscriberRepository(BloggerDbContext bloggerDbContext) : ISubscriberRepository { - + public IUnitOfWork UnitOfWork => bloggerDbContext; public async Task CreateAsync(Subscriber subscriber, CancellationToken cancellationToken) { await bloggerDbContext.Subscribers.AddAsync(subscriber, cancellationToken); diff --git a/tests/Blogger.IntegrationTests/Fixtures/BloggerDbContextFixture.cs b/tests/Blogger.IntegrationTests/Fixtures/BloggerDbContextFixture.cs index 3fec6bf..e86c861 100644 --- a/tests/Blogger.IntegrationTests/Fixtures/BloggerDbContextFixture.cs +++ b/tests/Blogger.IntegrationTests/Fixtures/BloggerDbContextFixture.cs @@ -1,13 +1,21 @@ using Blogger.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; namespace Blogger.IntegrationTests.Fixtures; public class BloggerDbContextFixture : EfDatabaseBaseFixture { + public BloggerDbContextFixture(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public IServiceProvider ServiceProvider { get; } + protected override BloggerDbContext BuildDbContext(DbContextOptions options) { - return new BloggerDbContext(options); + return ServiceProvider.GetRequiredService(); } } \ No newline at end of file