Skip to content

Commit

Permalink
Smart tags
Browse files Browse the repository at this point in the history
  • Loading branch information
NikolayPianikov committed Dec 19, 2024
1 parent 46cee16 commit f198a88
Show file tree
Hide file tree
Showing 32 changed files with 553 additions and 47 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ dotnet run
- [Class arguments](readme/class-arguments.md)
- [Root arguments](readme/root-arguments.md)
- [Tags](readme/tags.md)
- [Smart tags](readme/smart-tags.md)
- [Build up of an existing object](readme/build-up-of-an-existing-object.md)
- [Field injection](readme/field-injection.md)
- [Method injection](readme/method-injection.md)
Expand Down
4 changes: 2 additions & 2 deletions readme/consumer-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ partial class Composition
{
Serilog.ILogger transientILogger3;
Serilog.ILogger localLogger48 = _argLogger;
transientILogger3 = localLogger48.ForContext(new Type[1]{typeof(Dependency)}[0]);
transientILogger3 = localLogger48.ForContext( new Type[1]{typeof(Dependency)}[0]);
Serilog.ILogger transientILogger1;
Serilog.ILogger localLogger49 = _argLogger;
transientILogger1 = localLogger49.ForContext(new Type[1]{typeof(Service)}[0]);
transientILogger1 = localLogger49.ForContext( new Type[1]{typeof(Service)}[0]);
return new Service(transientILogger1, new Dependency(transientILogger3));
}
}
Expand Down
4 changes: 2 additions & 2 deletions readme/di-tracing-via-serilog.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ partial class Composition
{
Serilog.ILogger transientILogger0;
Serilog.ILogger localLogger1 = _argLogger;
transientILogger0 = localLogger1.ForContext(new Type[1]{typeof(Composition)}[0]);
transientILogger0 = localLogger1.ForContext( new Type[1]{typeof(Composition)}[0]);
return transientILogger0;
}
}
Expand All @@ -107,7 +107,7 @@ partial class Composition
OnNewInstance<Dependency>(ref transientDependency2, null, Lifetime.Transient);
Serilog.ILogger transientILogger1;
Serilog.ILogger localLogger0 = _argLogger;
transientILogger1 = localLogger0.ForContext(new Type[1]{typeof(Service)}[0]);
transientILogger1 = localLogger0.ForContext( new Type[1]{typeof(Service)}[0]);
Service transientService0 = new Service(transientILogger1, OnDependencyInjection<IDependency>(transientDependency2, null, Lifetime.Transient));
OnNewInstance<Service>(ref transientService0, null, Lifetime.Transient);
return OnDependencyInjection<IService>(transientService0, null, Lifetime.Transient);
Expand Down
184 changes: 184 additions & 0 deletions readme/smart-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#### Smart tags

