diff --git a/README.md b/README.md index a487d69..e84fd35 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ dotnet add package PipelineNet - [Middleware](#middleware) - [Pipelines](#pipelines) - [Chains of responsibility](#chains-of-responsibility) -- [Factories](#factories) +- [Middleware flow factories](#middleware-flow-factories) - [Middleware resolver](#middleware-resolver) - [ServiceProvider implementation](#serviceprovider-implementation) - [Unity implementation](#unity-implementation) @@ -197,7 +197,7 @@ result = await exceptionHandlersChain.Execute(new ArgumentException()); // Resul result = await exceptionHandlersChain.Execute(new InvalidOperationException()); // Result will be false ``` -## Factories +## Middleware flow factories Instead of instantiating pipelines and chains of responsibility directly, you can use factories to instantiate them: ```C# IAsyncPipelineFactory pipelineFactory = new AsyncPipelineFactory(new ActivatorMiddlewareResovler()); @@ -232,8 +232,16 @@ You can grab it from nuget with: Install-Package PipelineNet.ServiceProvider ``` -Use it as follows: +Use it with `ServiceCollectionExtensions`: ```C# +// The following line adds: +// - open generic middleware flow factories +// - service provider middleware resolver +// - all middleware from assembly +// with default scoped lifetime +services.AddPipelineNet(typeof(RoudCornersAsyncMiddleware).Assembly); +services.AddScoped(); + public interface IMyService { Task DoSomething(); @@ -243,12 +251,12 @@ public class MyService : IMyService { private readonly IAsyncPipelineFactory _pipelineFactory; - public MyPipelineFactory(IAsyncPipelineFactory pipelineFactory) + public MyService(IAsyncPipelineFactory pipelineFactory) { _pipelineFactory = pipelineFactory; } - public Task DoSomething() + public async Task DoSomething() { IAsyncPipeline pipeline = _pipelineFactory.Create() .Add() @@ -265,7 +273,6 @@ public class RoudCornersAsyncMiddleware : IAsyncMiddleware { private readonly ILogger _logger; - // The following constructor arguments will be provided by IServiceProvider public RoudCornersAsyncMiddleware(ILogger logger) { _logger = logger; @@ -280,60 +287,28 @@ public class RoudCornersAsyncMiddleware : IAsyncMiddleware } ``` -Register it using `IHttpContextAccessor` (recommended): +Alternatively, you can instantiate `ServiceProviderMiddlewareResolver` directly: ```C# -services.AddSingleton(); - -services.AddMiddlewareFromAssembly(typeof(RoudCornersAsyncMiddleware).Assembly, ServiceLifetime.Scoped); // Add all middleware from assembly - -services.AddHttpContextAccessor(); - -services.TryAddSingleton(); // Add middleware resolver - -services.TryAddSingleton(typeof(IPipelineFactory<>), typeof(AsyncPipelineFactory<>)); // Add pipeline and chain of responsibility factories -services.TryAddSingleton(typeof(IAsyncPipelineFactory<>), typeof(AsyncPipelineFactory<>)); -services.TryAddSingleton(typeof(IResponsibilityChainFactory<,>), typeof(ResponsibilityChainFactory<,>)); -services.TryAddSingleton(typeof(IAsyncResponsibilityChainFactory<,>), typeof(AsyncResponsibilityChainFactory<,>)); +services.AddMiddlewareFromAssembly(typeof(RoudCornersAsyncMiddleware).Assembly); -public class HttpContextAccessorMiddlewareResolver : IMiddlewareResolver +public class MyService : IMyService { - private readonly IHttpContextAccessor _httpContextAccessor; - - public HttpContextAccessorMiddlewareResolver(IHttpContextAccessor httpContextAccessor) + public async Task DoSomething() { - _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException("httpContextAccessor", - "An instance of IHttpContextAccessor must be provided."); - } + IServiceProvider serviceProvider = GetServiceProvider(); - public MiddlewareResolverResult Resolve(Type type) - { - var httpContext = _httpContextAccessor.HttpContext; - if (httpContext == null) throw new InvalidOperationException("HttpContext must not be null."); + IAsyncPipeline pipeline = new AsyncPipeline(new ServiceProviderMiddlewareResolver(serviceProvider)) + .Add() + .Add() + .Add(); - var middleware = httpContext.RequestServices.GetRequiredService(type); - bool isDisposable = false; + Bitmap image = (Bitmap) Image.FromFile("party-photo.png"); - return new MiddlewareResolverResult - { - Middleware = middleware, - IsDisposable = isDisposable - }; + await pipeline.Execute(image); } -} -``` -or `IServiceProvider`: -```C# -services.AddScoped(); - -services.AddMiddlewareFromAssembly(typeof(RoudCornersAsyncMiddleware).Assembly, ServiceLifetime.Scoped); // Add all middleware from assembly - -services.TryAddScoped(); // Add scoped middleware resolver so that scoped IServiceProvider is injected - -services.TryAddScoped(typeof(IPipelineFactory<>), typeof(AsyncPipelineFactory<>)); // Add pipeline and chain of responsibility factories -services.TryAddScoped(typeof(IAsyncPipelineFactory<>), typeof(AsyncPipelineFactory<>)); -services.TryAddScoped(typeof(IResponsibilityChainFactory<,>), typeof(ResponsibilityChainFactory<,>)); -services.TryAddScoped(typeof(IAsyncResponsibilityChainFactory<,>), typeof(AsyncResponsibilityChainFactory<,>)); + private IServiceProvider GetServiceProvider() => // Get service provider somehow +} ``` Note that `IServiceProvider` lifetime can vary based on the lifetime of the containing class. For example, if you resolve service from a scope, and it takes an `IServiceProvider`, it'll be a scoped instance. diff --git a/src/PipelineNet.ServiceProvider.Tests/PipelineNet.ServiceProvider.Tests.csproj b/src/PipelineNet.ServiceProvider.Tests/PipelineNet.ServiceProvider.Tests.csproj index fddb4dc..20bef78 100644 --- a/src/PipelineNet.ServiceProvider.Tests/PipelineNet.ServiceProvider.Tests.csproj +++ b/src/PipelineNet.ServiceProvider.Tests/PipelineNet.ServiceProvider.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/src/PipelineNet.ServiceProvider.Tests/ServiceCollectionExtensionsTests.cs b/src/PipelineNet.ServiceProvider.Tests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..1256325 --- /dev/null +++ b/src/PipelineNet.ServiceProvider.Tests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,168 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PipelineNet.Middleware; +using PipelineNet.Pipelines; +using PipelineNet.ServiceProvider.Pipelines.Factories; + +namespace PipelineNet.ServiceProvider.Tests +{ + public class ServiceCollectionExtensionsTests + { + #region Parameter definitions + public class Image + { + public static Image FromFile(string filename) => new Bitmap(); + } + + public class Bitmap : Image + { + } + #endregion + + #region Service definitions + public interface IMyService + { + Task DoSomething(); + } + + public class MyService : IMyService + { + private readonly IAsyncPipelineFactory _pipelineFactory; + + public MyService(IAsyncPipelineFactory pipelineFactory) + { + _pipelineFactory = pipelineFactory; + } + + public async Task DoSomething() + { + IAsyncPipeline pipeline = _pipelineFactory.Create() + .Add() + .Add() + .Add(); + + Bitmap image = (Bitmap) Image.FromFile("party-photo.png"); + + await pipeline.Execute(image); + } + } + #endregion + + #region Middleware definitions + public class RoudCornersAsyncMiddleware : IAsyncMiddleware + { + private readonly ILogger _logger; + + public RoudCornersAsyncMiddleware(ILogger logger) + { + _logger = logger; + } + + public async Task Run(Bitmap parameter, Func next) + { + _logger.LogInformation("Running RoudCornersAsyncMiddleware."); + // Handle somehow + await next(parameter); + } + } + + public class AddTransparencyAsyncMiddleware : IAsyncMiddleware + { + private readonly ILogger _logger; + + public AddTransparencyAsyncMiddleware(ILogger logger) + { + _logger = logger; + } + + public async Task Run(Bitmap parameter, Func next) + { + _logger.LogInformation("Running AddTransparencyAsyncMiddleware."); + // Handle somehow + await next(parameter); + } + } + + public class AddWatermarkAsyncMiddleware : IAsyncMiddleware + { + private readonly ILogger _logger; + + public AddWatermarkAsyncMiddleware(ILogger logger) + { + _logger = logger; + } + + public async Task Run(Bitmap parameter, Func next) + { + _logger.LogInformation("Running AddWatermarkAsyncMiddleware."); + // Handle somehow + await next(parameter); + } + } + #endregion + + #region Logger definitions + public class TestLoggerProvider : ILoggerProvider + { + private readonly List _messages; + + public TestLoggerProvider(List messages) + { + _messages = messages; + } + + public ILogger CreateLogger(string categoryName) => + new TestLogger(_messages); + + public void Dispose() + { + } + } + + public class TestLogger : ILogger + { + private readonly List _messages; + + public TestLogger(List messages) + { + _messages = messages; + } + + public IDisposable? BeginScope(TState state) where TState : notnull => + new NullScope(); + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => + _messages.Add(formatter(state, exception)); + + private class NullScope : IDisposable + { + public void Dispose() + { + } + } + } + #endregion + + [Fact] + public async Task AddPipelineNet_Works_Readme() + { + var messages = new List(); + var serviceProvider = new ServiceCollection() + .AddLogging(bulider => bulider.Services.AddSingleton(new TestLoggerProvider(messages))) + .AddPipelineNet(typeof(RoudCornersAsyncMiddleware).Assembly) + .AddScoped() + .BuildServiceProvider(); + var scope = serviceProvider.CreateScope(); + var service = scope.ServiceProvider.GetRequiredService(); + + await service.DoSomething(); + + Assert.Equal(3, messages.Count); + Assert.Equal("Running RoudCornersAsyncMiddleware.", messages[0]); + Assert.Equal("Running AddTransparencyAsyncMiddleware.", messages[1]); + Assert.Equal("Running AddWatermarkAsyncMiddleware.", messages[2]); + } + } +} diff --git a/src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs b/src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs index 2bf6a66..94b65de 100644 --- a/src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs +++ b/src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs @@ -1,6 +1,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using PipelineNet.Middleware; +using PipelineNet.MiddlewareResolver; +using PipelineNet.ServiceProvider.ChainsOfResponsibility.Factories; +using PipelineNet.ServiceProvider.MiddlewareResolver; +using PipelineNet.ServiceProvider.Pipelines.Factories; using System; using System.Collections.Generic; using System.Linq; @@ -13,6 +17,70 @@ namespace PipelineNet.ServiceProvider /// public static class ServiceCollectionExtensions { + /// + /// Adds core PipelineNet services. Adds all middleware from assemblies. + /// + /// The service collection. + /// Assemblies to scan. + /// The lifetime of the registered services and middleware. + /// The service collection. + public static IServiceCollection AddPipelineNet( + this IServiceCollection services, + IEnumerable assemblies, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + if (services == null) throw new ArgumentNullException("services"); + if (assemblies == null) throw new ArgumentNullException("assemblies"); + + services.AddPipelineNetCore(lifetime); + services.AddMiddlewareFromAssemblies(assemblies, lifetime); + + return services; + } + + /// + /// Adds core PipelineNet services. Adds all middleware from the assembly. + /// + /// The service collection. + /// The assembly to scan. + /// The lifetime of the registered services and middleware. + /// The service collection. + public static IServiceCollection AddPipelineNet( + this IServiceCollection services, + Assembly assembly, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + if (services == null) throw new ArgumentNullException("services"); + if (assembly == null) throw new ArgumentNullException("assembly"); + + services.AddPipelineNetCore(lifetime); + services.AddMiddlewareFromAssembly(assembly, lifetime); + + return services; + } + + /// + /// Adds core PipelineNet services. + /// + /// The service collection. + /// The lifetime of the registered services. + /// The service collection. + public static IServiceCollection AddPipelineNetCore( + this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + if (services == null) throw new ArgumentNullException("services"); + + services.TryAdd(new ServiceDescriptor(typeof(IMiddlewareResolver), typeof(ServiceProviderMiddlewareResolver), lifetime)); + + services.TryAdd(new ServiceDescriptor(typeof(IPipelineFactory<>), typeof(AsyncPipelineFactory<>), lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(IAsyncPipelineFactory<>), typeof(AsyncPipelineFactory<>), lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(IResponsibilityChainFactory<,>), typeof(ResponsibilityChainFactory<,>), lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(IAsyncResponsibilityChainFactory<,>), typeof(AsyncResponsibilityChainFactory<,>), lifetime)); + + return services; + } + /// /// Adds all middleware from assemblies. ///