Skip to content
Merged
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
5 changes: 3 additions & 2 deletions src/MinimalWorker.Generators/WorkerEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,9 @@ private static void EmitWorkerExtension(StringBuilder sb, List<WorkerInvocationM
var workerMap = new Dictionary<string, WorkerInvocationModel>();
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))
Expand Down
47 changes: 47 additions & 0 deletions test/MinimalWorker.Test/Fakes/FakeRepositories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,53 @@ public Task<string> GetAsync()
}
}

/// <summary>
/// Generic interface with multiple type parameters to test signature matching.
/// This simulates interfaces like Kafka's IConsumer&lt;TKey, TValue&gt;.
/// </summary>
public interface IMultipleConsumer<TKey, TValue, TExtra>
{
Task<(TKey Key, TValue Value, TExtra Extra)> ConsumeAsync(CancellationToken token);
}

/// <summary>
/// Implementation of IMultipleConsumer&lt;string, string, string&gt; for testing multi-type-argument generics.
/// </summary>
public class StringStringStringConsumer : IMultipleConsumer<string, string, string>
{
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}"));
}
}


/// <summary>
/// Generic interface with multiple type parameters to test signature matching.
/// This simulates interfaces like Kafka's IConsumer&lt;TKey, TValue&gt;.
/// </summary>
public interface IConsumer<TKey, TValue>
{
Task<(TKey Key, TValue Value)> ConsumeAsync(CancellationToken token);
}

/// <summary>
/// Implementation of IConsumer&lt;string, string&gt; for testing multi-type-argument generics.
/// </summary>
public class StringStringConsumer : IConsumer<string, string>
{
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}"));
}
}

/// <summary>
/// Interface for testing fail-fast behavior when dependencies are missing.
/// This should NOT be registered in DI to test missing dependency detection.
Expand Down
91 changes: 91 additions & 0 deletions test/MinimalWorker.Test/ServiceResolutionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TKey, TValue>)
// 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<IConsumer<string, string>, StringStringConsumer>();
})
.Build();

host.RunBackgroundWorker(async (IConsumer<string, string> 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<IMultipleConsumer<string, string, string>, StringStringStringConsumer>();
})
.Build();

host.RunBackgroundWorker(async (IMultipleConsumer<string, string, string> 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);
});
}
}