Skip to content

Commit 4a8a61f

Browse files
committed
add a correlation and causation ids manager to the frontend
1 parent 02d99cb commit 4a8a61f

38 files changed

Lines changed: 157 additions & 55 deletions

docs/correlation-causation-guide.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,22 @@ logger.LogInformation(
298298
3. **`LoggingEnricherMiddleware`**:
299299
- Automatically adds `CorrelationId` and `CausationId` to the structured logging scope for every request.
300300

301+
### Blazor Frontend Implementation
302+
303+
The Blazor frontend automatically manages and propagates these IDs using a dedicated service and message handler:
304+
305+
1. **`CorrelationService`**:
306+
- Stores the current `CorrelationId` (persistent per circuit/session).
307+
- Manages the `CausationId`, which updates dynamically.
308+
309+
2. **`AuthorizationMessageHandler`**:
310+
- Automatically injects `X-Correlation-ID` and `X-Causation-ID` headers into all outgoing API requests.
311+
- Captures `X-Event-ID` from backend responses to update the `CausationId` for subsequent calls.
312+
313+
3. **`BookStoreEventsService`**:
314+
- Updates the `CorrelationService` with the `EventId` from incoming Server-Sent Events (SSE).
315+
- Ensures that reactive UI updates and subsequent background data reloads are correctly linked to the event that triggered them.
316+
301317
## Summary
302318

303319
- **Correlation ID**: Tracks the entire business workflow

src/ApiService/BookStore.ApiService/Endpoints/JwtAuthenticationEndpoints.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ static async Task<IResult> ConfirmEmailAsync(
184184
var result = await userManager.ConfirmEmailAsync(user, code);
185185
if (result.Succeeded)
186186
{
187-
await bus.PublishAsync(new BookStore.Shared.Notifications.UserVerifiedNotification(user.Id, user.Email!, DateTimeOffset.UtcNow));
187+
await bus.PublishAsync(new BookStore.Shared.Notifications.UserVerifiedNotification(Guid.Empty, user.Id, user.Email!, DateTimeOffset.UtcNow));
188188
return Results.Ok("Email confirmed successfully.");
189189
}
190190

src/ApiService/BookStore.ApiService/Handlers/Books/BookCoverHandlers.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public static class BookCoverHandlers
6161

6262
// Return notification for SignalR
6363
var notification = new BookCoverUpdatedNotification(
64+
Guid.Empty,
6465
aggregate.Id,
6566
coverUrl);
6667

src/ApiService/BookStore.ApiService/Infrastructure/MartenCommitListener.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ async Task HandleUserChangeAsync(UserProfile profile, ChangeType _, Cancellation
119119
// - Shopping cart operations: BookAddedToCart, BookRemovedFromCart, CartItemQuantityUpdated, ShoppingCartCleared
120120
// "UserUpdated" is a good catch-all for ReactiveQuery invalidation.
121121

122-
IDomainEventNotification notification = new UserUpdatedNotification(profile.Id, timestamp);
122+
IDomainEventNotification notification = new UserUpdatedNotification(Guid.Empty, profile.Id, timestamp);
123123

124124
await NotifyAsync("User", notification, token);
125125
}
@@ -134,9 +134,9 @@ async Task HandleCategoryChangeAsync(CategoryProjection category, ChangeType cha
134134
var name = category.Names.Values.FirstOrDefault() ?? "Unknown";
135135
IDomainEventNotification notification = effectiveChangeType switch
136136
{
137-
ChangeType.Insert => new CategoryCreatedNotification(category.Id, name, category.LastModified),
138-
ChangeType.Update => new CategoryUpdatedNotification(category.Id, category.LastModified),
139-
ChangeType.Delete => new CategoryDeletedNotification(category.Id, category.LastModified),
137+
ChangeType.Insert => new CategoryCreatedNotification(Guid.Empty, category.Id, name, category.LastModified),
138+
ChangeType.Update => new CategoryUpdatedNotification(Guid.Empty, category.Id, category.LastModified),
139+
ChangeType.Delete => new CategoryDeletedNotification(Guid.Empty, category.Id, category.LastModified),
140140
_ => throw new ArgumentOutOfRangeException(nameof(effectiveChangeType))
141141
};
142142

@@ -153,9 +153,9 @@ async Task HandleBookChangeAsync(BookSearchProjection book, ChangeType changeTyp
153153
var timestamp = DateTimeOffset.UtcNow;
154154
IDomainEventNotification notification = effectiveChangeType switch
155155
{
156-
ChangeType.Insert => new BookCreatedNotification(book.Id, book.Title, timestamp),
157-
ChangeType.Update => new BookUpdatedNotification(book.Id, book.Title, timestamp),
158-
ChangeType.Delete => new BookDeletedNotification(book.Id, timestamp),
156+
ChangeType.Insert => new BookCreatedNotification(Guid.Empty, book.Id, book.Title, timestamp),
157+
ChangeType.Update => new BookUpdatedNotification(Guid.Empty, book.Id, book.Title, timestamp),
158+
ChangeType.Delete => new BookDeletedNotification(Guid.Empty, book.Id, timestamp),
159159
_ => throw new ArgumentOutOfRangeException(nameof(effectiveChangeType))
160160
};
161161

@@ -170,9 +170,9 @@ async Task HandleAuthorChangeAsync(AuthorProjection author, ChangeType changeTyp
170170

171171
IDomainEventNotification notification = effectiveChangeType switch
172172
{
173-
ChangeType.Insert => new AuthorCreatedNotification(author.Id, author.Name, author.LastModified),
174-
ChangeType.Update => new AuthorUpdatedNotification(author.Id, author.Name, author.LastModified),
175-
ChangeType.Delete => new AuthorDeletedNotification(author.Id, author.LastModified),
173+
ChangeType.Insert => new AuthorCreatedNotification(Guid.Empty, author.Id, author.Name, author.LastModified),
174+
ChangeType.Update => new AuthorUpdatedNotification(Guid.Empty, author.Id, author.Name, author.LastModified),
175+
ChangeType.Delete => new AuthorDeletedNotification(Guid.Empty, author.Id, author.LastModified),
176176
_ => throw new ArgumentOutOfRangeException(nameof(effectiveChangeType))
177177
};
178178

@@ -187,9 +187,9 @@ async Task HandlePublisherChangeAsync(PublisherProjection publisher, ChangeType
187187

188188
IDomainEventNotification notification = effectiveChangeType switch
189189
{
190-
ChangeType.Insert => new PublisherCreatedNotification(publisher.Id, publisher.Name, publisher.LastModified),
191-
ChangeType.Update => new PublisherUpdatedNotification(publisher.Id, publisher.Name, publisher.LastModified),
192-
ChangeType.Delete => new PublisherDeletedNotification(publisher.Id, publisher.LastModified),
190+
ChangeType.Insert => new PublisherCreatedNotification(Guid.Empty, publisher.Id, publisher.Name, publisher.LastModified),
191+
ChangeType.Update => new PublisherUpdatedNotification(Guid.Empty, publisher.Id, publisher.Name, publisher.LastModified),
192+
ChangeType.Delete => new PublisherDeletedNotification(Guid.Empty, publisher.Id, publisher.LastModified),
193193
_ => throw new ArgumentOutOfRangeException(nameof(effectiveChangeType))
194194
};
195195

@@ -202,7 +202,7 @@ async Task HandleBookStatisticsChangeAsync(BookStatistics stats, ChangeType _, C
202202

203203
// Emit BookUpdated so clients refetch the book (including new stats)
204204
// Title is unknown here, but usually not critical for simple invalidation signals
205-
IDomainEventNotification notification = new BookUpdatedNotification(stats.Id, "Statistics Updated", DateTimeOffset.UtcNow);
205+
IDomainEventNotification notification = new BookUpdatedNotification(Guid.Empty, stats.Id, "Statistics Updated", DateTimeOffset.UtcNow);
206206

207207
await NotifyAsync("Book", notification, token);
208208
}

src/Client/BookStore.Client/BookStoreEventsService.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class BookStoreEventsService : IAsyncDisposable
1414
{
1515
readonly HttpClient _httpClient;
1616
readonly ILogger<BookStoreEventsService> _logger;
17+
readonly Services.CorrelationService _correlationService;
1718
CancellationTokenSource? _cts;
1819
Task? _listenerTask;
1920

@@ -34,10 +35,14 @@ public class BookStoreEventsService : IAsyncDisposable
3435
{ "UserVerified", typeof(UserVerifiedNotification) }
3536
};
3637

37-
public BookStoreEventsService(HttpClient httpClient, ILogger<BookStoreEventsService> logger)
38+
public BookStoreEventsService(
39+
HttpClient httpClient,
40+
ILogger<BookStoreEventsService> logger,
41+
Services.CorrelationService correlationService)
3842
{
3943
_httpClient = httpClient;
4044
_logger = logger;
45+
_correlationService = correlationService;
4146
}
4247

4348
public void StartListening()
@@ -75,6 +80,11 @@ async Task ListenToStreamAsync(CancellationToken ct)
7580
var notification = DeserializeNotification(item.EventType, item.Data);
7681
if (notification != null)
7782
{
83+
if (notification.EventId != Guid.Empty)
84+
{
85+
_correlationService.UpdateCausationId(notification.EventId.ToString());
86+
}
87+
7888
OnNotificationReceived?.Invoke(notification);
7989
}
8090
}

src/Client/BookStore.Client/ICreateAuthorEndpoint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public partial interface ICreateAuthorEndpoint
2020
{
2121
[Headers("Content-Type: application/json")]
2222
[Post("/api/admin/authors")]
23-
Task Execute([Body] CreateAuthorRequest body, [Header("api-version")] object api_version, [Header("Accept-Language")] object accept_Language, [Header("X-Correlation-ID")] object x_Correlation_ID, [Header("X-Causation-ID")] object x_Causation_ID, CancellationToken cancellationToken = default);
23+
Task Execute([Body] CreateAuthorRequest body, [Header("api-version")] object api_version, [Header("Accept-Language")] object accept_Language, [Header("X-Correlation-ID")] object? x_Correlation_ID = null, [Header("X-Causation-ID")] object? x_Causation_ID = null, CancellationToken cancellationToken = default);
2424
}
2525

2626
}

src/Client/BookStore.Client/ICreateBookEndpoint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public partial interface ICreateBookEndpoint
2020
{
2121
[Headers("Content-Type: application/json")]
2222
[Post("/api/admin/books")]
23-
Task CreateBookAsync([Body] CreateBookRequest body, [Header("api-version")] object api_version, [Header("Accept-Language")] object accept_Language, [Header("X-Correlation-ID")] object x_Correlation_ID, [Header("X-Causation-ID")] object x_Causation_ID, CancellationToken cancellationToken = default);
23+
Task CreateBookAsync([Body] CreateBookRequest body, [Header("api-version")] object api_version, [Header("Accept-Language")] object accept_Language, [Header("X-Correlation-ID")] object? x_Correlation_ID = null, [Header("X-Causation-ID")] object? x_Causation_ID = null, CancellationToken cancellationToken = default);
2424
}
2525

2626
}

src/Client/BookStore.Client/ICreateCategoryEndpoint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public partial interface ICreateCategoryEndpoint
2020
{
2121
[Headers("Content-Type: application/json")]
2222
[Post("/api/admin/categories")]
23-
Task Execute([Body] CreateCategoryRequest body, [Header("api-version")] object api_version, [Header("Accept-Language")] object accept_Language, [Header("X-Correlation-ID")] object x_Correlation_ID, [Header("X-Causation-ID")] object x_Causation_ID, CancellationToken cancellationToken = default);
23+
Task Execute([Body] CreateCategoryRequest body, [Header("api-version")] object api_version, [Header("Accept-Language")] object accept_Language, [Header("X-Correlation-ID")] object? x_Correlation_ID = null, [Header("X-Causation-ID")] object? x_Causation_ID = null, CancellationToken cancellationToken = default);
2424
}
2525

2626
}

src/Client/BookStore.Client/ICreatePublisherEndpoint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public partial interface ICreatePublisherEndpoint
2020
{
2121
[Headers("Content-Type: application/json")]
2222
[Post("/api/admin/publishers")]
23-
Task Execute([Body] CreatePublisherRequest body, [Header("api-version")] object api_version, [Header("Accept-Language")] object accept_Language, [Header("X-Correlation-ID")] object x_Correlation_ID, [Header("X-Causation-ID")] object x_Causation_ID, CancellationToken cancellationToken = default);
23+
Task Execute([Body] CreatePublisherRequest body, [Header("api-version")] object api_version, [Header("Accept-Language")] object accept_Language, [Header("X-Correlation-ID")] object? x_Correlation_ID = null, [Header("X-Causation-ID")] object? x_Causation_ID = null, CancellationToken cancellationToken = default);
2424
}
2525

2626
}

src/Client/BookStore.Client/IGetAllBooksAdminEndpoint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace BookStore.Client
1919
public partial interface IGetAllBooksAdminEndpoint
2020
{
2121
[Get("/api/admin/books")]
22-
Task GetAllBooksAdminAsync([Header("api-version")] object api_version, [Header("Accept-Language")] object accept_Language, [Header("X-Correlation-ID")] object x_Correlation_ID, [Header("X-Causation-ID")] object x_Causation_ID, CancellationToken cancellationToken = default);
22+
Task GetAllBooksAdminAsync([Header("api-version")] object api_version, [Header("Accept-Language")] object accept_Language, [Header("X-Correlation-ID")] object? x_Correlation_ID = null, [Header("X-Causation-ID")] object? x_Causation_ID = null, CancellationToken cancellationToken = default);
2323
}
2424

2525
}

0 commit comments

Comments
 (0)