diff --git a/README.md b/README.md index 8a9bc5de1..6999fde47 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,7 @@ dotnet run - [Tag attribute](readme/tag-attribute.md) - [Type attribute](readme/type-attribute.md) - [Custom attributes](readme/custom-attributes.md) +- [Custom universal attribute](readme/custom-universal-attribute.md) ### Interception - [Decorator](readme/decorator.md) - [Interception](readme/interception.md) diff --git a/readme/constructor-ordinal-attribute.md b/readme/constructor-ordinal-attribute.md index 6dd93e595..43f0d5b88 100644 --- a/readme/constructor-ordinal-attribute.md +++ b/readme/constructor-ordinal-attribute.md @@ -32,8 +32,8 @@ class Service : IService DI.Setup(nameof(Composition)) .Arg("serviceName") - .Bind().To() - .Bind().To() + .Bind().To() + .Bind().To() // Composition root .Root("Root"); diff --git a/readme/custom-attributes.md b/readme/custom-attributes.md index 3a2271324..af7e1c872 100644 --- a/readme/custom-attributes.md +++ b/readme/custom-attributes.md @@ -29,31 +29,45 @@ class MyTagAttribute(object tag) : Attribute; | AttributeTargets.Field)] class MyTypeAttribute(Type type) : Attribute; +[AttributeUsage( + AttributeTargets.Parameter + | AttributeTargets.Property + | AttributeTargets.Field)] +class MyGenericTypeAttribute : Attribute; + interface IPerson; class Person([MyTag("NikName")] string name) : IPerson { + private object? _state; + [MyOrdinal(1)] [MyType(typeof(int))] internal object Id = ""; - public override string ToString() => $"{Id} {name}"; + [MyOrdinal(2)] + public void Initialize([MyGenericType] object state) => + _state = state; + + public override string ToString() => $"{Id} {name} {_state}"; } DI.Setup(nameof(PersonComposition)) .TagAttribute() .OrdinalAttribute() .TypeAttribute() + .TypeAttribute>() .Arg("personId") - .Bind("NikName").To(_ => "Nik") - .Bind().To() + .Bind().To(_ => new Uri("https://github.com/DevTeam/Pure.DI")) + .Bind("NikName").To(_ => "Nik") + .Bind().To() // Composition root .Root("Person"); var composition = new PersonComposition(personId: 123); var person = composition.Person; -person.ToString().ShouldBe("123 Nik"); +person.ToString().ShouldBe("123 Nik https://github.com/DevTeam/Pure.DI"); ``` The following partial class will be generated: @@ -82,9 +96,11 @@ partial class PersonComposition [MethodImpl(MethodImplOptions.AggressiveInlining)] get { + Uri transientUri2 = new Uri("https://github.com/DevTeam/Pure.DI"); string transientString1 = "Nik"; Person transientPerson0 = new Person(transientString1); transientPerson0.Id = _argPersonId; + transientPerson0.Initialize(transientUri2); return transientPerson0; } } @@ -100,11 +116,25 @@ classDiagram +IPerson Person } class Int32 + Uri --|> ISpanFormattable + Uri --|> IFormattable + Uri --|> ISerializable + class Uri class String Person --|> IPerson class Person { +Person(String name) ~Object Id + +Initialize(Object state) : Void + } + class ISpanFormattable { + <> + } + class IFormattable { + <> + } + class ISerializable { + <> } class IPerson { <> @@ -112,5 +142,6 @@ classDiagram PersonComposition ..> Person : IPerson Person Person *-- String : "NikName" String Person o-- Int32 : Argument "personId" + Person *-- Uri : Uri ``` diff --git a/readme/custom-universal-attribute.md b/readme/custom-universal-attribute.md new file mode 100644 index 000000000..52312514f --- /dev/null +++ b/readme/custom-universal-attribute.md @@ -0,0 +1,123 @@ +#### Custom universal attribute + +[![CSharp](https://img.shields.io/badge/C%23-code-blue.svg)](../tests/Pure.DI.UsageTests/Attributes/CustomUniversalAttributeScenario.cs) + +You can use a combined attribute, and each method in the list above has an optional parameter that defines the argument number (the default is 0) from where to get the appropriate metadata for _tag_, _ordinal_, or _type_. + + +```c# +[AttributeUsage( + AttributeTargets.Constructor + | AttributeTargets.Method + | AttributeTargets.Parameter + | AttributeTargets.Property + | AttributeTargets.Field)] +class InjectAttribute(object? tag = null, int ordinal = 0) : Attribute; + +interface IPerson; + +class Person([Inject("NikName")] string name) : IPerson +{ + private object? _state; + + [Inject(ordinal: 1)] + internal object Id = ""; + + public void Initialize([Inject] object state) => + _state = state; + + public override string ToString() => $"{Id} {name} {_state}"; +} + +DI.Setup(nameof(PersonComposition)) + .TagAttribute>() + .OrdinalAttribute>(1) + .TypeAttribute>() + .Arg("personId") + .Bind().To(_ => new Uri("https://github.com/DevTeam/Pure.DI")) + .Bind("NikName").To(_ => "Nik") + .Bind().To() + + // Composition root + .Root("Person"); + +var composition = new PersonComposition(personId: 123); +var person = composition.Person; +person.ToString().ShouldBe("123 Nik https://github.com/DevTeam/Pure.DI"); +``` + +The following partial class will be generated: + +```c# +partial class PersonComposition +{ + private readonly PersonComposition _root; + + private readonly int _argPersonId; + + public PersonComposition(int personId) + { + _argPersonId = personId; + _root = this; + } + + internal PersonComposition(PersonComposition parentScope) + { + _root = (parentScope ?? throw new ArgumentNullException(nameof(parentScope)))._root; + _argPersonId = _root._argPersonId; + } + + public IPerson Person + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + Uri transientUri2 = new Uri("https://github.com/DevTeam/Pure.DI"); + string transientString1 = "Nik"; + Person transientPerson0 = new Person(transientString1); + transientPerson0.Initialize(transientUri2); + transientPerson0.Id = _argPersonId; + return transientPerson0; + } + } +} +``` + +Class diagram: + +```mermaid +classDiagram + class PersonComposition { + <> + +IPerson Person + } + class Int32 + Uri --|> ISpanFormattable + Uri --|> IFormattable + Uri --|> ISerializable + class Uri + class String + Person --|> IPerson + class Person { + +Person(String name) + ~Object Id + +Initialize(Object state) : Void + } + class ISpanFormattable { + <> + } + class IFormattable { + <> + } + class ISerializable { + <> + } + class IPerson { + <> + } + PersonComposition ..> Person : IPerson Person + Person *-- String : "NikName" String + Person o-- Int32 : Argument "personId" + Person *-- Uri : Uri +``` + diff --git a/readme/member-ordinal-attribute.md b/readme/member-ordinal-attribute.md index c4c471bf1..2c538cfe7 100644 --- a/readme/member-ordinal-attribute.md +++ b/readme/member-ordinal-attribute.md @@ -49,7 +49,7 @@ DI.Setup(nameof(PersonComposition)) .Arg("personId") .Arg("personName") .Arg("personBirthday") - .Bind().To() + .Bind().To() // Composition root .Root("Person"); diff --git a/readme/tag-attribute.md b/readme/tag-attribute.md index 0a25d12bb..1df755e39 100644 --- a/readme/tag-attribute.md +++ b/readme/tag-attribute.md @@ -32,9 +32,9 @@ class Service( } DI.Setup(nameof(Composition)) - .Bind("Abc").To() - .Bind("Xyz").To() - .Bind().To() + .Bind("Abc").To() + .Bind("Xyz").To() + .Bind().To() // Composition root .Root("Root"); diff --git a/readme/type-attribute.md b/readme/type-attribute.md index f15f3b350..3971caf78 100644 --- a/readme/type-attribute.md +++ b/readme/type-attribute.md @@ -30,7 +30,7 @@ class Service( } DI.Setup(nameof(Composition)) - .Bind().To() + .Bind().To() // Composition root .Root("Root"); diff --git a/src/Pure.DI.Core/Core/ApiInvocationProcessor.cs b/src/Pure.DI.Core/Core/ApiInvocationProcessor.cs index e929e5a18..bdbdade06 100644 --- a/src/Pure.DI.Core/Core/ApiInvocationProcessor.cs +++ b/src/Pure.DI.Core/Core/ApiInvocationProcessor.cs @@ -220,25 +220,40 @@ public void ProcessInvocation( break; case nameof(IConfiguration.TypeAttribute): - if (genericName.TypeArgumentList.Arguments is [{ } typeAttributeType]) + if (TryGetAttributeType(genericName, semanticModel, out var typeAttributeType)) { - metadataVisitor.VisitTypeAttribute(new MdTypeAttribute(semanticModel, invocation.ArgumentList, semanticModel.GetTypeSymbol(typeAttributeType, cancellationToken), BuildConstantArgs(semanticModel, invocation.ArgumentList.Arguments) is [int positionVal] ? positionVal : 0)); + var attr = new MdTypeAttribute( + semanticModel, + invocation.ArgumentList, + typeAttributeType, + BuildConstantArgs(semanticModel, invocation.ArgumentList.Arguments) is [int positionVal] ? positionVal : 0); + metadataVisitor.VisitTypeAttribute(attr); } - + break; case nameof(IConfiguration.TagAttribute): - if (genericName.TypeArgumentList.Arguments is [{ } tagAttributeType]) + if (TryGetAttributeType(genericName, semanticModel, out var tagAttributeType)) { - metadataVisitor.VisitTagAttribute(new MdTagAttribute(semanticModel, invocation.ArgumentList, semanticModel.GetTypeSymbol(tagAttributeType, cancellationToken), BuildConstantArgs(semanticModel, invocation.ArgumentList.Arguments) is [int positionVal] ? positionVal : 0)); + var attr = new MdTagAttribute( + semanticModel, + invocation.ArgumentList, + tagAttributeType, + BuildConstantArgs(semanticModel, invocation.ArgumentList.Arguments) is [int positionVal] ? positionVal : 0); + metadataVisitor.VisitTagAttribute(attr); } - + break; case nameof(IConfiguration.OrdinalAttribute): - if (genericName.TypeArgumentList.Arguments is [{ } ordinalAttributeType]) + if (TryGetAttributeType(genericName, semanticModel, out var ordinalAttributeType)) { - metadataVisitor.VisitOrdinalAttribute(new MdOrdinalAttribute(semanticModel, invocation.ArgumentList, semanticModel.GetTypeSymbol(ordinalAttributeType, cancellationToken), BuildConstantArgs(semanticModel, invocation.ArgumentList.Arguments) is [int positionVal] ? positionVal : 0)); + var attr = new MdOrdinalAttribute( + semanticModel, + invocation.ArgumentList, + ordinalAttributeType, + BuildConstantArgs(semanticModel, invocation.ArgumentList.Arguments) is [int positionVal] ? positionVal : 0); + metadataVisitor.VisitOrdinalAttribute(attr); } break; @@ -272,6 +287,26 @@ public void ProcessInvocation( } } + private bool TryGetAttributeType( + GenericNameSyntax genericName, + SemanticModel semanticModel, + [NotNullWhen(true)] out INamedTypeSymbol? type) + { + if (genericName.TypeArgumentList.Arguments is not [{ } attributeTypeSyntax]) + { + type = default; + return false; + } + + type = semanticModel.GetTypeSymbol(attributeTypeSyntax, cancellationToken); + if (type.IsGenericType) + { + type = type.ConstructUnboundGenericType(); + } + + return true; + } + private static void VisitRoot( IArguments arguments, IMetadataVisitor metadataVisitor, diff --git a/src/Pure.DI.Core/Core/CompilationExtensions.cs b/src/Pure.DI.Core/Core/CompilationExtensions.cs index 8a8dc7b7d..960f31c87 100644 --- a/src/Pure.DI.Core/Core/CompilationExtensions.cs +++ b/src/Pure.DI.Core/Core/CompilationExtensions.cs @@ -4,14 +4,20 @@ namespace Pure.DI.Core; internal static class CompilationExtensions { [SuppressMessage("ReSharper", "HeapView.ClosureAllocation")] - public static IReadOnlyList GetAttributes(this ISymbol member, ITypeSymbol attributeType) => + public static IReadOnlyList GetAttributes(this ISymbol member, INamedTypeSymbol attributeType) => member .GetAttributes() - .Where(attr => attr.AttributeClass != null && SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeType)) + .Where(attr => attr.AttributeClass != null && SymbolEqualityComparer.Default.Equals(GetUnboundTypeSymbol(attr.AttributeClass), attributeType)) .ToArray(); public static LanguageVersion GetLanguageVersion(this Compilation compilation) => compilation is CSharpCompilation sharpCompilation ? sharpCompilation.LanguageVersion : LanguageVersion.Default; + + private static INamedTypeSymbol? GetUnboundTypeSymbol(INamedTypeSymbol? typeSymbol) => + typeSymbol is null + ? typeSymbol : typeSymbol.IsGenericType + ? typeSymbol.ConstructUnboundGenericType() + : typeSymbol; } \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/ImplementationDependencyNodeBuilder.cs b/src/Pure.DI.Core/Core/ImplementationDependencyNodeBuilder.cs index f4747a97d..0414c440b 100644 --- a/src/Pure.DI.Core/Core/ImplementationDependencyNodeBuilder.cs +++ b/src/Pure.DI.Core/Core/ImplementationDependencyNodeBuilder.cs @@ -4,6 +4,8 @@ // ReSharper disable ClassNeverInstantiated.Global namespace Pure.DI.Core; +using ITypeSymbol = Microsoft.CodeAnalysis.ITypeSymbol; + internal sealed class ImplementationDependencyNodeBuilder( ILogger logger, IBuilder> implementationVariantsBuilder) @@ -59,6 +61,16 @@ public IEnumerable Build(MdSetup setup) var methods = new List(); var fields = new List(); var properties = new List(); + + var allAttributesBuilder = ImmutableArray.CreateBuilder( + setup.OrdinalAttributes.Length + + setup.TagAttributes.Length + + setup.TypeAttributes.Length); + allAttributesBuilder.AddRange(setup.OrdinalAttributes); + allAttributesBuilder.AddRange(setup.TagAttributes); + allAttributesBuilder.AddRange(setup.TypeAttributes); + var allAttributes = allAttributesBuilder.MoveToImmutable(); + foreach (var member in GetMembers(implementationType)) { if (member.IsStatic || member.DeclaredAccessibility is not (Accessibility.Internal or Accessibility.Public or Accessibility.Friend)) @@ -71,10 +83,14 @@ public IEnumerable Build(MdSetup setup) case IMethodSymbol method: if (method.MethodKind == MethodKind.Ordinary) { - var ordinal = GetAttribute(setup.OrdinalAttributes, member, default(int?)); + var ordinal = GetAttribute(allAttributes, member, default(int?)) + ?? method.Parameters + .Select(param => GetAttribute(allAttributes, param, default(int?))) + .FirstOrDefault(i => i.HasValue); + if (ordinal.HasValue) { - methods.Add(new DpMethod(method, ordinal.Value, GetParameters(setup, method.Parameters, compilation, setup.TypeConstructor))); + methods.Add(new DpMethod(method, ordinal, GetParameters(setup, method.Parameters, compilation, setup.TypeConstructor))); } } @@ -83,10 +99,10 @@ public IEnumerable Build(MdSetup setup) case IFieldSymbol field: if (field is { IsReadOnly: false, IsStatic: false, IsConst: false }) { - var type = field.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - var ordinal = GetAttribute(setup.OrdinalAttributes, member, default(int?)); - if (ordinal.HasValue || field.IsRequired) + var ordinal = GetAttribute(allAttributes, member, default(int?)); + if (field.IsRequired || ordinal.HasValue) { + var type = field.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated); fields.Add( new DpField( field, @@ -102,10 +118,10 @@ public IEnumerable Build(MdSetup setup) case IPropertySymbol property: if (property is { IsReadOnly: false, IsStatic: false, IsIndexer: false }) { - var type = property.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - var ordinal = GetAttribute(setup.OrdinalAttributes, member, default(int?)); + var ordinal = GetAttribute(allAttributes, member, default(int?)); if (ordinal.HasValue || property.IsRequired) { + var type = property.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated); properties.Add( new DpProperty( property, @@ -154,7 +170,7 @@ public IEnumerable Build(MdSetup setup) } } } - + private IEnumerable CreateNodes(DependenciesToInjectionsCountWalker walker, IEnumerable implementations) => implementations .OrderByDescending(i => GetInjectionsCount(walker, i.Constructor)) @@ -224,8 +240,18 @@ private T GetAttribute( switch (attributeData.Count) { case 1: - var args = attributeData[0].ConstructorArguments; - if (attribute.ArgumentPosition > args.Length) + var attr = attributeData[0]; + if (typeof(ITypeSymbol).IsAssignableFrom(typeof(T)) && attr.AttributeClass is { IsGenericType: true, TypeArguments.Length: > 0 } attributeClass) + { + if (attribute.ArgumentPosition < attributeClass.TypeArguments.Length + && attributeClass.TypeArguments[attribute.ArgumentPosition] is { } typeSymbol) + { + return (T)typeSymbol; + } + } + + var args = attr.ConstructorArguments; + if (attribute.ArgumentPosition >= args.Length) { logger.CompileError($"The argument position {attribute.ArgumentPosition.ToString()} of attribute {attribute.Source} is out of range [0..{args.Length.ToString()}].", attribute.Source.GetLocation(), LogId.ErrorInvalidMetadata); } diff --git a/src/Pure.DI.Core/Core/Models/IMdAttribute.cs b/src/Pure.DI.Core/Core/Models/IMdAttribute.cs index 27eeba2db..9c7ef1c4d 100644 --- a/src/Pure.DI.Core/Core/Models/IMdAttribute.cs +++ b/src/Pure.DI.Core/Core/Models/IMdAttribute.cs @@ -7,7 +7,7 @@ internal interface IMdAttribute SyntaxNode Source { get; } - ITypeSymbol AttributeType { get; } + INamedTypeSymbol AttributeType { get; } int ArgumentPosition { get; } } \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/Models/MdOrdinalAttribute.cs b/src/Pure.DI.Core/Core/Models/MdOrdinalAttribute.cs index 01f51328c..b13bc80a4 100644 --- a/src/Pure.DI.Core/Core/Models/MdOrdinalAttribute.cs +++ b/src/Pure.DI.Core/Core/Models/MdOrdinalAttribute.cs @@ -5,7 +5,7 @@ namespace Pure.DI.Core.Models; internal readonly record struct MdOrdinalAttribute( SemanticModel SemanticModel, SyntaxNode Source, - ITypeSymbol AttributeType, + INamedTypeSymbol AttributeType, int ArgumentPosition) : IMdAttribute { public override string ToString() => $".OrdinalAttribute<{AttributeType}>({(ArgumentPosition != 0 ? ArgumentPosition.ToString() : string.Empty)})"; diff --git a/src/Pure.DI.Core/Core/Models/MdTagAttribute.cs b/src/Pure.DI.Core/Core/Models/MdTagAttribute.cs index a97d667dd..935ac0072 100644 --- a/src/Pure.DI.Core/Core/Models/MdTagAttribute.cs +++ b/src/Pure.DI.Core/Core/Models/MdTagAttribute.cs @@ -5,7 +5,7 @@ namespace Pure.DI.Core.Models; internal readonly record struct MdTagAttribute( SemanticModel SemanticModel, SyntaxNode Source, - ITypeSymbol AttributeType, + INamedTypeSymbol AttributeType, int ArgumentPosition) : IMdAttribute { public override string ToString() => $".TagAttribute<{AttributeType}>({(ArgumentPosition != 0 ? ArgumentPosition.ToString() : string.Empty)})"; diff --git a/src/Pure.DI.Core/Core/Models/MdTypeAttribute.cs b/src/Pure.DI.Core/Core/Models/MdTypeAttribute.cs index a5a24e6fd..408373299 100644 --- a/src/Pure.DI.Core/Core/Models/MdTypeAttribute.cs +++ b/src/Pure.DI.Core/Core/Models/MdTypeAttribute.cs @@ -4,7 +4,7 @@ namespace Pure.DI.Core.Models; internal readonly record struct MdTypeAttribute( SemanticModel SemanticModel, SyntaxNode Source, - ITypeSymbol AttributeType, + INamedTypeSymbol AttributeType, int ArgumentPosition) : IMdAttribute { public override string ToString() => $".TypeAttribute<{AttributeType}>({(ArgumentPosition != 0 ? ArgumentPosition.ToString() : string.Empty)})"; diff --git a/src/Pure.DI.Core/Pure.DI.Core.csproj b/src/Pure.DI.Core/Pure.DI.Core.csproj index d2ca82c56..41499ad2c 100644 --- a/src/Pure.DI.Core/Pure.DI.Core.csproj +++ b/src/Pure.DI.Core/Pure.DI.Core.csproj @@ -21,6 +21,7 @@ True GenericTypeArguments.g.tt + diff --git a/tests/Pure.DI.IntegrationTests/AttributeTests.cs b/tests/Pure.DI.IntegrationTests/AttributeTests.cs new file mode 100644 index 000000000..ea9e6f254 --- /dev/null +++ b/tests/Pure.DI.IntegrationTests/AttributeTests.cs @@ -0,0 +1,234 @@ +namespace Pure.DI.IntegrationTests; + +public class AttributeTests +{ + [Fact] + public async Task ShouldSupportTypeAttribute() + { + // Given + + // When + var result = await """ +using System; +using Pure.DI; + +namespace Sample +{ + interface IDependency { } + + class AbcDependency : IDependency { } + + class XyzDependency : IDependency { } + + interface IService + { + IDependency Dependency1 { get; } + + IDependency Dependency2 { get; } + } + + class Service : IService + { + public Service( + [Type(typeof(AbcDependency))] IDependency dependency1, + [Type(typeof(XyzDependency))] IDependency dependency2) + { + Dependency1 = dependency1; + Dependency2 = dependency2; + } + + public IDependency Dependency1 { get; } + + public IDependency Dependency2 { get; } + } + + partial class Composition + { + private static void SetupComposition() + { + DI.Setup(nameof(Composition)) + .Bind().To() + + // Composition root + .Root("Root"); + } + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + var service = composition.Root; + Console.WriteLine(service.Dependency1); + Console.WriteLine(service.Dependency2); + } + } +} +""".RunAsync(); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["Sample.AbcDependency", "Sample.XyzDependency"], result); + } + +#if ROSLYN4_8_OR_GREATER + [Fact] + public async Task ShouldSupportGenericTypeAttribute() + { + // Given + + // When + var result = await """ +using System; +using Pure.DI; + +namespace Sample +{ + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] + internal class TypeAttribute : Attribute + { + } + + interface IDependency { } + + class AbcDependency : IDependency { } + + class XyzDependency : IDependency { } + + interface IService + { + IDependency Dependency1 { get; } + + IDependency Dependency2 { get; } + } + + class Service : IService + { + public Service( + [Type] IDependency dependency1, + [Type] IDependency dependency2) + { + Dependency1 = dependency1; + Dependency2 = dependency2; + } + + public IDependency Dependency1 { get; } + + public IDependency Dependency2 { get; } + } + + partial class Composition + { + private static void SetupComposition() + { + DI.Setup(nameof(Composition)) + .TypeAttribute>() + .Bind().To() + + // Composition root + .Root("Root"); + } + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + var service = composition.Root; + Console.WriteLine(service.Dependency1); + Console.WriteLine(service.Dependency2); + } + } +} +""".RunAsync(new Options(LanguageVersion.CSharp11)); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["Sample.AbcDependency", "Sample.XyzDependency"], result); + } + + [Fact] + public async Task ShouldSupportCombinedGenericAttribute() + { + // Given + + // When + var result = await """ +using System; +using Pure.DI; + +namespace Sample +{ + [AttributeUsage( + AttributeTargets.Constructor + | AttributeTargets.Method + | AttributeTargets.Parameter + | AttributeTargets.Property + | AttributeTargets.Field)] + class InjectAttribute : Attribute + { + public InjectAttribute(object? tag = null, int ordinal = 0) + { + } + } + + interface IPerson {} + + class Person : IPerson + { + private object? _state; + + [Inject] + internal object Id = ""; + + private readonly string _name; + + public Person([Inject("NikName")] string name) + { + _name = name; + } + + public void Initialize([Inject] object state) => + _state = state; + + public override string ToString() => $"{Id} {_name} {_state}"; + } + + partial class Composition + { + private static void SetupComposition() + { + DI.Setup(nameof(Composition)) + .TagAttribute>(0) + .OrdinalAttribute>(1) + .TypeAttribute>() + .Arg("personId") + .Bind().To(_ => new Uri("https://github.com/DevTeam/Pure.DI")) + .Bind("NikName").To(_ => "Nik") + .Bind().To() + + // Composition root + .Root("Person"); + } + } + + public class Program + { + public static void Main() + { + var composition = new Composition(personId: 123); + var person = composition.Person; + Console.WriteLine(person); + } + } +} +""".RunAsync(new Options(LanguageVersion.CSharp11)); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["123 Nik https://github.com/DevTeam/Pure.DI"], result); + } +#endif +} \ No newline at end of file diff --git a/tests/Pure.DI.UsageTests/Attributes/ConstructorOrdinalAttributeScenario.cs b/tests/Pure.DI.UsageTests/Attributes/ConstructorOrdinalAttributeScenario.cs index e8ff591b9..1c6467bc2 100644 --- a/tests/Pure.DI.UsageTests/Attributes/ConstructorOrdinalAttributeScenario.cs +++ b/tests/Pure.DI.UsageTests/Attributes/ConstructorOrdinalAttributeScenario.cs @@ -52,8 +52,8 @@ public void Run() // { DI.Setup(nameof(Composition)) .Arg("serviceName") - .Bind().To() - .Bind().To() + .Bind().To() + .Bind().To() // Composition root .Root("Root"); diff --git a/tests/Pure.DI.UsageTests/Attributes/CustomAttributesScenario.cs b/tests/Pure.DI.UsageTests/Attributes/CustomAttributesScenario.cs index 8e67fe288..a01bd4b79 100644 --- a/tests/Pure.DI.UsageTests/Attributes/CustomAttributesScenario.cs +++ b/tests/Pure.DI.UsageTests/Attributes/CustomAttributesScenario.cs @@ -14,6 +14,7 @@ // ReSharper disable UnusedParameter.Local // ReSharper disable ClassNeverInstantiated.Global // ReSharper disable ArrangeTypeModifiers +// ReSharper disable MemberCanBePrivate.Global #pragma warning disable CS9113 // Parameter is unread. namespace Pure.DI.UsageTests.Attributes.CustomAttributesScenario; @@ -39,15 +40,27 @@ class MyTagAttribute(object tag) : Attribute; | AttributeTargets.Field)] class MyTypeAttribute(Type type) : Attribute; +[AttributeUsage( + AttributeTargets.Parameter + | AttributeTargets.Property + | AttributeTargets.Field)] +class MyGenericTypeAttribute : Attribute; + interface IPerson; class Person([MyTag("NikName")] string name) : IPerson { + private object? _state; + [MyOrdinal(1)] [MyType(typeof(int))] internal object Id = ""; - public override string ToString() => $"{Id} {name}"; + [MyOrdinal(2)] + public void Initialize([MyGenericType] object state) => + _state = state; + + public override string ToString() => $"{Id} {name} {_state}"; } // } @@ -62,16 +75,18 @@ public void Run() .TagAttribute() .OrdinalAttribute() .TypeAttribute() + .TypeAttribute>() .Arg("personId") - .Bind("NikName").To(_ => "Nik") - .Bind().To() + .Bind().To(_ => new Uri("https://github.com/DevTeam/Pure.DI")) + .Bind("NikName").To(_ => "Nik") + .Bind().To() // Composition root .Root("Person"); var composition = new PersonComposition(personId: 123); var person = composition.Person; - person.ToString().ShouldBe("123 Nik"); + person.ToString().ShouldBe("123 Nik https://github.com/DevTeam/Pure.DI"); // } composition.SaveClassDiagram(); } diff --git a/tests/Pure.DI.UsageTests/Attributes/CustomUniversalAttributeScenario.cs b/tests/Pure.DI.UsageTests/Attributes/CustomUniversalAttributeScenario.cs new file mode 100644 index 000000000..30a334ba9 --- /dev/null +++ b/tests/Pure.DI.UsageTests/Attributes/CustomUniversalAttributeScenario.cs @@ -0,0 +1,71 @@ +/* +$v=true +$p=11 +$d=Custom universal attribute +$h=You can use a combined attribute, and each method in the list above has an optional parameter that defines the argument number (the default is 0) from where to get the appropriate metadata for _tag_, _ordinal_, or _type_. +*/ + +// ReSharper disable ClassNeverInstantiated.Local +// ReSharper disable CheckNamespace +// ReSharper disable UnusedParameter.Local +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable ArrangeTypeModifiers +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedTypeParameter +// ReSharper disable UnusedMember.Global +#pragma warning disable CS9113 // Parameter is unread. +namespace Pure.DI.UsageTests.Attributes.CustomUniversalAttributeScenario; + +using Xunit; + +// { +[AttributeUsage( + AttributeTargets.Constructor + | AttributeTargets.Method + | AttributeTargets.Parameter + | AttributeTargets.Property + | AttributeTargets.Field)] +class InjectAttribute(object? tag = null, int ordinal = 0) : Attribute; + +interface IPerson; + +class Person([Inject("NikName")] string name) : IPerson +{ + private object? _state; + + [Inject(ordinal: 1)] + internal object Id = ""; + + public void Initialize([Inject] object state) => + _state = state; + + public override string ToString() => $"{Id} {name} {_state}"; +} +// } + +public class Scenario +{ + [Fact] + public void Run() + { + // Resolve = Off +// { + DI.Setup(nameof(PersonComposition)) + .TagAttribute>() + .OrdinalAttribute>(1) + .TypeAttribute>() + .Arg("personId") + .Bind().To(_ => new Uri("https://github.com/DevTeam/Pure.DI")) + .Bind("NikName").To(_ => "Nik") + .Bind().To() + + // Composition root + .Root("Person"); + + var composition = new PersonComposition(personId: 123); + var person = composition.Person; + person.ToString().ShouldBe("123 Nik https://github.com/DevTeam/Pure.DI"); +// } + composition.SaveClassDiagram(); + } +} \ No newline at end of file diff --git a/tests/Pure.DI.UsageTests/Attributes/MemberOrdinalAttributeScenario.cs b/tests/Pure.DI.UsageTests/Attributes/MemberOrdinalAttributeScenario.cs index ba69dd4e7..092f5757a 100644 --- a/tests/Pure.DI.UsageTests/Attributes/MemberOrdinalAttributeScenario.cs +++ b/tests/Pure.DI.UsageTests/Attributes/MemberOrdinalAttributeScenario.cs @@ -68,7 +68,7 @@ public void Run() .Arg("personId") .Arg("personName") .Arg("personBirthday") - .Bind().To() + .Bind().To() // Composition root .Root("Person"); diff --git a/tests/Pure.DI.UsageTests/Attributes/TagAttributeScenario.cs b/tests/Pure.DI.UsageTests/Attributes/TagAttributeScenario.cs index fab7042e5..f0d8cbbff 100644 --- a/tests/Pure.DI.UsageTests/Attributes/TagAttributeScenario.cs +++ b/tests/Pure.DI.UsageTests/Attributes/TagAttributeScenario.cs @@ -50,9 +50,9 @@ public void Run() // Resolve = Off // { DI.Setup(nameof(Composition)) - .Bind("Abc").To() - .Bind("Xyz").To() - .Bind().To() + .Bind("Abc").To() + .Bind("Xyz").To() + .Bind().To() // Composition root .Root("Root"); diff --git a/tests/Pure.DI.UsageTests/Attributes/TypeAttributeScenario.cs b/tests/Pure.DI.UsageTests/Attributes/TypeAttributeScenario.cs index 0d21b501e..ee2538302 100644 --- a/tests/Pure.DI.UsageTests/Attributes/TypeAttributeScenario.cs +++ b/tests/Pure.DI.UsageTests/Attributes/TypeAttributeScenario.cs @@ -50,7 +50,7 @@ public void Run() // Resolve = Off // { DI.Setup(nameof(Composition)) - .Bind().To() + .Bind().To() // Composition root .Root("Root");