[![CSharp](https://img.shields.io/badge/C%23-code-blue.svg)](../tests/Pure.DI.UsageTests/Basics/SmartTagsScenario.cs)

When you need to compose a large composition of objects, you may need a large number of tags. Strings or other constant values are not always convenient to use, because there can be infinitely many variants of numbers or strings. And if you specify one value in the binding, you can make a mistake and specify another value in the dependency, which will lead to a compilation error. The solution to this problem is to create an enumerable type and use its values as tags. _Pure.DI_ makes it easier to solve this problem.

When you specify a tag in a binding and the compiler can't determine what that value is, _Pure.DI_ will automatically create a constant for it inside the `Pure.DI.Tag` type. For the example below, the set of constants would look like this:

```c#
namespace Pure.DI
{
internal partial class Tag
{
public const string Abc = "Abc";
public const string Xyz = "Xyz";
}
}
```
In this way you can apply refactoring in the development environment. And also changes of tags in bindings will be automatically checked by the compiler. This will reduce the number of errors.
The example below also uses the `using static Pure.DI.Tag;` directive to access tags in `Pure.DI.Tag` without specifying a type name:


```c#
using static Pure.DI.Tag;
using static Pure.DI.Lifetime;

interface IDependency;

class AbcDependency : IDependency;

class XyzDependency : IDependency;

class Dependency : IDependency;

interface IService
{
IDependency Dependency1 { get; }

IDependency Dependency2 { get; }

IDependency Dependency3 { get; }
}

class Service(
[Tag(Abc)] IDependency dependency1,
[Tag(Xyz)] IDependency dependency2,
IDependency dependency3)
: IService
{
public IDependency Dependency1 { get; } = dependency1;

public IDependency Dependency2 { get; } = dependency2;

public IDependency Dependency3 { get; } = dependency3;
}

DI.Setup(nameof(Composition))
// The `default` tag is used to resolve dependencies
// when the tag was not specified by the consumer
.Bind<IDependency>(Abc, default).To<AbcDependency>()
.Bind<IDependency>(Xyz).As(Singleton).To<XyzDependency>()
.Bind<IService>().To<Service>()

// "XyzRoot" is root name, Xyz is tag
.Root<IDependency>("XyzRoot", Xyz)

// Specifies to create the composition root named "Root"
.Root<IService>("Root");

var composition = new Composition();
var service = composition.Root;
service.Dependency1.ShouldBeOfType<AbcDependency>();
service.Dependency2.ShouldBeOfType<XyzDependency>();
service.Dependency2.ShouldBe(composition.XyzRoot);
service.Dependency3.ShouldBeOfType<AbcDependency>();
```

The following partial class will be generated:

```c#
partial class Composition
{
private readonly Composition _root;
private readonly Lock _lock;

private XyzDependency? _singletonXyzDependency44;

[OrdinalAttribute(256)]
public Composition()
{
_root = this;
_lock = new Lock();
}

internal Composition(Composition parentScope)
{
_root = (parentScope ?? throw new ArgumentNullException(nameof(parentScope)))._root;
_lock = _root._lock;
}

public IDependency XyzRoot
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (_root._singletonXyzDependency44 is null)
{
using (_lock.EnterScope())
{
if (_root._singletonXyzDependency44 is null)
{
_root._singletonXyzDependency44 = new XyzDependency();
}
}
}

return _root._singletonXyzDependency44!;
}
}

public IService Root
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (_root._singletonXyzDependency44 is null)
{
using (_lock.EnterScope())
{
if (_root._singletonXyzDependency44 is null)
{
_root._singletonXyzDependency44 = new XyzDependency();
}
}
}

return new Service(new AbcDependency(), _root._singletonXyzDependency44!, new AbcDependency());
}
}
}
```

Class diagram:

```mermaid
---
config:
class:
hideEmptyMembersBox: true
---
classDiagram
Service --|> IService
XyzDependency --|> IDependency : "Xyz"
AbcDependency --|> IDependency : "Abc"
AbcDependency --|> IDependency
Composition ..> Service : IService Root
Composition ..> XyzDependency : IDependency XyzRoot
Service *-- AbcDependency : "Abc" IDependency
Service o-- "Singleton" XyzDependency : "Xyz" IDependency
Service *-- AbcDependency : IDependency
namespace Pure.DI.UsageTests.Basics.SmartTagsScenario {
class AbcDependency {
+AbcDependency()
}
class Composition {
<<partial>>
+IService Root
+IDependency XyzRoot
}
class IDependency {
<<interface>>
}
class IService {
<<interface>>
}
class Service {
+Service(IDependency dependency1, IDependency dependency2, IDependency dependency3)
}
class XyzDependency {
+XyzDependency()
}
}
```

2 changes: 1 addition & 1 deletion readme/tag-attribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ service.Dependency1.ShouldBeOfType<AbcDependency>();
service.Dependency2.ShouldBeOfType<XyzDependency>();
```

The tag can be a constant, a type, or a value of an enumerated type. This attribute is part of the API, but you can use your own attribute at any time, and this allows you to define them in the assembly and namespace you want.
The tag can be a constant, a type, a [smart tag](smart-tags.md), or a value of an `Enum` type. This attribute is part of the API, but you can use your own attribute at any time, and this allows you to define them in the assembly and namespace you want.

The following partial class will be generated:

Expand Down
2 changes: 1 addition & 1 deletion readme/tags.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ service.Dependency2.ShouldBe(composition.XyzRoot);
service.Dependency3.ShouldBeOfType<AbcDependency>();
```

The tag can be a constant, a type, or a value of an enumerated type. The _default_ and _null_ tags are also supported.
The tag can be a constant, a type, a [smart tag](smart-tags.md), or a value of an `Enum` type. The _default_ and _null_ tags are also supported.

The following partial class will be generated:

Expand Down
2 changes: 1 addition & 1 deletion src/Pure.DI.Core/Components/Api.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,7 @@ internal enum RootKinds
#if !NET20 && !NET35 && !NETSTANDARD1_0 && !NETSTANDARD1_1 && !NETSTANDARD1_2 && !NETSTANDARD1_3 && !NETSTANDARD1_4 && !NETSTANDARD1_5 && !NETSTANDARD1_6 && !NETCOREAPP1_0 && !NETCOREAPP1_1
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
#endif
internal class Tag
internal partial class Tag
{
private static readonly Tag Shared = new Tag();

Expand Down
7 changes: 7 additions & 0 deletions src/Pure.DI.Core/Core/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ internal class Attributes(ISemantic semantic)
: IAttributes
{
public T GetAttribute<TMdAttribute, T>(
SemanticModel semanticModel,
in ImmutableArray<TMdAttribute> metadata,
ISymbol member,
T defaultValue)
Expand Down Expand Up @@ -35,6 +36,12 @@ public T GetAttribute<TMdAttribute, T>(
var args = attr.ConstructorArguments;
if (attributeMetadata.ArgumentPosition >= args.Length)
{
if (attr.ApplicationSyntaxReference?.GetSyntax() is AttributeSyntax { ArgumentList: {} argumentList }
&& attributeMetadata.ArgumentPosition < argumentList.Arguments.Count)
{
return semantic.GetConstantValue<T>(semanticModel, argumentList.Arguments[attributeMetadata.ArgumentPosition].Expression) ?? defaultValue;
}

throw new CompileErrorException($"The argument position {attributeMetadata.ArgumentPosition.ToString()} of attribute {attributeMetadata.Source} is out of range [0..{args.Length.ToString()}].", attributeMetadata.Source.GetLocation(), LogId.ErrorInvalidMetadata);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Pure.DI.Core.Code;

internal sealed class ClassBuilder(
internal sealed class CompositionClassBuilder(
[Tag(typeof(UsingDeclarationsBuilder))]
IBuilder<CompositionCode, CompositionCode> usingDeclarations,
[Tag(typeof(FieldsBuilder))] IBuilder<CompositionCode, CompositionCode> fields,
Expand Down
86 changes: 86 additions & 0 deletions src/Pure.DI.Core/Core/Code/TagClassBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace Pure.DI.Core.Code;

internal class TagClassBuilder(
IInformation information,
ISmartTags smartTags,
IFormatter formatter,
IComments comments,
CancellationToken cancellationToken)
: IBuilder<TagContext, TagCode>
{
public TagCode Build(TagContext tagContext)
{
var tagToDependencies = (
from composition in tagContext.Compositions
from dependency in composition.Source.Graph.Edges
where dependency.Injection.Tag is string
group (composition, dependency) by (string)dependency.Injection.Tag!
).ToDictionary(i => i.Key, i => i.ToList());

cancellationToken.ThrowIfCancellationRequested();

var code = new LinesBuilder();
var tags = smartTags.GetAll();
// ReSharper disable once InvertIf
code.AppendLine("// <auto-generated/>");
code.AppendLine($"// by {information.Description}");

code.AppendLine($"namespace {Names.GeneratorName}");
code.AppendLine("{");
using (code.Indent())
{
code.AppendLine($"internal partial class {nameof(Tag)}");
code.AppendLine("{");
using (code.Indent())
{
var isFirst = true;
foreach (var tag in tags)
{
if (isFirst)
{
isFirst = false;
}
else
{
code.AppendLine();
}

code.AppendLine("/// <summary>");
code.AppendLine($"/// Atomically generated smart tag with value {comments.Escape(tag.Name.ValueToString())}.");
if (tagToDependencies.TryGetValue(tag.Name, out var dependencies))
{
code.AppendLine("/// Used by:");
code.AppendLine("/// <br/>");
var groupByComposition = dependencies.GroupBy(i => i.composition.Source.Source.Name.FullName);
foreach (var compositionGroup in groupByComposition)
{
code.AppendLine("/// <br/>");
code.AppendLine($"/// class {formatter.FormatRef(compositionGroup.Key)}");
code.AppendLine("/// <list type=\"bullet\">");
foreach (var (_, (_, dependencyNode, injection, target)) in compositionGroup
.OrderBy(i => i.dependency.Target.Binding.Id)
.ThenBy(i => i.dependency.Source.Binding.Id))
{
var tagStr = comments.Escape(injection.Tag != null && injection.Tag is not MdTagOnSites ? $"({injection.Tag})" : "");
code.AppendLine($"/// <item>{formatter.FormatRef(target.Type)} &lt;-- {formatter.FormatRef(injection.Type)}{tagStr} -- {formatter.FormatRef(dependencyNode.Type)} </item>");
}

code.AppendLine("/// </list>");
}


}

code.AppendLine("/// </summary>");
code.AppendLine($"public const string {tag.Name} = \"{tag.Name}\";");
}
}

code.AppendLine("}");
}

code.AppendLine("}");

return new TagCode(code);
}
}
4 changes: 4 additions & 0 deletions src/Pure.DI.Core/Core/Code/TagContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace Pure.DI.Core.Code;

internal record TagContext(
IReadOnlyCollection<CompositionCode> Compositions);
Loading

0 comments on commit f198a88

Please sign in to comment.