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..f4af46c 100644 --- a/test/MinimalWorker.Test/Fakes/FakeRepositories.cs +++ b/test/MinimalWorker.Test/Fakes/FakeRepositories.cs @@ -22,6 +22,53 @@ 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 IMultipleConsumer<string, 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>. +/// +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..197f9af 100644 --- a/test/MinimalWorker.Test/ServiceResolutionTests.cs +++ b/test/MinimalWorker.Test/ServiceResolutionTests.cs @@ -147,4 +147,95 @@ 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_Services1() + { + // 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; + + 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); + }); + } + + [Fact] + public async Task BackgroundWorker_Should_Resolve_MultiTypeArgument_Generic_Services2() + { + // Arrange + 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, TestConstants.MinContinuousExecutions, TestConstants.MaxContinuousExecutions); + Assert.All(consumedItems, item => + { + Assert.StartsWith("Key_", item.Key); + Assert.StartsWith("Value_", item.Value); + Assert.StartsWith("Extra_", item.Extra); + }); + } }