Skip to content

Commit 7105929

Browse files
kyurkchyanclaude
andauthored
Publish application layer as a NuGet (#41)
* Added application layer as NuGet * Added peek dlq command * Peek DLQ batch * Fixed batching * Fixed role-over scanning * PR comment fixes * Comment fixes * Code cleanup * Better name for application DI extension * Implemented support for diagnosing alerts without app insights * Fix App Insights initialization to occur before message peek Moved the Initialize call ahead of PeekAsync so it fires even when the queue turns out to be empty, preserving the fail-fast behaviour and restoring the existing tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Test fixes * More integration test fixes --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 83f667a commit 7105929

26 files changed

Lines changed: 1263 additions & 29 deletions

.github/workflows/alpha-release.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,16 @@ jobs:
119119
echo "version=$ALPHA_VERSION" >> $GITHUB_OUTPUT
120120
echo "Generated version: $ALPHA_VERSION"
121121
122-
- name: Pack
122+
- name: Pack CLI
123123
env:
124124
VERSION: ${{ steps.version.outputs.version }}
125125
run: dotnet pack src/ServiceBusToolset.CLI -c Release --no-build -o ./artifacts -p:Version="$VERSION"
126126

127+
- name: Pack Application
128+
env:
129+
VERSION: ${{ steps.version.outputs.version }}
130+
run: dotnet pack src/ServiceBusToolset.Application -c Release --no-build -o ./artifacts -p:Version="$VERSION"
131+
127132
- name: Push to NuGet
128133
env:
129134
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
@@ -133,3 +138,4 @@ jobs:
133138
exit 0
134139
fi
135140
dotnet nuget push ./artifacts/*.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate
141+
dotnet nuget push ./artifacts/*.snupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate

.github/workflows/stable-release.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,16 @@ jobs:
6363
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
6464
echo "Releasing version: $VERSION"
6565
66-
- name: Pack
66+
- name: Pack CLI
6767
env:
6868
VERSION: ${{ steps.version.outputs.version }}
6969
run: dotnet pack src/ServiceBusToolset.CLI -c Release --no-build -o ./artifacts -p:Version="$VERSION"
7070

71+
- name: Pack Application
72+
env:
73+
VERSION: ${{ steps.version.outputs.version }}
74+
run: dotnet pack src/ServiceBusToolset.Application -c Release --no-build -o ./artifacts -p:Version="$VERSION"
75+
7176
- name: Generate SBOM
7277
env:
7378
VERSION: ${{ steps.version.outputs.version }}
@@ -91,6 +96,7 @@ jobs:
9196
exit 1
9297
fi
9398
dotnet nuget push ./artifacts/*.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate
99+
dotnet nuget push ./artifacts/*.snupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate
94100
95101
- name: Upload package to GitHub Release
96102
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1

docs/integration-tests/integration_test_strategy.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ Each test instance builds a fresh `ServiceCollection`. This mirrors how the prod
172172
var services = new ServiceCollection();
173173

174174
// 1. Register the full Application layer — Mediator pipeline, DlqMessageService, IAppInsightsService
175-
services.AddApplication();
175+
services.AddServiceBusToolsetApplication();
176176

177177
// 2. Replace the client factory with the emulator-backed implementation
178178
services.AddSingleton<IServiceBusClientFactory>(
@@ -384,7 +384,7 @@ public class DiagnoseDlqIntegrationShould(ServiceBusEmulatorFixture fixture)
384384
}
385385
```
386386

387-
**How DI override order works:** `AddApplication()` registers `IAppInsightsService` as scoped (via `services.AddScoped<IAppInsightsService, AppInsightsService>()`). The `configureServices` delegate runs *after* `AddApplication()` and registers an NSubstitute singleton for the same interface. Microsoft's DI container resolves the *last* registration for a given service type, so the mock wins. The real `AppInsightsService` (which requires Azure credentials) is never instantiated.
387+
**How DI override order works:** `AddServiceBusToolsetApplication()` registers `IAppInsightsService` as scoped (via `services.AddScoped<IAppInsightsService, AppInsightsService>()`). The `configureServices` delegate runs *after* `AddServiceBusToolsetApplication()` and registers an NSubstitute singleton for the same interface. Microsoft's DI container resolves the *last* registration for a given service type, so the mock wins. The real `AppInsightsService` (which requires Azure credentials) is never instantiated.
388388

389389
### Complete Test Example
390390

src/ServiceBusToolset.Application/ApplicationDependencyInjectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace ServiceBusToolset.Application;
66

77
public static class ApplicationDependencyInjectionExtensions
88
{
9-
public static IServiceCollection AddApplication(this IServiceCollection services)
9+
public static IServiceCollection AddServiceBusToolsetApplication(this IServiceCollection services)
1010
{
1111
services.AddMediator(options =>
1212
{

src/ServiceBusToolset.Application/DeadLetters/DiagnoseDlq/Common/MessageDiagnostics.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,25 @@ internal static class MessageDiagnostics
5858
return (results, skipped);
5959
}
6060

61+
public static List<DiagnosticResult> CreateBasicResults(IReadOnlyList<ServiceBusReceivedMessage> messages)
62+
{
63+
var results = new List<DiagnosticResult>(messages.Count);
64+
foreach (var message in messages)
65+
{
66+
results.Add(new DiagnosticResult
67+
{
68+
MessageId = message.MessageId,
69+
Subject = message.Subject,
70+
DeadLetterReason = message.DeadLetterReason,
71+
Body = TryDecodeBody(message),
72+
EnqueuedTime = message.EnqueuedTime,
73+
OperationId = ExtractOperationId(message)
74+
});
75+
}
76+
77+
return results;
78+
}
79+
6180
public static string? ExtractOperationId(ServiceBusReceivedMessage message)
6281
{
6382
if (message.ApplicationProperties.TryGetValue("Diagnostic-Id", out var diagnosticId) &&
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Ardalis.Result;
2+
using Mediator;
3+
using ServiceBusToolset.Application.DeadLetters.DiagnoseDlq.Models;
4+
5+
namespace ServiceBusToolset.Application.DeadLetters.DiagnoseDlq;
6+
7+
public sealed record DiagnoseBatchCommand(string AppInsightsResourceId,
8+
IReadOnlyList<OperationInfo> Operations) : ICommand<Result<IReadOnlyList<DiagnosticResult>>>;
9+
10+
public sealed record OperationInfo(string OperationId,
11+
DateTimeOffset EnqueuedTime,
12+
string? MessageId,
13+
string? Subject,
14+
string? DeadLetterReason);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using Ardalis.Result;
2+
using Mediator;
3+
using ServiceBusToolset.Application.DeadLetters.DiagnoseDlq.Common.AppInsights;
4+
using ServiceBusToolset.Application.DeadLetters.DiagnoseDlq.Models;
5+
6+
namespace ServiceBusToolset.Application.DeadLetters.DiagnoseDlq;
7+
8+
public sealed class DiagnoseBatchCommandHandler(IAppInsightsService appInsightsService)
9+
: ICommandHandler<DiagnoseBatchCommand, Result<IReadOnlyList<DiagnosticResult>>>
10+
{
11+
public async ValueTask<Result<IReadOnlyList<DiagnosticResult>>> Handle(
12+
DiagnoseBatchCommand command,
13+
CancellationToken cancellationToken)
14+
{
15+
if (command.Operations.Count == 0)
16+
{
17+
return Result.Success<IReadOnlyList<DiagnosticResult>>([]);
18+
}
19+
20+
appInsightsService.Initialize(command.AppInsightsResourceId);
21+
22+
var operations = command.Operations
23+
.Select(op => (op.OperationId, op.EnqueuedTime))
24+
.ToList();
25+
26+
var diagnosticResults = await appInsightsService.DiagnoseBatchAsync(operations,
27+
null,
28+
cancellationToken);
29+
30+
var results = new List<DiagnosticResult>();
31+
// Deduplicate by OperationId — take the first occurrence if duplicates exist
32+
var operationsById = command.Operations
33+
.DistinctBy(op => op.OperationId)
34+
.ToDictionary(op => op.OperationId);
35+
36+
foreach (var (operationId, result) in diagnosticResults)
37+
{
38+
if (operationsById.TryGetValue(operationId, out var operation))
39+
{
40+
result.MessageId = operation.MessageId;
41+
result.Subject = operation.Subject;
42+
result.DeadLetterReason = operation.DeadLetterReason;
43+
results.Add(result);
44+
}
45+
}
46+
47+
return Result.Success<IReadOnlyList<DiagnosticResult>>(results);
48+
}
49+
}

src/ServiceBusToolset.Application/DeadLetters/DiagnoseDlq/DiagnoseDlqCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace ServiceBusToolset.Application.DeadLetters.DiagnoseDlq;
77

88
public sealed record DiagnoseDlqCommand(string FullyQualifiedNamespace,
99
EntityTarget Target,
10-
string AppInsightsResourceId,
10+
string? AppInsightsResourceId,
1111
int MaxMessages,
1212
DateTimeOffset? BeforeTime = null,
1313
IReadOnlySet<DlqCategoryKey>? CategoryFilter = null,

src/ServiceBusToolset.Application/DeadLetters/DiagnoseDlq/DiagnoseDlqCommandHandler.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using ServiceBusToolset.Application.DeadLetters.Common;
66
using ServiceBusToolset.Application.DeadLetters.DiagnoseDlq.Common;
77
using ServiceBusToolset.Application.DeadLetters.DiagnoseDlq.Common.AppInsights;
8+
using ServiceBusToolset.Application.DeadLetters.DiagnoseDlq.Models;
89

910
namespace ServiceBusToolset.Application.DeadLetters.DiagnoseDlq;
1011

@@ -28,8 +29,10 @@ public async ValueTask<Result<DiagnoseDlqResult>> Handle(
2829
DiagnoseDlqCommand command,
2930
CancellationToken cancellationToken)
3031
{
31-
// Initialize Application Insights connection
32-
appInsightsService.Initialize(command.AppInsightsResourceId);
32+
if (!string.IsNullOrEmpty(command.AppInsightsResourceId))
33+
{
34+
appInsightsService.Initialize(command.AppInsightsResourceId);
35+
}
3336

3437
await using var client = clientFactory.CreateClient(command.FullyQualifiedNamespace);
3538
await using var receiver = ReceiverFactory.CreateDlqReceiver(client, command.Target);
@@ -60,11 +63,21 @@ public async ValueTask<Result<DiagnoseDlqResult>> Handle(
6063
0));
6164
}
6265

63-
// Diagnose messages
64-
var (results, skipped) = await MessageDiagnostics.DiagnoseMessagesAsync(appInsightsService,
66+
List<DiagnosticResult> results;
67+
int skipped;
68+
69+
if (string.IsNullOrEmpty(command.AppInsightsResourceId))
70+
{
71+
results = MessageDiagnostics.CreateBasicResults(filteredMessages);
72+
skipped = 0;
73+
}
74+
else
75+
{
76+
(results, skipped) = await MessageDiagnostics.DiagnoseMessagesAsync(appInsightsService,
6577
filteredMessages,
6678
command.BatchProgress,
6779
cancellationToken);
80+
}
6881

6982
var resultsWithTelemetry = results
7083
.Count(r => r.Exceptions.Count > 0 || r.Traces.Count > 0 || r.FailedDependencies.Count > 0);

src/ServiceBusToolset.Application/DeadLetters/DiagnoseDlq/DiagnoseFromCache.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
using Mediator;
44
using ServiceBusToolset.Application.DeadLetters.DiagnoseDlq.Common;
55
using ServiceBusToolset.Application.DeadLetters.DiagnoseDlq.Common.AppInsights;
6+
using ServiceBusToolset.Application.DeadLetters.DiagnoseDlq.Models;
67

78
namespace ServiceBusToolset.Application.DeadLetters.DiagnoseDlq;
89

9-
public sealed record DiagnoseFromCacheCommand(string AppInsightsResourceId,
10+
public sealed record DiagnoseFromCacheCommand(string? AppInsightsResourceId,
1011
IReadOnlyList<ServiceBusReceivedMessage> MessagesToDiagnose,
1112
IProgress<(int Current, int Total)>? BatchProgress = null) : ICommand<Result<DiagnoseDlqResult>>;
1213

@@ -17,7 +18,10 @@ public async ValueTask<Result<DiagnoseDlqResult>> Handle(
1718
DiagnoseFromCacheCommand command,
1819
CancellationToken cancellationToken)
1920
{
20-
appInsightsService.Initialize(command.AppInsightsResourceId);
21+
if (!string.IsNullOrEmpty(command.AppInsightsResourceId))
22+
{
23+
appInsightsService.Initialize(command.AppInsightsResourceId);
24+
}
2125

2226
var messages = command.MessagesToDiagnose.ToList();
2327

@@ -29,10 +33,21 @@ public async ValueTask<Result<DiagnoseDlqResult>> Handle(
2933
0));
3034
}
3135

32-
var (results, skipped) = await MessageDiagnostics.DiagnoseMessagesAsync(appInsightsService,
36+
List<DiagnosticResult> results;
37+
int skipped;
38+
39+
if (string.IsNullOrEmpty(command.AppInsightsResourceId))
40+
{
41+
results = MessageDiagnostics.CreateBasicResults(messages);
42+
skipped = 0;
43+
}
44+
else
45+
{
46+
(results, skipped) = await MessageDiagnostics.DiagnoseMessagesAsync(appInsightsService,
3347
messages,
3448
command.BatchProgress,
3549
cancellationToken);
50+
}
3651

3752
var resultsWithTelemetry = results
3853
.Count(r => r.Exceptions.Count > 0 || r.Traces.Count > 0 || r.FailedDependencies.Count > 0);

0 commit comments

Comments
 (0)