diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..f1edd71
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,82 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# All files
+[*]
+indent_style = space
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+# C# files
+[*.cs]
+# Organize usings
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = false
+
+# this. preferences
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# var preferences
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
+
+# Expression-level preferences
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_prefer_auto_properties = true:suggestion
+
+# Null-checking preferences
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+
+# New line preferences
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+
+# Indentation preferences
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true
+
+# Naming conventions
+dotnet_naming_rule.private_fields_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.private_fields_should_be_camel_case.symbols = private_fields
+dotnet_naming_rule.private_fields_should_be_camel_case.style = camel_case_underscore_style
+
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private
+
+dotnet_naming_style.camel_case_underscore_style.required_prefix = _
+dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
+
+# JSON files
+[*.json]
+indent_size = 2
+
+# XML files
+[*.{xml,csproj,props,targets}]
+indent_size = 2
+
+# YAML files
+[*.{yml,yaml}]
+indent_size = 2
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..caa2195
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,50 @@
+# Build artifacts
+bin/
+obj/
+
+# IDE
+.vs/
+.vscode/
+*.user
+*.suo
+*.userosscache
+*.sln.docstates
+
+# NuGet
+*.nupkg
+**/packages/*
+!**/packages/build/
+
+# Test Results
+TestResults/
+*.trx
+
+# Rider
+.idea/
+*.sln.iml
+
+# macOS
+.DS_Store
+
+# Windows
+Thumbs.db
+ehthumbs.db
+Desktop.ini
+
+# Logs
+*.log
+logs/
+
+# Environment files
+.env
+.env.local
+.env.*.local
+appsettings.*.local.json
+
+# Database
+*.mdf
+*.ldf
+*.db
+
+# Published output
+publish/
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..7135ddc
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,10 @@
+
+
+ net9.0
+ enable
+ enable
+ false
+ true
+ $(NoWarn);1591
+
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..e10dba4
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,30 @@
+# Build stage
+FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
+WORKDIR /src
+
+# Copy csproj files and restore
+COPY ["iYHCT360.Domain/iYHCT360.Domain.csproj", "iYHCT360.Domain/"]
+COPY ["iYHCT360.Contracts/iYHCT360.Contracts.csproj", "iYHCT360.Contracts/"]
+COPY ["iYHCT360.Application/iYHCT360.Application.csproj", "iYHCT360.Application/"]
+COPY ["iYHCT360.Infrastructure/iYHCT360.Infrastructure.csproj", "iYHCT360.Infrastructure/"]
+COPY ["iYHCT360.WebAPI/iYHCT360.WebAPI.csproj", "iYHCT360.WebAPI/"]
+
+RUN dotnet restore "iYHCT360.WebAPI/iYHCT360.WebAPI.csproj"
+
+# Copy everything else and build
+COPY . .
+WORKDIR "/src/iYHCT360.WebAPI"
+RUN dotnet build "iYHCT360.WebAPI.csproj" -c Release -o /app/build
+
+# Publish stage
+FROM build AS publish
+RUN dotnet publish "iYHCT360.WebAPI.csproj" -c Release -o /app/publish /p:UseAppHost=false
+
+# Final stage
+FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "iYHCT360.WebAPI.dll"]
diff --git a/Dockerfile.admin b/Dockerfile.admin
new file mode 100644
index 0000000..e4d0a3c
--- /dev/null
+++ b/Dockerfile.admin
@@ -0,0 +1,30 @@
+# Build stage
+FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
+WORKDIR /src
+
+# Copy csproj files and restore
+COPY ["iYHCT360.Domain/iYHCT360.Domain.csproj", "iYHCT360.Domain/"]
+COPY ["iYHCT360.Contracts/iYHCT360.Contracts.csproj", "iYHCT360.Contracts/"]
+COPY ["iYHCT360.Application/iYHCT360.Application.csproj", "iYHCT360.Application/"]
+COPY ["iYHCT360.Infrastructure/iYHCT360.Infrastructure.csproj", "iYHCT360.Infrastructure/"]
+COPY ["iYHCT360.AdminAPI/iYHCT360.AdminAPI.csproj", "iYHCT360.AdminAPI/"]
+
+RUN dotnet restore "iYHCT360.AdminAPI/iYHCT360.AdminAPI.csproj"
+
+# Copy everything else and build
+COPY . .
+WORKDIR "/src/iYHCT360.AdminAPI"
+RUN dotnet build "iYHCT360.AdminAPI.csproj" -c Release -o /app/build
+
+# Publish stage
+FROM build AS publish
+RUN dotnet publish "iYHCT360.AdminAPI.csproj" -c Release -o /app/publish /p:UseAppHost=false
+
+# Final stage
+FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "iYHCT360.AdminAPI.dll"]
diff --git a/MyNewProjectName.AdminAPI/Controllers/BaseApiController.cs b/MyNewProjectName.AdminAPI/Controllers/BaseApiController.cs
new file mode 100644
index 0000000..c8e6188
--- /dev/null
+++ b/MyNewProjectName.AdminAPI/Controllers/BaseApiController.cs
@@ -0,0 +1,16 @@
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace MyNewProjectName.AdminAPI.Controllers;
+
+///
+/// Base API controller with common functionality
+///
+[ApiController]
+[Route("api/admin/[controller]")]
+public abstract class BaseApiController : ControllerBase
+{
+ private ISender? _mediator;
+
+ protected ISender Mediator => _mediator ??= HttpContext.RequestServices.GetRequiredService();
+}
diff --git a/MyNewProjectName.AdminAPI/Middleware/CorrelationIdMiddleware.cs b/MyNewProjectName.AdminAPI/Middleware/CorrelationIdMiddleware.cs
new file mode 100644
index 0000000..abcd391
--- /dev/null
+++ b/MyNewProjectName.AdminAPI/Middleware/CorrelationIdMiddleware.cs
@@ -0,0 +1,36 @@
+using Serilog.Context;
+
+namespace MyNewProjectName.AdminAPI.Middleware;
+
+///
+/// Middleware to generate and track correlation ID for each request
+/// This ID is added to HTTP headers and Serilog log context for easy tracing
+///
+public class CorrelationIdMiddleware
+{
+ private readonly RequestDelegate _next;
+ private const string CorrelationIdHeaderName = "X-Correlation-ID";
+ private const string CorrelationIdLogPropertyName = "CorrelationId";
+
+ public CorrelationIdMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ // Get correlation ID from request header, or generate a new one
+ var correlationId = context.Request.Headers[CorrelationIdHeaderName].FirstOrDefault()
+ ?? $"req-{Guid.NewGuid():N}";
+
+ // Add correlation ID to response header
+ context.Response.Headers[CorrelationIdHeaderName] = correlationId;
+
+ // Add correlation ID to Serilog log context
+ // All logs within this request will automatically include this correlation ID
+ using (LogContext.PushProperty(CorrelationIdLogPropertyName, correlationId))
+ {
+ await _next(context);
+ }
+ }
+}
diff --git a/MyNewProjectName.AdminAPI/Middleware/ExceptionHandlingMiddleware.cs b/MyNewProjectName.AdminAPI/Middleware/ExceptionHandlingMiddleware.cs
new file mode 100644
index 0000000..6757069
--- /dev/null
+++ b/MyNewProjectName.AdminAPI/Middleware/ExceptionHandlingMiddleware.cs
@@ -0,0 +1,114 @@
+using System.Net;
+using System.Text.Json;
+using MyNewProjectName.Contracts.DTOs.Responses;
+using MyNewProjectName.Domain.Exceptions;
+using Serilog;
+
+namespace MyNewProjectName.AdminAPI.Middleware;
+
+///
+/// Global exception handling middleware
+/// Catches all unhandled exceptions, logs them to Serilog with full details,
+/// and returns a standardized error response without exposing stack traces
+///
+public class ExceptionHandlingMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly Serilog.ILogger _logger;
+
+ public ExceptionHandlingMiddleware(RequestDelegate next, Serilog.ILogger logger)
+ {
+ _next = next;
+ _logger = logger;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ try
+ {
+ await _next(context);
+ }
+ catch (Exception ex)
+ {
+ await HandleExceptionAsync(context, ex);
+ }
+ }
+
+ private async Task HandleExceptionAsync(HttpContext context, Exception exception)
+ {
+ // Get correlation ID from header (set by CorrelationIdMiddleware)
+ var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault() ?? "unknown";
+
+ // Log full exception details to Serilog (including stack trace)
+ // This will be searchable by CorrelationId
+ _logger.Error(
+ exception,
+ "Unhandled exception occurred. CorrelationId: {CorrelationId}, Path: {Path}, Method: {Method}",
+ correlationId,
+ context.Request.Path,
+ context.Request.Method
+ );
+
+ // Safety check: If response has already started, we cannot modify headers/status code
+ // This prevents InvalidOperationException: "Headers are read-only, response has already started"
+ if (context.Response.HasStarted)
+ {
+ _logger.Warning(
+ "Response has already started. Cannot modify HTTP status code or headers. CorrelationId: {CorrelationId}",
+ correlationId
+ );
+ return;
+ }
+
+ var response = context.Response;
+
+ // Clear any existing response data
+ response.Clear();
+
+ response.ContentType = "application/json";
+
+ // Determine status code and user-friendly message
+ var (statusCode, message, errors) = exception switch
+ {
+ ValidationException validationEx => (
+ HttpStatusCode.BadRequest,
+ "Validation failed",
+ validationEx.Errors.SelectMany(e => e.Value).ToList()),
+
+ NotFoundException => (
+ HttpStatusCode.NotFound,
+ exception.Message,
+ new List()),
+
+ DomainException => (
+ HttpStatusCode.BadRequest,
+ exception.Message,
+ new List()),
+
+ UnauthorizedAccessException => (
+ HttpStatusCode.Unauthorized,
+ "Unauthorized",
+ new List()),
+
+ _ => (
+ HttpStatusCode.InternalServerError,
+ "An error occurred while processing your request",
+ new List())
+ };
+
+ response.StatusCode = (int)statusCode;
+
+ // Return standardized error response (NO stack trace exposed to client)
+ var result = JsonSerializer.Serialize(new ApiResponse
+ {
+ Success = false,
+ Message = message,
+ Errors = errors.Any() ? errors : null
+ }, new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+
+ await response.WriteAsync(result);
+ }
+}
diff --git a/MyNewProjectName.AdminAPI/Middleware/RequestResponseLoggingMiddleware.cs b/MyNewProjectName.AdminAPI/Middleware/RequestResponseLoggingMiddleware.cs
new file mode 100644
index 0000000..c7a6009
--- /dev/null
+++ b/MyNewProjectName.AdminAPI/Middleware/RequestResponseLoggingMiddleware.cs
@@ -0,0 +1,133 @@
+using System.Diagnostics;
+using System.Text;
+using Microsoft.Extensions.Configuration;
+using Serilog;
+
+namespace MyNewProjectName.AdminAPI.Middleware;
+
+///
+/// Middleware to log request and response bodies
+/// WARNING: This can generate large log files. Use with caution in production.
+/// Consider enabling only for specific environments or endpoints.
+///
+public class RequestResponseLoggingMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly Serilog.ILogger _logger;
+ private readonly bool _enableRequestLogging;
+ private readonly bool _enableResponseLogging;
+
+ // Paths that should NOT be logged (e.g., health checks, metrics)
+ private static readonly string[] ExcludedPaths = new[]
+ {
+ "/health",
+ "/metrics",
+ "/favicon.ico"
+ };
+
+ public RequestResponseLoggingMiddleware(
+ RequestDelegate next,
+ Serilog.ILogger logger,
+ IConfiguration configuration)
+ {
+ _next = next;
+ _logger = logger;
+ _enableRequestLogging = configuration.GetValue("Logging:EnableRequestLogging", false);
+ _enableResponseLogging = configuration.GetValue("Logging:EnableResponseLogging", false);
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ // Skip logging for excluded paths
+ if (ExcludedPaths.Any(path => context.Request.Path.StartsWithSegments(path)))
+ {
+ await _next(context);
+ return;
+ }
+
+ var stopwatch = Stopwatch.StartNew();
+ var requestBody = string.Empty;
+ var responseBody = string.Empty;
+
+ // Log request
+ if (_enableRequestLogging)
+ {
+ requestBody = await ReadRequestBodyAsync(context.Request);
+ _logger.Information(
+ "Request: {Method} {Path} {QueryString} | Body: {RequestBody}",
+ context.Request.Method,
+ context.Request.Path,
+ context.Request.QueryString,
+ requestBody
+ );
+ }
+
+ // Capture response body
+ var originalBodyStream = context.Response.Body;
+ using var responseBodyStream = new MemoryStream();
+ context.Response.Body = responseBodyStream;
+
+ try
+ {
+ await _next(context);
+ }
+ finally
+ {
+ stopwatch.Stop();
+
+ // Log response
+ if (_enableResponseLogging)
+ {
+ responseBody = await ReadResponseBodyAsync(context.Response);
+ await responseBodyStream.CopyToAsync(originalBodyStream);
+
+ _logger.Information(
+ "Response: {StatusCode} | Duration: {Duration}ms | Body: {ResponseBody}",
+ context.Response.StatusCode,
+ stopwatch.ElapsedMilliseconds,
+ responseBody
+ );
+ }
+ else
+ {
+ await responseBodyStream.CopyToAsync(originalBodyStream);
+ }
+
+ context.Response.Body = originalBodyStream;
+ }
+ }
+
+ private static async Task ReadRequestBodyAsync(HttpRequest request)
+ {
+ // Enable buffering to allow reading the body multiple times
+ request.EnableBuffering();
+
+ using var reader = new StreamReader(
+ request.Body,
+ encoding: Encoding.UTF8,
+ detectEncodingFromByteOrderMarks: false,
+ leaveOpen: true);
+
+ var body = await reader.ReadToEndAsync();
+ request.Body.Position = 0; // Reset position for next middleware
+
+ // Truncate very long bodies to avoid huge logs
+ return body.Length > 10000 ? body[..10000] + "... (truncated)" : body;
+ }
+
+ private static async Task ReadResponseBodyAsync(HttpResponse response)
+ {
+ response.Body.Seek(0, SeekOrigin.Begin);
+ using var reader = new StreamReader(
+ response.Body,
+ encoding: Encoding.UTF8,
+ detectEncodingFromByteOrderMarks: false,
+ leaveOpen: true);
+
+ var body = await reader.ReadToEndAsync();
+ response.Body.Seek(0, SeekOrigin.Begin); // Reset position
+
+ // Truncate very long bodies to avoid huge logs
+ return body.Length > 10000 ? body[..10000] + "... (truncated)" : body;
+ }
+}
diff --git a/MyNewProjectName.AdminAPI/MyNewProjectName.AdminAPI.csproj b/MyNewProjectName.AdminAPI/MyNewProjectName.AdminAPI.csproj
new file mode 100644
index 0000000..4441521
--- /dev/null
+++ b/MyNewProjectName.AdminAPI/MyNewProjectName.AdminAPI.csproj
@@ -0,0 +1,33 @@
+
+
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MyNewProjectName.AdminAPI/MyNewProjectName.AdminAPI.http b/MyNewProjectName.AdminAPI/MyNewProjectName.AdminAPI.http
new file mode 100644
index 0000000..dd9f259
--- /dev/null
+++ b/MyNewProjectName.AdminAPI/MyNewProjectName.AdminAPI.http
@@ -0,0 +1,6 @@
+@MyNewProjectName.AdminAPI_HostAddress = http://localhost:5011
+
+GET {{MyNewProjectName.AdminAPI_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/MyNewProjectName.AdminAPI/Program.cs b/MyNewProjectName.AdminAPI/Program.cs
new file mode 100644
index 0000000..fd1b666
--- /dev/null
+++ b/MyNewProjectName.AdminAPI/Program.cs
@@ -0,0 +1,76 @@
+using Microsoft.Extensions.Hosting;
+using MyNewProjectName.Application;
+using MyNewProjectName.Infrastructure;
+using MyNewProjectName.Infrastructure.Extensions;
+using MyNewProjectName.AdminAPI.Middleware;
+using Serilog;
+
+// Create host builder with Serilog
+var builder = WebApplication.CreateBuilder(args);
+
+// Configure Serilog
+builder.Host.UseSerilogLogging(builder.Configuration);
+
+// Add OpenTelemetry distributed tracing
+builder.Services.AddOpenTelemetryTracing("MyNewProjectName.AdminAPI");
+
+// Add services to the container.
+builder.Services.AddControllers();
+
+// Add Application Layer
+builder.Services.AddApplication();
+
+// Add Infrastructure Layer
+builder.Services.AddInfrastructure(builder.Configuration);
+
+// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
+builder.Services.AddOpenApi();
+
+// Add Swagger
+builder.Services.AddEndpointsApiExplorer();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.MapOpenApi();
+}
+
+// Middleware pipeline order is critical:
+// 1. CorrelationIdMiddleware - Must be first to track all requests
+app.UseMiddleware();
+
+// 2. RequestResponseLoggingMiddleware - Optional, enable only when needed
+// WARNING: This can generate large log files. Enable only for specific environments.
+// For AdminAPI, you might want to enable this more often for audit purposes.
+// Configure in appsettings.json: "Logging:EnableRequestLogging" and "Logging:EnableResponseLogging"
+app.UseMiddleware();
+
+app.UseHttpsRedirection();
+
+// 3. Authentication (built-in)
+app.UseAuthentication();
+
+// 4. Authorization (built-in)
+app.UseAuthorization();
+
+// 6. ExceptionHandlingMiddleware - Must be last to catch all exceptions
+app.UseMiddleware();
+
+app.MapControllers();
+
+try
+{
+ Log.Information("Starting MyNewProjectName.AdminAPI");
+ app.Run();
+}
+catch (Exception ex)
+{
+ Log.Fatal(ex, "Application terminated unexpectedly");
+ throw;
+}
+finally
+{
+ Log.CloseAndFlush();
+}
diff --git a/MyNewProjectName.AdminAPI/Properties/launchSettings.json b/MyNewProjectName.AdminAPI/Properties/launchSettings.json
new file mode 100644
index 0000000..b2f08be
--- /dev/null
+++ b/MyNewProjectName.AdminAPI/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5011",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:7298;http://localhost:5011",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/MyNewProjectName.AdminAPI/appsettings.Development.json b/MyNewProjectName.AdminAPI/appsettings.Development.json
new file mode 100644
index 0000000..ff66ba6
--- /dev/null
+++ b/MyNewProjectName.AdminAPI/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/MyNewProjectName.AdminAPI/appsettings.json b/MyNewProjectName.AdminAPI/appsettings.json
new file mode 100644
index 0000000..2aa297e
--- /dev/null
+++ b/MyNewProjectName.AdminAPI/appsettings.json
@@ -0,0 +1,43 @@
+{
+ "ConnectionStrings": {
+ "DefaultConnection": "Server=localhost;Database=MyNewProjectNameDb;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
+ },
+ "Jwt": {
+ "SecretKey": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!",
+ "Issuer": "MyNewProjectName",
+ "Audience": "MyNewProjectName",
+ "ExpirationInMinutes": 60,
+ "RefreshTokenExpirationInDays": 7
+ },
+ "Redis": {
+ "ConnectionString": "localhost:6379",
+ "InstanceName": "MyNewProjectName:",
+ "DefaultExpirationInMinutes": 30
+ },
+ "Serilog": {
+ "MinimumLevel": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.EntityFrameworkCore": "Warning",
+ "System": "Warning"
+ },
+ "WriteToConsole": true,
+ "WriteToFile": true,
+ "FilePath": "logs/log-.txt",
+ "RollingInterval": "Day",
+ "RetainedFileCountLimit": 31,
+ "SeqUrl": null,
+ "ElasticsearchUrl": null
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.EntityFrameworkCore": "Warning"
+ },
+ "EnableRequestLogging": false,
+ "EnableResponseLogging": false
+ },
+ "AllowedHosts": "*"
+}
diff --git a/MyNewProjectName.Application/Behaviors/LoggingBehavior.cs b/MyNewProjectName.Application/Behaviors/LoggingBehavior.cs
new file mode 100644
index 0000000..7faa916
--- /dev/null
+++ b/MyNewProjectName.Application/Behaviors/LoggingBehavior.cs
@@ -0,0 +1,49 @@
+using System.Diagnostics;
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace MyNewProjectName.Application.Behaviors;
+
+///
+/// Logging behavior for MediatR pipeline
+///
+public class LoggingBehavior : IPipelineBehavior
+ where TRequest : notnull
+{
+ private readonly ILogger> _logger;
+
+ public LoggingBehavior(ILogger> logger)
+ {
+ _logger = logger;
+ }
+
+ public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
+ {
+ var requestName = typeof(TRequest).Name;
+
+ _logger.LogInformation("Handling {RequestName}", requestName);
+
+ var stopwatch = Stopwatch.StartNew();
+
+ try
+ {
+ var response = await next();
+
+ stopwatch.Stop();
+
+ _logger.LogInformation("Handled {RequestName} in {ElapsedMilliseconds}ms",
+ requestName, stopwatch.ElapsedMilliseconds);
+
+ return response;
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+
+ _logger.LogError(ex, "Error handling {RequestName} after {ElapsedMilliseconds}ms",
+ requestName, stopwatch.ElapsedMilliseconds);
+
+ throw;
+ }
+ }
+}
diff --git a/MyNewProjectName.Application/Behaviors/ValidationBehavior.cs b/MyNewProjectName.Application/Behaviors/ValidationBehavior.cs
new file mode 100644
index 0000000..42e6a5d
--- /dev/null
+++ b/MyNewProjectName.Application/Behaviors/ValidationBehavior.cs
@@ -0,0 +1,47 @@
+using FluentValidation;
+using MediatR;
+
+namespace MyNewProjectName.Application.Behaviors;
+
+///
+/// Validation behavior for MediatR pipeline
+///
+public class ValidationBehavior : IPipelineBehavior
+ where TRequest : notnull
+{
+ private readonly IEnumerable> _validators;
+
+ public ValidationBehavior(IEnumerable> validators)
+ {
+ _validators = validators;
+ }
+
+ public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
+ {
+ if (!_validators.Any())
+ {
+ return await next();
+ }
+
+ var context = new ValidationContext(request);
+
+ var validationResults = await Task.WhenAll(
+ _validators.Select(v => v.ValidateAsync(context, cancellationToken)));
+
+ var failures = validationResults
+ .Where(r => r.Errors.Any())
+ .SelectMany(r => r.Errors)
+ .ToList();
+
+ if (failures.Any())
+ {
+ var errors = failures
+ .GroupBy(e => e.PropertyName, e => e.ErrorMessage)
+ .ToDictionary(g => g.Key, g => g.ToArray());
+
+ throw new MyNewProjectName.Domain.Exceptions.ValidationException(errors);
+ }
+
+ return await next();
+ }
+}
diff --git a/MyNewProjectName.Application/DependencyInjection.cs b/MyNewProjectName.Application/DependencyInjection.cs
new file mode 100644
index 0000000..07b1d46
--- /dev/null
+++ b/MyNewProjectName.Application/DependencyInjection.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using FluentValidation;
+using MediatR;
+using Microsoft.Extensions.DependencyInjection;
+using MyNewProjectName.Application.Behaviors;
+
+namespace MyNewProjectName.Application;
+
+///
+/// Dependency Injection for Application Layer
+///
+public static class DependencyInjection
+{
+ public static IServiceCollection AddApplication(this IServiceCollection services)
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+
+ // Register AutoMapper
+ services.AddAutoMapper(assembly);
+
+ // Register MediatR
+ services.AddMediatR(cfg =>
+ {
+ cfg.RegisterServicesFromAssembly(assembly);
+ // Logging phải đứng đầu tiên để ghi nhận request đến
+ cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
+ // Sau đó mới đến Validation
+ cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
+ });
+
+ // Register FluentValidation validators
+ services.AddValidatorsFromAssembly(assembly);
+
+ return services;
+ }
+}
diff --git a/MyNewProjectName.Application/Features/Sample/Commands/CreateSample/CreateSampleCommand.cs b/MyNewProjectName.Application/Features/Sample/Commands/CreateSample/CreateSampleCommand.cs
new file mode 100644
index 0000000..fb356d0
--- /dev/null
+++ b/MyNewProjectName.Application/Features/Sample/Commands/CreateSample/CreateSampleCommand.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace MyNewProjectName.Application.Features.Sample.Commands.CreateSample;
+
+///
+/// Command to create a new sample
+///
+public record CreateSampleCommand(string Name, string? Description) : IRequest;
diff --git a/MyNewProjectName.Application/Features/Sample/Commands/CreateSample/CreateSampleCommandHandler.cs b/MyNewProjectName.Application/Features/Sample/Commands/CreateSample/CreateSampleCommandHandler.cs
new file mode 100644
index 0000000..73affa1
--- /dev/null
+++ b/MyNewProjectName.Application/Features/Sample/Commands/CreateSample/CreateSampleCommandHandler.cs
@@ -0,0 +1,37 @@
+using MyNewProjectName.Domain.Entities;
+using MyNewProjectName.Domain.Interfaces;
+using MediatR;
+
+namespace MyNewProjectName.Application.Features.Sample.Commands.CreateSample;
+
+///
+/// Handler for CreateSampleCommand
+///
+public class CreateSampleCommandHandler : IRequestHandler
+{
+ private readonly IRepository _repository;
+ private readonly IUnitOfWork _unitOfWork;
+
+ public CreateSampleCommandHandler(IRepository repository, IUnitOfWork unitOfWork)
+ {
+ _repository = repository;
+ _unitOfWork = unitOfWork;
+ }
+
+ public async Task Handle(CreateSampleCommand request, CancellationToken cancellationToken)
+ {
+ var entity = new SampleEntity
+ {
+ Id = Guid.NewGuid(),
+ Name = request.Name,
+ Description = request.Description,
+ IsActive = true,
+ CreatedAt = DateTime.UtcNow
+ };
+
+ await _repository.AddAsync(entity, cancellationToken);
+ await _unitOfWork.SaveChangesAsync(cancellationToken);
+
+ return entity.Id;
+ }
+}
diff --git a/MyNewProjectName.Application/Features/Sample/Commands/CreateSample/CreateSampleCommandValidator.cs b/MyNewProjectName.Application/Features/Sample/Commands/CreateSample/CreateSampleCommandValidator.cs
new file mode 100644
index 0000000..45ac8d1
--- /dev/null
+++ b/MyNewProjectName.Application/Features/Sample/Commands/CreateSample/CreateSampleCommandValidator.cs
@@ -0,0 +1,19 @@
+using FluentValidation;
+
+namespace MyNewProjectName.Application.Features.Sample.Commands.CreateSample;
+
+///
+/// Validator for CreateSampleCommand
+///
+public class CreateSampleCommandValidator : AbstractValidator
+{
+ public CreateSampleCommandValidator()
+ {
+ RuleFor(x => x.Name)
+ .NotEmpty().WithMessage("Name is required.")
+ .MaximumLength(200).WithMessage("Name must not exceed 200 characters.");
+
+ RuleFor(x => x.Description)
+ .MaximumLength(1000).WithMessage("Description must not exceed 1000 characters.");
+ }
+}
diff --git a/MyNewProjectName.Application/Features/Sample/Queries/GetSamples/GetSamplesQuery.cs b/MyNewProjectName.Application/Features/Sample/Queries/GetSamples/GetSamplesQuery.cs
new file mode 100644
index 0000000..7ea2f09
--- /dev/null
+++ b/MyNewProjectName.Application/Features/Sample/Queries/GetSamples/GetSamplesQuery.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace MyNewProjectName.Application.Features.Sample.Queries.GetSamples;
+
+///
+/// Query to get all samples
+///
+public record GetSamplesQuery : IRequest>;
diff --git a/MyNewProjectName.Application/Features/Sample/Queries/GetSamples/GetSamplesQueryHandler.cs b/MyNewProjectName.Application/Features/Sample/Queries/GetSamples/GetSamplesQueryHandler.cs
new file mode 100644
index 0000000..4d8da3c
--- /dev/null
+++ b/MyNewProjectName.Application/Features/Sample/Queries/GetSamples/GetSamplesQueryHandler.cs
@@ -0,0 +1,27 @@
+using AutoMapper;
+using MyNewProjectName.Domain.Entities;
+using MyNewProjectName.Domain.Interfaces;
+using MediatR;
+
+namespace MyNewProjectName.Application.Features.Sample.Queries.GetSamples;
+
+///
+/// Handler for GetSamplesQuery
+///
+public class GetSamplesQueryHandler : IRequestHandler>
+{
+ private readonly IRepository _repository;
+ private readonly IMapper _mapper;
+
+ public GetSamplesQueryHandler(IRepository repository, IMapper mapper)
+ {
+ _repository = repository;
+ _mapper = mapper;
+ }
+
+ public async Task> Handle(GetSamplesQuery request, CancellationToken cancellationToken)
+ {
+ var entities = await _repository.GetAllAsync(cancellationToken);
+ return _mapper.Map>(entities);
+ }
+}
diff --git a/MyNewProjectName.Application/Features/Sample/Queries/GetSamples/SampleDto.cs b/MyNewProjectName.Application/Features/Sample/Queries/GetSamples/SampleDto.cs
new file mode 100644
index 0000000..9dd60c7
--- /dev/null
+++ b/MyNewProjectName.Application/Features/Sample/Queries/GetSamples/SampleDto.cs
@@ -0,0 +1,22 @@
+using AutoMapper;
+using MyNewProjectName.Application.Mappings;
+using MyNewProjectName.Domain.Entities;
+
+namespace MyNewProjectName.Application.Features.Sample.Queries.GetSamples;
+
+///
+/// Sample DTO
+///
+public class SampleDto : IMapFrom
+{
+ public Guid Id { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public string? Description { get; set; }
+ public bool IsActive { get; set; }
+ public DateTime CreatedAt { get; set; }
+
+ public void Mapping(Profile profile)
+ {
+ profile.CreateMap();
+ }
+}
diff --git a/MyNewProjectName.Application/Interfaces/Common/IJwtTokenGenerator.cs b/MyNewProjectName.Application/Interfaces/Common/IJwtTokenGenerator.cs
new file mode 100644
index 0000000..ed4c0f1
--- /dev/null
+++ b/MyNewProjectName.Application/Interfaces/Common/IJwtTokenGenerator.cs
@@ -0,0 +1,14 @@
+// using MyNewProjectName.Domain.Entities;
+// using System.Security.Claims;
+
+// namespace MyNewProjectName.Application.Interfaces.Common;
+
+// public interface IJwtTokenGenerator
+// {
+// string GenerateAccessToken(User user, List roles, Guid tenantId);
+
+// string GenerateRefreshToken();
+
+// ClaimsPrincipal GetPrincipalFromExpiredToken(string token);
+// }
+
diff --git a/MyNewProjectName.Application/Interfaces/Common/IPasswordHasher.cs b/MyNewProjectName.Application/Interfaces/Common/IPasswordHasher.cs
new file mode 100644
index 0000000..09434be
--- /dev/null
+++ b/MyNewProjectName.Application/Interfaces/Common/IPasswordHasher.cs
@@ -0,0 +1,8 @@
+namespace MyNewProjectName.Application.Interfaces.Common;
+
+public interface IPasswordHasher
+{
+ string Hash(string password);
+ bool Verify(string password, string hashedPassword);
+}
+
diff --git a/MyNewProjectName.Application/Interfaces/ICurrentUserService.cs b/MyNewProjectName.Application/Interfaces/ICurrentUserService.cs
new file mode 100644
index 0000000..f886d81
--- /dev/null
+++ b/MyNewProjectName.Application/Interfaces/ICurrentUserService.cs
@@ -0,0 +1,12 @@
+namespace MyNewProjectName.Application.Interfaces;
+
+///
+/// Interface for getting current user information
+///
+public interface ICurrentUserService
+{
+ string? UserId { get; }
+ string? UserName { get; }
+ bool? IsAuthenticated { get; }
+ string? Role { get; }
+}
diff --git a/MyNewProjectName.Application/Interfaces/IDateTimeService.cs b/MyNewProjectName.Application/Interfaces/IDateTimeService.cs
new file mode 100644
index 0000000..6a627b3
--- /dev/null
+++ b/MyNewProjectName.Application/Interfaces/IDateTimeService.cs
@@ -0,0 +1,10 @@
+namespace MyNewProjectName.Application.Interfaces;
+
+///
+/// Interface for date time operations (for testability)
+///
+public interface IDateTimeService
+{
+ DateTime Now { get; }
+ DateTime UtcNow { get; }
+}
diff --git a/MyNewProjectName.Application/Mappings/IMapFrom.cs b/MyNewProjectName.Application/Mappings/IMapFrom.cs
new file mode 100644
index 0000000..ef99d1c
--- /dev/null
+++ b/MyNewProjectName.Application/Mappings/IMapFrom.cs
@@ -0,0 +1,11 @@
+using AutoMapper;
+
+namespace MyNewProjectName.Application.Mappings;
+
+///
+/// Interface for auto mapping registration
+///
+public interface IMapFrom
+{
+ void Mapping(Profile profile) => profile.CreateMap(typeof(T), GetType());
+}
diff --git a/MyNewProjectName.Application/Mappings/MappingProfile.cs b/MyNewProjectName.Application/Mappings/MappingProfile.cs
new file mode 100644
index 0000000..0a07120
--- /dev/null
+++ b/MyNewProjectName.Application/Mappings/MappingProfile.cs
@@ -0,0 +1,55 @@
+using System.Reflection;
+using AutoMapper;
+
+namespace MyNewProjectName.Application.Mappings;
+
+///
+/// AutoMapper profile that auto-registers mappings from IMapFrom interface
+///
+public class MappingProfile : Profile
+{
+ public MappingProfile()
+ {
+ ApplyMappingsFromAssembly(Assembly.GetExecutingAssembly());
+ }
+
+ private void ApplyMappingsFromAssembly(Assembly assembly)
+ {
+ var mapFromType = typeof(IMapFrom<>);
+
+ var mappingMethodName = nameof(IMapFrom