Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
39 changes: 39 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: legal-assistant

services:
web-api:
image: ${DOCKER_REGISTRY-}webapi
container_name: web-api
build:
context: .
dockerfile: src/Web.Api/Dockerfile
ports:
- 10000:8080 # HTTP
- 10001:8081 # HTTPS
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
18 changes: 6 additions & 12 deletions src/Infrastructure/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ private static IServiceCollection AddDatabase(
IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("DefaultConnection")
?? "Server=(localdb)\\mssqllocaldb;Database=LegalAssistantDb;Trusted_Connection=true;MultipleActiveResultSets=true";
?? "Host=localhost;Database=legal-assistant;Username=postgres;Password=postgres";

services.AddDbContext<DataContext>(options =>
{
options.UseSqlServer(connectionString, sqlOptions =>
sqlOptions.EnableRetryOnFailure(
options.UseNpgsql(connectionString, npgsqlOptions =>
npgsqlOptions.EnableRetryOnFailure(
maxRetryCount: 3,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null));
errorCodesToAdd: null));

#if DEBUG
options.EnableSensitiveDataLogging();
Expand Down Expand Up @@ -109,16 +109,10 @@ private static IServiceCollection AddExternalServices(
this IServiceCollection services,
IConfiguration configuration)
{
// Email Service
services.Configure<EmailSettings>(configuration.GetSection("EmailSettings"));

// File Storage
services.Configure<FileStorageSettings>(configuration.GetSection("FileStorageSettings"));

// JWT Settings
services.Configure<JwtSettings>(configuration.GetSection("JwtSettings"));

// TODO: Add external service clients
// TODO: Add external service clients when needed
// services.AddHttpClient<IExternalLegalApiClient, ExternalLegalApiClient>();

return services;
Expand Down Expand Up @@ -156,7 +150,7 @@ public static IServiceCollection AddHealthChecks(
var connectionString = configuration.GetConnectionString("DefaultConnection");
if (!string.IsNullOrEmpty(connectionString))
{
healthChecks.AddSqlServer(connectionString, name: "database");
healthChecks.AddNpgSql(connectionString, name: "database");
}

// Redis health check (if configured)
Expand Down
4 changes: 2 additions & 2 deletions src/Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand All @@ -21,7 +21,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Npgsql" Version="8.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="8.0.1" />
</ItemGroup>

Expand Down
197 changes: 197 additions & 0 deletions src/Web.Api/Controllers/V1/HealthController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using System.Reflection;
using System.Security.Cryptography;

namespace Web.Api.Controllers.V1;

/// <summary>
/// Health check controller for system monitoring and testing
/// </summary>
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
public sealed class HealthController : BaseController
{
private readonly ILogger<HealthController> _logger;

public HealthController(ILogger<HealthController> logger)
{
_logger = logger;
}

/// <summary>
/// Basic health check endpoint
/// </summary>
/// <returns>System health status</returns>
[HttpGet]
[ProducesResponseType(typeof(HealthResponse), StatusCodes.Status200OK)]
public IActionResult GetHealth()
{
_logger.LogInformation("Health check requested");

var response = new HealthResponse
{
Status = "Healthy",
Timestamp = DateTime.UtcNow,
Version = GetApplicationVersion(),
Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production",
MachineName = Environment.MachineName,
ProcessId = Environment.ProcessId,
UpTime = GetUpTime()
};

return Ok(response);
}

/// <summary>
/// Detailed system information for testing
/// </summary>
/// <returns>Detailed system information</returns>
[HttpGet("detailed")]
[ProducesResponseType(typeof(DetailedHealthResponse), StatusCodes.Status200OK)]
public IActionResult GetDetailedHealth()
{
_logger.LogInformation("Detailed health check requested");

var response = new DetailedHealthResponse
{
Status = "Healthy",
Timestamp = DateTime.UtcNow,
Version = GetApplicationVersion(),
Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production",
MachineName = Environment.MachineName,
ProcessId = Environment.ProcessId,
UpTime = GetUpTime(),
SystemInfo = new SystemInfo
{
OperatingSystem = Environment.OSVersion.ToString(),
ProcessorCount = Environment.ProcessorCount,
WorkingSet = Environment.WorkingSet,
RuntimeVersion = Environment.Version.ToString(),
CurrentDirectory = Environment.CurrentDirectory
},
Services = new ServicesStatus
{
Database = "Connected", // TODO: Check actual database connection
Cache = "Available", // TODO: Check Redis if configured
ExternalApis = "Online" // TODO: Check external services
}
};

return Ok(response);
}

/// <summary>
/// Simple ping endpoint for quick availability check
/// </summary>
/// <returns>Pong response</returns>
[HttpGet("ping")]
[ProducesResponseType(typeof(PingResponse), StatusCodes.Status200OK)]
public IActionResult Ping()
{
return Ok(new PingResponse
{
Message = "Pong",
Timestamp = DateTime.UtcNow,
RequestId = HttpContext.TraceIdentifier
});
}

/// <summary>
/// Test endpoint that always returns success for testing purposes
/// </summary>
/// <returns>Test success response</returns>
[HttpGet("test")]
[ProducesResponseType(typeof(TestResponse), StatusCodes.Status200OK)]
public IActionResult Test()
{
_logger.LogInformation("Test endpoint called");

return Ok(new TestResponse
{
Success = true,
Message = "Legal Assistant API is working correctly!",
Timestamp = DateTime.UtcNow,
TestData = new
{
RandomNumber = RandomNumberGenerator.GetInt32(1, 1000),
CurrentUser = User?.Identity?.Name ?? "Anonymous",
Headers = Request.Headers.Count,
Request.Method,
Path = Request.Path.Value
}
});
}

private static string GetApplicationVersion()
{
var assembly = Assembly.GetExecutingAssembly();
var version = assembly.GetName().Version;
return version?.ToString() ?? "Unknown";
}

private static TimeSpan GetUpTime()
{
return DateTime.UtcNow - Process.GetCurrentProcess().StartTime.ToUniversalTime();
}
}

#region Response Models

public sealed record HealthResponse
{
public required string Status { get; init; }
public required DateTime Timestamp { get; init; }
public required string Version { get; init; }
public required string Environment { get; init; }
public required string MachineName { get; init; }
public required int ProcessId { get; init; }
public required TimeSpan UpTime { get; init; }
}

public sealed record DetailedHealthResponse
{
public required string Status { get; init; }
public required DateTime Timestamp { get; init; }
public required string Version { get; init; }
public required string Environment { get; init; }
public required string MachineName { get; init; }
public required int ProcessId { get; init; }
public required TimeSpan UpTime { get; init; }
public required SystemInfo SystemInfo { get; init; }
public required ServicesStatus Services { get; init; }
}

public sealed record SystemInfo
{
public required string OperatingSystem { get; init; }
public required int ProcessorCount { get; init; }
public required long WorkingSet { get; init; }
public required string RuntimeVersion { get; init; }
public required string CurrentDirectory { get; init; }
}

public sealed record ServicesStatus
{
public required string Database { get; init; }
public required string Cache { get; init; }
public required string ExternalApis { get; init; }
}

public sealed record PingResponse
{
public required string Message { get; init; }
public required DateTime Timestamp { get; init; }
public required string RequestId { get; init; }
}

public sealed record TestResponse
{
public required bool Success { get; init; }
public required string Message { get; init; }
public required DateTime Timestamp { get; init; }
public required object TestData { get; init; }
}

#endregion
28 changes: 28 additions & 0 deletions src/Web.Api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This Docker file uses the .NET 8 runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Directory.Build.props", "."]
COPY ["src/Web.Api/Web.Api.csproj", "src/Web.Api/"]
COPY ["src/Infrastructure/Infrastructure.csproj", "src/Infrastructure/"]
COPY ["src/Application/Application.csproj", "src/Application/"]
COPY ["src/Domain/Domain.csproj", "src/Domain/"]
RUN dotnet restore "./src/Web.Api/Web.Api.csproj"
COPY . .
WORKDIR "/src/src/Web.Api"
RUN dotnet build "./Web.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Web.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Web.Api.dll"]
13 changes: 11 additions & 2 deletions src/Web.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@
// Add Global Exception Middleware
app.UseMiddleware<GlobalExceptionMiddleware>();

app.UseHttpsRedirection();
// Only use HTTPS redirection in production or when HTTPS is properly configured
if (app.Environment.IsProduction() || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_HTTPS_PORT")))
{
app.UseHttpsRedirection();
}

// Add CORS
app.UseCors("DefaultPolicy");
Expand All @@ -76,9 +80,14 @@
// Map controllers
app.MapControllers();

// Map Health Checks
// Map Health Checks endpoint
// Endpoint /health để monitoring và load balancer kiểm tra application health
// Trả về 200 OK nếu app healthy, 503 Service Unavailable nếu có vấn đề
app.MapHealthChecks("/health");

// Tự động chuyển hướng đến swagger
app.MapGet("/", () => Results.Redirect("/swagger"));

// Ensure database is created
await EnsureDatabaseCreatedAsync(app);

Expand Down
4 changes: 2 additions & 2 deletions src/Web.Api/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5221",
"applicationUrl": "http://localhost:10000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Expand All @@ -24,7 +24,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7260;http://localhost:5221",
"applicationUrl": "https://localhost:10001;http://localhost:10000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Expand Down
3 changes: 3 additions & 0 deletions src/Web.Api/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=legal-assistant;Username=postgres;Password=postgres;Port=5432"
}
}
Loading