Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 3 additions & 27 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
53 changes: 53 additions & 0 deletions scripts/deploy-gcloud.ps1
Original file line number Diff line number Diff line change
@@ -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"
53 changes: 53 additions & 0 deletions scripts/deploy-gcloud.sh
Original file line number Diff line number Diff line change
@@ -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"
4 changes: 2 additions & 2 deletions src/Application/Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="FluentValidation" Version="12.0.0" />
<PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="FluentValidation" Version="12.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
</ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/Application/Common/Behaviors/AuthorizationBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TRe
{
if (request is not IAuthorizedRequest authorizedRequest)
{
return await next();
return await next(cancellationToken);
}

var requirement = authorizedRequest.AuthorizationRequirement;
Expand Down Expand Up @@ -121,6 +121,6 @@ public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TRe
logger.LogDebug("Authorization passed for user {UserId} to {RequestName}",
currentUser.UserId, typeof(TRequest).Name);

return await next();
return await next(cancellationToken);
}
}
4 changes: 2 additions & 2 deletions src/Application/Common/Behaviors/CachingBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TRe
{
if (request is not ICacheableQuery cacheableQuery)
{
return await next();
return await next(cancellationToken);
}

var cacheKey = cacheableQuery.CacheKey;
Expand All @@ -51,7 +51,7 @@ public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TRe
logger.LogDebug("Cache miss for key: {CacheKey}", cacheKey);

// Execute the request
var response = await next();
var response = await next(cancellationToken);

// Cache the response
var cacheOptions = new MemoryCacheEntryOptions();
Expand Down
22 changes: 17 additions & 5 deletions src/Application/Common/Behaviors/DomainEventBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,31 @@ public sealed class DomainEventBehavior<TRequest, TResponse>(
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> 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<>));

if (isCommand)
{
logger.LogDebug("Command completed, collecting domain events from entities");

// Get all tracked entities with domain events
if (!unitOfWork.HasActiveTransaction)
{
var entitiesBeforeSave = unitOfWork.GetEntitiesWithDomainEvents().ToList();

if (entitiesBeforeSave.Any())
{
await unitOfWork.SaveChangesAsync(cancellationToken);
logger.LogDebug("Changes saved for non-transactional command");
}
}

// At this point, changes are persisted (either by TransactionBehavior or above)
// Now it's safe to dispatch events
var entitiesWithEvents = unitOfWork.GetEntitiesWithDomainEvents().ToList();

if (entitiesWithEvents.Any())
Expand Down
2 changes: 1 addition & 1 deletion src/Application/Common/Behaviors/LoggingBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TRe

try
{
var response = await next();
var response = await next(cancellationToken);

stopwatch.Stop();
logger.LogInformation("Completed request {RequestName} in {ElapsedMilliseconds}ms",
Expand Down
2 changes: 1 addition & 1 deletion src/Application/Common/Behaviors/PerformanceBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TRe
{
var stopwatch = Stopwatch.StartNew();

var response = await next();
var response = await next(cancellationToken);

stopwatch.Stop();

Expand Down
19 changes: 11 additions & 8 deletions src/Application/Common/Behaviors/TransactionBehavior.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Application.Common;
using MediatR;
using Microsoft.Extensions.Logging;

Expand All @@ -16,6 +17,7 @@ public interface ITransactionalCommand
/// <typeparam name="TRequest">Request type</typeparam>
/// <typeparam name="TResponse">Response type</typeparam>
public sealed class TransactionBehavior<TRequest, TResponse>(
IUnitOfWork unitOfWork,
ILogger<TransactionBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
Expand All @@ -24,28 +26,29 @@ public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TRe
{
if (request is not ITransactionalCommand)
{
return await next();
return await next(cancellationToken);
}

logger.LogDebug("Starting transaction for {RequestName}", typeof(TRequest).Name);

// TODO: Implement actual transaction logic when database context is available
// using var transaction = await context.Database.BeginTransactionAsync(cancellationToken);
await unitOfWork.BeginTransactionAsync(cancellationToken);

try
{
var response = await next();
var response = await next(cancellationToken);

// TODO: Commit transaction
// await transaction.CommitAsync(cancellationToken);
// Save changes within transaction
await unitOfWork.SaveChangesAsync(cancellationToken);
logger.LogDebug("Changes saved for {RequestName}", typeof(TRequest).Name);

await unitOfWork.CommitTransactionAsync(cancellationToken);

logger.LogDebug("Transaction committed for {RequestName}", typeof(TRequest).Name);
return response;
}
catch (Exception ex)
{
// TODO: Rollback transaction
// await transaction.RollbackAsync(cancellationToken);
await unitOfWork.RollbackTransactionAsync(cancellationToken);

logger.LogError(ex, "Transaction rolled back for {RequestName}", typeof(TRequest).Name);
throw new InvalidOperationException($"Transaction for {typeof(TRequest).Name} failed", ex);
Expand Down
4 changes: 2 additions & 2 deletions src/Application/Common/Behaviors/ValidationBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TRe
{
if (!validators.Any())
{
return await next();
return await next(cancellationToken);
}

var context = new ValidationContext<TRequest>(request);
Expand All @@ -35,6 +35,6 @@ public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TRe
throw new Exceptions.ValidationException(failures);
}

return await next();
return await next(cancellationToken);
}
}
19 changes: 19 additions & 0 deletions src/Application/Common/DomainEventNotification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Domain.Common;
using MediatR;

namespace Application.Common;

/// <summary>
/// Wrapper to convert domain events to MediatR notifications
/// </summary>
/// <typeparam name="TDomainEvent">Domain event type</typeparam>
public sealed class DomainEventNotification<TDomainEvent> : INotification
where TDomainEvent : IDomainEvent
{
public TDomainEvent DomainEvent { get; }

public DomainEventNotification(TDomainEvent domainEvent)
{
DomainEvent = domainEvent;
}
}
2 changes: 1 addition & 1 deletion src/Application/Common/IDomainEventHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Application.Common;
/// Domain event handler interface
/// </summary>
/// <typeparam name="TDomainEvent">Domain event type</typeparam>
public interface IDomainEventHandler<in TDomainEvent> : INotificationHandler<TDomainEvent>
public interface IDomainEventHandler<TDomainEvent> : INotificationHandler<DomainEventNotification<TDomainEvent>>
where TDomainEvent : IDomainEvent
{
}
22 changes: 21 additions & 1 deletion src/Application/Common/IUnitOfWork.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
namespace Application.Common;

/// <summary>
/// Unit of Work interface for managing domain events and transactions
/// Unit of Work pattern for managing domain events, transactions, and database changes
/// </summary>
public interface IUnitOfWork
{
/// <summary>
/// Check if there's an active transaction
/// </summary>
bool HasActiveTransaction { get; }

/// <summary>
/// Get all entities with domain events from the current context
/// </summary>
Expand All @@ -16,4 +21,19 @@ public interface IUnitOfWork
/// Save changes to the database
/// </summary>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Begin a new database transaction
/// </summary>
Task BeginTransactionAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Commit the current transaction
/// </summary>
Task CommitTransactionAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Rollback the current transaction
/// </summary>
Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
}
Loading