diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 48b53467..d2450894 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -43,6 +43,11 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/linux-macOS-CI.yml b/.github/workflows/linux-macOS-CI.yml index 117c7186..9d6f44e2 100644 --- a/.github/workflows/linux-macOS-CI.yml +++ b/.github/workflows/linux-macOS-CI.yml @@ -29,6 +29,7 @@ jobs: 5.0.x 6.0.x 7.0.x + 8.0.x - name: Restore run: dotnet restore - name: Test @@ -36,4 +37,5 @@ jobs: dotnet test test/stashbox.tests.csproj -c Release -f netcoreapp3.1 --no-restore dotnet test test/stashbox.tests.csproj -c Release -f net5.0 --no-restore dotnet test test/stashbox.tests.csproj -c Release -f net6.0 --no-restore - dotnet test test/stashbox.tests.csproj -c Release -f net7.0 --no-restore \ No newline at end of file + dotnet test test/stashbox.tests.csproj -c Release -f net7.0 --no-restore + dotnet test test/stashbox.tests.csproj -c Release -f net8.0 --no-restore \ No newline at end of file diff --git a/.github/workflows/sonar-analysis.yml b/.github/workflows/sonar-analysis.yml index 064d6294..5bd5e90e 100644 --- a/.github/workflows/sonar-analysis.yml +++ b/.github/workflows/sonar-analysis.yml @@ -26,6 +26,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x - name: Cache SonarCloud packages uses: actions/cache@v3 with: diff --git a/.version b/.version index c0c99913..22f097e2 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -5.12.2 \ No newline at end of file +5.13.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fb33d48a..be69ee76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v5.13.0] - 2023-11-18 +### Added +- .NET 8.0 target. +- [#134](https://github.com/z4kn4fein/stashbox/issues/134) Concept of [Auto lifetime](https://z4kn4fein.github.io/stashbox/docs/guides/lifetimes#auto-lifetime): + - It aligns to the lifetime of the resolved service's dependencies. When the underlying service has a dependency with a higher lifespan, this lifetime will inherit that lifespan up to a given boundary. +- Auto injection of `required` members. +- MS.DI compatibility features for supporting [keyed services](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#keyed-di-services): + - `DependencyName` attribute. When a parameter is marked with this attribute, the container will pass the given dependency's name to it. + - `WithUniversalName()` container configuration method. It sets the universal name which is a special name that allows named resolution work for any given name. + - `WithAdditionalDependencyNameAttribute()` container configuration method. It adds an attribute type that is considered a dependency name indicator just like the [`DependencyName` attribute](https://z4kn4fein.github.io/stashbox/docs/guides/service-resolution#attributes). + - `WithAdditionalDependencyAttribute()` container configuration method. It adds an attribute type that is considered a dependency indicator just like the [`Dependency` attribute](https://z4kn4fein.github.io/stashbox/docs/guides/service-resolution#attributes). + ## [v5.12.2] - 2023-09-05 ### Fixed - There was an issue where using decorators with instance registrations resulted in resolution failure. @@ -398,6 +410,7 @@ The validation was executed only at the expression tree building phase, so an al - Removed the legacy container extension functionality. - Removed the support of PCL v259. +[v5.13.0]: https://github.com/z4kn4fein/stashbox/compare/5.12.2...5.13.0 [v5.12.2]: https://github.com/z4kn4fein/stashbox/compare/5.12.1...5.12.2 [v5.12.1]: https://github.com/z4kn4fein/stashbox/compare/5.11.1...5.12.1 [v5.11.1]: https://github.com/z4kn4fein/stashbox/compare/5.11.0...5.11.1 diff --git a/README.md b/README.md index 43ed2b96..97fe8c51 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Stashbox is a lightweight, fast, and portable dependency injection framework for Github (stable) | NuGet (stable) | Fuget (stable) | NuGet (pre-release) --- | --- |---------------------------------------------------------------------------------------------------------------------------------| --- -[![Github release](https://img.shields.io/github/release/z4kn4fein/stashbox.svg)](https://github.com/z4kn4fein/stashbox/releases) | [![NuGet Version](https://buildstats.info/nuget/Stashbox)](https://www.nuget.org/packages/Stashbox/) | [![Stashbox on fuget.org](https://www.fuget.org/packages/Stashbox/badge.svg?v=5.12.2)](https://www.fuget.org/packages/Stashbox) | [![Nuget pre-release](https://img.shields.io/nuget/vpre/Stashbox)](https://www.nuget.org/packages/Stashbox/) +[![Github release](https://img.shields.io/github/release/z4kn4fein/stashbox.svg)](https://github.com/z4kn4fein/stashbox/releases) | [![NuGet Version](https://buildstats.info/nuget/Stashbox)](https://www.nuget.org/packages/Stashbox/) | [![Stashbox on fuget.org](https://www.fuget.org/packages/Stashbox/badge.svg?v=5.13.0)](https://www.fuget.org/packages/Stashbox) | [![Nuget pre-release](https://img.shields.io/nuget/vpre/Stashbox)](https://www.nuget.org/packages/Stashbox/) ## Core Attributes - 🚀 Fast, thread-safe, and lock-free operations. diff --git a/appveyor-release.yml b/appveyor-release.yml index 76219b77..3eeee1d2 100644 --- a/appveyor-release.yml +++ b/appveyor-release.yml @@ -30,6 +30,10 @@ install: $env:build_version = Get-Content ".version" Update-AppveyorBuild -Version "$env:build_version-$env:appveyor_build_number" dotnet tool install -g InheritDocTool + + Invoke-WebRequest 'https://dot.net/v1/dotnet-install.ps1' -OutFile dotnet-install.ps1 + ./dotnet-install.ps1 -Channel 7.0 + ./dotnet-install.ps1 -Channel 8.0 dotnet_csproj: patch: true diff --git a/appveyor.yml b/appveyor.yml index bc577632..0d549ca2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,6 +20,10 @@ install: $env:build_version = Get-Content ".version" Update-AppveyorBuild -Version "$env:build_version-preview-$env:appveyor_build_number" dotnet tool install -g InheritDocTool + + Invoke-WebRequest 'https://dot.net/v1/dotnet-install.ps1' -OutFile dotnet-install.ps1 + ./dotnet-install.ps1 -Channel 7.0 + ./dotnet-install.ps1 -Channel 8.0 dotnet_csproj: patch: true @@ -46,6 +50,7 @@ test_script: - dotnet test test\stashbox.tests.csproj -f net5.0 -c %configuration% --no-build - dotnet test test\stashbox.tests.csproj -f net6.0 -c %configuration% --no-build - dotnet test test\stashbox.tests.csproj -f net7.0 -c %configuration% --no-build +- dotnet test test\stashbox.tests.csproj -f net8.0 -c %configuration% --no-build artifacts: - path: artifacts\Stashbox.*.nupkg diff --git a/docs/docs/configuration/container-configuration.md b/docs/docs/configuration/container-configuration.md index a7d032cd..86c57224 100644 --- a/docs/docs/configuration/container-configuration.md +++ b/docs/docs/configuration/container-configuration.md @@ -68,6 +68,7 @@ new StashboxContainer(options => options ## Auto member-injection With this option, you can enable or disable the auto member-injection without [attributes](/docs/guides/service-resolution#attributes). +
@@ -140,9 +141,26 @@ new StashboxContainer(options => options
+ +
-:::note -Member selection filter: `config.WithAutoMemberInjection(filter: member => member.Type != typeof(IJob))` +#### Member selection filter +You can pass your own member selection logic to control which members should be auto injected. + +
+
+ +```cs +new StashboxContainer(options => options + .WithAutoMemberInjection( + filter: member => member.Type != typeof(ILogger))); +``` + +
+
+ +:::info +Members defined with C# 11's [`required`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required) keyword are automatically injected by the container. ::: ## Constructor selection @@ -328,6 +346,57 @@ new StashboxContainer(options => options +## Named service resolution + +
+ +### `WithUniversalName` +Sets the universal name that represents a special name which allows named resolution work for any given name. + +
+
+ +```cs +new StashboxContainer(options => options + .WithUniversalName("Any")); +``` + +
+
+ + +
+ +### `WithAdditionalDependencyNameAttribute` +Adds an attribute type that is considered a dependency name indicator just like the [`DependencyName` attribute](/docs/guides/service-resolution#attributes). + +
+
+ +```cs +new StashboxContainer(options => options + .WithAdditionalDependencyNameAttribute()); +``` + +
+
+ + +
+ +### `WithAdditionalDependencyAttribute` +Adds an attribute type that is considered a dependency indicator just like the [`Dependency` attribute](/docs/guides/service-resolution#attributes). + +
+
+ +```cs +new StashboxContainer(options => options + .WithAdditionalDependencyAttribute()); +``` + +
+
## Default value injection diff --git a/docs/docs/configuration/registration-configuration.md b/docs/docs/configuration/registration-configuration.md index 07c94e66..f7737ce7 100644 --- a/docs/docs/configuration/registration-configuration.md +++ b/docs/docs/configuration/registration-configuration.md @@ -374,7 +374,7 @@ container.Register(config => config
### `WithPerScopedRequestLifetime` -Sets the lifetime to `PerScopedRequestLifetime`. That means this registration will behave like a singleton within every scoped resolution request. +Sets the lifetime to `PerScopedRequestLifetime`. This lifetime will create a new instance between scoped services. This means that every scoped service will get a different instance but within their dependency tree it will behave as a singleton.
@@ -390,6 +390,40 @@ container.Register(options => options
+### `WithPerRequestLifetime` +Sets the lifetime to `PerRequestLifetime`. This lifetime will create a new instance between resolution requests. Within the request the same instance will be re-used. + +
+
+ +```cs +container.Register(options => options + .WithPerRequestLifetime()); +``` + +
+
+ + +
+ +### `WithAutoLifetime` +Sets the lifetime to auto lifetime. This lifetime aligns to the lifetime of the resolved service's dependencies. When the underlying service has a dependency with a higher lifespan, this lifetime will inherit that lifespan up to a given boundary. + +
+
+ +```cs +container.Register(options => options + .WithAutoLifetime(Lifetimes.Scoped /* boundary lifetime */)); +``` + +
+
+ + +
+ ### `WithLifetime` Sets a custom lifetime for the registration. @@ -708,7 +742,9 @@ container.Register(options => options
- +:::info +Members defined with C# 11's [`required`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required) keyword are automatically injected by the container. +::: ## Injection parameters diff --git a/docs/docs/getting-started/introduction.md b/docs/docs/getting-started/introduction.md index 4ae34da2..52b46efa 100644 --- a/docs/docs/getting-started/introduction.md +++ b/docs/docs/getting-started/introduction.md @@ -12,7 +12,7 @@ Stashbox and its extensions are distributed via [NuGet](https://www.nuget.org/pa You can install the package by typing the following into the Package Manager Console: ```powershell -Install-Package Stashbox -Version 5.12.2 +Install-Package Stashbox -Version 5.13.0 ``` @@ -20,7 +20,7 @@ Install-Package Stashbox -Version 5.12.2 You can install the package by using the dotnet cli: ```bash -dotnet add package Stashbox --version 5.12.2 +dotnet add package Stashbox --version 5.13.0 ``` @@ -28,7 +28,7 @@ dotnet add package Stashbox --version 5.12.2 You can add the package into the package references of your `.csproj`: ```xml - + ``` diff --git a/docs/docs/getting-started/overview.md b/docs/docs/getting-started/overview.md index ea82b8c8..f049c349 100644 --- a/docs/docs/getting-started/overview.md +++ b/docs/docs/getting-started/overview.md @@ -17,7 +17,7 @@ These are the latest available stable and pre-release versions: Github (stable) | NuGet (stable) | Fuget (stable) | NuGet (daily) --- | --- |---------------------------------------------------------------------------------------------------------------------------------| --- -[![Github release](https://img.shields.io/github/release/z4kn4fein/stashbox.svg)](https://github.com/z4kn4fein/stashbox/releases) | [![NuGet Version](https://buildstats.info/nuget/Stashbox)](https://www.nuget.org/packages/Stashbox/) | [![Stashbox on fuget.org](https://www.fuget.org/packages/Stashbox/badge.svg?v=5.12.2)](https://www.fuget.org/packages/Stashbox) | [![Nuget pre-release](https://img.shields.io/nuget/vpre/Stashbox)](https://www.nuget.org/packages/Stashbox/) +[![Github release](https://img.shields.io/github/release/z4kn4fein/stashbox.svg)](https://github.com/z4kn4fein/stashbox/releases) | [![NuGet Version](https://buildstats.info/nuget/Stashbox)](https://www.nuget.org/packages/Stashbox/) | [![Stashbox on fuget.org](https://www.fuget.org/packages/Stashbox/badge.svg?v=5.13.0)](https://www.fuget.org/packages/Stashbox) | [![Nuget pre-release](https://img.shields.io/nuget/vpre/Stashbox)](https://www.nuget.org/packages/Stashbox/) ## Core attributes - 🚀 Fast, thread-safe, and lock-free operations. diff --git a/docs/docs/guides/lifetimes.md b/docs/docs/guides/lifetimes.md index e57974ef..2f73fd7a 100644 --- a/docs/docs/guides/lifetimes.md +++ b/docs/docs/guides/lifetimes.md @@ -156,10 +156,6 @@ It is the same as scoped lifetime, except the given service will be selected onl You can also let a service [define](/docs/guides/scopes#service-as-scope) its own named scope. During registration, this scope can be referred to by its name upon using a named scope lifetime. -:::note -Services with named scope lifetime are disposed when the related named scope is being disposed. -::: -
@@ -213,6 +209,10 @@ DbJobExecutor executor = scope.Resolve();
+:::note +Services with named scope lifetime are disposed when the related named scope is being disposed. +::: + ## Per-request lifetime @@ -249,6 +249,66 @@ container.Register(options => options +## Auto lifetime + + +
+ +The requested service's lifetime will align to the lifetime of its dependencies. When the requested service has a dependency with a higher lifespan, this lifetime will inherit that lifespan up to a given boundary. + +
+
+ +```cs +container.Register(options => options + .WithAutoLifetime(Lifetimes.Scoped /* boundary lifetime */)); +``` + +
+
+ + +
+ +If the requested service has auto lifetime with a scoped boundary and it has only transient dependencies, it'll inherit their transient lifetime. + +
+
+ +```cs +container.Register(); + +container.Register(options => options + .WithAutoLifetime(Lifetimes.Scoped /* boundary lifetime */)); + +// job has transient lifetime. +var job = container.Resolve(); +``` + +
+
+ + +
+ +When there's a dependency with higher lifespan than the given boundary, the requested service will get the boundary lifetime. + +
+
+ +```cs +container.RegisterSingleton(); + +container.Register(options => options + .WithAutoLifetime(Lifetimes.Scoped /* boundary lifetime */)); + +// job has scoped lifetime. +var job = container.Resolve(); +``` + +
+
+ ## Custom lifetime If you'd like to use a custom lifetime, you can create your implementation by inheriting either from `FactoryLifetimeDescriptor` or from `ExpressionLifetimeDescriptor`, depending on how do you want to manage the service instances. diff --git a/docs/docs/guides/service-resolution.md b/docs/docs/guides/service-resolution.md index 1d11898f..f4b0653d 100644 --- a/docs/docs/guides/service-resolution.md +++ b/docs/docs/guides/service-resolution.md @@ -19,6 +19,8 @@ Stashbox, by default, uses the constructor that has the most parameters it knows [Property/field injection](/docs/configuration/registration-configuration#property-field-injection) is also supported in cases where constructor injection is not applicable. +Members defined with C# 11's [`required`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required) keyword are automatically injected by the container. + :::info [Constructor selection](/docs/configuration/container-configuration#constructor-selection) and [property/field injection](/docs/configuration/container-configuration#auto-member-injection) is also configurable container-wide. ::: @@ -97,6 +99,8 @@ Attributes can give you control over how Stashbox selects dependencies for a ser - **On a property/field**: first, it enables *auto-injection* on the marked property/field (even if it wasn't configured at registration explicitly), and just as with the method parameter, it allows [named resolution](/docs/getting-started/glossary#named-resolution). +**DependencyName attribute**: marks a parameter to let the container know that it must pass the given dependency's name to it. + **InjectionMethod attribute**: marks a method to be called when the requested service is instantiated. @@ -147,6 +151,24 @@ container.Register(); IJob job = container.Resolve(); ``` + + + +```cs +class DbBackup : IJob +{ + public string Name { get; set; } + + public DbBackup([DependencyName] string name) + { } +} + +container.Register("Backup"); + +// job.Name is "Backup". +IJob job = container.Resolve(); +``` + @@ -176,19 +198,84 @@ IJob job = container.Resolve(); :::caution -Attributes provide a more straightforward configuration, but using them also tightens the bond between your application and Stashbox. If you consider this an issue, the same functionality is available on the *registration API* as [dependency binding](/docs/guides/service-resolution#dependency-binding). +Attributes provide a more straightforward configuration, but using them also tightens the bond between your application and Stashbox. If you consider this an issue, you can use the [dependency binding](/docs/guides/service-resolution#dependency-binding) API or [your own attributes](/docs/guides/service-resolution#using-your-own-attributes). ::: +### Using your own attributes + + +
+There's an option to extend the container's dependency finding mechanism with your own attributes. + +- **Additional Dependency attributes**: you can use the [`.WithAdditionalDependencyAttribute()`](/docs/configuration/container-configuration#withadditionaldependencyattribute) container configuration option to let the container know that it should watch for additional attributes besides the built-in [`Dependency`](/docs/guides/service-resolution#attributes) attribute upon building up the [resolution tree](/docs/getting-started/glossary#resolution-tree). + +- **Additional DependencyName attributes**: you can use the [`.WithAdditionalDependencyNameAttribute()`](/docs/configuration/container-configuration#withadditionaldependencynameattribute) container configuration option to use additional dependency name indicator attributes besides the built-in [`DependencyName`](/docs/guides/service-resolution#attributes) attribute. + +
+
+ + + + +```cs +class DbBackup : IJob +{ + [CustomDependency("Console")] + public ILogger Logger { get; set; } + + public DbBackup() + { } +} + +var container = new StashboxContainer(options => options + .WithAdditionalDependencyAttribute()); + +container.Register("Console"); +container.Register("File"); + +container.Register(); + +// the container will resolve DbBackup with ConsoleLogger. +IJob job = container.Resolve(); +``` + + + + +```cs +class DbBackup : IJob +{ + public string Name { get; set; } + + public DbBackup([CustomName] string name) + { } +} + +var container = new StashboxContainer(options => options + .WithAdditionalDependencyNameAttribute()); + +container.Register("Backup"); + +// job.Name is "Backup". +IJob job = container.Resolve(); +``` + + + + +
+
+ ## Dependency binding
-The same dependency configuration as attributes is available on the registration configuration API. +The same dependency configuration functionality as attributes, but without attributes. -- **Bind to parameter**: it has the same functionality as the [Dependency attribute](/docs/guides/service-resolution#attributes) on a constructor or method parameter, enabling the [named resolution](/docs/getting-started/glossary#named-resolution). +- **Binding to a parameter**: the same functionality as the [`Dependency`](/docs/guides/service-resolution#attributes) attribute on a constructor or method parameter, enabling [named resolution](/docs/getting-started/glossary#named-resolution). -- **Bind to property/field**: it has the same functionality as the [Dependency attribute](/docs/guides/service-resolution#attributes), enabling the injection of the given property/field. +- **Binding to a property/field**: the same functionality as the [`Dependency`](/docs/guides/service-resolution#attributes) attribute, enabling the injection of the given property/field. :::info There are further dependency binding options [available](/docs/configuration/registration-configuration#dependency-configuration) on the registration configuration API. diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index 72836f0f..d8d6deb1 100644 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -24,7 +24,7 @@ function HomepageHeader() {
-
{'$'} dotnet add package Stashbox --version 5.12.2
+
{'$'} dotnet add package Stashbox --version 5.13.0
diff --git a/src/Attributes/DependencyNameAttribute.cs b/src/Attributes/DependencyNameAttribute.cs new file mode 100644 index 00000000..497209be --- /dev/null +++ b/src/Attributes/DependencyNameAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Stashbox.Attributes; + +/// +/// When a parameter is marked with this attribute, the container will pass the given dependency's name to it. +/// +[AttributeUsage(AttributeTargets.Parameter)] +public class DependencyNameAttribute : Attribute +{ +} \ No newline at end of file diff --git a/src/Configuration/ContainerConfiguration.cs b/src/Configuration/ContainerConfiguration.cs index 76d79796..cc0c21e4 100644 --- a/src/Configuration/ContainerConfiguration.cs +++ b/src/Configuration/ContainerConfiguration.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; +using Stashbox.Utils.Data; namespace Stashbox.Configuration; @@ -90,6 +91,12 @@ public class ContainerConfiguration /// public bool LifetimeValidationEnabled { get; internal set; } + internal object? UniversalName { get; set; } + + internal ExpandableArray? AdditionalDependencyNameAttributeTypes { get; set; } + + internal ExpandableArray? AdditionalDependencyAttributeTypes { get; set; } + /// /// A delegate to use external expression compilers. /// @@ -112,7 +119,10 @@ private ContainerConfiguration(bool trackTransientsForDisposalEnabled, Func? autoMemberInjectionFilter, LifetimeDescriptor defaultLifetime, bool lifetimeValidationEnabled, - Func? externalExpressionCompiler) + Func? externalExpressionCompiler, + object? universalName, + ExpandableArray? additionalDependencyNameAttributeTypes, + ExpandableArray? additionalDependencyAttributeTypes) { this.TrackTransientsForDisposalEnabled = trackTransientsForDisposalEnabled; this.RegistrationBehavior = registrationBehavior; @@ -130,6 +140,9 @@ private ContainerConfiguration(bool trackTransientsForDisposalEnabled, this.DefaultLifetime = defaultLifetime; this.LifetimeValidationEnabled = lifetimeValidationEnabled; this.ExternalExpressionCompiler = externalExpressionCompiler; + this.UniversalName = universalName; + this.AdditionalDependencyNameAttributeTypes = additionalDependencyNameAttributeTypes; + this.AdditionalDependencyAttributeTypes = additionalDependencyAttributeTypes; } internal ContainerConfiguration Clone() => @@ -148,5 +161,8 @@ internal ContainerConfiguration Clone() => this.AutoMemberInjectionFilter, this.DefaultLifetime, this.LifetimeValidationEnabled, - this.ExternalExpressionCompiler); + this.ExternalExpressionCompiler, + this.UniversalName, + this.AdditionalDependencyNameAttributeTypes, + this.AdditionalDependencyAttributeTypes); } \ No newline at end of file diff --git a/src/Configuration/ContainerConfigurator.cs b/src/Configuration/ContainerConfigurator.cs index f4f7d6f6..08e1421c 100644 --- a/src/Configuration/ContainerConfigurator.cs +++ b/src/Configuration/ContainerConfigurator.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; +using Stashbox.Attributes; +using Stashbox.Utils.Data; namespace Stashbox.Configuration; @@ -168,4 +170,41 @@ public ContainerConfigurator WithExpressionCompiler(Func + /// Sets the universal name that represents a special name which allows named resolution work for any given name. + /// + /// The universal name. + /// The container configurator. + public ContainerConfigurator WithUniversalName(object name) + { + this.ContainerConfiguration.UniversalName = name; + return this; + } + + /// + /// Adds an attribute type that is considered a dependency name indicator just like . + /// + /// The attribute type. + /// The container configurator. + public ContainerConfigurator WithAdditionalDependencyNameAttribute() + where TAttribute : Attribute + { + this.ContainerConfiguration.AdditionalDependencyNameAttributeTypes ??= new ExpandableArray(); + this.ContainerConfiguration.AdditionalDependencyNameAttributeTypes.Add(typeof(TAttribute)); + return this; + } + + /// + /// Adds an attribute type that is considered a dependency indicator just like . + /// + /// The attribute type. + /// The container configurator. + public ContainerConfigurator WithAdditionalDependencyAttribute() + where TAttribute : Attribute + { + this.ContainerConfiguration.AdditionalDependencyAttributeTypes ??= new ExpandableArray(); + this.ContainerConfiguration.AdditionalDependencyAttributeTypes.Add(typeof(TAttribute)); + return this; + } } \ No newline at end of file diff --git a/src/Exceptions/CircularDependencyException.cs b/src/Exceptions/CircularDependencyException.cs index 7f22fa5b..ad6d4b13 100644 --- a/src/Exceptions/CircularDependencyException.cs +++ b/src/Exceptions/CircularDependencyException.cs @@ -26,6 +26,9 @@ public CircularDependencyException(Type? type, Exception? innerException = null) } /// +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to the serialization ctor +#endif protected CircularDependencyException(SerializationInfo info, StreamingContext context) : base(info, context) { } diff --git a/src/Exceptions/CompositionRootNotFoundException.cs b/src/Exceptions/CompositionRootNotFoundException.cs index 481e9907..155b3dfd 100644 --- a/src/Exceptions/CompositionRootNotFoundException.cs +++ b/src/Exceptions/CompositionRootNotFoundException.cs @@ -20,6 +20,9 @@ public CompositionRootNotFoundException(Assembly assembly, Exception? innerExcep { } /// +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to the serialization ctor +#endif protected CompositionRootNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) { } diff --git a/src/Exceptions/ConstructorNotFoundException.cs b/src/Exceptions/ConstructorNotFoundException.cs index af7d3aa1..5118a910 100644 --- a/src/Exceptions/ConstructorNotFoundException.cs +++ b/src/Exceptions/ConstructorNotFoundException.cs @@ -40,6 +40,9 @@ public ConstructorNotFoundException(Type type, Type argument, Exception? innerEx { } /// +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to the serialization ctor +#endif protected ConstructorNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) { } diff --git a/src/Exceptions/InvalidRegistrationException.cs b/src/Exceptions/InvalidRegistrationException.cs index 47f34b21..69bbb9e5 100644 --- a/src/Exceptions/InvalidRegistrationException.cs +++ b/src/Exceptions/InvalidRegistrationException.cs @@ -27,6 +27,9 @@ public InvalidRegistrationException(Type? type, string message, Exception? inner } /// +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to the serialization ctor +#endif protected InvalidRegistrationException(SerializationInfo info, StreamingContext context) : base(info, context) { } diff --git a/src/Exceptions/LifetimeValidationFailedException.cs b/src/Exceptions/LifetimeValidationFailedException.cs index 45b559a9..5724e1a5 100644 --- a/src/Exceptions/LifetimeValidationFailedException.cs +++ b/src/Exceptions/LifetimeValidationFailedException.cs @@ -26,6 +26,9 @@ public LifetimeValidationFailedException(Type? type, string message) } /// +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to the serialization ctor +#endif protected LifetimeValidationFailedException(SerializationInfo info, StreamingContext context) : base(info, context) { } diff --git a/src/Exceptions/ResolutionFailedException.cs b/src/Exceptions/ResolutionFailedException.cs index 226eac5b..41327ffb 100644 --- a/src/Exceptions/ResolutionFailedException.cs +++ b/src/Exceptions/ResolutionFailedException.cs @@ -31,6 +31,9 @@ public ResolutionFailedException(Type? type, } /// +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to the serialization ctor +#endif protected ResolutionFailedException(SerializationInfo info, StreamingContext context) : base(info, context) { } diff --git a/src/Exceptions/ServiceAlreadyRegisteredException.cs b/src/Exceptions/ServiceAlreadyRegisteredException.cs index fb10dc67..40d86743 100644 --- a/src/Exceptions/ServiceAlreadyRegisteredException.cs +++ b/src/Exceptions/ServiceAlreadyRegisteredException.cs @@ -28,6 +28,9 @@ public ServiceAlreadyRegisteredException(Type? type, Exception? innerException = } /// +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to the serialization ctor +#endif protected ServiceAlreadyRegisteredException(SerializationInfo info, StreamingContext context) : base(info, context) { } diff --git a/src/Extensions/TypeExtensions.cs b/src/Extensions/TypeExtensions.cs index 54550eaa..f938bd3f 100644 --- a/src/Extensions/TypeExtensions.cs +++ b/src/Extensions/TypeExtensions.cs @@ -113,7 +113,7 @@ public static TypeInformation AsTypeInformation(this ParameterInfo parameter, ContainerConfiguration containerConfiguration) { var customAttributes = parameter.GetCustomAttributes(); - var dependencyName = parameter.GetDependencyAttribute()?.Name; + var dependencyName = parameter.GetNameFromDependencyAttribute(containerConfiguration); if (serviceRegistration != null) { @@ -141,6 +141,7 @@ public static TypeInformation AsTypeInformation(this ParameterInfo parameter, parameter.Name, parameter.HasDefaultValue(), parameter.DefaultValue, + parameter.HasDependencyNameAttribute(containerConfiguration), null); } @@ -150,7 +151,7 @@ public static TypeInformation AsTypeInformation(this MemberInfo member, ContainerConfiguration containerConfiguration) { var customAttributes = member.GetCustomAttributes(); - var dependencyName = member.GetDependencyAttribute()?.Name; + var dependencyName = member.GetNameFromDependencyAttribute(containerConfiguration); if (serviceRegistration != null) { @@ -174,6 +175,7 @@ public static TypeInformation AsTypeInformation(this MemberInfo member, member.Name, false, null, + member.HasDependencyNameAttribute(containerConfiguration), null); } @@ -274,6 +276,16 @@ public static bool SatisfiesGenericConstraintsOf(this Type implementationType, T return true; } + public static bool HasDependencyNameAttribute(this ParameterInfo parameterInfo, ContainerConfiguration containerConfiguration) => + parameterInfo.GetCustomAttributes(TypeCache.Type, false).FirstOrDefault() != null || + parameterInfo.CustomAttributes.Any(ca => containerConfiguration.AdditionalDependencyNameAttributeTypes != null && + containerConfiguration.AdditionalDependencyNameAttributeTypes.Contains(ca.AttributeType)); + + public static bool HasDependencyNameAttribute(this MemberInfo memberInfo, ContainerConfiguration containerConfiguration) => + memberInfo.GetCustomAttributes(TypeCache.Type, false).FirstOrDefault() != null || + memberInfo.CustomAttributes.Any(ca => containerConfiguration.AdditionalDependencyNameAttributeTypes != null && + containerConfiguration.AdditionalDependencyNameAttributeTypes.Contains(ca.AttributeType)); + public static bool IsNullableType(this Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == TypeCache.NullableType; @@ -321,10 +333,13 @@ private static bool FilterProperty(this PropertyInfo prop, Dictionary private static bool HasPublicParameterlessConstructor(this Type type) => Array.Find(type.GetConstructors(), c => c.GetParameters().Length == 0) != null; - private static DependencyAttribute? GetDependencyAttribute(this MemberInfo property) + private static object? GetNameFromDependencyAttribute(this MemberInfo property, ContainerConfiguration containerConfiguration) { var attr = property.GetCustomAttributes(TypeCache.Type, false).FirstOrDefault(); - return attr as DependencyAttribute; + if (attr != null) return (attr as DependencyAttribute)?.Name; + + var msAttr = property.CustomAttributes.FirstOrDefault(a => containerConfiguration.AdditionalDependencyAttributeTypes != null && + containerConfiguration.AdditionalDependencyAttributeTypes.Contains(a.AttributeType)); + return msAttr?.ConstructorArguments.FirstOrDefault().Value; } - private static DependencyAttribute? GetDependencyAttribute(this ParameterInfo parameter) + private static object? GetNameFromDependencyAttribute(this ParameterInfo parameter, ContainerConfiguration containerConfiguration) { var attr = parameter.GetCustomAttributes(TypeCache.Type, false).FirstOrDefault(); - return attr as DependencyAttribute; + if (attr != null) return (attr as DependencyAttribute)?.Name; + + var msAttr = parameter.CustomAttributes.FirstOrDefault(a => containerConfiguration.AdditionalDependencyAttributeTypes != null && + containerConfiguration.AdditionalDependencyAttributeTypes.Contains(a.AttributeType)); + return msAttr?.ConstructorArguments.FirstOrDefault().Value; + } + + private static bool HasDependencyAttribute(this MemberInfo property, ContainerConfiguration containerConfiguration) + { + return property.GetCustomAttributes(TypeCache.Type, false).FirstOrDefault() != null || + property.CustomAttributes.Any(a => containerConfiguration.AdditionalDependencyAttributeTypes != null && + containerConfiguration.AdditionalDependencyAttributeTypes.Contains(a.AttributeType)); } + +#if HAS_REQUIRED + private static bool HasRequiredAttribute(this MemberInfo memberInfo) => + memberInfo.GetCustomAttributes(TypeCache.Type, false).FirstOrDefault() != null; +#endif private static InjectionMethodAttribute? GetInjectionAttribute(this MemberInfo method) { diff --git a/src/Lifetime/AutoLifetime.cs b/src/Lifetime/AutoLifetime.cs new file mode 100644 index 00000000..b9f8267b --- /dev/null +++ b/src/Lifetime/AutoLifetime.cs @@ -0,0 +1,42 @@ +using System.Linq.Expressions; +using Stashbox.Registration; +using Stashbox.Resolution; + +namespace Stashbox.Lifetime; + +internal class AutoLifetime : LifetimeDescriptor +{ + private LifetimeDescriptor selectedLifetime; + + internal override bool StoreResultInLocalVariable => this.selectedLifetime.StoreResultInLocalVariable; + + protected internal override int LifeSpan => this.selectedLifetime.LifeSpan; + + public AutoLifetime(LifetimeDescriptor boundaryLifetime) + { + this.selectedLifetime = boundaryLifetime; + } + + private protected override Expression? BuildLifetimeAppliedExpression(ServiceRegistration serviceRegistration, + ResolutionContext resolutionContext, TypeInformation typeInformation) + { + var tracker = new ResolutionContext.AutoLifetimeTracker(); + var context = resolutionContext.BeginAutoLifetimeTrackingContext(tracker); + var expression = GetExpressionForRegistration(serviceRegistration, context, typeInformation); + this.selectedLifetime = tracker.HighestRankingLifetime.LifeSpan <= this.selectedLifetime.LifeSpan + ? tracker.HighestRankingLifetime + : this.selectedLifetime; + + var func = expression?.CompileDelegate(context, context.CurrentContainerContext.ContainerConfiguration); + if (func == null) return null; + + var final = Expression.Invoke(func.AsConstant(), context.CurrentScopeParameter, context.RequestContextParameter); + + return this.selectedLifetime.ApplyLifetimeToExpression(final, serviceRegistration, resolutionContext, typeInformation); + } + + internal override Expression? ApplyLifetimeToExpression(Expression? expression, + ServiceRegistration serviceRegistration, + ResolutionContext resolutionContext, TypeInformation typeInformation) => + this.selectedLifetime.ApplyLifetimeToExpression(expression, serviceRegistration, resolutionContext, typeInformation); +} \ No newline at end of file diff --git a/src/Lifetime/EmptyLifetime.cs b/src/Lifetime/EmptyLifetime.cs index 3d481c60..b52b515f 100644 --- a/src/Lifetime/EmptyLifetime.cs +++ b/src/Lifetime/EmptyLifetime.cs @@ -9,4 +9,9 @@ internal class EmptyLifetime : LifetimeDescriptor private protected override Expression? BuildLifetimeAppliedExpression(ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) => null; + + internal override Expression? ApplyLifetimeToExpression(Expression? expression, + ServiceRegistration serviceRegistration, + ResolutionContext resolutionContext, TypeInformation typeInformation) + => null; } \ No newline at end of file diff --git a/src/Lifetime/ExpressionLifetimeDescriptor.cs b/src/Lifetime/ExpressionLifetimeDescriptor.cs index ea3a3e01..b5b0fed6 100644 --- a/src/Lifetime/ExpressionLifetimeDescriptor.cs +++ b/src/Lifetime/ExpressionLifetimeDescriptor.cs @@ -14,17 +14,23 @@ public abstract class ExpressionLifetimeDescriptor : LifetimeDescriptor ResolutionContext resolutionContext, TypeInformation typeInformation) { var expression = GetExpressionForRegistration(serviceRegistration, resolutionContext, typeInformation); - return expression == null ? null : this.ApplyLifetime(expression, serviceRegistration, resolutionContext, typeInformation.Type); + return this.ApplyLifetimeToExpression(expression, serviceRegistration, resolutionContext, typeInformation); } + internal override Expression? ApplyLifetimeToExpression(Expression? expression, + ServiceRegistration serviceRegistration, + ResolutionContext resolutionContext, TypeInformation typeInformation) => expression == null + ? null + : this.ApplyLifetime(expression, serviceRegistration, resolutionContext, typeInformation); + /// /// Derived types are using this method to apply their lifetime to the instance creation. /// /// The expression the lifetime should apply to. /// The service registration. /// The info about the actual resolution. - /// The type of the resolved service. + /// The type information of the resolved service. /// The lifetime managed expression. protected abstract Expression ApplyLifetime(Expression expression, ServiceRegistration serviceRegistration, - ResolutionContext resolutionContext, Type resolveType); + ResolutionContext resolutionContext, TypeInformation typeInformation); } \ No newline at end of file diff --git a/src/Lifetime/FactoryLifetimeDescriptor.cs b/src/Lifetime/FactoryLifetimeDescriptor.cs index e0dc9e9e..7d575f3a 100644 --- a/src/Lifetime/FactoryLifetimeDescriptor.cs +++ b/src/Lifetime/FactoryLifetimeDescriptor.cs @@ -14,8 +14,15 @@ public abstract class FactoryLifetimeDescriptor : LifetimeDescriptor ResolutionContext resolutionContext, TypeInformation typeInformation) { var factory = GetFactoryDelegateForRegistration(serviceRegistration, resolutionContext, typeInformation); - return factory == null ? null : this.ApplyLifetime(factory, serviceRegistration, resolutionContext, typeInformation.Type); + return factory == null ? null : this.ApplyLifetime(factory, serviceRegistration, resolutionContext, typeInformation); } + + internal override Expression? ApplyLifetimeToExpression(Expression? expression, + ServiceRegistration serviceRegistration, + ResolutionContext resolutionContext, TypeInformation typeInformation) => expression == null + ? null + : this.ApplyLifetime(expression.CompileDelegate(resolutionContext, resolutionContext.CurrentContainerContext.ContainerConfiguration), + serviceRegistration, resolutionContext, typeInformation); private static Func? GetFactoryDelegateForRegistration(ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) @@ -46,8 +53,8 @@ public abstract class FactoryLifetimeDescriptor : LifetimeDescriptor /// The factory which can be used to instantiate the service. /// The service registration. /// The info about the actual resolution. - /// The type of the resolved service. + /// The type information of the resolved service. /// The lifetime managed expression. protected abstract Expression ApplyLifetime(Func factory, ServiceRegistration serviceRegistration, - ResolutionContext resolutionContext, Type resolveType); + ResolutionContext resolutionContext, TypeInformation typeInformation); } \ No newline at end of file diff --git a/src/Lifetime/LifetimeDescriptor.cs b/src/Lifetime/LifetimeDescriptor.cs index 7f3aadef..bf915aeb 100644 --- a/src/Lifetime/LifetimeDescriptor.cs +++ b/src/Lifetime/LifetimeDescriptor.cs @@ -12,14 +12,13 @@ namespace Stashbox.Lifetime; /// public abstract class LifetimeDescriptor { - - private protected virtual bool StoreResultInLocalVariable => false; + internal virtual bool StoreResultInLocalVariable => false; /// /// An indicator used to validate the lifetime configuration of the resolution tree. /// Services with longer life-span shouldn't contain dependencies with shorter ones. /// - protected virtual int LifeSpan => 0; + protected internal virtual int LifeSpan => 0; /// /// The name of the lifetime, used only for diagnostic reasons. @@ -50,6 +49,11 @@ protected LifetimeDescriptor() $"{serviceRegistration.ImplementationType} ({this.Name}|{this.LifeSpan})"); } + if (resolutionContext.AutoLifetimeTracking != null && this.LifeSpan > resolutionContext.AutoLifetimeTracking.HighestRankingLifetime.LifeSpan) + { + resolutionContext.AutoLifetimeTracking.HighestRankingLifetime = this; + } + if (!this.StoreResultInLocalVariable) return this.BuildLifetimeAppliedExpression(serviceRegistration, resolutionContext, typeInformation); @@ -72,6 +76,9 @@ protected LifetimeDescriptor() private protected abstract Expression? BuildLifetimeAppliedExpression(ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation); + + internal abstract Expression? ApplyLifetimeToExpression(Expression? expression, ServiceRegistration serviceRegistration, + ResolutionContext resolutionContext, TypeInformation typeInformation); private protected static Expression? GetExpressionForRegistration(ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) diff --git a/src/Lifetime/Lifetimes.cs b/src/Lifetime/Lifetimes.cs index 0a0bea8d..514a4ad9 100644 --- a/src/Lifetime/Lifetimes.cs +++ b/src/Lifetime/Lifetimes.cs @@ -33,7 +33,17 @@ public static class Lifetimes /// /// Produces a NamedScope lifetime. /// + /// The name of the scope. + /// A named-scope lifetime. public static LifetimeDescriptor NamedScope(object name) => new NamedScopeLifetime(name); + /// + /// Produces a lifetime that aligns to the lifetime of the resolved service's dependencies. + /// When the underlying service has a dependency with a higher lifespan, this lifetime will inherit that lifespan up to a given boundary. + /// + /// The lifetime that represents a boundary which the derived lifetime must not exceed. + /// An auto lifetime. + public static LifetimeDescriptor Auto(LifetimeDescriptor boundaryLifetime) => new AutoLifetime(boundaryLifetime); + internal static readonly LifetimeDescriptor Empty = new EmptyLifetime(); } \ No newline at end of file diff --git a/src/Lifetime/NamedScopeLifetime.cs b/src/Lifetime/NamedScopeLifetime.cs index fd825062..aeb50ed3 100644 --- a/src/Lifetime/NamedScopeLifetime.cs +++ b/src/Lifetime/NamedScopeLifetime.cs @@ -21,7 +21,7 @@ public class NamedScopeLifetime : FactoryLifetimeDescriptor public readonly object ScopeName; /// - protected override int LifeSpan => 10; + protected internal override int LifeSpan => 10; /// /// Constructs a . @@ -34,12 +34,13 @@ public NamedScopeLifetime(object scopeName) /// protected override Expression ApplyLifetime(Func factory, - ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, Type resolveType) => + ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) => GetScopeValueMethod.CallStaticMethod(resolutionContext.CurrentScopeParameter, resolutionContext.RequestContextParameter, factory.AsConstant(), serviceRegistration.ImplementationType.AsConstant(), - serviceRegistration.RegistrationId.AsConstant(), + serviceRegistration.GetDiscriminator(typeInformation, + resolutionContext.CurrentContainerContext.ContainerConfiguration).AsConstant(), this.ScopeName.AsConstant()); private static object GetScopedValue(IResolutionScope currentScope, IRequestContext requestContext, diff --git a/src/Lifetime/PerRequestLifetime.cs b/src/Lifetime/PerRequestLifetime.cs index 54745c2a..06dc6117 100644 --- a/src/Lifetime/PerRequestLifetime.cs +++ b/src/Lifetime/PerRequestLifetime.cs @@ -12,18 +12,19 @@ namespace Stashbox.Lifetime; public class PerRequestLifetime : FactoryLifetimeDescriptor { /// - private protected override bool StoreResultInLocalVariable => true; + internal override bool StoreResultInLocalVariable => true; /// protected override Expression ApplyLifetime(Func factory, - ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, Type resolveType) + ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) { resolutionContext.RequestConfiguration.RequiresRequestContext = true; return resolutionContext.RequestContextParameter .ConvertTo(TypeCache.Type) .CallMethod(Constants.GetOrAddInstanceMethod, - serviceRegistration.RegistrationId.AsConstant(), + serviceRegistration.GetDiscriminator(typeInformation, + resolutionContext.CurrentContainerContext.ContainerConfiguration).AsConstant(), factory.AsConstant(), resolutionContext.CurrentScopeParameter); } diff --git a/src/Lifetime/PerScopedRequestLifetime.cs b/src/Lifetime/PerScopedRequestLifetime.cs index fc336711..984c1f68 100644 --- a/src/Lifetime/PerScopedRequestLifetime.cs +++ b/src/Lifetime/PerScopedRequestLifetime.cs @@ -6,5 +6,5 @@ public class PerScopedRequestLifetime : TransientLifetime { /// - private protected override bool StoreResultInLocalVariable => true; + internal override bool StoreResultInLocalVariable => true; } \ No newline at end of file diff --git a/src/Lifetime/ScopedLifetime.cs b/src/Lifetime/ScopedLifetime.cs index 6909f51f..c6264af9 100644 --- a/src/Lifetime/ScopedLifetime.cs +++ b/src/Lifetime/ScopedLifetime.cs @@ -13,14 +13,14 @@ namespace Stashbox.Lifetime; public class ScopedLifetime : FactoryLifetimeDescriptor { /// - protected override int LifeSpan => 10; + protected internal override int LifeSpan => 10; /// - private protected override bool StoreResultInLocalVariable => true; + internal override bool StoreResultInLocalVariable => true; /// protected override Expression ApplyLifetime(Func factory, - ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, Type resolveType) + ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) { if (resolutionContext.CurrentContainerContext.ContainerConfiguration.LifetimeValidationEnabled && resolutionContext.IsRequestedFromRoot) @@ -29,7 +29,8 @@ protected override Expression ApplyLifetime(Func - protected override int LifeSpan => 20; + protected internal override int LifeSpan => 20; /// - private protected override bool StoreResultInLocalVariable => true; + internal override bool StoreResultInLocalVariable => true; /// protected override Expression ApplyLifetime(Func factory, - ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, Type resolveType) + ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) { var rootScope = resolutionContext.RequestInitiatorContainerContext.ContainerConfiguration.ReBuildSingletonsInChildContainerEnabled ? resolutionContext.RequestInitiatorContainerContext.RootScope @@ -28,12 +28,14 @@ protected override Expression ApplyLifetime(Func protected override Expression ApplyLifetime(Expression expression, - ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, Type resolveType) => + ServiceRegistration serviceRegistration, ResolutionContext resolutionContext, TypeInformation typeInformation) => expression; } \ No newline at end of file diff --git a/src/Registration/Fluent/BaseFluentConfigurator.cs b/src/Registration/Fluent/BaseFluentConfigurator.cs index 61d5dde0..079bb830 100644 --- a/src/Registration/Fluent/BaseFluentConfigurator.cs +++ b/src/Registration/Fluent/BaseFluentConfigurator.cs @@ -99,6 +99,14 @@ public TConfigurator WithLifetime(LifetimeDescriptor lifetime) /// /// The configurator itself. public TConfigurator WithPerRequestLifetime() => this.WithLifetime(Lifetimes.PerRequest); + + /// + /// Sets the lifetime to auto lifetime. This lifetime aligns to the lifetime of the resolved service's dependencies. + /// When the underlying service has a dependency with a higher lifespan, this lifetime will inherit that lifespan up to a given boundary. + /// + /// The lifetime that represents a boundary which the derived lifetime must not exceed. + /// The configurator itself. + public TConfigurator WithAutoLifetime(LifetimeDescriptor boundaryLifetime) => this.WithLifetime(Lifetimes.Auto(boundaryLifetime)); /// /// Sets a scope name condition for the registration, it will be used only when a scope with the given name requests it. diff --git a/src/Registration/SelectionRules/ConditionRule.cs b/src/Registration/SelectionRules/ConditionRule.cs index 7f1aed65..231658cd 100644 --- a/src/Registration/SelectionRules/ConditionRule.cs +++ b/src/Registration/SelectionRules/ConditionRule.cs @@ -9,7 +9,7 @@ public bool IsValidForCurrentRequest(TypeInformation typeInformation, ServiceRegistration registration, ResolutionContext resolutionContext, out bool shouldIncrementWeight) { var conditions = registration.Options.GetOrDefault(RegistrationOption.ConditionOptions); - if (conditions is not null) + if (conditions != null) { shouldIncrementWeight = ServiceRegistration.IsUsableForCurrentContext(typeInformation, conditions); return shouldIncrementWeight; diff --git a/src/Registration/SelectionRules/EnumerableNameRule.cs b/src/Registration/SelectionRules/EnumerableNameRule.cs index c2e203b9..67f5c040 100644 --- a/src/Registration/SelectionRules/EnumerableNameRule.cs +++ b/src/Registration/SelectionRules/EnumerableNameRule.cs @@ -15,7 +15,8 @@ public bool IsValidForCurrentRequest(TypeInformation typeInformation, if (typeInformation.DependencyName != null && registration.Name != null && - registration.Name.Equals(typeInformation.DependencyName)) + (registration.Name.Equals(typeInformation.DependencyName) || + registration.Name.Equals(resolutionContext.CurrentContainerContext.ContainerConfiguration.UniversalName))) { shouldIncrementWeight = true; return true; diff --git a/src/Registration/SelectionRules/NameRule.cs b/src/Registration/SelectionRules/NameRule.cs index 26d34ef6..9655f4d6 100644 --- a/src/Registration/SelectionRules/NameRule.cs +++ b/src/Registration/SelectionRules/NameRule.cs @@ -21,6 +21,14 @@ public bool IsValidForCurrentRequest(TypeInformation typeInformation, shouldIncrementWeight = true; return true; } + + if (typeInformation.DependencyName != null && + registration.Name != null && + registration.Name.Equals(resolutionContext.CurrentContainerContext.ContainerConfiguration.UniversalName)) + { + shouldIncrementWeight = false; + return true; + } if (typeInformation.DependencyName == null && registration.Name != null && diff --git a/src/Registration/ServiceRegistration.cs b/src/Registration/ServiceRegistration.cs index 581af370..41fbe8c1 100644 --- a/src/Registration/ServiceRegistration.cs +++ b/src/Registration/ServiceRegistration.cs @@ -73,6 +73,24 @@ internal ServiceRegistration(Type implementationType, object? name, this.RegistrationOrder = order ?? ReserveRegistrationOrder(); } + /// + /// Returns the service discriminator used to distinguish service instances. + /// If used for service name it returns the requested dependency name's hash. + /// Otherwise, the service's registration identifier is used. + /// + /// The type info of the requested type. + /// The container configuration. + /// The registration's discriminator. + public int GetDiscriminator(TypeInformation typeInformation, ContainerConfiguration containerConfiguration) + { + if (containerConfiguration.UniversalName != null && + containerConfiguration.UniversalName.Equals(this.Name) && + typeInformation.DependencyName != null) + return typeInformation.DependencyName.GetHashCode() ^ this.RegistrationId; + + return this.RegistrationId; + } + internal void Replaces(ServiceRegistration serviceRegistration) => this.RegistrationOrder = serviceRegistration.RegistrationOrder; diff --git a/src/Resolution/ResolutionContext.cs b/src/Resolution/ResolutionContext.cs index 1b226fc2..03a3317b 100644 --- a/src/Resolution/ResolutionContext.cs +++ b/src/Resolution/ResolutionContext.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Linq.Expressions; using System.Runtime.CompilerServices; +using Stashbox.Lifetime; namespace Stashbox.Resolution; @@ -17,8 +18,13 @@ public class ResolutionContext { internal class PerRequestConfiguration { - public bool RequiresRequestContext { get; set; } - public bool FactoryDelegateCacheEnabled { get; set; } + public bool RequiresRequestContext; + public bool FactoryDelegateCacheEnabled; + } + + internal class AutoLifetimeTracker + { + public LifetimeDescriptor HighestRankingLifetime = Lifetimes.Transient; } private readonly bool shouldFallBackToRequestInitiatorContext; @@ -42,6 +48,7 @@ internal class PerRequestConfiguration internal readonly bool UnknownTypeCheckDisabled; internal readonly RequestContext RequestContext; internal readonly bool IsValidationContext; + internal readonly AutoLifetimeTracker? AutoLifetimeTracking; /// /// True if null result is allowed, otherwise false. @@ -101,6 +108,7 @@ private ResolutionContext(IEnumerable initialScopeNames, this.RequestConfiguration.FactoryDelegateCacheEnabled = this.PerResolutionRequestCacheEnabled = dependencyOverrides == null; this.RequestContext = dependencyOverrides != null ? RequestContext.FromOverrides(dependencyOverrides) : RequestContext.Begin(); this.IsValidationContext = isValidationContext; + this.AutoLifetimeTracking = null; this.ExpressionOverrides = dependencyOverrides == null && (knownInstances == null || knownInstances.IsEmpty) ? null @@ -128,6 +136,7 @@ private ResolutionContext(PerRequestConfiguration perRequestConfiguration, HashTree? expressionOverrides, ExpandableArray[]> parameterExpressions, RequestContext requestContext, + AutoLifetimeTracker? autoLifetimeTracker, ResolutionBehavior resolutionBehavior, ResolutionBehavior requestInitiatorResolutionBehavior, bool nullResultAllowed, @@ -165,6 +174,7 @@ private ResolutionContext(PerRequestConfiguration perRequestConfiguration, this.IsValidationContext = isValidationContext; this.ResolutionBehavior = resolutionBehavior; this.RequestInitiatorResolutionBehavior = requestInitiatorResolutionBehavior; + this.AutoLifetimeTracking = autoLifetimeTracker; } /// @@ -262,6 +272,12 @@ internal ResolutionContext BeginDecoratingContext(Type decoratingType, IEnumerab internal ResolutionContext BeginLifetimeValidationContext(int lifeSpan, string currentlyLifeSpanValidatingService) => this.Clone(currentLifeSpan: lifeSpan, nameOfServiceLifeSpanValidatingAgainst: currentlyLifeSpanValidatingService); + internal ResolutionContext BeginAutoLifetimeTrackingContext(AutoLifetimeTracker autoLifetimeTracker) => + this.Clone(autoLifetimeTracker: autoLifetimeTracker, + definedVariables: new Tree(), + singleInstructions: new ExpandableArray(), + cachedExpressions: new Tree()); + private static HashTree ProcessDependencyOverrides(object[]? dependencyOverrides, ImmutableTree? knownInstances) { var result = new HashTree(); @@ -297,6 +313,7 @@ private ResolutionContext Clone( IContainerContext? currentContainerContext = null, ExpandableArray[]>? parameterExpressions = null, ResolutionBehavior? resolutionBehavior = null, + AutoLifetimeTracker? autoLifetimeTracker = null, string? nameOfServiceLifeSpanValidatingAgainst = null, int? currentLifeSpan = null, bool? perResolutionRequestCacheEnabled = null, @@ -318,6 +335,7 @@ private ResolutionContext Clone( this.ExpressionOverrides, parameterExpressions ?? this.ParameterExpressions, this.RequestContext, + autoLifetimeTracker ?? this.AutoLifetimeTracking, resolutionBehavior ?? this.ResolutionBehavior, this.RequestInitiatorResolutionBehavior, this.NullResultAllowed, diff --git a/src/Resolution/ResolutionStrategy.cs b/src/Resolution/ResolutionStrategy.cs index d029907e..29194970 100644 --- a/src/Resolution/ResolutionStrategy.cs +++ b/src/Resolution/ResolutionStrategy.cs @@ -46,6 +46,9 @@ public ServiceContext BuildExpressionForType(ResolutionContext resolutionContext resolutionContext.RequestConfiguration.RequiresRequestContext = true; return resolutionContext.RequestContextParameter.AsServiceContext(); } + + if (typeInformation is { HasDependencyNameAttribute: true, Parent: not null }) + return typeInformation.Parent.DependencyName.AsConstant().ConvertTo(typeInformation.Type).AsServiceContext(); if (typeInformation.IsDependency) { diff --git a/src/Resolution/TypeInformation.cs b/src/Resolution/TypeInformation.cs index d66b6c84..20a02713 100644 --- a/src/Resolution/TypeInformation.cs +++ b/src/Resolution/TypeInformation.cs @@ -49,6 +49,11 @@ public class TypeInformation /// public readonly object? DefaultValue; + /// + /// Indicates whether the dependency has a or similar attribute. + /// + public bool HasDependencyNameAttribute { get; set; } + /// /// The parent type's metadata. /// @@ -68,11 +73,12 @@ internal TypeInformation(Type type, object? dependencyName) this.DefaultValue = null; this.Parent = null; this.IsDependency = false; + this.HasDependencyNameAttribute = false; } internal TypeInformation(Type type, Type? parentType, TypeInformation? parent, object? dependencyName, IEnumerable? customAttributes, string? parameterOrMemberName, - bool hasDefaultValue, object? defaultValue, Type? metaDataType) + bool hasDefaultValue, object? defaultValue, bool hasDependencyNameAttribute, Type? metaDataType) { this.Type = type; this.ParentType = parentType; @@ -83,7 +89,8 @@ internal TypeInformation(Type type, Type? parentType, TypeInformation? parent, o this.DefaultValue = defaultValue; this.MetadataType = metaDataType; this.Parent = parent; - this.IsDependency = parentType != null; + this.IsDependency = parentType != null; + this.HasDependencyNameAttribute = hasDependencyNameAttribute; } /// @@ -102,6 +109,7 @@ public TypeInformation Clone(Type type, object? dependencyName = null, Type? met this.ParameterOrMemberName, this.HasDefaultValue, this.DefaultValue, + this.HasDependencyNameAttribute, metadataType ?? this.MetadataType); internal static readonly TypeInformation Empty = new(TypeCache.Type, null); diff --git a/src/stashbox.csproj b/src/stashbox.csproj index 8b7213e9..b5f1683e 100644 --- a/src/stashbox.csproj +++ b/src/stashbox.csproj @@ -1,6 +1,6 @@  - net45;net461;netstandard2.0;netstandard2.1;net5.0;net6.0;net7.0 + net45;net461;netstandard2.0;netstandard2.1;net5.0;net6.0;net7.0;net8.0 Stashbox Stashbox Stashbox @@ -66,7 +66,12 @@ Stashbox .NET 7.0 - HAS_ASYNC_DISPOSABLE + HAS_ASYNC_DISPOSABLE;HAS_REQUIRED + + + + Stashbox .NET 8.0 + HAS_ASYNC_DISPOSABLE;HAS_REQUIRED @@ -87,7 +92,7 @@ - + diff --git a/test/InjectionMemberTests.cs b/test/InjectionMemberTests.cs index f50da29c..efd6ad14 100644 --- a/test/InjectionMemberTests.cs +++ b/test/InjectionMemberTests.cs @@ -164,6 +164,22 @@ public void InjectionMemberTests_Throws_Field() Assert.Throws(() => container.Resolve()); } + +#if HAS_REQUIRED + [Fact] + public void InjectionMemberTests_AutoInject_Required() + { + using var container = new StashboxContainer() + .Register() + .Register() + .Register(); + + var inst = container.Resolve(); + + Assert.NotNull(inst.Test4); + Assert.NotNull(inst.Test5); + } +#endif interface ITest { } @@ -223,4 +239,11 @@ class Test7 private Test4 test4; #pragma warning restore 169 } +#if HAS_REQUIRED + class Test8 + { + public required Test4 Test4 { get; init; } + public required Test5 Test5; + } +#endif } \ No newline at end of file diff --git a/test/IssueTests/46_AspNetCore_Failing_spec_tests_forconstrained_generics.cs b/test/IssueTests/46_AspNetCore_Failing_spec_tests_forconstrained_generics.cs index 51b3074d..cfdb6dc8 100644 --- a/test/IssueTests/46_AspNetCore_Failing_spec_tests_forconstrained_generics.cs +++ b/test/IssueTests/46_AspNetCore_Failing_spec_tests_forconstrained_generics.cs @@ -111,6 +111,21 @@ public void ResolvesMixedOpenClosedGenericsAsEnumerable() } } +interface IFakeService +{ +} + +interface IFakeSingletonService : IFakeService +{ +} + +interface IFakeEveryService : + IFakeService, + IFakeSingletonService, + IFakeOpenGenericService +{ +} + interface IFakeOpenGenericService { T Value { get; } @@ -172,7 +187,7 @@ class ClassImplementingIComparable : IComparable public int CompareTo(ClassImplementingIComparable other) => 0; } -class FakeService : IFakeOpenGenericService, IDisposable +class FakeService : IFakeEveryService, IDisposable { public PocoClass Value { get; set; } diff --git a/test/KeyedTests.cs b/test/KeyedTests.cs new file mode 100644 index 00000000..953e4636 --- /dev/null +++ b/test/KeyedTests.cs @@ -0,0 +1,381 @@ +using System; +using System.Linq; +using Stashbox.Attributes; +using Stashbox.Configuration; +using Stashbox.Resolution; +using Stashbox.Tests.IssueTests; +using Xunit; + +namespace Stashbox.Tests; + +public class KeyedTests +{ + private static object UniversalName = new(); + + [Fact] + public void ResolveKeyedService() + { + var service1 = new Service(); + var service2 = new Service(); + using var container = new StashboxContainer(config => config + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.Register(c => c.WithInstance(service1).WithName("service1").WithSingletonLifetime()); + container.Register(c => c.WithInstance(service2).WithName("service2").WithSingletonLifetime()); + + Assert.Null(container.ResolveOrDefault()); + Assert.Same(service1, container.ResolveOrDefault("service1")); + Assert.Same(service2, container.ResolveOrDefault("service2")); + } + + [Fact] + public void ResolveKeyedOpenGenericService() + { + using var container = new StashboxContainer(config => config + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.Register(typeof(IFakeOpenGenericService<>), typeof(FakeOpenGenericService<>), c => c.WithName("my-service")); + container.RegisterSingleton(); + + // Act + var genericService = container.ResolveOrDefault>("my-service"); + var singletonService = container.ResolveOrDefault(); + + // Assert + Assert.Same(singletonService, genericService.Value); + } + + [Fact] + public void ResolveKeyedServices() + { + var service1 = new Service(); + var service2 = new Service(); + var service3 = new Service(); + var service4 = new Service(); + using var container = new StashboxContainer(config => config + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.Register(c => c.WithName("first-service").WithInstance(service1).WithSingletonLifetime()); + container.Register(c => c.WithName("service").WithInstance(service2).WithSingletonLifetime()); + container.Register(c => c.WithName("service").WithInstance(service3).WithSingletonLifetime()); + container.Register(c => c.WithName("service").WithInstance(service4).WithSingletonLifetime()); + + var firstSvc = container.ResolveAll("first-service").ToList(); + Assert.Single(firstSvc); + Assert.Same(service1, firstSvc[0]); + + var services = container.ResolveAll("service").ToList(); + Assert.Equal(new[] { service2, service3, service4 }, services); + } + + [Fact] + public void ResolveKeyedGenericServices() + { + var service1 = new FakeService(); + var service2 = new FakeService(); + var service3 = new FakeService(); + var service4 = new FakeService(); + using var container = new StashboxContainer(config => config + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.Register>(c => c.WithName("first-service") + .WithInstance(service1).WithSingletonLifetime()); + container.Register>(c => c.WithName("service") + .WithInstance(service2).WithSingletonLifetime()); + container.Register>(c => c.WithName("service") + .WithInstance(service3).WithSingletonLifetime()); + container.Register>(c => c.WithName("service") + .WithInstance(service4).WithSingletonLifetime()); + + var firstSvc = container.ResolveAll>("first-service").ToList(); + Assert.Single(firstSvc); + Assert.Same(service1, firstSvc[0]); + + var services = container.ResolveAll>("service").ToList(); + Assert.Equal(new[] { service2, service3, service4 }, services); + } + + [Fact] + public void ResolveKeyedServiceSingletonInstance() + { + var service = new Service(); + using var container = new StashboxContainer(config => config + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.Register(c => c.WithName("service1").WithInstance(service).WithSingletonLifetime()); + + Assert.Null(container.ResolveOrDefault()); + Assert.Same(service, container.ResolveOrDefault("service1")); + } + + [Fact] + public void ResolveKeyedServiceSingletonInstanceWithKeyInjection() + { + var serviceKey = "this-is-my-service"; + using var container = new StashboxContainer(config => config + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.RegisterSingleton(serviceKey); + + Assert.Null(container.ResolveOrDefault()); + var svc = container.ResolveOrDefault(serviceKey); + Assert.NotNull(svc); + Assert.Equal(serviceKey, svc.ToString()); + } + + [Fact] + public void ResolveKeyedServiceSingletonInstanceWithAnyKey() + { + using var container = new StashboxContainer(config => config + .WithUniversalName(UniversalName) + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.RegisterSingleton(UniversalName); + + Assert.Null(container.ResolveOrDefault()); + + var serviceKey1 = "some-key"; + var svc1 = container.ResolveOrDefault(serviceKey1); + Assert.NotNull(svc1); + Assert.Equal(serviceKey1, svc1.ToString()); + + var serviceKey2 = "some-other-key"; + var svc2 = container.ResolveOrDefault(serviceKey2); + Assert.NotNull(svc2); + Assert.Equal(serviceKey2, svc2.ToString()); + } + + [Fact] + public void ResolveKeyedServicesSingletonInstanceWithAnyKey() + { + var service1 = new FakeService(); + var service2 = new FakeService(); + + using var container = new StashboxContainer(config => config + .WithUniversalName(UniversalName) + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.Register>(c => c.WithName(UniversalName) + .WithInstance(service1).WithSingletonLifetime()); + container.Register>(c => c.WithName("some-key") + .WithInstance(service2).WithSingletonLifetime()); + + var services = container.ResolveAll>("some-key").ToList(); + Assert.Equal(new[] { service1, service2 }, services); + } + + [Fact] + public void ResolveKeyedServiceSingletonInstanceWithKeyedParameter() + { + using var container = new StashboxContainer(config => config + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.RegisterSingleton("service1"); + container.RegisterSingleton("service2"); + container.RegisterSingleton(); + + Assert.Null(container.ResolveOrDefault()); + var svc = container.ResolveOrDefault(); + Assert.NotNull(svc); + Assert.Equal("service1", svc.Service1.ToString()); + Assert.Equal("service2", svc.Service2.ToString()); + } + + [Fact] + public void ResolveKeyedServiceSingletonInstanceWithKeyedParameterWithAdditionalAttribute() + { + using var container = new StashboxContainer(config => config + .WithAdditionalDependencyAttribute() + .WithAdditionalDependencyNameAttribute() + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.RegisterSingleton("service1"); + container.RegisterSingleton("service2"); + container.RegisterSingleton(); + + Assert.Null(container.ResolveOrDefault()); + var svc = container.ResolveOrDefault(); + Assert.NotNull(svc); + Assert.Equal("service1", svc.Service1.ToString()); + Assert.Equal("service2", svc.Service2.ToString()); + } + + [Fact] + public void ResolveKeyedServiceSingletonFactory() + { + var service = new Service(); + using var container = new StashboxContainer(config => config + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.Register(c => c.WithName("service1") + .WithFactory(() => service).WithSingletonLifetime()); + + Assert.Null(container.ResolveOrDefault()); + Assert.Same(service, container.ResolveOrDefault("service1")); + } + + [Fact] + public void ResolveKeyedServiceSingletonFactoryWithAnyKey() + { + using var container = new StashboxContainer(config => config + .WithUniversalName(UniversalName) + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.Register(c => c.WithName(UniversalName) + .WithFactory(t => new Service((string)t.DependencyName)).WithSingletonLifetime()); + + Assert.Null(container.ResolveOrDefault()); + + for (int i = 0; i < 3; i++) + { + var key = "service" + i; + var s1 = container.ResolveOrDefault(key); + var s2 = container.ResolveOrDefault(key); + Assert.Same(s1, s2); + Assert.Equal(key, s1.ToString()); + } + } + + [Fact] + public void ResolveKeyedServiceSingletonFactoryWithAnyKeyIgnoreWrongType() + { + using var container = new StashboxContainer(config => config + .WithUniversalName(UniversalName) + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.Register(UniversalName); + + Assert.Null(container.ResolveOrDefault()); + Assert.NotNull(container.ResolveOrDefault(87)); + Assert.ThrowsAny(() => container.Resolve(new object())); + } + + [Fact] + public void ResolveKeyedServiceSingletonType() + { + using var container = new StashboxContainer(config => config + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.RegisterSingleton("service1"); + + Assert.Null(container.ResolveOrDefault()); + Assert.Equal(typeof(Service), container.ResolveOrDefault("service1")!.GetType()); + } + + [Fact] + public void ResolveKeyedServiceTransientFactory() + { + using var container = new StashboxContainer(config => config + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.Register(c => c.WithName("service1") + .WithFactory(t => new Service((string)t.DependencyName))); + + Assert.Null(container.ResolveOrDefault()); + var first = container.ResolveOrDefault("service1"); + var second = container.ResolveOrDefault("service1"); + Assert.NotSame(first, second); + Assert.Equal("service1", first.ToString()); + Assert.Equal("service1", second.ToString()); + } + + [Fact] + public void ResolveKeyedServiceTransientType() + { + using var container = new StashboxContainer(config => config + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.Register("service1"); + + Assert.Null(container.ResolveOrDefault()); + var first = container.ResolveOrDefault("service1"); + var second = container.ResolveOrDefault("service1"); + Assert.NotSame(first, second); + } + + [Fact] + public void ResolveKeyedServiceTransientTypeWithAnyKey() + { + using var container = new StashboxContainer(config => config + .WithUniversalName(UniversalName) + .WithDisposableTransientTracking() + .WithRegistrationBehavior(Rules.RegistrationBehavior.PreserveDuplications)); + container.Register(UniversalName); + + Assert.Null(container.ResolveOrDefault()); + var first = container.ResolveOrDefault("service1"); + var second = container.ResolveOrDefault("service1"); + Assert.NotSame(first, second); + } + + internal interface IService + { + } + + internal class Service : IService + { + private readonly string id; + + public Service() => id = Guid.NewGuid().ToString(); + + public Service([DependencyName] string id) => this.id = id; + + public override string ToString() => id; + } + + internal class Service2 : IService + { + private readonly string id; + + public Service2() => id = Guid.NewGuid().ToString(); + + public Service2([AdditionalName] string id) => this.id = id; + + public override string ToString() => id; + } + + internal class OtherService + { + public OtherService( + [Dependency("service1")] IService service1, + [Dependency("service2")] IService service2) + { + Service1 = service1; + Service2 = service2; + } + + public IService Service1 { get; } + + public IService Service2 { get; } + } + + internal class OtherService2 + { + public OtherService2( + [AdditionalDependency("service1")] IService service1, + [AdditionalDependency("service2")] IService service2) + { + Service1 = service1; + Service2 = service2; + } + + public IService Service1 { get; } + + public IService Service2 { get; } + } + + internal class ServiceWithIntKey : IService + { + private readonly int id; + + public ServiceWithIntKey([DependencyName] int id) => this.id = id; + } + + internal class AdditionalNameAttribute : Attribute { } + + internal class AdditionalDependencyAttribute : Attribute + { + public AdditionalDependencyAttribute(string name) + { } + } +} \ No newline at end of file diff --git a/test/LifetimeTests.cs b/test/LifetimeTests.cs index 2ee6f8a1..1b9f52c4 100644 --- a/test/LifetimeTests.cs +++ b/test/LifetimeTests.cs @@ -492,6 +492,72 @@ public void LifetimeTests_PerRequest_With_Singleton() Assert.Same(inst.Test5, inst.Test6.Test5); } + + [Theory] + [ClassData(typeof(CompilerTypeTestData))] + public void LifetimeTests_AutoLifetime(CompilerType compilerType) + { + using IStashboxContainer container = new StashboxContainer(c => c.WithCompiler(compilerType)); + + container.Register(); + container.Register(c => c.WithSingletonLifetime()); + container.Register(c => c.WithAutoLifetime(Lifetimes.Singleton)); + + Assert.Same(container.Resolve(), container.Resolve()); + } + + [Theory] + [ClassData(typeof(CompilerTypeTestData))] + public void LifetimeTests_AutoLifetime_Scoped(CompilerType compilerType) + { + using IStashboxContainer container = new StashboxContainer(c => c.WithCompiler(compilerType)); + + container.Register(); + container.Register(c => c.WithSingletonLifetime()); + container.Register(c => c.WithAutoLifetime(Lifetimes.Scoped)); + + Assert.NotSame(container.BeginScope().Resolve(), container.BeginScope().Resolve()); + + var scope = container.BeginScope(); + Assert.Same(scope.Resolve(), scope.Resolve()); + } + + [Theory] + [ClassData(typeof(CompilerTypeTestData))] + public void LifetimeTests_AutoLifetime_Transient(CompilerType compilerType) + { + using IStashboxContainer container = new StashboxContainer(c => c.WithCompiler(compilerType)); + + container.Register(); + container.Register(c => c.WithSingletonLifetime()); + container.Register(c => c.WithAutoLifetime(Lifetimes.Transient)); + + Assert.NotSame(container.Resolve(), container.Resolve()); + } + + [Theory] + [ClassData(typeof(CompilerTypeTestData))] + public void LifetimeTests_AutoLifetime_Remains_Transient(CompilerType compilerType) + { + using IStashboxContainer container = new StashboxContainer(c => c.WithCompiler(compilerType)); + + container.Register(); + container.Register(); + container.Register(c => c.WithAutoLifetime(Lifetimes.Singleton)); + + Assert.NotSame(container.Resolve(), container.Resolve()); + } + + [Theory] + [ClassData(typeof(CompilerTypeTestData))] + public void LifetimeTests_AutoLifetime_Without_Dependency(CompilerType compilerType) + { + using IStashboxContainer container = new StashboxContainer(c => c.WithCompiler(compilerType)); + + container.Register(c => c.WithAutoLifetime(Lifetimes.Singleton)); + + Assert.NotSame(container.Resolve(), container.Resolve()); + } interface ITest1 { string Name { get; set; } } diff --git a/test/stashbox.tests.csproj b/test/stashbox.tests.csproj index 3f430ba8..cda89e9a 100644 --- a/test/stashbox.tests.csproj +++ b/test/stashbox.tests.csproj @@ -1,7 +1,7 @@  - net452;netcoreapp3.1;net5.0;net6.0;net7.0 + net452;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 Stashbox.Tests Stashbox.Tests true @@ -10,10 +10,14 @@ false - + HAS_ASYNC_DISPOSABLE + + HAS_ASYNC_DISPOSABLE;HAS_REQUIRED + +