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
-
+