Skip to content

Commit

Permalink
Add support for auto-activated keyed singletons
Browse files Browse the repository at this point in the history
  • Loading branch information
Martin Taillefer committed Jul 28, 2023
1 parent 45356df commit bb47a31
Show file tree
Hide file tree
Showing 11 changed files with 927 additions and 53 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<NoWarn>$(NoWarn);AD0001</NoWarn>

<!-- Experimental warnings are for customers, not for this repo -->
<NoWarn>$(NoWarn);EXTEXP0001;EXTEXP0002;EXTEXP0003;EXTEXP0004;EXTEXP0005;EXTEXP0006;EXTEXP0007;EXTEXP0008;EXTEXP0009;EXTEXP0010;EXTEXP0011</NoWarn>
<NoWarn>$(NoWarn);EXTEXP0001;EXTEXP0002;EXTEXP0003;EXTEXP0004;EXTEXP0005;EXTEXP0006;EXTEXP0007;EXTEXP0008;EXTEXP0009;EXTEXP0010;EXTEXP0011;EXTEXP0012</NoWarn>

<!-- NU5104: A stable release of a package should not have a prerelease dependency -->
<NoWarn>$(NoWarn);NU5104</NoWarn>
Expand Down
3 changes: 2 additions & 1 deletion docs/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ if desired.
| `EXTEXP0008` | Resource monitoring experiments |
| `EXTEXP0009` | Hosting experiments |
| `EXTEXP0010` | Object pool experiments |

| `EXTEXP0011` | Document database experiments |
| `EXTEXP0012` | Auto-activation experiments |

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,85 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extension methods for automatically activating singletons after application starts.
/// </summary>
public static class AutoActivationExtensions
public static partial class AutoActivationExtensions
{
/// <summary>
/// Enforces singleton activation at startup time rather then at runtime.
/// </summary>
/// <typeparam name="TService">The type of the service to add.</typeparam>
/// <typeparam name="TService">The type of the service to activate.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IServiceCollection Activate<TService>(this IServiceCollection services)
where TService : class
{
_ = Throw.IfNull(services);

_ = services.AddHostedService<AutoActivationHostedService>()
.AddOptions<AutoActivatorOptions>()
.Configure(ao =>
{
var constructed = typeof(IEnumerable<TService>);
if (ao.AutoActivators.Contains(constructed))
{
return;
}
if (ao.AutoActivators.Remove(typeof(TService)))
{
_ = ao.AutoActivators.Add(constructed);
return;
}
_ = ao.AutoActivators.Add(typeof(TService));
});
_ = services
.AddHostedService<AutoActivationHostedService>()
.AddOptions<AutoActivatorOptions>()
.Configure(ao =>
{
var constructed = typeof(IEnumerable<TService>);
if (ao.AutoActivators.Contains(constructed))
{
return;
}
if (ao.AutoActivators.Remove(typeof(TService)))
{
_ = ao.AutoActivators.Add(constructed);
return;
}
_ = ao.AutoActivators.Add(typeof(TService));
});

return services;
}

/// <summary>
/// Enforces singleton activation at startup time rather then at runtime.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
/// <param name="serviceType">The type of the service to activate.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
[Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)]
[UnconditionalSuppressMessage(
"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "Addressed with [DynamicallyAccessedMembers]")]
public static IServiceCollection Activate(this IServiceCollection services, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType)
{
_ = Throw.IfNull(services);
_ = Throw.IfNull(serviceType);

_ = services
.AddHostedService<AutoActivationHostedService>()
.AddOptions<AutoActivatorOptions>()
.Configure(ao =>
{
var constructed = typeof(IEnumerable<>).MakeGenericType(serviceType);
if (ao.AutoActivators.Contains(constructed))
{
return;
}
if (ao.AutoActivators.Remove(serviceType))
{
_ = ao.AutoActivators.Add(constructed);
return;
}
_ = ao.AutoActivators.Add(serviceType);
});

return services;
}
Expand Down Expand Up @@ -270,34 +311,6 @@ public static void TryAddActivatedSingleton<TService>(this IServiceCollection se
services.TryAddAndActivate<TService>(ServiceDescriptor.Singleton<TService>(implementationFactory));
}

[UnconditionalSuppressMessage(
"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "Addressed with [DynamicallyAccessedMembers]")]
internal static IServiceCollection Activate(this IServiceCollection services, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType)
{
_ = services.AddHostedService<AutoActivationHostedService>()
.AddOptions<AutoActivatorOptions>()
.Configure(ao =>
{
var constructed = typeof(IEnumerable<>).MakeGenericType(serviceType);
if (ao.AutoActivators.Contains(constructed))
{
return;
}
if (ao.AutoActivators.Remove(serviceType))
{
_ = ao.AutoActivators.Add(constructed);
return;
}
_ = ao.AutoActivators.Add(serviceType);
});

return services;
}

private static void TryAddAndActivate<TService>(this IServiceCollection services, ServiceDescriptor descriptor)
where TService : class
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace Microsoft.Extensions.DependencyInjection;
internal sealed class AutoActivationHostedService : IHostedService
{
private readonly Type[] _autoActivators;
private readonly (Type, object?)[] _keyedAutoActivators;
private readonly IServiceProvider _provider;

public AutoActivationHostedService(IServiceProvider provider, IOptions<AutoActivatorOptions> options)
Expand All @@ -23,6 +24,7 @@ public AutoActivationHostedService(IServiceProvider provider, IOptions<AutoActiv
var value = Throw.IfMemberNull(options, options.Value);

_autoActivators = value.AutoActivators.ToArray();
_keyedAutoActivators = value.KeyedAutoActivators.ToArray();
}

public Task StartAsync(CancellationToken cancellationToken)
Expand All @@ -32,6 +34,11 @@ public Task StartAsync(CancellationToken cancellationToken)
_ = _provider.GetRequiredService(singleton);
}

foreach (var (serviceType, serviceKey) in _keyedAutoActivators)
{
_ = _provider.GetRequiredKeyedService(serviceType, serviceKey);
}

return Task.CompletedTask;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ namespace Microsoft.Extensions.DependencyInjection;
internal sealed class AutoActivatorOptions
{
public HashSet<Type> AutoActivators { get; } = new();
public HashSet<(Type serviceType, object? serviceKey)> KeyedAutoActivators { get; } = new();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
<Workstream>Fundamentals</Workstream>
</PropertyGroup>

<PropertyGroup>
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
</PropertyGroup>

<PropertyGroup>
<Stage>normal</Stage>
<MinCodeCoverage>100</MinCodeCoverage>
Expand Down
3 changes: 2 additions & 1 deletion src/Shared/DiagnosticIds/Experiments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Microsoft.Shared.DiagnosticIds;
/// </summary>
/// <remarks>
/// When adding a new experiment, add a corresponding suppression to the root <c>Directory.Build.targets</c> file, and add a documentation entry to
/// <c>docs/Experiments.md</c>.
/// <c>docs/list-of-diagnostics.md</c>.
/// </remarks>
internal static class Experiments
{
Expand All @@ -29,4 +29,5 @@ internal static class Experiments
internal const string Hosting = "EXTEXP0009";
internal const string ObjectPool = "EXTEXP0010";
internal const string DocumentDb = "EXTEXP0011";
internal const string AutoActivation = "EXTEXP0012";
}
Loading

0 comments on commit bb47a31

Please sign in to comment.