Skip to content

Commit be54609

Browse files
Add multi-implementation To bindings and usage scenario
1 parent c559b70 commit be54609

10 files changed

Lines changed: 9400 additions & 758 deletions

File tree

‎src/Pure.DI.Core/Components/Api.g.cs‎

Lines changed: 9073 additions & 757 deletions
Large diffs are not rendered by default.

‎src/Pure.DI.Core/Components/Api.g.tt‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3151,6 +3151,40 @@ var allowsRefStructConstraints = string.Join(Environment.NewLine, GetSuffixes(0,
31513151
where T : allows ref struct
31523152
#endif
31533153
;
3154+
<#
3155+
for (var i = 2; i <= lifetimeBindsCount; i++)
3156+
{
3157+
var typeArgs = string.Join(", ", GetSuffixes(0, i - 1).Select(suffix => $"T{suffix}"));
3158+
var comments = string.Join(Environment.NewLine, GetSuffixes(0, i - 1).Select(suffix => $" /// <typeparam name=\"T{suffix}\">Implementation type {(suffix == "" ? "" : suffix + " ")}to bind.</typeparam>"));
3159+
var allowsRefStructBlock = GetAllowsRefStructBlock(GetSuffixes(0, i - 1).Select(suffix => $"T{suffix}"), " ");
3160+
#>
3161+
3162+
/// <summary>
3163+
/// Creates a separate binding for each implementation type, using the current contract(s), tags, and lifetime.
3164+
/// <example>
3165+
/// <code>
3166+
/// DI.Setup("Composition")
3167+
/// .Bind&lt;IService&gt;(Tag.Unique)
3168+
/// .As(Lifetime.Singleton)
3169+
/// .To&lt;Service1, Service2&gt;()
3170+
/// .Root&lt;IService[]&gt;("Services");
3171+
/// </code>
3172+
/// </example>
3173+
/// </summary>
3174+
<#=comments#>
3175+
/// <returns>Configuration interface for fluent chaining.</returns>
3176+
/// <remarks>
3177+
/// Use <see cref="Tag.Unique"/> to include every implementation in a collection.
3178+
/// Use <see cref="Tag.Type"/> to tag each binding with its implementation type for individual resolution.
3179+
/// With no tag or the same ordinary tag, later implementations override earlier ones and a warning is reported.
3180+
/// </remarks>
3181+
/// <seealso cref="IConfiguration.Bind{T}"/>
3182+
/// <seealso cref="As"/>
3183+
IConfiguration To<<#=typeArgs#>>()<#=allowsRefStructBlock#>
3184+
;
3185+
<#
3186+
}
3187+
#>
31543188

31553189
/// <summary>
31563190
/// Binds to a factory delegate; dependencies can be resolved via <see cref="IContext"/>.
@@ -3924,6 +3958,21 @@ var allowsRefStructConstraints = string.Join(Environment.NewLine, GetSuffixes(0,
39243958
{
39253959
return this;
39263960
}
3961+
<#
3962+
for (var i = 2; i <= lifetimeBindsCount; i++)
3963+
{
3964+
var typeArgs = string.Join(", ", GetSuffixes(0, i - 1).Select(suffix => $"T{suffix}"));
3965+
var allowsRefStructBlock = GetAllowsRefStructBlock(GetSuffixes(0, i - 1).Select(suffix => $"T{suffix}"), " ");
3966+
#>
3967+
3968+
/// <inheritdoc />
3969+
public IConfiguration To<<#=typeArgs#>>()<#=allowsRefStructBlock#>
3970+
{
3971+
return this;
3972+
}
3973+
<#
3974+
}
3975+
#>
39273976

39283977
/// <inheritdoc />
39293978
public IConfiguration To<T>(global::System.Func<IContext, T> factory)

‎src/Pure.DI.Core/Core/ApiInvocationProcessor.cs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,28 @@ MemberAccessExpressionSyntax memberAccess when memberAccess.Kind() == SyntaxKind
490490
metadataVisitor.VisitImplementation(new MdImplementation(semanticModel, invocation, implementationType));
491491
break;
492492

493+
// .To<T1, T2, ...>()
494+
case [_, _, ..] when toInvocationArgs is []:
495+
var method = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
496+
var implementations = new List<MdImplementation>(toInvocationTypeArgs.Count);
497+
for (var index = 0; index < toInvocationTypeArgs.Count; index++)
498+
{
499+
var typeArgument = toInvocationTypeArgs[index];
500+
var multiImplementationType = method?.TypeArguments.Length == toInvocationTypeArgs.Count
501+
&& method.TypeArguments[index] is INamedTypeSymbol namedType
502+
? namedType
503+
: semantic.GetTypeSymbol<INamedTypeSymbol>(semanticModel, typeArgument);
504+
if (typeArgument is NullableTypeSyntax && multiImplementationType.IsReferenceType)
505+
{
506+
multiImplementationType = (INamedTypeSymbol)multiImplementationType.WithNullableAnnotation(NullableAnnotation.Annotated);
507+
}
508+
509+
implementations.Add(new MdImplementation(semanticModel, typeArgument, multiImplementationType));
510+
}
511+
512+
metadataVisitor.VisitImplementations(implementations);
513+
break;
514+
493515
default:
494516
NotSupported(invocation);
495517
break;

‎src/Pure.DI.Core/Core/BindingBuilder.cs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,24 @@ public void AddContract(in MdContract contract) =>
7272
public void AddTag(in MdTag tag) =>
7373
_tags.Add(tag);
7474

75+
public void CopyConfigurationTo(IBindingBuilder target)
76+
{
77+
foreach (var contract in _contracts)
78+
{
79+
target.AddContract(contract);
80+
}
81+
82+
foreach (var tag in _tags)
83+
{
84+
target.AddTag(tag);
85+
}
86+
87+
if (_lifetime is {} lifetime)
88+
{
89+
target.Lifetime = lifetime;
90+
}
91+
}
92+
7593
public MdBinding Build(MdSetup setup)
7694
{
7795
if (_semanticModel is not {} semanticModel || _source is not {} source)

‎src/Pure.DI.Core/Core/IBindingBuilder.cs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,7 @@ interface IBindingBuilder
1616

1717
void AddTag(in MdTag tag);
1818

19+
void CopyConfigurationTo(IBindingBuilder target);
20+
1921
MdBinding Build(MdSetup setup);
20-
}
22+
}

‎src/Pure.DI.Core/Core/IMetadataVisitor.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ interface IMetadataVisitor
1414

1515
void VisitImplementation(in MdImplementation implementation);
1616

17+
void VisitImplementations(IReadOnlyList<MdImplementation> implementations);
18+
1719
void VisitFactory(in MdFactory factory);
1820

1921
void VisitResolver(in MdResolver resolver);

‎src/Pure.DI.Core/Core/MetadataWalkerBase.cs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ public virtual void VisitImplementation(in MdImplementation implementation)
9898
{
9999
}
100100

101+
public virtual void VisitImplementations(IReadOnlyList<MdImplementation> implementations)
102+
{
103+
foreach (var implementation in implementations)
104+
{
105+
VisitImplementation(implementation);
106+
}
107+
}
108+
101109
public virtual void VisitFactory(in MdFactory factory)
102110
{
103111
foreach (var md in factory.Resolvers)

‎src/Pure.DI.Core/Core/SetupsBuilder.cs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,19 @@ public void VisitImplementation(in MdImplementation implementation)
9696
FinishBinding();
9797
}
9898

99+
public void VisitImplementations(IReadOnlyList<MdImplementation> implementations)
100+
{
101+
for (var index = 0; index < implementations.Count; index++)
102+
{
103+
var currentBuilder = _bindingBuilder;
104+
VisitImplementation(implementations[index]);
105+
if (index < implementations.Count - 1)
106+
{
107+
currentBuilder.CopyConfigurationTo(_bindingBuilder);
108+
}
109+
}
110+
}
111+
99112
public void VisitFactory(in MdFactory factory)
100113
{
101114
_bindingBuilder.Factory = factory;
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
namespace Pure.DI.IntegrationTests;
2+
3+
using Core;
4+
5+
public class MultipleImplementationsTests
6+
{
7+
[Fact]
8+
public async Task ShouldWarnWhenMultipleImplementationsHaveNoTag()
9+
{
10+
var result = await """
11+
using System;
12+
using Pure.DI;
13+
14+
namespace Sample;
15+
16+
interface IService { }
17+
sealed class Service1 : IService { }
18+
sealed class Service2 : IService { }
19+
20+
partial class Composition
21+
{
22+
void Setup() => DI.Setup()
23+
.Bind<IService>().To<Service1, Service2>()
24+
.Root<IService>("Service");
25+
}
26+
27+
public class Program
28+
{
29+
public static void Main() => Console.WriteLine(new Composition().Service.GetType().Name);
30+
}
31+
""".RunAsync(new Options(LanguageVersion.CSharp12));
32+
33+
result.Errors.ShouldBeEmpty(result);
34+
result.StdOut.ShouldBe(["Service2"], result);
35+
result.Warnings.Count.ShouldBe(2, result);
36+
result.Warnings.Count(i => i.Id == LogId.WarningOverriddenBinding && i.Locations.FirstOrDefault().GetSource() == "Service2").ShouldBe(1, result);
37+
result.Warnings.Count(i => i.Id == LogId.WarningBindingNotUsed && i.Locations.FirstOrDefault().GetSource() == "Service1").ShouldBe(1, result);
38+
}
39+
40+
[Fact]
41+
public async Task ShouldWarnWhenMultipleImplementationsShareAnOrdinaryTag()
42+
{
43+
var result = await """
44+
using System;
45+
using Pure.DI;
46+
47+
namespace Sample;
48+
49+
interface IService { }
50+
sealed class Service1 : IService { }
51+
sealed class Service2 : IService { }
52+
53+
partial class Composition
54+
{
55+
void Setup() => DI.Setup()
56+
.Bind<IService>("main").To<Service1, Service2>()
57+
.Root<IService>("Service", "main");
58+
}
59+
60+
public class Program
61+
{
62+
public static void Main() => Console.WriteLine(new Composition().Service.GetType().Name);
63+
}
64+
""".RunAsync(new Options(LanguageVersion.CSharp12));
65+
66+
result.Errors.ShouldBeEmpty(result);
67+
result.StdOut.ShouldBe(["Service2"], result);
68+
result.Warnings.Count.ShouldBe(2, result);
69+
result.Warnings.Count(i => i.Id == LogId.WarningOverriddenBinding && i.Locations.FirstOrDefault().GetSource() == "Service2").ShouldBe(1, result);
70+
result.Warnings.Count(i => i.Id == LogId.WarningBindingNotUsed && i.Locations.FirstOrDefault().GetSource() == "Service1").ShouldBe(1, result);
71+
}
72+
73+
[Fact]
74+
public async Task ShouldSupportSixtyFourImplementationTypes()
75+
{
76+
var implementations = string.Join(", ", Enumerable.Repeat("Service", 64));
77+
var source = """
78+
using System;
79+
using Pure.DI;
80+
81+
namespace Sample;
82+
83+
interface IService { }
84+
sealed class Service : IService { }
85+
86+
partial class Composition
87+
{
88+
void Setup() => DI.Setup()
89+
.Bind<IService>(Tag.Unique).To<IMPLEMENTATIONS>()
90+
.Root<IService[]>("Services");
91+
}
92+
93+
public class Program
94+
{
95+
public static void Main() => Console.WriteLine(new Composition().Services.Length);
96+
}
97+
""".Replace("IMPLEMENTATIONS", implementations);
98+
99+
var result = await source.RunAsync(new Options(LanguageVersion.CSharp12));
100+
101+
result.Success.ShouldBeTrue(result);
102+
result.StdOut.ShouldBe(["64"], result);
103+
}
104+
105+
[Fact]
106+
public async Task ShouldBindMultipleImplementationsWithLifetime()
107+
{
108+
var result = await """
109+
using System;
110+
using System.Linq;
111+
using Pure.DI;
112+
113+
namespace Sample;
114+
115+
interface IService { }
116+
sealed class Service1 : IService { }
117+
sealed class Service2 : IService { }
118+
sealed class Service3 : IService { }
119+
120+
partial class Composition
121+
{
122+
void Setup() => DI.Setup()
123+
.Bind<IService>(Tag.Unique).As(Lifetime.Singleton)
124+
.To<Service1, Service2, Service3>()
125+
.Root<IService[]>("Services");
126+
}
127+
128+
public class Program
129+
{
130+
public static void Main()
131+
{
132+
var composition = new Composition();
133+
var services = composition.Services;
134+
Console.WriteLine(string.Join(",", services.Select(service => service.GetType().Name)));
135+
Console.WriteLine(services.SequenceEqual(composition.Services));
136+
}
137+
}
138+
""".RunAsync(new Options(LanguageVersion.CSharp12));
139+
140+
result.Success.ShouldBeTrue(result);
141+
result.StdOut.ShouldBe(["Service1,Service2,Service3", "True"], result);
142+
}
143+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
$v=true
3+
$t=Basics
4+
$p=34
5+
$d=Multiple implementations
6+
$sa=Tag Unique
7+
$sa=Tag Type
8+
$sa=Simplified lifetime-specific bindings
9+
$h=Use `To<T1, T2, ...>()` to bind several implementations to the same contract in one call. Each type becomes a separate binding with the configured lifetime and tags. Up to 64 implementation types are supported.
10+
$h=Use `Tag.Unique` when a collection should contain every implementation. Pure.DI assigns a distinct tag to each binding, so an array or `IEnumerable<T>` can collect them all.
11+
$f=You can use `Tag.Type` instead of `Tag.Unique` to give each binding the implementation type as its tag. For example, `EmailChannel` gets the tag `typeof(EmailChannel)`, which can also be used to resolve that implementation individually.
12+
$f=A regular shared tag follows the usual binding rules: later bindings for the same contract and tag override earlier ones. `Tag.Unique` keeps each implementation available to collection injection.
13+
$f=See also: [Tag Type](tag-type.md), [Tag Unique](tag-unique.md), [Simplified lifetime-specific bindings](simplified-lifetime-specific-bindings.md).
14+
$r=Shouldly
15+
*/
16+
17+
// ReSharper disable CheckNamespace
18+
// ReSharper disable ArrangeTypeModifiers
19+
// ReSharper disable ClassNeverInstantiated.Global
20+
21+
namespace Pure.DI.UsageTests.Basics.MultipleImplementationsScenario;
22+
23+
using Xunit;
24+
25+
// {
26+
//# using Pure.DI;
27+
// }
28+
29+
public class Scenario
30+
{
31+
[Fact]
32+
public void Run()
33+
{
34+
// Resolve = Off
35+
// {
36+
DI.Setup(nameof(Composition))
37+
// Each channel gets a separate binding with Singleton lifetime.
38+
.Bind<INotificationChannel>(Tag.Unique)
39+
.As(Lifetime.Singleton)
40+
.To<EmailChannel, SmsChannel, PushChannel>()
41+
.Root<INotificationChannel[]>("Channels");
42+
43+
var composition = new Composition();
44+
var channels = composition.Channels;
45+
46+
channels.Length.ShouldBe(3);
47+
channels[0].ShouldBeOfType<EmailChannel>();
48+
channels[1].ShouldBeOfType<SmsChannel>();
49+
channels[2].ShouldBeOfType<PushChannel>();
50+
51+
var channelsAgain = composition.Channels;
52+
for (var index = 0; index < channels.Length; index++)
53+
{
54+
channelsAgain[index].ShouldBeSameAs(channels[index]);
55+
}
56+
// }
57+
composition.SaveClassDiagram();
58+
}
59+
}
60+
61+
// {
62+
interface INotificationChannel;
63+
64+
class EmailChannel : INotificationChannel;
65+
66+
class SmsChannel : INotificationChannel;
67+
68+
class PushChannel : INotificationChannel;
69+
// }

0 commit comments

Comments
 (0)