Skip to content

Commit

Permalink
Add scope support for ServiceProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
mayuki committed Jan 10, 2022
1 parent 0ffaaef commit ccc7c27
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 32 deletions.
16 changes: 16 additions & 0 deletions src/Cocona.Core/Application/ICoconaServiceProviderScopeSupport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Cocona.Application
{
public interface ICoconaServiceProviderScopeSupport
{
(IDisposable Scope, IServiceProvider ScopedServiceProvider) CreateScope(IServiceProvider serviceProvider);
#if NET5_0_OR_GREATER || NETSTANDARD2_1
(IAsyncDisposable Scope, IServiceProvider ScopedServiceProvider) CreateAsyncScope(IServiceProvider serviceProvider);
#endif
}
}
75 changes: 44 additions & 31 deletions src/Cocona.Core/Command/Dispatcher/CoconaCommandDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,21 @@ namespace Cocona.Command.Dispatcher
public class CoconaCommandDispatcher : ICoconaCommandDispatcher
{
private readonly IServiceProvider _serviceProvider;
private readonly ICoconaServiceProviderScopeSupport _serviceProviderScopeSupport;
private readonly ICoconaCommandDispatcherPipelineBuilder _dispatcherPipelineBuilder;
private readonly ICoconaInstanceActivator _activator;
private readonly ICoconaAppContextAccessor _appContext;

public CoconaCommandDispatcher(
IServiceProvider serviceProvider,
ICoconaServiceProviderScopeSupport serviceProviderScopeSupport,
ICoconaCommandDispatcherPipelineBuilder dispatcherPipelineBuilder,
ICoconaInstanceActivator activator,
ICoconaAppContextAccessor appContext
)
{
_serviceProvider = serviceProvider;
_serviceProviderScopeSupport = serviceProviderScopeSupport;
_dispatcherPipelineBuilder = dispatcherPipelineBuilder;
_activator = activator;
_appContext = appContext;
Expand All @@ -42,44 +45,54 @@ public async ValueTask<int> DispatchAsync(CommandResolverResult commandResolverR

var dispatchAsync = _dispatcherPipelineBuilder.Build();

// Activate a command type.
var commandInstance = default(object);
var shouldCleanup = false;
if (matchedCommand.Target is not null)
{
commandInstance = matchedCommand.Target;
}
else if (matchedCommand.CommandType.GetConstructors().Any() && !matchedCommand.Method.IsStatic)
#if NET5_0_OR_GREATER || NETSTANDARD2_1
var (scope, serviceProvider) = _serviceProviderScopeSupport.CreateAsyncScope(_serviceProvider);
await using (scope)
#else
var (scope, serviceProvider) = _serviceProviderScopeSupport.CreateScope(_serviceProvider);
using (scope)
#endif
{
shouldCleanup = true;
commandInstance = _activator.GetServiceOrCreateInstance(_serviceProvider, matchedCommand.CommandType);
if (commandInstance == null) throw new InvalidOperationException($"Unable to activate command type '{matchedCommand.CommandType.FullName}'");
}

// Set CoconaAppContext
_appContext.Current = new CoconaAppContext(matchedCommand, cancellationToken);
_appContext.Current.Features.Set<ICoconaCommandFeature>(new CoconaCommandFeature(commandResolverResult.CommandCollection, matchedCommand, subCommandStack, commandInstance));
// Activate a command type.
var commandInstance = default(object);
var shouldCleanup = false;
if (matchedCommand.Target is not null)
{
commandInstance = matchedCommand.Target;
}
else if (matchedCommand.CommandType.GetConstructors().Any() && !matchedCommand.Method.IsStatic)
{
shouldCleanup = true;
commandInstance = _activator.GetServiceOrCreateInstance(serviceProvider, matchedCommand.CommandType);
if (commandInstance == null) throw new InvalidOperationException($"Unable to activate command type '{matchedCommand.CommandType.FullName}'");
}

// Dispatch the command
try
{
var ctx = new CommandDispatchContext(matchedCommand, parsedCommandLine, commandInstance, cancellationToken);
return await dispatchAsync(ctx).ConfigureAwait(false);
}
finally
{
if (shouldCleanup)
// Set CoconaAppContext
_appContext.Current = new CoconaAppContext(matchedCommand, cancellationToken);
_appContext.Current.Features.Set<ICoconaCommandFeature>(new CoconaCommandFeature(commandResolverResult.CommandCollection, matchedCommand, subCommandStack, commandInstance));

// Dispatch the command
try
{
var ctx = new CommandDispatchContext(matchedCommand, parsedCommandLine, commandInstance, cancellationToken);
return await dispatchAsync(ctx).ConfigureAwait(false);
}
finally
{
switch (commandInstance)
if (shouldCleanup)
{
switch (commandInstance)
{
#if NET5_0_OR_GREATER || NETSTANDARD2_1
case IAsyncDisposable asyncDisposable:
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
break;
case IAsyncDisposable asyncDisposable:
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
break;
#endif
case IDisposable disposable:
disposable.Dispose();
break;
case IDisposable disposable:
disposable.Dispose();
break;
}
}
}
}
Expand Down
24 changes: 23 additions & 1 deletion src/Cocona.Lite/Lite/CoconaLiteServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Cocona.Application;

namespace Cocona.Lite
{
public class CoconaLiteServiceProvider : IServiceProvider, IDisposable, ICoconaServiceProviderIsService
public class CoconaLiteServiceProvider : IServiceProvider, IDisposable, ICoconaServiceProviderIsService, ICoconaServiceProviderScopeSupport
{
private readonly Dictionary<Type, ServiceDescriptor[]> _descriptorsByService;
private readonly List<IDisposable> _disposables;
Expand All @@ -21,6 +22,7 @@ public bool IsService(Type serviceType)
{
if (serviceType == typeof(IServiceProvider)) return true;
if (serviceType == typeof(ICoconaServiceProviderIsService)) return true;
if (serviceType == typeof(ICoconaServiceProviderScopeSupport)) return true;

// IEnumerable<T>
if (serviceType.IsConstructedGenericType && serviceType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
Expand All @@ -45,6 +47,7 @@ public object GetService(Type serviceType)
{
if (serviceType == typeof(IServiceProvider)) return this;
if (serviceType == typeof(ICoconaServiceProviderIsService)) return this;
if (serviceType == typeof(ICoconaServiceProviderScopeSupport)) return this;

// IEnumerable<T>
if (serviceType.IsConstructedGenericType && serviceType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
Expand Down Expand Up @@ -92,5 +95,24 @@ public void Dispose()

_disposables.Clear();
}

// NOTE: Cocona.Lite's ServiceProvider does not support `Scoped`.
(IDisposable Scope, IServiceProvider ScopedServiceProvider) ICoconaServiceProviderScopeSupport.CreateScope(IServiceProvider serviceProvider)
=> (new NullDisposable(), serviceProvider);

#if NET5_0_OR_GREATER || NETSTANDARD2_1
(IAsyncDisposable Scope, IServiceProvider ScopedServiceProvider) ICoconaServiceProviderScopeSupport.CreateAsyncScope(IServiceProvider serviceProvider)
=> (new NullDisposable(), serviceProvider);
#endif

private class NullDisposable :
IDisposable
#if NET5_0_OR_GREATER || NETSTANDARD2_1
, IAsyncDisposable
#endif
{
public void Dispose() {}
public ValueTask DisposeAsync() => default;
}
}
}
2 changes: 2 additions & 0 deletions src/Cocona/Hosting/CoconaServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ internal static IServiceCollection AddCoconaCore(this IServiceCollection service
#if COCONA_LITE
services.AddSingleton<ICoconaInstanceActivator>(_ => new CoconaLiteInstanceActivator());
services.AddSingleton<ICoconaServiceProviderIsService>(sp => sp.GetRequiredService<ICoconaServiceProviderIsService>());
services.AddSingleton<ICoconaServiceProviderScopeSupport>(sp => sp.GetRequiredService<ICoconaServiceProviderScopeSupport>());
#else
services.AddSingleton<ICoconaInstanceActivator>(_ => new CoconaInstanceActivator());
services.AddSingleton<ICoconaServiceProviderIsService, CoconaServiceProviderIsService>();
services.AddSingleton<ICoconaServiceProviderScopeSupport, CoconaServiceProviderScopeSupport>();
#endif

services.TryAddSingleton<ICoconaCommandProvider>(sp =>
Expand Down
27 changes: 27 additions & 0 deletions src/Cocona/Hosting/CoconaServiceProviderScopeSupport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Cocona.Application;
using Microsoft.Extensions.DependencyInjection;

namespace Cocona.Hosting
{
public class CoconaServiceProviderScopeSupport : ICoconaServiceProviderScopeSupport
{
public (IDisposable Scope, IServiceProvider ScopedServiceProvider) CreateScope(IServiceProvider serviceProvider)
{
var scope = serviceProvider.CreateScope();
return (scope, scope.ServiceProvider);
}

#if NET5_0_OR_GREATER || NETSTANDARD2_1
public (IAsyncDisposable Scope, IServiceProvider ScopedServiceProvider) CreateAsyncScope(IServiceProvider serviceProvider)
{
var scope = serviceProvider.CreateAsyncScope();
return (scope, scope.ServiceProvider);
}
#endif
}
}
2 changes: 2 additions & 0 deletions test/Cocona.Test/Command/CoconaConsoleAppBaseTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Text;
using System.Threading.Tasks;
using Cocona.Command.Binder.Validation;
using Cocona.Hosting;
using Xunit;

namespace Cocona.Test.Command
Expand All @@ -32,6 +33,7 @@ private ServiceCollection CreateDefaultServices<TCommand>(string[] args)
services.AddTransient<ICoconaCommandResolver, CoconaCommandResolver>();
services.AddSingleton<ICoconaAppContextAccessor, CoconaAppContextAccessor>();
services.AddSingleton<ICoconaInstanceActivator, CoconaInstanceActivator>();
services.AddSingleton<ICoconaServiceProviderScopeSupport, CoconaServiceProviderScopeSupport>();
services.AddSingleton<ILoggerFactory, LoggerFactory>();
services.AddSingleton<ICoconaCommandDispatcherPipelineBuilder>(serviceProvider =>
new CoconaCommandDispatcherPipelineBuilder(serviceProvider, serviceProvider.GetService<ICoconaInstanceActivator>())
Expand Down
42 changes: 42 additions & 0 deletions test/Cocona.Test/Command/CommandDispatcher/DispatcherTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using System.Text;
using System.Threading.Tasks;
using Cocona.Command.Binder.Validation;
using Cocona.Hosting;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Xunit;

Expand All @@ -37,6 +38,7 @@ private ServiceCollection CreateDefaultServices<TCommand>(string[] args, Action<
services.AddSingleton<ICoconaAppContextAccessor, CoconaAppContextAccessor>();
services.AddSingleton<ILoggerFactory, LoggerFactory>();
services.AddSingleton<ICoconaInstanceActivator, CoconaInstanceActivator>();
services.AddSingleton<ICoconaServiceProviderScopeSupport, CoconaServiceProviderScopeSupport>();
services.AddSingleton<ICoconaCommandDispatcherPipelineBuilder>(
serviceProvider =>
{
Expand Down Expand Up @@ -468,6 +470,46 @@ ValueTask IAsyncDisposable.DisposeAsync()
}
#endif

[Fact]
public async Task ServiceProvider_Scoped()
{
var services = CreateDefaultServices<TestCommand_ServiceProvider_Scoped>(new string[] { });
services.AddSingleton<DisposeCounter>();
services.AddScoped<TestService_ServiceProvider_Scoped>();
var serviceProvider = services.BuildServiceProvider();

var dispatcher = serviceProvider.GetService<ICoconaCommandDispatcher>();
var resolvedCommand = serviceProvider.GetRequiredService<ICoconaCommandResolver>().ParseAndResolve(
serviceProvider.GetRequiredService<ICoconaCommandProvider>().GetCommandCollection(),
serviceProvider.GetRequiredService<ICoconaCommandLineArgumentProvider>().GetArguments()
);
var result = await dispatcher.DispatchAsync(resolvedCommand);
serviceProvider.GetRequiredService<DisposeCounter>().Count.Should().Be(1);
}

public class TestService_ServiceProvider_Scoped : IDisposable
{
private readonly DisposeCounter _counter;

public TestService_ServiceProvider_Scoped(DisposeCounter counter)
{
_counter = counter;
}

public void Dispose()
{
_counter.Count++;
}
}

public class TestCommand_ServiceProvider_Scoped
{
public TestCommand_ServiceProvider_Scoped(TestService_ServiceProvider_Scoped counter)
{
}
public void Hello() { }
}

public class NoCommand
{ }

Expand Down

0 comments on commit ccc7c27

Please sign in to comment.