Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
29 changes: 29 additions & 0 deletions audit/repros/u13/extracted/NetEvolve.Pulse.Extensibility.nuspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>NetEvolve.Pulse.Extensibility</id>
<version>1.0.0</version>
<title>NetEvolve.Pulse.Extensibility</title>
<authors>NetEvolve, samtrion</authors>
<license type="expression">MIT</license>
<licenseUrl>https://licenses.nuget.org/MIT</licenseUrl>
<readme>README.md</readme>
<projectUrl>https://github.com/dailydevops/pulse</projectUrl>
<description>Extensibility contracts and abstractions for the Pulse CQRS mediator library. This package defines the core interfaces and abstractions required to implement the mediator pattern with Command Query Responsibility Segregation (CQRS) principles. It provides strongly-typed contracts for commands (ICommand, ICommandHandler), queries (IQuery), events (IEvent, IEventHandler), and request/response patterns (IRequest, IRequestHandler). The extensibility model includes interceptor interfaces (ICommandInterceptor, IQueryInterceptor, IEventInterceptor, IRequestInterceptor) for implementing cross-cutting concerns such as validation, logging, caching, authentication, and transaction management. This package is designed to be framework-agnostic and serves as the foundation for building testable, maintainable, and decoupled application architectures. Perfect for domain-driven design (DDD), clean architecture, and hexagonal architecture patterns where business logic needs to be isolated from infrastructure concerns.</description>
<releaseNotes>https://github.com/dailydevops/pulse/releases</releaseNotes>
<copyright>Copyright @ NetEvolve 2026</copyright>
<tags>netevolve cqrs mediator extensibility</tags>
<repository type="git" url="https://github.com/dailydevops/pulse.git" branch="refs/heads/audit/phase2-r01-U11-U15" commit="45e0681d88dc7ea4a033e415e92bd99bcbd174bc" />
<dependencies>
<group targetFramework="net10.0">
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.8" exclude="Build,Analyzers" />
</group>
<group targetFramework="net8.0">
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.8" exclude="Build,Analyzers" />
</group>
<group targetFramework="net9.0">
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.8" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>
</package>
51 changes: 51 additions & 0 deletions audit/repros/u13/verify.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# U13 — NuGet metadata: assert <icon> is present and <readme> is present in the generated nuspec.
# Run from repo root: pwsh audit/repros/u13/verify.ps1
#
# Expected state today:
# - On .NET SDK 10.x: FAILS on <icon> only (SDK auto-detects README.md adjacent to csproj).
# - On .NET SDK 8.x: FAILS on BOTH <icon> and <readme>.

$ErrorActionPreference = 'Stop'
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..\..\')
$projectPath = Join-Path $repoRoot 'src\NetEvolve.Pulse.Extensibility\NetEvolve.Pulse.Extensibility.csproj'
$outDir = Join-Path $PSScriptRoot 'pack-out'
$null = New-Item -ItemType Directory -Force -Path $outDir

# Build then pack (pack alone won't rebuild dependencies reliably)
& dotnet build $projectPath -c Release --nologo -v minimal | Out-Host
if ($LASTEXITCODE -ne 0) { throw "dotnet build failed" }

# Locate the produced nupkg
$nupkg = Get-ChildItem (Join-Path $repoRoot 'src\NetEvolve.Pulse.Extensibility\bin\Release') -Filter '*.nupkg' |
Where-Object { $_.Name -notlike '*.symbols.nupkg' } |
Select-Object -First 1
if (-not $nupkg) { throw "No .nupkg produced" }

# Extract the nuspec
$extracted = Join-Path $outDir 'extracted'
if (Test-Path $extracted) { Remove-Item -Recurse -Force $extracted }
$null = New-Item -ItemType Directory -Force -Path $extracted
Expand-Archive -Path $nupkg.FullName -DestinationPath $extracted -Force

$nuspecPath = Get-ChildItem $extracted -Filter '*.nuspec' | Select-Object -First 1
if (-not $nuspecPath) { throw "nuspec not found inside .nupkg" }

[xml]$nuspec = Get-Content $nuspecPath.FullName
$ns = New-Object System.Xml.XmlNamespaceManager($nuspec.NameTable)
$ns.AddNamespace('n', 'http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd')

$iconNode = $nuspec.SelectSingleNode('/n:package/n:metadata/n:icon', $ns)
$readmeNode = $nuspec.SelectSingleNode('/n:package/n:metadata/n:readme', $ns)

$failures = @()
if (-not $iconNode) { $failures += 'U13: <icon> missing from nuspec (logo.png at repo root is not packed).' }
if (-not $readmeNode) { $failures += 'U13: <readme> missing from nuspec.' }

if ($failures.Count -gt 0) {
Write-Host '--- U13 ASSERTION FAILURES ---' -ForegroundColor Red
$failures | ForEach-Object { Write-Host $_ -ForegroundColor Red }
exit 1
}

Write-Host 'U13: Both <icon> and <readme> are present in the nuspec.' -ForegroundColor Green
exit 0
35 changes: 35 additions & 0 deletions audit/repros/u14/build.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

Willkommen bei .NET 8.0!
---------------------
SDK-Version: 8.0.421

Telemetrie
---------
Die .NET-Tools erfassen Nutzungsdaten, damit wir die Plattform stetig verbessern können. Diese werden von Microsoft erfasst und mit der Community geteilt. Sie können das Erfassen von Telemetriedaten deaktivieren, indem Sie die Umgebungsvariable DOTNET_CLI_TELEMETRY_OPTOUT in Ihrer bevorzugten Shell auf "1" oder TRUE festlegen.

Weitere Informationen zu Telemetriedaten in .NET-CLI-Tools finden Sie hier: https://aka.ms/dotnet-cli-telemetry

----------------
Ein ASP.NET Core-HTTPS-Entwicklungszertifikat installiert.
Um dem Zertifikat zu vertrauen, führen Sie "dotnet dev-certs https --trust" aus
Informationen zu HTTPS: https://aka.ms/dotnet-https

----------------
Schreiben Sie Ihre erste App: https://aka.ms/dotnet-hello-world
Neuigkeiten: https://aka.ms/dotnet-whats-new
Dokumentation: https://aka.ms/dotnet-docs
Probleme melden und Quelle in GitHub suchen: https://github.com/dotnet/core
verwenden Sie "dotnet --help", um verfügbare Befehle anzuzeigen, oder besuchen Sie https://aka.ms/dotnet-cli
--------------------------------------------------------------------------------------
Wiederherzustellende Projekte werden ermittelt...
C:\Program Files\dotnet\sdk\8.0.421\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.TargetFrameworkInference.targets(166,5): error NETSDK1045: Das aktuelle .NET SDK unterstützt .NET 9.0 nicht als Ziel. Geben Sie entweder .NET 8.0 oder niedriger als Ziel ein, oder verwenden Sie eine Version des .NET SDK, die .NET 9.0 unterstützt. .NET SDK von https://aka.ms/dotnet/download herunterladen [D:\sources\dailydevops\pulse\.claude\worktrees\agent-aeb9770965eb05553\src\NetEvolve.Pulse.Extensibility\NetEvolve.Pulse.Extensibility.csproj::TargetFramework=net9.0]
C:\Program Files\dotnet\sdk\8.0.421\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.TargetFrameworkInference.targets(166,5): error NETSDK1045: Das aktuelle .NET SDK unterstützt .NET 9.0 nicht als Ziel. Geben Sie entweder .NET 8.0 oder niedriger als Ziel ein, oder verwenden Sie eine Version des .NET SDK, die .NET 9.0 unterstützt. .NET SDK von https://aka.ms/dotnet/download herunterladen [D:\sources\dailydevops\pulse\.claude\worktrees\agent-aeb9770965eb05553\src\NetEvolve.Pulse\NetEvolve.Pulse.csproj::TargetFramework=net9.0]

Fehler beim Buildvorgang.

C:\Program Files\dotnet\sdk\8.0.421\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.TargetFrameworkInference.targets(166,5): error NETSDK1045: Das aktuelle .NET SDK unterstützt .NET 9.0 nicht als Ziel. Geben Sie entweder .NET 8.0 oder niedriger als Ziel ein, oder verwenden Sie eine Version des .NET SDK, die .NET 9.0 unterstützt. .NET SDK von https://aka.ms/dotnet/download herunterladen [D:\sources\dailydevops\pulse\.claude\worktrees\agent-aeb9770965eb05553\src\NetEvolve.Pulse.Extensibility\NetEvolve.Pulse.Extensibility.csproj::TargetFramework=net9.0]
C:\Program Files\dotnet\sdk\8.0.421\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.TargetFrameworkInference.targets(166,5): error NETSDK1045: Das aktuelle .NET SDK unterstützt .NET 9.0 nicht als Ziel. Geben Sie entweder .NET 8.0 oder niedriger als Ziel ein, oder verwenden Sie eine Version des .NET SDK, die .NET 9.0 unterstützt. .NET SDK von https://aka.ms/dotnet/download herunterladen [D:\sources\dailydevops\pulse\.claude\worktrees\agent-aeb9770965eb05553\src\NetEvolve.Pulse\NetEvolve.Pulse.csproj::TargetFramework=net9.0]
0 Warnung(en)
2 Fehler

Verstrichene Zeit 00:00:16.95
9 changes: 9 additions & 0 deletions audit/repros/u14/global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"sdk": {
"version": "8.0.421",
"rollForward": "disable"
},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
55 changes: 55 additions & 0 deletions audit/repros/u14/verify.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# U14 — Pin .NET 8 SDK at the repo root and attempt to build NetEvolve.Pulse.csproj.
# Expected: build FAILS with CS1003/CS8400/CS9999 on extension(...) member blocks.
# Run from repo root: pwsh audit/repros/u14/verify.ps1
#
# This script:
# 1. Backs up the existing global.json at repo root.
# 2. Copies the pinned-to-net8 global.json into the repo root.
# 3. Runs `dotnet build` on the core NetEvolve.Pulse project.
# 4. Restores the original global.json regardless of outcome.
# 5. Exits 0 if build succeeds (assumption REFUTED), exits 1 if build fails (assumption CONFIRMED).

$ErrorActionPreference = 'Stop'
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..\..\')
$originalGlobal = Join-Path $repoRoot 'global.json'
$pinnedGlobal = Join-Path $PSScriptRoot 'global.json'
$backupGlobal = Join-Path $PSScriptRoot 'global.json.repo-backup'

# Sanity check: .NET 8 SDK must be installed.
$sdks = & dotnet --list-sdks
if (-not ($sdks -match '^8\.')) {
Write-Host 'U14 SKIPPED: .NET 8 SDK not installed on this machine.' -ForegroundColor Yellow
exit 2
}

try {
if (Test-Path $originalGlobal) {
Copy-Item -Path $originalGlobal -Destination $backupGlobal -Force
}
Copy-Item -Path $pinnedGlobal -Destination $originalGlobal -Force

Push-Location $repoRoot
try {
& dotnet --version | Out-Host
& dotnet build src\NetEvolve.Pulse\NetEvolve.Pulse.csproj --nologo -v minimal 2>&1 | Tee-Object -FilePath (Join-Path $PSScriptRoot 'build.log') | Out-Host
$exit = $LASTEXITCODE
}
finally {
Pop-Location
}

if ($exit -eq 0) {
Write-Host 'U14: BUILD SUCCEEDED on .NET 8 SDK — assumption REFUTED.' -ForegroundColor Green
exit 0
}
else {
Write-Host "U14: BUILD FAILED on .NET 8 SDK (exit=$exit) — assumption CONFIRMED." -ForegroundColor Red
exit 1
}
}
finally {
if (Test-Path $backupGlobal) {
Copy-Item -Path $backupGlobal -Destination $originalGlobal -Force
Remove-Item -Path $backupGlobal -Force
}
}
128 changes: 128 additions & 0 deletions audit/verification/round-01-U11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# U11 Verification

**Status:** CONFIRMED

**Evidence:**
- `src/NetEvolve.Pulse.AzureServiceBus/AzureServiceBusExtensions.cs:51-58` — silently `Remove()`s any prior `IMessageTransport` descriptor and adds the ASB transport with no diagnostic, log, or warning.
- `src/NetEvolve.Pulse.Kafka/KafkaExtensions.cs:50-57` — identical silent remove-and-replace pattern.
- `src/NetEvolve.Pulse.RabbitMQ/RabbitMqExtensions.cs:53-60` — same pattern (verified by inspection).
- `src/NetEvolve.Pulse/OutboxExtensions.cs:94-101` — `UseMessageTransport<T>()` does the same.

**Reasoning:**
All four `Use*Transport` extensions linear-scan the `IServiceCollection` for an existing `IMessageTransport` descriptor and call `services.Remove(existing)` without logging, throwing, or surfacing any diagnostic. Calling `UseAzureServiceBusTransport(...)` followed by `UseKafkaTransport()` (or any other order/combination) leaves exactly one transport — the last one registered — and the consumer has no way to learn this short of inspecting the `IServiceCollection` themselves. The existing test `UseAzureServiceBusTransport_replaces_existing_transport` (line 116-126) actually asserts the silent-overwrite behavior as if it were correct. The failing test below registers BOTH ASB and Kafka transports on the same builder and asserts that a diagnostic surface (logged warning, thrown exception, or `IOptions`-backed flag) exists — currently nothing is emitted.

**Failing test (if confirmed):**
- Path: `tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusExtensionsTests.cs` (added `TransportOverwriteDiagnosticTests` class) and equivalent under Kafka folder for completeness — but the canonical repro is placed at `tests/NetEvolve.Pulse.Tests.Unit/Outbox/TransportOverwriteDiagnosticTests.cs`.
- Status: written
- Test code:
```csharp
namespace NetEvolve.Pulse.Tests.Unit.Outbox;

using Confluent.Kafka;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NetEvolve.Extensions.TUnit;
using NetEvolve.Pulse;
using NetEvolve.Pulse.Extensibility;
using NetEvolve.Pulse.Extensibility.Outbox;
using TUnit.Core;

// U11 — Registering two transports on the same builder must surface a diagnostic.
// Today UseAzureServiceBusTransport followed by UseKafkaTransport silently drops the ASB transport.
[TestGroup("U11")]
public sealed class TransportOverwriteDiagnosticTests
{
private const string FakeAsbConnectionString =
"Endpoint=sb://localhost/;SharedAccessKeyName=Root;SharedAccessKey=Fake=";

[Test]
public async Task Registering_Both_AzureServiceBus_And_Kafka_Should_Surface_Diagnostic()
{
// Arrange
var loggerProvider = new CapturingLoggerProvider();
IServiceCollection services = new ServiceCollection();
_ = services.AddLogging(builder => builder.AddProvider(loggerProvider));

// Pre-register fake Kafka prerequisites so KafkaMessageTransport can resolve.
_ = services.AddSingleton<IProducer<string, string>>(_ => new ProducerBuilder<string, string>(
new ProducerConfig { BootstrapServers = "localhost:9092" }).Build());
_ = services.AddSingleton<IAdminClient>(_ => new AdminClientBuilder(
new AdminClientConfig { BootstrapServers = "localhost:9092" }).Build());

// Act — last write wins today; we want either an exception OR a logged warning.
_ = services.AddPulse(config =>
{
_ = config.UseAzureServiceBusTransport(o => o.ConnectionString = FakeAsbConnectionString);
_ = config.UseKafkaTransport();
});

// Assert — at least one of the following diagnostic surfaces must exist:
// (a) a Warning-or-higher log entry referencing IMessageTransport / overwrite, OR
// (b) UseKafkaTransport threw, OR
// (c) both registrations remain (so the user can detect the conflict via DI).
var transportDescriptorCount = services.Count(d => d.ServiceType == typeof(IMessageTransport));
var warningLogged = loggerProvider.Entries.Exists(e =>
e.Level >= LogLevel.Warning
&& (e.Message.Contains("IMessageTransport", System.StringComparison.OrdinalIgnoreCase)
|| e.Message.Contains("overwrit", System.StringComparison.OrdinalIgnoreCase)
|| e.Message.Contains("replac", System.StringComparison.OrdinalIgnoreCase)));

var diagnosticSurfaced = warningLogged || transportDescriptorCount > 1;

_ = await Assert.That(diagnosticSurfaced)
.IsTrue()
.Because(
"Registering two transports must not silently drop the first; "
+ $"got {transportDescriptorCount} IMessageTransport descriptor(s) and "
+ $"{loggerProvider.Entries.Count} log entries (none at Warning+ mentioning transport overwrite).");
}

private sealed class CapturingLoggerProvider : ILoggerProvider
{
public List<LogEntry> Entries { get; } = new();

public ILogger CreateLogger(string categoryName) => new CapturingLogger(categoryName, Entries);

public void Dispose() { }

public sealed record LogEntry(string Category, LogLevel Level, string Message);

private sealed class CapturingLogger : ILogger
{
private readonly string _category;
private readonly List<LogEntry> _entries;

public CapturingLogger(string category, List<LogEntry> entries)
{
_category = category;
_entries = entries;
}

public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;

public bool IsEnabled(LogLevel logLevel) => true;

public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
_entries.Add(new LogEntry(_category, logLevel, formatter(state, exception)));
}

private sealed class NullScope : IDisposable
{
public static readonly NullScope Instance = new();
public void Dispose() { }
}
}
}
}
```

**Notes:**
- The existing test `UseAzureServiceBusTransport_replaces_existing_transport` at `tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusExtensionsTests.cs:116-126` essentially codifies the current footgun. Phase 3 should decide whether the desired behavior is: (a) throw when a second transport is registered, (b) keep both and let consumer pick via keyed services, or (c) log a Warning with a clear message.
- Same pattern exists in `RabbitMqExtensions.cs:53-60` and the generic `UseMessageTransport<T>` in `src/NetEvolve.Pulse/OutboxExtensions.cs:94-101`.
- The repro intentionally uses fake Kafka prerequisites so it can run as a unit test without a broker. `BuildServiceProvider()` is not invoked because the assertion is about the diagnostic surface emitted by `Use*Transport`, not transport instantiation.
Loading
Loading