Skip to content

Commit 6121109

Browse files
authored
refactor: improve exception handling, modernize API versioning, and simplify Kafka setup
* refactor: add global IExceptionHandler and simplify CustomControllerBase * refactor: migrate from Microsoft.AspNetCore.Mvc.Versioning to Asp.Versioning.Mvc 8.1.0 * chore: add Asp.Versioning global using to all service projects * refactor: wire UseExceptionHandler middleware and rename AddApiVersioning calls * refactor: remove Zookeeper, migrate Kafka to KRaft mode (cp-kafka 7.8.0)
1 parent 28ed09e commit 6121109

27 files changed

Lines changed: 99 additions & 98 deletions

File tree

Directory.Packages.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
</ItemGroup>
2727

2828
<ItemGroup Label="API Versioning">
29-
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.1.0" />
30-
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.1.0" />
29+
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
30+
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
3131
</ItemGroup>
3232

3333
<ItemGroup Label="API Gateway">

docker-compose.yml

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,21 @@ services:
3232
- './scripts/db_init.sql:/docker-entrypoint-initdb.d/db_init.sql'
3333
restart: on-failure
3434

35-
zookeeper:
36-
image: confluentinc/cp-zookeeper:7.6.1
37-
ports:
38-
- '2181:2181'
39-
environment:
40-
ZOOKEEPER_CLIENT_PORT: 2181
41-
ZOOKEEPER_TICK_TIME: 2000
42-
volumes:
43-
- zookeeper-data:/var/lib/zookeeper/data
44-
- zookeeper-log:/var/lib/zookeeper/log
45-
restart: always
46-
4735
kafka:
48-
image: confluentinc/cp-kafka:7.6.1
36+
image: confluentinc/cp-kafka:7.8.0
4937
ports:
5038
- '29092:29092'
5139
- '9092:9092'
5240
environment:
53-
KAFKA_BROKER_ID: 1
54-
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
55-
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
41+
KAFKA_NODE_ID: 1
42+
KAFKA_PROCESS_ROLES: 'broker,controller'
43+
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:9093'
44+
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
45+
KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
46+
KAFKA_LISTENERS: 'PLAINTEXT://kafka:29092,CONTROLLER://kafka:9093,PLAINTEXT_HOST://0.0.0.0:9092'
5647
KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka:29092,PLAINTEXT_HOST://kafka:9092'
48+
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
49+
CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk'
5750
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
5851
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
5952
KAFKA_LOG_DIRS: /var/lib/kafka/data
@@ -71,9 +64,6 @@ services:
7164
volumes:
7265
- kafka-data:/var/lib/kafka/data
7366
restart: always
74-
depends_on:
75-
zookeeper:
76-
condition: service_started
7767

7868
kafka-ui:
7969
image: provectuslabs/kafka-ui:latest
@@ -359,8 +349,6 @@ networks:
359349

360350
volumes:
361351
postgres:
362-
zookeeper-data:
363-
zookeeper-log:
364352
kafka-data:
365353
pgadmin:
366354
debezium-data:

src/Core/EcommerceDDD.Core.Infrastructure/EcommerceDDD.Core.Infrastructure.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
<PackageReference Include="Duende.IdentityServer.EntityFramework" />
1515
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
1616
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
17-
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" />
18-
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" />
17+
<PackageReference Include="Asp.Versioning.Mvc" />
18+
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" />
1919
<PackageReference Include="Microsoft.EntityFrameworkCore" />
2020
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
2121
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />

