From 6a656d9924226421e9f01a237429528169b13c2a Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Sat, 8 Jun 2024 14:38:05 +0300 Subject: [PATCH] A special tag that allows injection by name --- README.md | 1 + readme/ArrayDetails.md | 20 +- readme/EnumDetails.md | 20 +- readme/FuncDetails.md | 16 +- readme/SingletonDetails.md | 14 +- readme/TransientDetails.md | 14 +- readme/name-tags.md | 109 +++++ src/Pure.DI.Core/Core/BindingBuilder.cs | 10 +- .../Core/Code/ClassDiagramBuilder.cs | 4 +- src/Pure.DI.Core/Core/Code/CodeExtensions.cs | 1 + .../ImplementationDependencyNodeBuilder.cs | 42 +- src/Pure.DI.Core/Core/Models/Injection.cs | 25 +- src/Pure.DI.Core/Core/Models/MdTag.cs | 29 ++ tests/Pure.DI.IntegrationTests/SetupTests.cs | 106 ----- tests/Pure.DI.IntegrationTests/TagsTests.cs | 431 ++++++++++++++++++ tests/Pure.DI.Tests/InjectionTests.cs | 54 +++ tests/Pure.DI.Tests/Pure.DI.Tests.csproj | 6 +- .../Advanced/NameTagsScenario.cs | 99 ++++ .../Advanced/SeveralPartialClassesScenario.cs | 2 +- 19 files changed, 829 insertions(+), 174 deletions(-) create mode 100644 readme/name-tags.md create mode 100644 tests/Pure.DI.Tests/InjectionTests.cs create mode 100644 tests/Pure.DI.UsageTests/Advanced/NameTagsScenario.cs diff --git a/README.md b/README.md index 6999fde47..8fd69d7ea 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,7 @@ dotnet run - [Composition root kinds](readme/composition-root-kinds.md) - [Tag Type](readme/tag-type.md) - [Tag Unique](readme/tag-unique.md) +- [Name tags](readme/name-tags.md) - [A few partial classes](readme/a-few-partial-classes.md) - [Partial class](readme/partial-class.md) - [Dependent compositions](readme/dependent-compositions.md) diff --git a/readme/ArrayDetails.md b/readme/ArrayDetails.md index 17a6dff01..8e17a8d5b 100644 --- a/readme/ArrayDetails.md +++ b/readme/ArrayDetails.md @@ -57,17 +57,17 @@ classDiagram class IService4 { <> } - CompositionRoot *-- Service1 : IService1 - CompositionRoot *-- "3 " Service2Array : IService2 - CompositionRoot *-- Service3 : IService3 - CompositionRoot *-- "2 " Service4 : IService4 + CompositionRoot *-- Service1 : IService1 + CompositionRoot *-- "3 " Service2Array : IService2 + CompositionRoot *-- Service3 : IService3 + CompositionRoot *-- "2 " Service4 : IService4 Array ..> CompositionRoot : CompositionRoot TestPureDIByCR() - Service1 *-- Service2Array : IService2 - Service2Array *-- ArrayᐸIService3ᐳ : ArrayᐸIService3ᐳ - Service3 *-- "2 " Service4 : IService4 - Service3v2 *-- "2 " Service4 : IService4 - Service3v3 *-- "2 " Service4 : IService4 - Service3v4 *-- "2 " Service4 : IService4 + Service1 *-- Service2Array : IService2 + Service2Array *-- ArrayᐸIService3ᐳ : ArrayᐸIService3ᐳ + Service3 *-- "2 " Service4 : IService4 + Service3v2 *-- "2 " Service4 : IService4 + Service3v3 *-- "2 " Service4 : IService4 + Service3v4 *-- "2 " Service4 : IService4 ArrayᐸIService3ᐳ *-- Service3 : IService3 ArrayᐸIService3ᐳ *-- Service3v2 : 2 IService3 ArrayᐸIService3ᐳ *-- Service3v3 : 3 IService3 diff --git a/readme/EnumDetails.md b/readme/EnumDetails.md index fc6d1573a..5b6e9b939 100644 --- a/readme/EnumDetails.md +++ b/readme/EnumDetails.md @@ -57,17 +57,17 @@ classDiagram class IService4 { <> } - CompositionRoot *-- Service1 : IService1 - CompositionRoot *-- "3 " Service2Enum : IService2 - CompositionRoot *-- Service3 : IService3 - CompositionRoot *-- "2 " Service4 : IService4 + CompositionRoot *-- Service1 : IService1 + CompositionRoot *-- "3 " Service2Enum : IService2 + CompositionRoot *-- Service3 : IService3 + CompositionRoot *-- "2 " Service4 : IService4 Enum ..> CompositionRoot : CompositionRoot TestPureDIByCR() - Service1 *-- Service2Enum : IService2 - Service2Enum o-- "PerBlock" IEnumerableᐸIService3ᐳ : IEnumerableᐸIService3ᐳ - Service3 *-- "2 " Service4 : IService4 - Service3v2 *-- "2 " Service4 : IService4 - Service3v3 *-- "2 " Service4 : IService4 - Service3v4 *-- "2 " Service4 : IService4 + Service1 *-- Service2Enum : IService2 + Service2Enum o-- "PerBlock" IEnumerableᐸIService3ᐳ : IEnumerableᐸIService3ᐳ + Service3 *-- "2 " Service4 : IService4 + Service3v2 *-- "2 " Service4 : IService4 + Service3v3 *-- "2 " Service4 : IService4 + Service3v4 *-- "2 " Service4 : IService4 IEnumerableᐸIService3ᐳ *-- Service3 : IService3 IEnumerableᐸIService3ᐳ *-- Service3v2 : 2 IService3 IEnumerableᐸIService3ᐳ *-- Service3v3 : 3 IService3 diff --git a/readme/FuncDetails.md b/readme/FuncDetails.md index f3671355c..d9ba7c133 100644 --- a/readme/FuncDetails.md +++ b/readme/FuncDetails.md @@ -45,15 +45,15 @@ classDiagram class IService4 { <> } - CompositionRoot *-- Service1 : IService1 - CompositionRoot *-- "3 " Service2Func : IService2 - CompositionRoot *-- Service3 : IService3 - CompositionRoot *-- "2 " Service4 : IService4 + CompositionRoot *-- Service1 : IService1 + CompositionRoot *-- "3 " Service2Func : IService2 + CompositionRoot *-- Service3 : IService3 + CompositionRoot *-- "2 " Service4 : IService4 Func ..> CompositionRoot : CompositionRoot TestPureDIByCR() - Service1 *-- Service2Func : IService2 - Service2Func o-- "PerBlock" FuncᐸIService3ᐳ : FuncᐸIService3ᐳ - Service3 *-- "2 " Service4 : IService4 - FuncᐸIService3ᐳ *-- Service3 : IService3 + Service1 *-- Service2Func : IService2 + Service2Func o-- "PerBlock" FuncᐸIService3ᐳ : FuncᐸIService3ᐳ + Service3 *-- "2 " Service4 : IService4 + FuncᐸIService3ᐳ *-- Service3 : IService3 ``` ### Generated code diff --git a/readme/SingletonDetails.md b/readme/SingletonDetails.md index 941c3e13e..edcb663f7 100644 --- a/readme/SingletonDetails.md +++ b/readme/SingletonDetails.md @@ -44,14 +44,14 @@ classDiagram class IService4 { <> } - CompositionRoot o-- "Scoped" Service1 : IService1 - CompositionRoot *-- "3 " Service2 : IService2 - CompositionRoot *-- Service3 : IService3 - CompositionRoot o-- "2 Scoped" Service4 : IService4 + CompositionRoot o-- "Scoped" Service1 : IService1 + CompositionRoot *-- "3 " Service2 : IService2 + CompositionRoot *-- Service3 : IService3 + CompositionRoot o-- "2 Scoped" Service4 : IService4 Singleton ..> CompositionRoot : CompositionRoot TestPureDIByCR() - Service1 *-- Service2 : IService2 - Service2 *-- "5 " Service3 : IService3 - Service3 o-- "2 Scoped" Service4 : IService4 + Service1 *-- Service2 : IService2 + Service2 *-- "5 " Service3 : IService3 + Service3 o-- "2 Scoped" Service4 : IService4 ``` ### Generated code diff --git a/readme/TransientDetails.md b/readme/TransientDetails.md index 981184d8f..7bb967010 100644 --- a/readme/TransientDetails.md +++ b/readme/TransientDetails.md @@ -44,14 +44,14 @@ classDiagram class IService4 { <> } - CompositionRoot *-- Service1 : IService1 - CompositionRoot *-- "3 " Service2 : IService2 - CompositionRoot *-- Service3 : IService3 - CompositionRoot *-- "2 " Service4 : IService4 + CompositionRoot *-- Service1 : IService1 + CompositionRoot *-- "3 " Service2 : IService2 + CompositionRoot *-- Service3 : IService3 + CompositionRoot *-- "2 " Service4 : IService4 Transient ..> CompositionRoot : CompositionRoot TestPureDIByCR() - Service1 *-- Service2 : IService2 - Service2 *-- "5 " Service3 : IService3 - Service3 *-- "2 " Service4 : IService4 + Service1 *-- Service2 : IService2 + Service2 *-- "5 " Service3 : IService3 + Service3 *-- "2 " Service4 : IService4 ``` ### Generated code diff --git a/readme/name-tags.md b/readme/name-tags.md new file mode 100644 index 000000000..15ad62294 --- /dev/null +++ b/readme/name-tags.md @@ -0,0 +1,109 @@ +#### Name tags + +[![CSharp](https://img.shields.io/badge/C%23-code-blue.svg)](../tests/Pure.DI.UsageTests/Advanced/NameTagsScenario.cs) + +Name tag are ordinary string tag in the special format: the full name of the type member and the name of the target element to be injected, separated by ":". For example for the _MyNamespace_ namespace and the type _Class1_: + +- `MyNamespace.Class1.Class1:state1 - the tag corresponds to the constructor argument named _state_ of type _MyNamespace.Class1_ +- `MyNamespace.Class1.DoSomething:myArg` - the tag corresponds to the _myArg_ argument of the _DoSomething_ method +- `MyNamespace.Class1.MyData` - the tag corresponds to property or field _MyData_ + +>For generic types, the type name also contains the number of type parameters, for example, for `IDictionary` the type name would be _IDictionary`2_. For example, _MyNamespace.Consumer`1.Consumer:myDep_ in this code: + + +```c# +interface IDependency; + +class AbcDependency : IDependency; + +class XyzDependency : IDependency; + +class Consumer(IDependency myDep) +{ + public IDependency Dependency { get; } = myDep; +} + +interface IService +{ + IDependency Dependency1 { get; } + + IDependency Dependency2 { get; } + + IDependency Dependency3 { get; } + + IDependency Dependency4 { get; } +} + +class Service( + IDependency dependency1, + IDependency dependency2, + Consumer consumer) + : IService +{ + public IDependency Dependency1 { get; } = dependency1; + + public IDependency Dependency2 { get; } = dependency2; + + public required IDependency Dependency3 { init; get; } + + public IDependency Dependency4 => consumer.Dependency; +} + +DI.Setup(nameof(Composition)) + .Bind( + "MyNamespace.Service.Service:dependency1", + "MyNamespace.Consumer`1.Consumer:myDep") + .To() + .Bind("MyNamespace.Service.Service:dependency2", + "MyNamespace.Service:Dependency3") + .To() + .Bind().To>() + .Bind().To() + + // Specifies to create the composition root named "Root" + .Root("Root"); + +var composition = new Composition(); +var service = composition.Root; +service.Dependency1.ShouldBeOfType(); +service.Dependency2.ShouldBeOfType(); +service.Dependency3.ShouldBeOfType(); +service.Dependency4.ShouldBeOfType(); +``` + +>[!WARNING] +>Although this approach can be useful for specifying exactly what to inject, it can be more expensive to maintain, so it is recommended to use attributes like `[Tag(...)]`. + + +Class diagram: + +```mermaid +classDiagram + class Composition { + <> + +IService Root + } + AbcDependency --|> IDependency : "Pure.DI.UsageTests.Basics.NameTagsScenario.Service.Service:dependency1" + AbcDependency --|> IDependency + class AbcDependency { + +AbcDependency() + } + XyzDependency --|> IDependency : "Pure.DI.UsageTests.Basics.NameTagsScenario.Service.Service:dependency2" + class XyzDependency { + +XyzDependency() + } + Service --|> IService + class Service { + +Service(IDependency dependency1, IDependency dependency2, IDependency dependency3) + } + class IDependency { + <> + } + class IService { + <> + } + Composition ..> Service : IService Root + Service *-- "2 " AbcDependency : IDependency + Service o-- "Singleton" XyzDependency : IDependency +``` + diff --git a/src/Pure.DI.Core/Core/BindingBuilder.cs b/src/Pure.DI.Core/Core/BindingBuilder.cs index 6fb1870ae..ed173caca 100644 --- a/src/Pure.DI.Core/Core/BindingBuilder.cs +++ b/src/Pure.DI.Core/Core/BindingBuilder.cs @@ -128,8 +128,8 @@ 1 when source, setup, semanticModel, - _contracts.Select(i => i with { Tags = i.Tags.Select(tag => BuildTag(tag, type, id)).ToImmutableArray()}).ToImmutableArray(), - _tags.Select(tag => BuildTag(tag, type, id)).ToImmutableArray(), + _contracts.Select(i => i with { Tags = i.Tags.Select(tag => BuildTag(tag, type, id, i.Tags.ToList())).ToImmutableArray()}).ToImmutableArray(), + _tags.Select(tag => BuildTag(tag, type, id, _tags)).ToImmutableArray(), _lifetime ?? _defaultLifetime?.Lifetime, _implementation, _factory, @@ -151,7 +151,7 @@ 1 when } } - private static MdTag BuildTag(MdTag tag, ITypeSymbol? type, Lazy id) + private static MdTag BuildTag(MdTag tag, ITypeSymbol? type, Lazy id, IReadOnlyCollection tags) { if (type is null || tag.Value is null) { @@ -164,10 +164,10 @@ private static MdTag BuildTag(MdTag tag, ITypeSymbol? type, Lazy id) switch (tagVal) { case Tag.Type: - return tag with { Value = type }; + return MdTag.CreateTypeTag(tag, type); case Tag.Unique: - return tag with { Value = new UniqueTag(id.Value) }; + return MdTag.CreateUniqueTag(tag, id.Value); } } diff --git a/src/Pure.DI.Core/Core/Code/ClassDiagramBuilder.cs b/src/Pure.DI.Core/Core/Code/ClassDiagramBuilder.cs index c4720f086..d39b56dee 100644 --- a/src/Pure.DI.Core/Core/Code/ClassDiagramBuilder.cs +++ b/src/Pure.DI.Core/Core/Code/ClassDiagramBuilder.cs @@ -199,10 +199,10 @@ private static string FormatCardinality(int count, Lifetime lifetime) } private string FormatDependency(Dependency dependency, FormatOptions options) => - $"{(dependency.Injection.Tag == default ? "" : FormatTag(dependency.Injection.Tag) + " ")}{FormatSymbol(dependency.Injection.Type, options)}"; + $"{(dependency.Injection.Tag is null or MdTag.NameTagObject ? "" : FormatTag(dependency.Injection.Tag) + " ")}{FormatSymbol(dependency.Injection.Type, options)}"; private static string FormatTag(object? tag) => - tag != default ? $"{tag.ValueToString("").Replace("\"", "\\\"")} " : ""; + tag is null or MdTag.NameTagObject ? "" : $"{tag.ValueToString("").Replace("\"", "\\\"")} "; private static string FormatTags(IEnumerable tags) => string.Join(", ", tags.Distinct().Select(FormatTag).OrderBy(i => i)); diff --git a/src/Pure.DI.Core/Core/Code/CodeExtensions.cs b/src/Pure.DI.Core/Core/Code/CodeExtensions.cs index e966842b8..6082cdb72 100644 --- a/src/Pure.DI.Core/Core/Code/CodeExtensions.cs +++ b/src/Pure.DI.Core/Core/Code/CodeExtensions.cs @@ -22,6 +22,7 @@ public static string ValueToString(this object? tag, string defaultValue = "null char ch => $"'{ch.ToString()}'", Enum en => $"{en.GetType()}.{en}", ITypeSymbol => $"typeof({tag})", + MdTag.NameTagObject => defaultValue, not null => tag.ToString(), _ => defaultValue }; diff --git a/src/Pure.DI.Core/Core/ImplementationDependencyNodeBuilder.cs b/src/Pure.DI.Core/Core/ImplementationDependencyNodeBuilder.cs index 0414c440b..03d3ee583 100644 --- a/src/Pure.DI.Core/Core/ImplementationDependencyNodeBuilder.cs +++ b/src/Pure.DI.Core/Core/ImplementationDependencyNodeBuilder.cs @@ -4,13 +4,41 @@ // ReSharper disable ClassNeverInstantiated.Global namespace Pure.DI.Core; -using ITypeSymbol = Microsoft.CodeAnalysis.ITypeSymbol; +using System.Reflection; internal sealed class ImplementationDependencyNodeBuilder( ILogger logger, IBuilder> implementationVariantsBuilder) : IBuilder> { + private static readonly SymbolDisplayFormat NameTagQualifiedFormat; + + static ImplementationDependencyNodeBuilder() + { + NameTagQualifiedFormat = new SymbolDisplayFormat( + genericsOptions: + SymbolDisplayGenericsOptions.IncludeTypeParameters, + typeQualificationStyle: + SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + memberOptions: + SymbolDisplayMemberOptions.IncludeType | + SymbolDisplayMemberOptions.IncludeContainingType, + miscellaneousOptions: + SymbolDisplayMiscellaneousOptions.UseSpecialTypes + | SymbolDisplayMiscellaneousOptions.CollapseTupleTypes + ); + + var qualifiedNameArityFormat = NameTagQualifiedFormat.GetType().GetField("QualifiedNameArityFormat", BindingFlags.Static | BindingFlags.NonPublic); + var format = (SymbolDisplayFormat?)qualifiedNameArityFormat?.GetValue(null) + ?? new SymbolDisplayFormat(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); + + NameTagQualifiedFormat = + format + .WithGenericsOptions(SymbolDisplayGenericsOptions.IncludeTypeParameters) + .WithMemberOptions(SymbolDisplayMemberOptions.IncludeType | SymbolDisplayMemberOptions.IncludeContainingType) + .WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseSpecialTypes | SymbolDisplayMiscellaneousOptions.CollapseTupleTypes); + } + public IEnumerable Build(MdSetup setup) { var injectionsWalker = new DependenciesToInjectionsCountWalker(); @@ -109,7 +137,7 @@ public IEnumerable Build(MdSetup setup) ordinal, new Injection( GetAttribute(setup.TypeAttributes, field, setup.TypeConstructor?.Construct(compilation, type) ?? type), - GetAttribute(setup.TagAttributes, field, default(object?))))); + GetAttribute(setup.TagAttributes, field, default(object?)) ?? CreateNameTag(field)))); } } @@ -128,7 +156,7 @@ public IEnumerable Build(MdSetup setup) ordinal, new Injection( GetAttribute(setup.TypeAttributes, property, setup.TypeConstructor?.Construct(compilation, type) ?? type), - GetAttribute(setup.TagAttributes, property, default(object?))))); + GetAttribute(setup.TagAttributes, property, default(object?)) ?? CreateNameTag(property)))); } } @@ -222,11 +250,17 @@ private ImmutableArray GetParameters( parameter, new Injection( GetAttribute(setup.TypeAttributes, parameter, typeConstructor?.Construct(compilation, type) ?? type), - GetAttribute(setup.TagAttributes, parameter, default(object?))))); + GetAttribute(setup.TagAttributes, parameter, default(object?)) ?? CreateNameTag(parameter)))); } return dependenciesBuilder.MoveToImmutable(); } + + private static object CreateNameTag(ISymbol symbol) + { + var memberName = symbol.ContainingSymbol.ToDisplayString(NameTagQualifiedFormat); + return MdTag.CreateNameTagValue($"{memberName}:{symbol.Name}"); + } private T GetAttribute( in ImmutableArray attributeMetadata, diff --git a/src/Pure.DI.Core/Core/Models/Injection.cs b/src/Pure.DI.Core/Core/Models/Injection.cs index 9e039c02b..345b23689 100644 --- a/src/Pure.DI.Core/Core/Models/Injection.cs +++ b/src/Pure.DI.Core/Core/Models/Injection.cs @@ -1,26 +1,27 @@ namespace Pure.DI.Core.Models; +using System.Runtime.CompilerServices; using Code; internal readonly record struct Injection( ITypeSymbol Type, object? Tag) { - public override string ToString() => $"{Type}{(Tag != default ? $"({Tag.ValueToString()})" : "")}"; + public override string ToString() => $"{Type}{(Tag != default && Tag is not MdTag.NameTagObject ? $"({Tag.ValueToString()})" : "")}"; - public bool Equals(Injection other) => - SymbolEqualityComparer.Default.Equals(Type, other.Type) && EqualTags(Tag, other.Tag); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Injection other) => + (ReferenceEquals(Type, other.Type) || SymbolEqualityComparer.Default.Equals(Type, other.Type)) + && EqualTags(Tag, other.Tag); public override int GetHashCode() => SymbolEqualityComparer.Default.GetHashCode(Type); - public static bool EqualTags(object? tag, object? otherTag) - { - if (ReferenceEquals(tag, MdTag.ContextTag)) - { - return true; - } - - return ReferenceEquals(otherTag, MdTag.ContextTag) || Equals(tag, otherTag); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool EqualTags(object? tag, object? otherTag) => + ReferenceEquals(tag, MdTag.ContextTag) + || ReferenceEquals(otherTag, MdTag.ContextTag) + || (tag is MdTag.NameTagObject nameTagObject && nameTagObject.Equals(otherTag)) + || (otherTag is MdTag.NameTagObject otherNameTagObject && otherNameTagObject.Equals(tag)) + || Equals(tag, otherTag); } \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/Models/MdTag.cs b/src/Pure.DI.Core/Core/Models/MdTag.cs index d6d6f6212..83b8b24a5 100644 --- a/src/Pure.DI.Core/Core/Models/MdTag.cs +++ b/src/Pure.DI.Core/Core/Models/MdTag.cs @@ -7,11 +7,21 @@ internal readonly record struct MdTag( { public static readonly object ContextTag = new ContextTagObject(); + public static MdTag CreateTypeTag(MdTag tag, ITypeSymbol? type) => + tag with { Value = type }; + + public static MdTag CreateUniqueTag(MdTag tag, int id) => + tag with { Value = new UniqueTag(id) }; + + public static object CreateNameTagValue(string name) => + new NameTagObject(name); + public override string ToString() => Value switch { null => "null", string => $"\"{Value}\"", + NameTagObject => "", _ => Value.ToString() }; @@ -19,4 +29,23 @@ private class ContextTagObject { public override string ToString() => "ContextTag"; } + + internal class NameTagObject(string name) + { + public override string ToString() => $"NameTag(\"{name}\")"; + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return true; + if (ReferenceEquals(this, obj)) return true; + return obj switch + { + string str => Equals(name, str), + NameTagObject nameTag => true, + _ => false + }; + } + + public override int GetHashCode() => name.GetHashCode(); + } } \ No newline at end of file diff --git a/tests/Pure.DI.IntegrationTests/SetupTests.cs b/tests/Pure.DI.IntegrationTests/SetupTests.cs index 34b98da42..205098008 100644 --- a/tests/Pure.DI.IntegrationTests/SetupTests.cs +++ b/tests/Pure.DI.IntegrationTests/SetupTests.cs @@ -1602,112 +1602,6 @@ public static void Main() // Then result.Success.ShouldBeTrue(result); } - - [Fact] - public async Task ShouldSupportTagUnique() - { - // Given - - // When - var result = await """ -namespace Sample -{ - using System; - using System.Linq; - using System.Collections.Generic; - using Pure.DI; - using Sample; - - internal interface IDep { } - - internal class Dep1: IDep { } - - internal class Dep2: IDep { } - - internal interface IService { } - - internal class Service: IService - { - public Service(IEnumerable deps) - { - Console.WriteLine(deps.Count()); - } - } - - internal partial class Composition - { - void Setup() => - DI.Setup("Composition") - .Bind(Tag.Unique).To() - .Bind(Tag.Unique).To() - .Bind().To() - .Root("Root"); - } - - public class Program - { - public static void Main() - { - var composition = new Composition(); - Console.WriteLine(composition.Root); - } - } -} -""".RunAsync(); - - // Then - result.Success.ShouldBeTrue(result); - result.StdOut.ShouldBe(["2", "Sample.Service"], result); - } - - [Fact] - public async Task ShouldSupportTagType() - { - // Given - - // When - var result = await """ -namespace Sample -{ - using System; - using Pure.DI; - using Sample; - - internal class Dep { } - - internal interface IService { } - - internal class Service: IService - { - public Service([Tag(typeof(Dep))] Dep dep) { } - } - - internal partial class Composition - { - void Setup() => - DI.Setup("Composition") - .Bind(Tag.Type).To() - .Bind(Tag.Type).To() - .Root("Root1", typeof(Service)) - .Root("Root2", typeof(Service)); - } - - public class Program - { - public static void Main() - { - var composition = new Composition(); - Console.WriteLine(composition.Root1); - Console.WriteLine(composition.Root2); - } - } -} -""".RunAsync(); - - // Then - result.Success.ShouldBeTrue(result); - result.StdOut.ShouldBe(["Sample.Service", "Sample.Service"], result); - } #if ROSLYN4_8_OR_GREATER [Fact] diff --git a/tests/Pure.DI.IntegrationTests/TagsTests.cs b/tests/Pure.DI.IntegrationTests/TagsTests.cs index ba648865b..9bf2409bd 100644 --- a/tests/Pure.DI.IntegrationTests/TagsTests.cs +++ b/tests/Pure.DI.IntegrationTests/TagsTests.cs @@ -387,4 +387,435 @@ public static void Main() result.StdOut.ShouldBe(["1", "2", "2"], result); } #endif + + [Fact] + public async Task ShouldSupportTagUnique() + { + // Given + + // When + var result = await """ +namespace Sample +{ + using System; + using System.Linq; + using System.Collections.Generic; + using Pure.DI; + using Sample; + + internal interface IDep { } + + internal class Dep1: IDep { } + + internal class Dep2: IDep { } + + internal interface IService { } + + internal class Service: IService + { + public Service(IEnumerable deps) + { + Console.WriteLine(deps.Count()); + } + } + + internal partial class Composition + { + void Setup() => + DI.Setup("Composition") + .Bind(Tag.Unique).To() + .Bind(Tag.Unique).To() + .Bind().To() + .Root("Root"); + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + Console.WriteLine(composition.Root); + } + } +} +""".RunAsync(); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["2", "Sample.Service"], result); + } + + [Fact] + public async Task ShouldSupportTagType() + { + // Given + + // When + var result = await """ +namespace Sample +{ + using System; + using Pure.DI; + using Sample; + + internal class Dep { } + + internal interface IService { } + + internal class Service: IService + { + public Service([Tag(typeof(Dep))] Dep dep) { } + } + + internal partial class Composition + { + void Setup() => + DI.Setup("Composition") + .Bind(Tag.Type).To() + .Bind(Tag.Type).To() + .Root("Root1", typeof(Service)) + .Root("Root2", typeof(Service)); + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + Console.WriteLine(composition.Root1); + Console.WriteLine(composition.Root2); + } + } +} +""".RunAsync(); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["Sample.Service", "Sample.Service"], result); + } + + [Fact] + public async Task ShouldSupportTagNameWhenCtor() + { + // Given + + // When + var result = await """ +namespace Sample +{ + using System; + using Pure.DI; + using Sample; + + internal class Dep { } + + internal interface IService { } + + internal class Service: IService + { + public Service(Dep dep) { } + } + + internal partial class Composition + { + void Setup() => + DI.Setup("Composition") + .Bind("Sample.Service.Service:dep").To() + .Bind().To() + .Root("Root"); + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + Console.WriteLine(composition.Root); + } + } +} +""".RunAsync(); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["Sample.Service"], result); + } + + [Fact] + public async Task ShouldSupportTagNameWhenInterfaceInCtor() + { + // Given + + // When + var result = await """ +namespace Sample +{ + using System; + using Pure.DI; + using Sample; + + internal interface IDep { } + + internal class Dep: IDep { } + + internal interface IService { } + + internal class Service: IService + { + public Service(IDep dep) { } + } + + internal partial class Composition + { + void Setup() => + DI.Setup("Composition") + .Bind("Sample.Service.Service:dep").To() + .Bind().To() + .Root("Root"); + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + Console.WriteLine(composition.Root); + } + } +} +""".RunAsync(); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["Sample.Service"], result); + } + + [Fact] + public async Task ShouldSupportTagNameWhenGenericInCtor() + { + // Given + + // When + var result = await """ +namespace Sample +{ + using System; + using Pure.DI; + using Sample; + + internal interface IAbc { } + + internal interface IDep { } + + internal class Dep: IDep { } + + internal interface IService { } + + internal class Service: IService + { + public Service(IDep dep) { } + } + + internal partial class Composition + { + void Setup() => + DI.Setup("Composition") + .Hint(Hint.Resolve, "Off") + .Bind("Sample.Service`1.Service:dep").To() + .Bind().To>() + .Root>("Root"); + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + Console.WriteLine(composition.Root()); + } + } +} +""".RunAsync(); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["Sample.Service`1[System.Int32]"], result); + } + +#if ROSLYN4_8_OR_GREATER + [Fact] + public async Task ShouldSupportSeveralTagNames() + { + // Given + + // When + var result = await """ +namespace Sample +{ + using System; + using Pure.DI; + using Sample; + + internal interface IDep { } + + internal class Dep: IDep { } + + internal interface IService { } + + internal class Service: IService + { + public Service(Dep dep, IDep abc) { } + private IDep? _myProp; + + public required IDep MyProp + { + init + { + _myProp = value; + Console.WriteLine(value); + } + + get + { + return _myProp!; + } + } + } + + internal partial class Composition + { + void Setup() => + DI.Setup("Composition") + .Bind("Sample.Service.Service:dep", "Sample.Service.Service:abc", "Sample.Service:MyProp").To() + .Bind().To() + .Root("Root"); + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + Console.WriteLine(composition.Root); + } + } +} +""".RunAsync(new Options(LanguageVersion.CSharp11)); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["Sample.Dep", "Sample.Service"], result); + } + + [Fact] + public async Task ShouldSupportTagNameWhenRequiredProperty() + { + // Given + + // When + var result = await """ +namespace Sample +{ + using System; + using Pure.DI; + using Sample; + + internal interface IDep { } + + internal class Dep: IDep { } + + internal interface IService { } + + internal class Service: IService + { + private IDep? _myProp; + + public required IDep MyProp + { + init + { + _myProp = value; + } + + get + { + return _myProp!; + } + } + } + + internal partial class Composition + { + void Setup() => + DI.Setup("Composition") + .Bind("Sample.Service:MyProp").To() + .Bind().To() + .Root("Root"); + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + Console.WriteLine(composition.Root); + Console.WriteLine(composition.Root.MyProp); + } + } +} +""".RunAsync(new Options(LanguageVersion.CSharp11)); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["Sample.Service", "Sample.Dep"], result); + } + + [Fact] + public async Task ShouldSupportTagNameWhenRequiredField() + { + // Given + + // When + var result = await """ +namespace Sample +{ + using System; + using Pure.DI; + using Sample; + + internal class Dep { } + + internal interface IService { } + + internal class Service: IService + { + public required Dep? MyField; + } + + internal partial class Composition + { + void Setup() => + DI.Setup("Composition") + .Bind("Sample.Service:MyField").To() + .Bind().To() + .Root("Root"); + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + Console.WriteLine(composition.Root); + Console.WriteLine(composition.Root.MyField); + } + } +} +""".RunAsync(new Options(LanguageVersion.CSharp11)); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["Sample.Service", "Sample.Dep"], result); + } +#endif } \ No newline at end of file diff --git a/tests/Pure.DI.Tests/InjectionTests.cs b/tests/Pure.DI.Tests/InjectionTests.cs new file mode 100644 index 000000000..953ae9e9e --- /dev/null +++ b/tests/Pure.DI.Tests/InjectionTests.cs @@ -0,0 +1,54 @@ +namespace Pure.DI.Tests; + +using Core.Models; +using Microsoft.CodeAnalysis; +using Moq; + +public class InjectionTests +{ + private static readonly ITypeSymbol TypeSymbol1 = Mock.Of(); + private static readonly ITypeSymbol TypeSymbol2 = Mock.Of(); + + [Theory] + [MemberData(nameof(GetData))] + internal void ShouldImplementEqualityMethods(int id, Injection injection1, Injection injection2, bool expectedEqual) + { + // Given + // ReSharper disable once RedundantAssignment + id++; + + // When + var hashCode1 = injection1.GetHashCode(); + var hashCode2 = injection2.GetHashCode(); + var actualEqual = Equals(injection1, injection2); + + // Then + actualEqual.ShouldBe(expectedEqual); + if (expectedEqual) + { + hashCode1.ShouldBe(hashCode2); + } + } + + public static IEnumerable GetData() + { + yield return [0, new Injection(TypeSymbol1, null), new Injection(TypeSymbol1, null), true]; + yield return [1, new Injection(TypeSymbol1, 1), new Injection(TypeSymbol1, 1), true]; + yield return [2, new Injection(TypeSymbol1, "Abc"), new Injection(TypeSymbol1, "Abc"), true]; + yield return [3, new Injection(TypeSymbol1, null), new Injection(TypeSymbol1, MdTag.ContextTag), true]; + yield return [4, new Injection(TypeSymbol1, "Abc"), new Injection(TypeSymbol1, MdTag.ContextTag), true]; + yield return [5, new Injection(TypeSymbol1, null), new Injection(TypeSymbol1, new MdTag.NameTagObject("Abc")), true]; + yield return [6, new Injection(TypeSymbol1, new MdTag.NameTagObject("Abc")), new Injection(TypeSymbol1, new MdTag.NameTagObject("Abc")), true]; + yield return [7, new Injection(TypeSymbol1, "Abc"), new Injection(TypeSymbol1, new MdTag.NameTagObject("Abc")), true]; + yield return [8, new Injection(TypeSymbol1, new MdTag.NameTagObject("Abc")), new Injection(TypeSymbol1, MdTag.ContextTag), true]; + yield return [14, new Injection(TypeSymbol1, new MdTag.NameTagObject("Abc")), new Injection(TypeSymbol1, new MdTag.NameTagObject("Xyz")), true]; + + yield return [9, new Injection(TypeSymbol1, null), new Injection(TypeSymbol2, null), false]; + yield return [10, new Injection(TypeSymbol1, null), new Injection(TypeSymbol2, 1), false]; + yield return [11, new Injection(TypeSymbol1, null), new Injection(TypeSymbol1, 1), false]; + yield return [12, new Injection(TypeSymbol1, null), new Injection(TypeSymbol1, "Abc"), false]; + yield return [13, new Injection(TypeSymbol1, new MdTag.NameTagObject("Abc")), new Injection(TypeSymbol2, new MdTag.NameTagObject("Abc")), false]; + yield return [15, new Injection(TypeSymbol1, new MdTag.NameTagObject("Abc")), new Injection(TypeSymbol2, MdTag.ContextTag), false]; + yield return [16, new Injection(TypeSymbol1, "Xyz"), new Injection(TypeSymbol1, new MdTag.NameTagObject("Abc")), false]; + } +} \ No newline at end of file diff --git a/tests/Pure.DI.Tests/Pure.DI.Tests.csproj b/tests/Pure.DI.Tests/Pure.DI.Tests.csproj index 9cd0125d8..843f5f5ae 100644 --- a/tests/Pure.DI.Tests/Pure.DI.Tests.csproj +++ b/tests/Pure.DI.Tests/Pure.DI.Tests.csproj @@ -2,9 +2,11 @@ - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Pure.DI.UsageTests/Advanced/NameTagsScenario.cs b/tests/Pure.DI.UsageTests/Advanced/NameTagsScenario.cs new file mode 100644 index 000000000..d36d60653 --- /dev/null +++ b/tests/Pure.DI.UsageTests/Advanced/NameTagsScenario.cs @@ -0,0 +1,99 @@ +/* +$v=true +$p=5 +$d=Name tags +$h=Name tag are ordinary string tag in the special format: the full name of the type member and the name of the target element to be injected, separated by ":". For example for the _MyNamespace_ namespace and the type _Class1_: +$h= +$h=- `MyNamespace.Class1.Class1:state1 - the tag corresponds to the constructor argument named _state_ of type _MyNamespace.Class1_ +$h=- `MyNamespace.Class1.DoSomething:myArg` - the tag corresponds to the _myArg_ argument of the _DoSomething_ method +$h=- `MyNamespace.Class1.MyData` - the tag corresponds to property or field _MyData_ +$h= +$h=>For generic types, the type name also contains the number of type parameters, for example, for `IDictionary` the type name would be _IDictionary`2_. For example, _MyNamespace.Consumer`1.Consumer:myDep_ in this code: +$f=>[!WARNING] +$f=>Although this approach can be useful for specifying exactly what to inject, it can be more expensive to maintain, so it is recommended to use attributes like `[Tag(...)]`. +*/ + +// ReSharper disable ClassNeverInstantiated.Local +// ReSharper disable CheckNamespace +// ReSharper disable UnusedType.Global +// ReSharper disable ArrangeTypeModifiers +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable UnusedTypeParameter +#pragma warning disable CS9113 // Parameter is unread. +namespace MyNamespace; + +using Pure.DI; +using Pure.DI.UsageTests; +using Xunit; + +// { +interface IDependency; + +class AbcDependency : IDependency; + +class XyzDependency : IDependency; + +class Consumer(IDependency myDep) +{ + public IDependency Dependency { get; } = myDep; +} + +interface IService +{ + IDependency Dependency1 { get; } + + IDependency Dependency2 { get; } + + IDependency Dependency3 { get; } + + IDependency Dependency4 { get; } +} + +class Service( + IDependency dependency1, + IDependency dependency2, + Consumer consumer) + : IService +{ + public IDependency Dependency1 { get; } = dependency1; + + public IDependency Dependency2 { get; } = dependency2; + + public required IDependency Dependency3 { init; get; } + + public IDependency Dependency4 => consumer.Dependency; +} +// } + +public class Scenario +{ + [Fact] + public void Run() + { + // Resolve = Off +// { + DI.Setup(nameof(Composition)) + .Bind( + "MyNamespace.Service.Service:dependency1", + "MyNamespace.Consumer`1.Consumer:myDep") + .To() + .Bind("MyNamespace.Service.Service:dependency2", + "MyNamespace.Service:Dependency3") + .To() + .Bind().To>() + .Bind().To() + + // Specifies to create the composition root named "Root" + .Root("Root"); + + var composition = new Composition(); + var service = composition.Root; + service.Dependency1.ShouldBeOfType(); + service.Dependency2.ShouldBeOfType(); + service.Dependency3.ShouldBeOfType(); + service.Dependency4.ShouldBeOfType(); +// } + composition.SaveClassDiagram(); + } +} \ No newline at end of file diff --git a/tests/Pure.DI.UsageTests/Advanced/SeveralPartialClassesScenario.cs b/tests/Pure.DI.UsageTests/Advanced/SeveralPartialClassesScenario.cs index 4e8722c76..119e58d48 100644 --- a/tests/Pure.DI.UsageTests/Advanced/SeveralPartialClassesScenario.cs +++ b/tests/Pure.DI.UsageTests/Advanced/SeveralPartialClassesScenario.cs @@ -1,6 +1,6 @@ /* $v=true -$p=5 +$p=6 $d=A few partial classes $h=The setting code for one Composition can be located in several methods and/or in several partial classes. */