diff --git a/docker-compose.yml b/docker-compose.yml index fbcd972..2c153f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,32 +8,8 @@ services: context: . dockerfile: src/Web.Api/Dockerfile ports: - - 10000:8080 # HTTP - - 10001:8081 # HTTPS + - 10000:10000 # HTTP environment: - ASPNETCORE_ENVIRONMENT=Development - - ConnectionStrings__DefaultConnection=Host=postgres;Database=legal-assistant;Username=postgres;Password=postgres;Port=5432 - depends_on: - - postgres - - postgres: - image: postgres:17 - container_name: postgres - environment: - - POSTGRES_DB=legal-assistant - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres - - TZ=UTC - - PGTZ=UTC - volumes: - - ./.containers/db:/var/lib/postgresql/data - ports: - - 5432:5432 - - # seq: - # image: datalust/seq:2024.3 - # container_name: seq - # environment: - # - ACCEPT_EULA=Y - # ports: - # - 8081:80 + - ASPNETCORE_URLS=http://+:10000 + # Using remote DB from appsettings.json (69.164.244.36:5555) \ No newline at end of file diff --git a/scripts/deploy-gcloud.ps1 b/scripts/deploy-gcloud.ps1 new file mode 100644 index 0000000..4bbf929 --- /dev/null +++ b/scripts/deploy-gcloud.ps1 @@ -0,0 +1,53 @@ +# Deploy Docker image to Google Cloud Artifact Registry +# Project: lucky-union-472503-c7 +# Region: asia-southeast1 +# Repository: backendnetcore + +# Variables +$PROJECT_ID = "lucky-union-472503-c7" +$REGION = "asia-southeast1" +$REPOSITORY = "backendnetcore" +$IMAGE_NAME = "webapi" +$VERSION = "1.0.0" +$LOCAL_IMAGE = "webapi:latest" +$REMOTE_IMAGE = "$REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/$IMAGE_NAME`:$VERSION" + +Write-Host "=== Google Cloud Artifact Registry Deployment ===" -ForegroundColor Cyan +Write-Host "Project ID: $PROJECT_ID" +Write-Host "Region: $REGION" +Write-Host "Repository: $REPOSITORY" +Write-Host "Image: $IMAGE_NAME`:$VERSION" +Write-Host "" + +# Step 1: Configure Docker authentication for GCP (SKIP repository check - it's slow) +Write-Host "Step 1: Configuring Docker authentication..." -ForegroundColor Yellow +gcloud auth configure-docker "$REGION-docker.pkg.dev" --quiet +Write-Host "" + +# Step 2: Build Docker image locally (if needed) +Write-Host "Step 2: Building Docker image..." -ForegroundColor Yellow +# docker-compose build +docker-compose build +Write-Host "" + +# Step 3: Tag the image +Write-Host "Step 3: Tagging image..." -ForegroundColor Yellow +Write-Host "From: $LOCAL_IMAGE" +Write-Host "To: $REMOTE_IMAGE" +docker tag $LOCAL_IMAGE $REMOTE_IMAGE +Write-Host "" + +# Step 4: Push to Google Artifact Registry +Write-Host "Step 4: Pushing image to Google Cloud..." -ForegroundColor Yellow +docker push $REMOTE_IMAGE +Write-Host "" + +Write-Host "=== Deployment Complete ===" -ForegroundColor Green +Write-Host "Image pushed: $REMOTE_IMAGE" +Write-Host "" +Write-Host "To deploy to Cloud Run:" -ForegroundColor Cyan +Write-Host "gcloud run deploy legal-assistant-api \" +Write-Host " --image $REMOTE_IMAGE \" +Write-Host " --platform managed \" +Write-Host " --region $REGION \" +Write-Host " --allow-unauthenticated" diff --git a/scripts/deploy-gcloud.sh b/scripts/deploy-gcloud.sh new file mode 100644 index 0000000..acfb4ac --- /dev/null +++ b/scripts/deploy-gcloud.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# Deploy Docker image to Google Cloud Artifact Registry +# Project: lucky-union-472503-c7 +# Region: asia-southeast1 +# Repository: backendnetcore + +# Variables +PROJECT_ID="lucky-union-472503-c7" +REGION="asia-southeast1" +REPOSITORY="backendnetcore" +IMAGE_NAME="webapi" +VERSION="1.0.0" +LOCAL_IMAGE="webapi:latest" +REMOTE_IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE_NAME}:${VERSION}" + +echo "=== Google Cloud Artifact Registry Deployment ===" +echo "Project ID: ${PROJECT_ID}" +echo "Region: ${REGION}" +echo "Repository: ${REPOSITORY}" +echo "Image: ${IMAGE_NAME}:${VERSION}" +echo "" + +# Step 1: Configure Docker authentication for GCP (SKIP repository check - it's slow) +echo "Step 1: Configuring Docker authentication..." +gcloud auth configure-docker ${REGION}-docker.pkg.dev --quiet +echo "" + +# Step 2: Build Docker image locally (if needed) +echo "Step 2: Building Docker image..." +docker-compose build +echo "" + +# Step 3: Tag the image +echo "Step 3: Tagging image..." +echo "From: ${LOCAL_IMAGE}" +echo "To: ${REMOTE_IMAGE}" +docker tag ${LOCAL_IMAGE} ${REMOTE_IMAGE} +echo "" + +# Step 4: Push to Google Artifact Registry +echo "Step 4: Pushing image to Google Cloud..." +docker push ${REMOTE_IMAGE} +echo "" + +echo "=== Deployment Complete ===" +echo "Image pushed: ${REMOTE_IMAGE}" +echo "" +echo "To deploy to Cloud Run:" +echo "gcloud run deploy legal-assistant-api \\" +echo " --image ${REMOTE_IMAGE} \\" +echo " --platform managed \\" +echo " --region ${REGION} \\" +echo " --allow-unauthenticated" diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index a5a0eeb..4650bbb 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/Application/Common/Behaviors/AuthorizationBehavior.cs b/src/Application/Common/Behaviors/AuthorizationBehavior.cs index bf5a539..83fbe23 100644 --- a/src/Application/Common/Behaviors/AuthorizationBehavior.cs +++ b/src/Application/Common/Behaviors/AuthorizationBehavior.cs @@ -77,7 +77,7 @@ public async Task Handle(TRequest request, RequestHandlerDelegate Handle(TRequest request, RequestHandlerDelegate Handle(TRequest request, RequestHandlerDelegate Handle(TRequest request, RequestHandlerDelegate( { public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { - // Execute the command first - var response = await next(); + // Execute the next behavior (TransactionBehavior or Handler) + // For ITransactionalCommand: TransactionBehavior will handle SaveChanges + Commit + // For normal commands: Handler executes directly, we need to save + var response = await next(cancellationToken); - // Only dispatch events for commands, not queries - // Check if request implements ICommand<> (generic command interface) var isCommand = request.GetType().GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommand<>)); @@ -30,7 +30,19 @@ public async Task Handle(TRequest request, RequestHandlerDelegate Handle(TRequest request, RequestHandlerDelegate Handle(TRequest request, RequestHandlerDelegateRequest type /// Response type public sealed class TransactionBehavior( + IUnitOfWork unitOfWork, ILogger> logger) : IPipelineBehavior where TRequest : notnull @@ -24,28 +26,29 @@ public async Task Handle(TRequest request, RequestHandlerDelegate Handle(TRequest request, RequestHandlerDelegate(request); @@ -35,6 +35,6 @@ public async Task Handle(TRequest request, RequestHandlerDelegate +/// Wrapper to convert domain events to MediatR notifications +/// +/// Domain event type +public sealed class DomainEventNotification : INotification + where TDomainEvent : IDomainEvent +{ + public TDomainEvent DomainEvent { get; } + + public DomainEventNotification(TDomainEvent domainEvent) + { + DomainEvent = domainEvent; + } +} diff --git a/src/Application/Common/IDomainEventHandler.cs b/src/Application/Common/IDomainEventHandler.cs index b10af39..8bda293 100644 --- a/src/Application/Common/IDomainEventHandler.cs +++ b/src/Application/Common/IDomainEventHandler.cs @@ -7,7 +7,7 @@ namespace Application.Common; /// Domain event handler interface /// /// Domain event type -public interface IDomainEventHandler : INotificationHandler +public interface IDomainEventHandler : INotificationHandler> where TDomainEvent : IDomainEvent { } diff --git a/src/Application/Common/IUnitOfWork.cs b/src/Application/Common/IUnitOfWork.cs index b621b2b..899b503 100644 --- a/src/Application/Common/IUnitOfWork.cs +++ b/src/Application/Common/IUnitOfWork.cs @@ -3,10 +3,15 @@ namespace Application.Common; /// -/// Unit of Work interface for managing domain events and transactions +/// Unit of Work pattern for managing domain events, transactions, and database changes /// public interface IUnitOfWork { + /// + /// Check if there's an active transaction + /// + bool HasActiveTransaction { get; } + /// /// Get all entities with domain events from the current context /// @@ -16,4 +21,19 @@ public interface IUnitOfWork /// Save changes to the database /// Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// Begin a new database transaction + /// + Task BeginTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Commit the current transaction + /// + Task CommitTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Rollback the current transaction + /// + Task RollbackTransactionAsync(CancellationToken cancellationToken = default); } diff --git a/src/Application/EventHandlers/EmailVerifiedEventHandler.cs b/src/Application/EventHandlers/EmailVerifiedEventHandler.cs index cd916fa..ada9572 100644 --- a/src/Application/EventHandlers/EmailVerifiedEventHandler.cs +++ b/src/Application/EventHandlers/EmailVerifiedEventHandler.cs @@ -13,29 +13,31 @@ public sealed class EmailVerifiedEventHandler( ILogger logger) : IDomainEventHandler { - public async Task Handle(EmailVerifiedEvent notification, CancellationToken cancellationToken) + public async Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) { + var @event = notification.DomainEvent; + logger.LogInformation( "Sending welcome email to verified user {UserId} at {Email}", - notification.UserId, - notification.Email); + @event.UserId, + @event.Email); try { await emailService.SendWelcomeEmailAsync( - notification.Email, - notification.FullName, + @event.Email, + @event.FullName, cancellationToken); logger.LogInformation( "Welcome email sent successfully to {Email}", - notification.Email); + @event.Email); } catch (Exception ex) { logger.LogError(ex, "Failed to send welcome email to {Email}", - notification.Email); + @event.Email); } } } diff --git a/src/Application/EventHandlers/UserCreatedEventHandler.cs b/src/Application/EventHandlers/UserCreatedEventHandler.cs index d7ff131..6bd9eb4 100644 --- a/src/Application/EventHandlers/UserCreatedEventHandler.cs +++ b/src/Application/EventHandlers/UserCreatedEventHandler.cs @@ -14,44 +14,46 @@ public sealed class UserCreatedEventHandler( ILogger logger) : IDomainEventHandler { - public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken) + public async Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) { + var @event = notification.DomainEvent; + // If user registered with social provider (Google, etc.), email is already verified // Skip sending verification email - if (notification.IsEmailVerified) + if (@event.IsEmailVerified) { logger.LogInformation( "User {UserId} registered with verified email (social provider). Skipping verification email.", - notification.UserId); + @event.UserId); return; } logger.LogInformation( "Sending email verification to user {UserId} at {Email}", - notification.UserId, - notification.Email); + @event.UserId, + @event.Email); try { // Generate email confirmation token using Identity - var token = await tokenService.GenerateEmailConfirmationTokenAsync(notification.UserId); - var verificationLink = $"http://localhost:10000/api/v1/auth/verify-email?userId={notification.UserId}&token={Uri.EscapeDataString(token)}"; + var token = await tokenService.GenerateEmailConfirmationTokenAsync(@event.UserId); + var verificationLink = $"http://localhost:10000/api/v1/auth/verify-email?userId={@event.UserId}&token={Uri.EscapeDataString(token)}"; await emailService.SendEmailVerificationAsync( - notification.Email, - notification.FullName, + @event.Email, + @event.FullName, verificationLink, cancellationToken); logger.LogInformation( "Verification email sent successfully to {Email}", - notification.Email); + @event.Email); } catch (Exception ex) { logger.LogError(ex, "Failed to send verification email to {Email}", - notification.Email); + @event.Email); // Don't throw - we don't want email failures to break the registration flow } } diff --git a/src/Application/Features/Auth/Register/RegisterCommand.cs b/src/Application/Features/Auth/Register/RegisterCommand.cs index efeab0a..3ca87dc 100644 --- a/src/Application/Features/Auth/Register/RegisterCommand.cs +++ b/src/Application/Features/Auth/Register/RegisterCommand.cs @@ -1,12 +1,14 @@ using Application.Common; +using Application.Common.Behaviors; using Domain.Common; namespace Application.Features.Auth.Register; /// /// Command to register a new user +/// Requires transaction because it creates both Domain user and Identity user /// -public sealed record RegisterCommand : ICommand> +public sealed record RegisterCommand : ICommand>, ITransactionalCommand { /// /// User's email address diff --git a/src/Domain/Common/IDomainEvent.cs b/src/Domain/Common/IDomainEvent.cs index f47633e..acda71b 100644 --- a/src/Domain/Common/IDomainEvent.cs +++ b/src/Domain/Common/IDomainEvent.cs @@ -1,11 +1,9 @@ -using MediatR; - namespace Domain.Common; /// /// Marker interface for domain events /// -public interface IDomainEvent : INotification +public interface IDomainEvent { /// /// When the event occurred diff --git a/src/Domain/Domain.csproj b/src/Domain/Domain.csproj index a19c932..fa71b7a 100644 --- a/src/Domain/Domain.csproj +++ b/src/Domain/Domain.csproj @@ -6,8 +6,4 @@ enable - - - - diff --git a/src/Infrastructure/Data/Contexts/DataContext.Configuration.cs b/src/Infrastructure/Data/Contexts/DataContext.Configuration.cs new file mode 100644 index 0000000..4401ffc --- /dev/null +++ b/src/Infrastructure/Data/Contexts/DataContext.Configuration.cs @@ -0,0 +1,43 @@ +using Infrastructure.Identity; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace Infrastructure.Data.Contexts; + +/// +/// DataContext partial class for Identity configuration +/// +public sealed partial class DataContext +{ + private void ConfigureIdentityTables(ModelBuilder builder) + { + builder.Entity(entity => + { + entity.ToTable("Users", Schemas.Identity); + + entity.Property(u => u.FullName) + .IsRequired() + .HasMaxLength(200); + + entity.Property(u => u.IsDeleted) + .IsRequired() + .HasDefaultValue(false); + + entity.Property(u => u.CreatedAt) + .IsRequired(); + + entity.Property(u => u.UpdatedAt) + .IsRequired(); + + // Query filter for soft delete + entity.HasQueryFilter(u => !u.IsDeleted); + }); + + builder.Entity(entity => entity.ToTable("Roles", Schemas.Identity)); + builder.Entity>(entity => entity.ToTable("UserRoles", Schemas.Identity)); + builder.Entity>(entity => entity.ToTable("UserClaims", Schemas.Identity)); + builder.Entity>(entity => entity.ToTable("UserLogins", Schemas.Identity)); + builder.Entity>(entity => entity.ToTable("RoleClaims", Schemas.Identity)); + builder.Entity>(entity => entity.ToTable("UserTokens", Schemas.Identity)); + } +} diff --git a/src/Infrastructure/Data/Contexts/DataContext.UnitOfWork.cs b/src/Infrastructure/Data/Contexts/DataContext.UnitOfWork.cs new file mode 100644 index 0000000..2bf24fe --- /dev/null +++ b/src/Infrastructure/Data/Contexts/DataContext.UnitOfWork.cs @@ -0,0 +1,82 @@ +using Domain.Common; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Infrastructure.Data.Contexts; + +/// +/// DataContext partial class for IUnitOfWork implementation +/// +public sealed partial class DataContext +{ + // IUnitOfWork - Property + public bool HasActiveTransaction => _currentTransaction != null; + + // IUnitOfWork - Domain Events + public IEnumerable GetEntitiesWithDomainEvents() + { + return ChangeTracker + .Entries() + .Where(e => e.Entity.DomainEvents.Any()) + .Select(e => e.Entity) + .ToList(); + } + + // IUnitOfWork - Transactions + public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) + { + if (_currentTransaction != null) + { + return; // Already in transaction + } + + _currentTransaction = await Database.BeginTransactionAsync(cancellationToken); + } + + public async Task CommitTransactionAsync(CancellationToken cancellationToken = default) + { + if (_currentTransaction == null) + { + throw new InvalidOperationException("No active transaction to commit"); + } + + try + { + await SaveChangesAsync(cancellationToken); + await _currentTransaction.CommitAsync(cancellationToken); + } + catch + { + await RollbackTransactionAsync(cancellationToken); + throw; + } + finally + { + if (_currentTransaction != null) + { + await _currentTransaction.DisposeAsync(); + _currentTransaction = null; + } + } + } + + public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) + { + if (_currentTransaction == null) + { + return; // No transaction to rollback + } + + try + { + await _currentTransaction.RollbackAsync(cancellationToken); + } + finally + { + if (_currentTransaction != null) + { + await _currentTransaction.DisposeAsync(); + _currentTransaction = null; + } + } + } +} diff --git a/src/Infrastructure/Data/Contexts/DataContext.cs b/src/Infrastructure/Data/Contexts/DataContext.cs index 08b994b..038a863 100644 --- a/src/Infrastructure/Data/Contexts/DataContext.cs +++ b/src/Infrastructure/Data/Contexts/DataContext.cs @@ -1,17 +1,17 @@ using Application.Common; -using Domain.Common; using Domain.Entities; using Infrastructure.Identity; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; namespace Infrastructure.Data.Contexts; /// /// Database context cho Legal Assistant application with Identity integration /// -public sealed class DataContext : IdentityDbContext< +public sealed partial class DataContext : IdentityDbContext< ApplicationUser, ApplicationRole, Guid, @@ -19,8 +19,10 @@ public sealed class DataContext : IdentityDbContext< IdentityUserRole, IdentityUserLogin, IdentityRoleClaim, - IdentityUserToken>, IDataContext, IUnitOfWork + IdentityUserToken>, IUnitOfWork { + private IDbContextTransaction? _currentTransaction; + public DataContext(DbContextOptions options) : base(options) { } @@ -50,46 +52,4 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) optionsBuilder.EnableDetailedErrors(); #endif } - - private void ConfigureIdentityTables(ModelBuilder builder) - { - builder.Entity(entity => - { - entity.ToTable("Users", Schemas.Identity); - - entity.Property(u => u.FullName) - .IsRequired() - .HasMaxLength(200); - - entity.Property(u => u.IsDeleted) - .IsRequired() - .HasDefaultValue(false); - - entity.Property(u => u.CreatedAt) - .IsRequired(); - - entity.Property(u => u.UpdatedAt) - .IsRequired(); - - // Query filter for soft delete - entity.HasQueryFilter(u => !u.IsDeleted); - }); - - builder.Entity(entity => entity.ToTable("Roles", Schemas.Identity)); - builder.Entity>(entity => entity.ToTable("UserRoles", Schemas.Identity)); - builder.Entity>(entity => entity.ToTable("UserClaims", Schemas.Identity)); - builder.Entity>(entity => entity.ToTable("UserLogins", Schemas.Identity)); - builder.Entity>(entity => entity.ToTable("RoleClaims", Schemas.Identity)); - builder.Entity>(entity => entity.ToTable("UserTokens", Schemas.Identity)); - } - - // IUnitOfWork implementation - public IEnumerable GetEntitiesWithDomainEvents() - { - return ChangeTracker - .Entries() - .Where(e => e.Entity.DomainEvents.Any()) - .Select(e => e.Entity) - .ToList(); - } } diff --git a/src/Infrastructure/Data/Contexts/IDataContext.cs b/src/Infrastructure/Data/Contexts/IDataContext.cs deleted file mode 100644 index 1285cfc..0000000 --- a/src/Infrastructure/Data/Contexts/IDataContext.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Infrastructure.Data.Contexts; - -/// -/// Interface cho Legal Assistant Database Context -/// -public interface IDataContext -{ - /// - /// Lưu các thay đổi vào database - /// - Task SaveChangesAsync(CancellationToken cancellationToken = default); - - /// - /// Lưu các thay đổi vào database (synchronous) - /// - int SaveChanges(); - - /// - /// Dispose context - /// - ValueTask DisposeAsync(); -} diff --git a/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs index c56364b..99b09a6 100644 --- a/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs +++ b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -67,8 +67,7 @@ private static IServiceCollection AddDatabase( #endif }); - // Register as interface - services.AddScoped(provider => provider.GetRequiredService()); + // Register DataContext as IUnitOfWork services.AddScoped(provider => provider.GetRequiredService()); return services; diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index ca9462c..363c974 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -18,10 +18,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + diff --git a/src/Infrastructure/Repositories/UserRepository.cs b/src/Infrastructure/Repositories/UserRepository.cs index 0886270..68fc84a 100644 --- a/src/Infrastructure/Repositories/UserRepository.cs +++ b/src/Infrastructure/Repositories/UserRepository.cs @@ -40,21 +40,22 @@ public UserRepository(DataContext context, UserManager userMana } /// - /// Update user in database + /// Update user - marks entity as modified in ChangeTracker + /// SaveChanges will be called by UnitOfWork (in CommandHandler or TransactionBehavior) /// - public async Task UpdateAsync(User user, CancellationToken cancellationToken = default) + public Task UpdateAsync(User user, CancellationToken cancellationToken = default) { _context.DomainUsers.Update(user); - await _context.SaveChangesAsync(cancellationToken); + return Task.CompletedTask; } /// - /// Create new user in database + /// Create new user - adds entity to ChangeTracker + /// SaveChanges will be called by UnitOfWork (in CommandHandler or TransactionBehavior) /// public async Task CreateAsync(User user, CancellationToken cancellationToken = default) { var entry = await _context.DomainUsers.AddAsync(user, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); return entry.Entity; } diff --git a/src/Infrastructure/Services/DomainEventDispatcher.cs b/src/Infrastructure/Services/DomainEventDispatcher.cs index 75fdf9f..99cbf4e 100644 --- a/src/Infrastructure/Services/DomainEventDispatcher.cs +++ b/src/Infrastructure/Services/DomainEventDispatcher.cs @@ -39,7 +39,14 @@ public async Task DispatchEventAsync(IDomainEvent domainEvent, CancellationToken { logger.LogDebug("Dispatching domain event: {EventType}", domainEvent.GetType().Name); - await mediator.Publish(domainEvent, cancellationToken); + // Wrap domain event in notification wrapper for MediatR + var notificationType = typeof(DomainEventNotification<>).MakeGenericType(domainEvent.GetType()); + var notification = Activator.CreateInstance(notificationType, domainEvent); + + if (notification is INotification mediatRNotification) + { + await mediator.Publish(mediatRNotification, cancellationToken); + } logger.LogDebug("Successfully dispatched domain event: {EventType}", domainEvent.GetType().Name); } diff --git a/src/Web.Api/Extensions/ApplicationServiceExtensions.cs b/src/Web.Api/Extensions/ApplicationServiceExtensions.cs index 62b9b7d..175a621 100644 --- a/src/Web.Api/Extensions/ApplicationServiceExtensions.cs +++ b/src/Web.Api/Extensions/ApplicationServiceExtensions.cs @@ -31,8 +31,8 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection services.AddScoped(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehavior<,>)); services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); services.AddScoped(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>)); - services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>)); services.AddScoped(typeof(IPipelineBehavior<,>), typeof(DomainEventBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>)); // Add Application services services.AddHttpContextAccessor(); diff --git a/src/Web.Api/Web.Api.csproj b/src/Web.Api/Web.Api.csproj index 29cf7be..dc4dfc6 100644 --- a/src/Web.Api/Web.Api.csproj +++ b/src/Web.Api/Web.Api.csproj @@ -8,11 +8,11 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - +