From e58f39c1a9315ee19a7bcd74b5549134020351ef Mon Sep 17 00:00:00 2001 From: Joshua Ryder Date: Mon, 19 Jan 2026 20:03:52 +0100 Subject: [PATCH 1/8] bugfix --- src/MinimalWorker.Generators/WorkerEmitter.cs | 5 +- .../Fakes/FakeRepositories.cs | 23 +++++++++ .../ServiceResolutionTests.cs | 49 +++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/MinimalWorker.Generators/WorkerEmitter.cs b/src/MinimalWorker.Generators/WorkerEmitter.cs index 151a5b2..af49597 100644 --- a/src/MinimalWorker.Generators/WorkerEmitter.cs +++ b/src/MinimalWorker.Generators/WorkerEmitter.cs @@ -316,8 +316,9 @@ private static void EmitWorkerExtension(StringBuilder sb, List(); foreach (var worker in workers) { - // Build signature from worker parameters - strip global:: prefix to match runtime format - var paramTypes = string.Join(",", worker.Parameters.Select(p => p.Type.Replace("global::", ""))); + // Build signature from worker parameters - strip global:: prefix and normalize spacing to match runtime format + // Runtime uses FormatTypeName which joins generic args with "," (no space), so we must do the same + var paramTypes = string.Join(",", worker.Parameters.Select(p => p.Type.Replace("global::", "").Replace(", ", ","))); var signature = $"{worker.Type}:{paramTypes}"; if (!workerMap.ContainsKey(signature)) diff --git a/test/MinimalWorker.Test/Fakes/FakeRepositories.cs b/test/MinimalWorker.Test/Fakes/FakeRepositories.cs index 2412853..f944048 100644 --- a/test/MinimalWorker.Test/Fakes/FakeRepositories.cs +++ b/test/MinimalWorker.Test/Fakes/FakeRepositories.cs @@ -22,6 +22,29 @@ public Task GetAsync() } } +/// +/// Generic interface with multiple type parameters to test signature matching. +/// This simulates interfaces like Kafka's IConsumer<TKey, TValue>. +/// +public interface IConsumer +{ + Task<(TKey Key, TValue Value)> ConsumeAsync(CancellationToken token); +} + +/// +/// Implementation of IConsumer<string, string> for testing multi-type-argument generics. +/// +public class StringStringConsumer : IConsumer +{ + private int _counter; + + public Task<(string Key, string Value)> ConsumeAsync(CancellationToken token) + { + var count = Interlocked.Increment(ref _counter); + return Task.FromResult(($"Key_{count}", $"Value_{count}")); + } +} + /// /// Interface for testing fail-fast behavior when dependencies are missing. /// This should NOT be registered in DI to test missing dependency detection. diff --git a/test/MinimalWorker.Test/ServiceResolutionTests.cs b/test/MinimalWorker.Test/ServiceResolutionTests.cs index e052a55..22c0c60 100644 --- a/test/MinimalWorker.Test/ServiceResolutionTests.cs +++ b/test/MinimalWorker.Test/ServiceResolutionTests.cs @@ -147,4 +147,53 @@ public async Task BackgroundWorker_Should_Resolve_Transient_Services() // All iterations use the same injected instance (resolved once when worker started) Assert.Single(instanceIds.Distinct()); } + + [Fact] + public async Task BackgroundWorker_Should_Resolve_MultiTypeArgument_Generic_Services() + { + // Arrange + // This test verifies that generic services with multiple type arguments (like IConsumer) + // are correctly matched between compile-time (source generator) and runtime signatures. + // The source generator uses ToDisplayString which produces "IConsumer" (with space) + // while runtime FormatTypeName produces "IConsumer" (no space). + // This test ensures the signature normalization works correctly. + BackgroundWorkerExtensions.ClearRegistrations(); + var consumedItems = new System.Collections.Concurrent.ConcurrentBag<(string Key, string Value)>(); + Exception? workerException = null; + + using var host = Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddSingleton, StringStringConsumer>(); + }) + .Build(); + + host.RunBackgroundWorker(async (IConsumer consumer, CancellationToken token) => + { + var item = await consumer.ConsumeAsync(token); + consumedItems.Add(item); + await Task.Delay(10, token); + }) + .WithErrorHandler(ex => + { + workerException = ex; + }); + + // Act + await host.StartAsync(); + await Task.Delay(100); + await host.StopAsync(); + + // Assert + if (workerException != null) + { + throw new Exception($"Worker failed: {workerException.Message}", workerException); + } + Assert.InRange(consumedItems.Count, TestConstants.MinContinuousExecutions, TestConstants.MaxContinuousExecutions); + Assert.All(consumedItems, item => + { + Assert.StartsWith("Key_", item.Key); + Assert.StartsWith("Value_", item.Value); + }); + } } From 8f9c19c76f96e0f6862ce95cbf7fd89701c20eaa Mon Sep 17 00:00:00 2001 From: Joshua Ryder Date: Mon, 19 Jan 2026 21:49:10 +0100 Subject: [PATCH 2/8] add extra tests --- .../Fakes/FakeRepositories.cs | 24 +++++++++ .../ServiceResolutionTests.cs | 53 ++++++++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/test/MinimalWorker.Test/Fakes/FakeRepositories.cs b/test/MinimalWorker.Test/Fakes/FakeRepositories.cs index f944048..92dcd18 100644 --- a/test/MinimalWorker.Test/Fakes/FakeRepositories.cs +++ b/test/MinimalWorker.Test/Fakes/FakeRepositories.cs @@ -22,6 +22,30 @@ public Task GetAsync() } } +/// +/// Generic interface with multiple type parameters to test signature matching. +/// This simulates interfaces like Kafka's IConsumer<TKey, TValue>. +/// +public interface IMultipleConsumer +{ + Task<(TKey Key, TValue Value, TExtra Extra)> ConsumeAsync(CancellationToken token); +} + +/// +/// Implementation of IConsumer<string, string> for testing multi-type-argument generics. +/// +public class StringStringStringConsumer : IMultipleConsumer +{ + private int _counter; + + public Task<(string Key, string Value, string Extra)> ConsumeAsync(CancellationToken token) + { + var count = Interlocked.Increment(ref _counter); + return Task.FromResult(($"Key_{count}", $"Value_{count}", $"Extra_{count}")); + } +} + + /// /// Generic interface with multiple type parameters to test signature matching. /// This simulates interfaces like Kafka's IConsumer<TKey, TValue>. diff --git a/test/MinimalWorker.Test/ServiceResolutionTests.cs b/test/MinimalWorker.Test/ServiceResolutionTests.cs index 22c0c60..5f5cf8c 100644 --- a/test/MinimalWorker.Test/ServiceResolutionTests.cs +++ b/test/MinimalWorker.Test/ServiceResolutionTests.cs @@ -149,7 +149,7 @@ public async Task BackgroundWorker_Should_Resolve_Transient_Services() } [Fact] - public async Task BackgroundWorker_Should_Resolve_MultiTypeArgument_Generic_Services() + public async Task BackgroundWorker_Should_Resolve_MultiTypeArgument_Generic_Services1() { // Arrange // This test verifies that generic services with multiple type arguments (like IConsumer) @@ -189,7 +189,56 @@ public async Task BackgroundWorker_Should_Resolve_MultiTypeArgument_Generic_Serv { throw new Exception($"Worker failed: {workerException.Message}", workerException); } - Assert.InRange(consumedItems.Count, TestConstants.MinContinuousExecutions, TestConstants.MaxContinuousExecutions); + Assert.InRange(consumedItems.Count, 9, 10); + Assert.All(consumedItems, item => + { + Assert.StartsWith("Key_", item.Key); + Assert.StartsWith("Value_", item.Value); + }); + } + + [Fact] + public async Task BackgroundWorker_Should_Resolve_MultiTypeArgument_Generic_Services2() + { + // Arrange + // This test verifies that generic services with multiple type arguments (like IConsumer) + // are correctly matched between compile-time (source generator) and runtime signatures. + // The source generator uses ToDisplayString which produces "IConsumer" (with space) + // while runtime FormatTypeName produces "IConsumer" (no space). + // This test ensures the signature normalization works correctly. + BackgroundWorkerExtensions.ClearRegistrations(); + var consumedItems = new System.Collections.Concurrent.ConcurrentBag<(string Key, string Value, string Extra)>(); + Exception? workerException = null; + + using var host = Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddSingleton, StringStringStringConsumer>(); + }) + .Build(); + + host.RunBackgroundWorker(async (IMultipleConsumer consumer, CancellationToken token) => + { + var item = await consumer.ConsumeAsync(token); + consumedItems.Add(item); + await Task.Delay(10, token); + }) + .WithErrorHandler(ex => + { + workerException = ex; + }); + + // Act + await host.StartAsync(); + await Task.Delay(100); + await host.StopAsync(); + + // Assert + if (workerException != null) + { + throw new Exception($"Worker failed: {workerException.Message}", workerException); + } + Assert.InRange(consumedItems.Count, 9, 10); Assert.All(consumedItems, item => { Assert.StartsWith("Key_", item.Key); From 3bc86af21374a69d0395dc65cdbabbcf04c22f60 Mon Sep 17 00:00:00 2001 From: Joshua Ryder Date: Mon, 19 Jan 2026 21:50:14 +0100 Subject: [PATCH 3/8] documentation --- test/MinimalWorker.Test/ServiceResolutionTests.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/MinimalWorker.Test/ServiceResolutionTests.cs b/test/MinimalWorker.Test/ServiceResolutionTests.cs index 5f5cf8c..bfff184 100644 --- a/test/MinimalWorker.Test/ServiceResolutionTests.cs +++ b/test/MinimalWorker.Test/ServiceResolutionTests.cs @@ -153,10 +153,6 @@ public async Task BackgroundWorker_Should_Resolve_MultiTypeArgument_Generic_Serv { // Arrange // This test verifies that generic services with multiple type arguments (like IConsumer) - // are correctly matched between compile-time (source generator) and runtime signatures. - // The source generator uses ToDisplayString which produces "IConsumer" (with space) - // while runtime FormatTypeName produces "IConsumer" (no space). - // This test ensures the signature normalization works correctly. BackgroundWorkerExtensions.ClearRegistrations(); var consumedItems = new System.Collections.Concurrent.ConcurrentBag<(string Key, string Value)>(); Exception? workerException = null; @@ -201,11 +197,6 @@ public async Task BackgroundWorker_Should_Resolve_MultiTypeArgument_Generic_Serv public async Task BackgroundWorker_Should_Resolve_MultiTypeArgument_Generic_Services2() { // Arrange - // This test verifies that generic services with multiple type arguments (like IConsumer) - // are correctly matched between compile-time (source generator) and runtime signatures. - // The source generator uses ToDisplayString which produces "IConsumer" (with space) - // while runtime FormatTypeName produces "IConsumer" (no space). - // This test ensures the signature normalization works correctly. BackgroundWorkerExtensions.ClearRegistrations(); var consumedItems = new System.Collections.Concurrent.ConcurrentBag<(string Key, string Value, string Extra)>(); Exception? workerException = null; From 89cb7648fd8dd9e562eab7c573e852e53d9b4b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Jesper=20Kr=C3=A6gp=C3=B8th=20Ryder?= Date: Mon, 19 Jan 2026 21:54:52 +0100 Subject: [PATCH 4/8] Update test/MinimalWorker.Test/Fakes/FakeRepositories.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/MinimalWorker.Test/Fakes/FakeRepositories.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/MinimalWorker.Test/Fakes/FakeRepositories.cs b/test/MinimalWorker.Test/Fakes/FakeRepositories.cs index 92dcd18..f4af46c 100644 --- a/test/MinimalWorker.Test/Fakes/FakeRepositories.cs +++ b/test/MinimalWorker.Test/Fakes/FakeRepositories.cs @@ -32,7 +32,7 @@ public interface IMultipleConsumer } /// -/// Implementation of IConsumer<string, string> for testing multi-type-argument generics. +/// Implementation of IMultipleConsumer<string, string, string> for testing multi-type-argument generics. /// public class StringStringStringConsumer : IMultipleConsumer { From 7c34d4e861a3bb0acd50fa1a3c2316088179f5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Jesper=20Kr=C3=A6gp=C3=B8th=20Ryder?= Date: Mon, 19 Jan 2026 21:55:17 +0100 Subject: [PATCH 5/8] Update test/MinimalWorker.Test/ServiceResolutionTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/MinimalWorker.Test/ServiceResolutionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/MinimalWorker.Test/ServiceResolutionTests.cs b/test/MinimalWorker.Test/ServiceResolutionTests.cs index bfff184..3440025 100644 --- a/test/MinimalWorker.Test/ServiceResolutionTests.cs +++ b/test/MinimalWorker.Test/ServiceResolutionTests.cs @@ -229,7 +229,7 @@ public async Task BackgroundWorker_Should_Resolve_MultiTypeArgument_Generic_Serv { throw new Exception($"Worker failed: {workerException.Message}", workerException); } - Assert.InRange(consumedItems.Count, 9, 10); + Assert.InRange(consumedItems.Count, TestConstants.MinContinuousExecutions, TestConstants.MaxContinuousExecutions); Assert.All(consumedItems, item => { Assert.StartsWith("Key_", item.Key); From 0fb5dd1467c15deee9726687a56f4571fbd7c615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Jesper=20Kr=C3=A6gp=C3=B8th=20Ryder?= Date: Mon, 19 Jan 2026 21:55:41 +0100 Subject: [PATCH 6/8] Update test/MinimalWorker.Test/ServiceResolutionTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/MinimalWorker.Test/ServiceResolutionTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/MinimalWorker.Test/ServiceResolutionTests.cs b/test/MinimalWorker.Test/ServiceResolutionTests.cs index 3440025..38e635b 100644 --- a/test/MinimalWorker.Test/ServiceResolutionTests.cs +++ b/test/MinimalWorker.Test/ServiceResolutionTests.cs @@ -153,6 +153,7 @@ public async Task BackgroundWorker_Should_Resolve_MultiTypeArgument_Generic_Serv { // Arrange // This test verifies that generic services with multiple type arguments (like IConsumer) + // are correctly resolved from the DI container and injected into background workers. BackgroundWorkerExtensions.ClearRegistrations(); var consumedItems = new System.Collections.Concurrent.ConcurrentBag<(string Key, string Value)>(); Exception? workerException = null; From 929d732bd264cabc34aa582a7d45430d3a4a55d9 Mon Sep 17 00:00:00 2001 From: Joshua Ryder Date: Mon, 19 Jan 2026 21:56:32 +0100 Subject: [PATCH 7/8] fix --- test/MinimalWorker.Test/ServiceResolutionTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/MinimalWorker.Test/ServiceResolutionTests.cs b/test/MinimalWorker.Test/ServiceResolutionTests.cs index 38e635b..5c8519b 100644 --- a/test/MinimalWorker.Test/ServiceResolutionTests.cs +++ b/test/MinimalWorker.Test/ServiceResolutionTests.cs @@ -235,6 +235,7 @@ public async Task BackgroundWorker_Should_Resolve_MultiTypeArgument_Generic_Serv { Assert.StartsWith("Key_", item.Key); Assert.StartsWith("Value_", item.Value); + Assert.StartsWith("Extra_", item.Extra); }); } } From 9778ec9a1d28fe1cf795ad14a37c10ea97bfce62 Mon Sep 17 00:00:00 2001 From: Joshua Ryder Date: Mon, 19 Jan 2026 21:58:11 +0100 Subject: [PATCH 8/8] fix --- test/MinimalWorker.Test/ServiceResolutionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/MinimalWorker.Test/ServiceResolutionTests.cs b/test/MinimalWorker.Test/ServiceResolutionTests.cs index 5c8519b..197f9af 100644 --- a/test/MinimalWorker.Test/ServiceResolutionTests.cs +++ b/test/MinimalWorker.Test/ServiceResolutionTests.cs @@ -186,7 +186,7 @@ public async Task BackgroundWorker_Should_Resolve_MultiTypeArgument_Generic_Serv { throw new Exception($"Worker failed: {workerException.Message}", workerException); } - Assert.InRange(consumedItems.Count, 9, 10); + Assert.InRange(consumedItems.Count, TestConstants.MinContinuousExecutions, TestConstants.MaxContinuousExecutions); Assert.All(consumedItems, item => { Assert.StartsWith("Key_", item.Key);