src/Core/EcommerceDDD.Core.Infrastructure/Extensions/CoreInfrastructureExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ public static IServiceCollection AddCoreInfrastructure(this IServiceCollection s
1111
services
1212
.AddMemoryCache()
1313
.AddHttpContextAccessor()
14+
// Exception handling
15+
.AddExceptionHandler<GlobalExceptionHandler>()
16+
.AddProblemDetails()
1417
// CQRS
1518
.AddScoped<ICommandBus, CommandBus>()
1619
.AddScoped<IQueryBus, QueryBus>()

src/Core/EcommerceDDD.Core.Infrastructure/GlobalUsings.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
global using Microsoft.AspNetCore.Builder;
2828
global using Microsoft.AspNetCore.Http;
2929
global using Microsoft.AspNetCore.Mvc;
30-
global using Microsoft.AspNetCore.Mvc.Versioning;
30+
global using Asp.Versioning;
3131
global using Microsoft.Extensions.Caching.Memory;
3232
global using Microsoft.Extensions.Configuration;
3333
global using Microsoft.Extensions.DependencyInjection;

src/Core/EcommerceDDD.Core.Infrastructure/WebApi/ApiVersioningExtension.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
namespace EcommerceDDD.Core.Infrastructure.WebApi;
1+
namespace EcommerceDDD.Core.Infrastructure.WebApi;
22

33
public static class ApiVersioningExtension
44
{
5-
public static IServiceCollection AddApiVersioning(this IServiceCollection services, string apiVersion = ApiVersions.V2)
5+
public static IServiceCollection AddApiVersioning(
6+
this IServiceCollection services, string apiVersion = ApiVersions.V2)
67
{
78
services.AddApiVersioning(options =>
89
{
910
options.AssumeDefaultVersionWhenUnspecified = true;
10-
options.DefaultApiVersion = ApiVersion.Parse(apiVersion);
11+
options.DefaultApiVersion = ApiVersionParser.Default.Parse(apiVersion);
1112
options.ReportApiVersions = true;
1213
options.ApiVersionReader = new UrlSegmentApiVersionReader();
13-
});
14-
15-
services.AddVersionedApiExplorer(options =>
14+
})
15+
.AddApiExplorer(options =>
1616
{
1717
options.GroupNameFormat = "'v'VVV";
1818
options.SubstituteApiVersionInUrl = true;
Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace EcommerceDDD.Core.Infrastructure.WebApi;
1+
namespace EcommerceDDD.Core.Infrastructure.WebApi;
22

33
public class CustomControllerBase : ControllerBase
44
{
@@ -18,57 +18,21 @@ protected CustomControllerBase(ICommandBus commandBus, IQueryBus queryBus)
1818
protected async new Task<IActionResult> Response<TResult>(IQuery<TResult> query,
1919
CancellationToken cancellationToken)
2020
{
21-
TResult result;
22-
23-
try
24-
{
25-
result = await _queryBus
26-
.SendAsync(query, cancellationToken);
27-
}
28-
catch (OperationCanceledException)
29-
{
30-
return StatusCode(500, "Operation was canceled.");
31-
}
32-
catch (Exception e)
33-
{
34-
return BadRequestActionResult(e.Message);
35-
}
36-
21+
var result = await _queryBus.SendAsync(query, cancellationToken);
3722
return Ok(new ApiResponse<TResult>
3823
{
3924
Data = result,
4025
Success = true
4126
});
4227
}
4328

44-
protected async new Task<IActionResult> Response(ICommand command,
29+
protected async new Task<IActionResult> Response(ICommand command,
4530
CancellationToken cancellationToken)
4631
{
47-
try
48-
{
49-
await _commandBus
50-
.SendAsync(command, cancellationToken);
51-
}
52-
catch (OperationCanceledException)
53-
{
54-
// Handle cancellation
55-
return StatusCode(500, "Operation was canceled.");
56-
}
57-
catch (Exception e)
58-
{
59-
return BadRequestActionResult(e.Message);
60-
}
61-
32+
await _commandBus.SendAsync(command, cancellationToken);
6233
return Ok(new ApiResponse<IActionResult>
6334
{
6435
Success = true
6536
});
6637
}
67-
68-
protected IActionResult BadRequestActionResult(string resultErrors)
69-
=> BadRequest(new ApiResponse<IActionResult>
70-
{
71-
Success = false,
72-
Message = resultErrors
73-
});
74-
}
38+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
namespace EcommerceDDD.Core.Infrastructure.WebApi;
2+
3+
public class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
4+
: Microsoft.AspNetCore.Diagnostics.IExceptionHandler
5+
{
6+
public async ValueTask<bool> TryHandleAsync(
7+
HttpContext httpContext,
8+
Exception exception,
9+
CancellationToken cancellationToken)
10+
{
11+
if (exception is OperationCanceledException)
12+
{
13+
logger.LogInformation("Request was canceled.");
14+
return true;
15+
}
16+
17+
var (statusCode, message) = exception switch
18+
{
19+
BusinessRuleException e => (StatusCodes.Status422UnprocessableEntity, e.Message),
20+
RecordNotFoundException e => (StatusCodes.Status404NotFound, e.Message),
21+
ApplicationLogicException e => (StatusCodes.Status500InternalServerError, e.Message),
22+
_ => (StatusCodes.Status500InternalServerError, "An unexpected error occurred.")
23+
};
24+
25+
if (statusCode >= 500)
26+
logger.LogError(exception, "Server error: {Message}", exception.Message);
27+
else
28+
logger.LogWarning("Request error ({StatusCode}): {Message}", statusCode, exception.Message);
29+
30+
httpContext.Response.StatusCode = statusCode;
31+
httpContext.Response.ContentType = "application/json";
32+
33+
var response = new ApiResponse<object>
34+
{
35+
Success = false,
36+
Message = message
37+
};
38+
39+
await httpContext.Response.WriteAsJsonAsync(response, cancellationToken);
40+
return true;
41+
}
42+
}
Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace EcommerceDDD.IdentityServer.Controllers;
1+
namespace EcommerceDDD.IdentityServer.Controllers;
22

33
[AllowAnonymous]
44
[ApiController]
@@ -13,38 +13,24 @@ public class AccountsController(IIdentityManager identityManager) : CustomContro
1313
[ProducesResponseType(typeof(LoginResult), StatusCodes.Status200OK)]
1414
public async Task<IActionResult> UserLogin(LoginRequest request)
1515
{
16-
try
17-
{
18-
LoginResult result = await _identityManager
19-
.AuthUserByCredentials(request);
16+
LoginResult result = await _identityManager
17+
.AuthUserByCredentials(request);
2018

21-
return Ok(result);
22-
}
23-
catch (Exception e)
24-
{
25-
return BadRequestActionResult(e.Message);
26-
}
19+
return Ok(result);
2720
}
2821

2922
[HttpPost, Route("register")]
3023
[MapToApiVersion(ApiVersions.V2)]
3124
[ProducesResponseType(typeof(UserRegisteredResult), StatusCodes.Status200OK)]
3225
public async Task<IActionResult> Register(RegisterUserRequest request)
3326
{
34-
try
35-
{
36-
UserRegisteredResult result = await _identityManager
37-
.RegisterNewUser(request);
27+
UserRegisteredResult result = await _identityManager
28+
.RegisterNewUser(request);
3829

39-
return Ok(new
40-
{
41-
data = result,
42-
success = result.Succeeded
43-
});
44-
}
45-
catch (Exception e)
46-
{
47-
return BadRequestActionResult(e.Message);
48-
}
30+
return Ok(new
31+
{
32+
data = result,
33+
success = result.Succeeded
34+
});
4935
}
5036
}

src/Crosscutting/EcommerceDDD.IdentityServer/GlobalUsings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
global using Microsoft.AspNetCore.Authorization;
1919
global using Microsoft.AspNetCore.Identity;
2020
global using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
21+
global using Asp.Versioning;
2122
global using Microsoft.AspNetCore.Mvc;
2223
global using Microsoft.EntityFrameworkCore;
2324
global using Microsoft.Extensions.Options;

0 commit comments

Comments
 (0)