Skip to content

Commit

Permalink
Add assembly scanning ServiceCollectionExtensions and ServiceProvider…
Browse files Browse the repository at this point in the history
…MiddlewareResolver
  • Loading branch information
mariusz96 committed Aug 9, 2024
1 parent bd73c5c commit 452aeb8
Show file tree
Hide file tree
Showing 33 changed files with 651 additions and 38 deletions.
115 changes: 96 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ dotnet add package PipelineNet
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*

- [Simple example](#simple-example)
- [Pipeline vs Chain of responsibility](#pipeline-vs-chain-of-responsibility)
- [Middleware](#middleware)
- [Pipelines](#pipelines)
- [Chains of responsibility](#chains-of-responsibility)
- [Middleware resolver](#middleware-resolver)
- [License](#license)
- [Simple example](#simple-example)
- [Pipeline vs Chain of responsibility](#pipeline-vs-chain-of-responsibility)
- [Middleware](#middleware)
- [Pipelines](#pipelines)
- [Chains of responsibility](#chains-of-responsibility)
- [Factories](#factories)
- [Middleware resolver](#middleware-resolver)
- [ServiceProvider implementation](#serviceprovider-implementation)
- [Unity implementation](#unity-implementation)
- [License](#license)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Expand Down Expand Up @@ -194,6 +197,23 @@ result = await exceptionHandlersChain.Execute(new ArgumentException()); // Resul
result = await exceptionHandlersChain.Execute(new InvalidOperationException()); // Result will be false
```

## Factories
Instead of instantiating pipelines and chains of responsibility directly, you can use factories to instantiate them:
```C#
IAsyncPipelineFactory<Bitmap> pipelineFactory = new AsyncPipelineFactory<Bitmap>(new ActivatorMiddlewareResovler());

IAsyncPipeline<Bitmap> pipeline = pipelineFactory.Create()
.Add<RoudCornersAsyncMiddleware>()
.Add<AddTransparencyAsyncMiddleware>()
.Add<AddWatermarkAsyncMiddleware>();
```

There are four types of factories:
- `PipelineFactory<TParamater>` for pipelines.
- `AsyncPipelineFactory<TParamater>` for asynchronous pipelines.
- `ResponsibilityChainFactory<TParameter, TReturn>` for chains of responsibility.
- `AsyncResponsibilityChainFactory<TParameter, TReturn>` for asynchronous chains of responsibility.

## Middleware resolver
You may be wondering what is all this `ActivatorMiddlewareResolver` class being passed to every instance of pipeline and chain of responsibility.
This is a default implementation of the `IMiddlewareResolver`, which is used to create instances of the middleware types.
Expand All @@ -213,38 +233,39 @@ Install-Package PipelineNet.ServiceProvider
```

Use it as follows:

```C#
services.AddScoped<IMyPipelineFactory, MyPipelineFactory>();

public interface IMyPipelineFactory
public interface IMyService
{
IAsyncPipeline<Bitmap> CreatePipeline();
Task DoSomething();
}

public class MyPipelineFactory : IMyPipelineFactory
public class MyService : IMyService
{
private readonly IServiceProvider _serviceProvider;
private readonly IAsyncPipelineFactory<Bitmap> _pipelineFactory;

public MyPipelineFactory(IServiceProvider serviceProvider)
public MyPipelineFactory(IAsyncPipelineFactory<Bitmap> pipelineFactory)
{
_serviceProvider = serviceProvider;
_pipelineFactory = pipelineFactory;
}

public IAsyncPipeline<Bitmap> CreatePipeline()
public Task DoSomething()
{
return new AsyncPipeline<Bitmap>(new ActivatorUtilitiesMiddlewareResolver(_serviceProvider)) // Pass ActivatorUtilitiesMiddlewareResolver
IAsyncPipeline<Bitmap> pipeline = _pipelineFactory.Create()
.Add<RoudCornersAsyncMiddleware>()
.Add<AddTransparencyAsyncMiddleware>()
.Add<AddWatermarkAsyncMiddleware>();

Bitmap image = (Bitmap) Image.FromFile("party-photo.png");

await pipeline.Execute(image);
}
}

public class RoudCornersAsyncMiddleware : IAsyncMiddleware<Bitmap>
{
private readonly ILogger<RoudCornersAsyncMiddleware> _logger;

// The following constructor argument will be provided by IServiceProvider
// The following constructor arguments will be provided by IServiceProvider
public RoudCornersAsyncMiddleware(ILogger<RoudCornersAsyncMiddleware> logger)
{
_logger = logger;
Expand All @@ -259,6 +280,62 @@ public class RoudCornersAsyncMiddleware : IAsyncMiddleware<Bitmap>
}
```

And register it using `IHttpContextAccessor` (recommended):
```C#
services.AddSingleton<IMyService, MyService>();

services.AddMiddlewareFromAssembly(typeof(RoudCornersAsyncMiddleware).Assembly, ServiceLifetime.Scoped); // Add all middleware from assembly
services.AddHttpContextAccessor();

services.TryAddSingleton<IMiddlewareResolver, HttpContextAccessorMiddlewareResolver>(); // 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<,>));

public class HttpContextAccessorMiddlewareResolver : IMiddlewareResolver
{
private readonly IHttpContextAccessor _httpContextAccessor;

public HttpContextAccessorMiddlewareResolver(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException("httpContextAccessor",
"An instance of IHttpContextAccessor must be provided.");
}

public MiddlewareResolverResult Resolve(Type type)
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null) throw new InvalidOperationException("HttpContext must not be null.");

var middleware = httpContext.RequestServices.GetRequiredService(type);
bool isDisposable = false;

return new MiddlewareResolverResult
{
Middleware = middleware,
IsDisposable = isDisposable
};
}
}
```

or `IServiceProvider`:
```C#
services.AddScoped<IMyService, MyService>();

services.AddMiddlewareFromAssembly(typeof(RoudCornersAsyncMiddleware).Assembly, ServiceLifetime.Scoped); // Add all middleware from assembly
services.TryAddScoped<IMiddlewareResolver, ServiceProviderMiddlewareResolver>(); // Add scoped middleware resolver so that scoped IServiceProvider gets 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<,>));
```

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.

For more information on dependency injection, see: [Dependency injection - .NET](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ public void Resolve_ResolvesDisposableMiddleware()
}

[Fact]
public void Resolve_TransientServiceGetsDisposed1()
public void Resolve_TransientServiceGetsDisposedWhenRootServiceProviderIsDisposed()
{
var serviceProvider = new ServiceCollection()
.AddTransient<ITransientService, TransientService>()
Expand All @@ -201,7 +201,7 @@ public void Resolve_TransientServiceGetsDisposed1()
}

[Fact]
public void Resolve_TransientServiceGetsDisposed2()
public void Resolve_TransientServiceGetsDisposedWhenScopeIsDisposed()
{
var serviceProvider = new ServiceCollection()
.AddTransient<ITransientService, TransientService>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using Microsoft.Extensions.DependencyInjection;
using PipelineNet.ServiceProvider.MiddlewareResolver;
using PipelineNet.Middleware;

namespace PipelineNet.ServiceProvider.Tests.MiddlewareResolver
{
public class ServiceProviderMiddlewareResolverTests
{
#region Service defintions
public interface ITransientService
{
}

public class TransientService : ITransientService
{
}

public interface IScopedService
{
}

public class ScopedService : IScopedService
{
}

public interface ISingletonService
{
}

public class SingletonService : ISingletonService
{
}
#endregion

#region Middleware definitions
public class ParameterlessConstructorMiddleware : IMiddleware<object>
{
public void Run(object parameter, Action<object> next)
{
}
}

public class TransientMiddleware : IMiddleware<object>
{
public ITransientService Service { get; }

public TransientMiddleware(ITransientService service)
{
Service = service;
}

public void Run(object parameter, Action<object> next)
{
}
}

public class ScopedMiddleware : IMiddleware<object>
{
public IScopedService Service { get; }

public ScopedMiddleware(IScopedService service)
{
Service = service;
}

public void Run(object parameter, Action<object> next)
{
}
}

public class SingletonMiddleware : IMiddleware<object>
{
public ISingletonService Service { get; }

public SingletonMiddleware(ISingletonService service)
{
Service = service;
}

public void Run(object parameter, Action<object> next)
{
}
}

public class DisposableMiddleware : IMiddleware<object>, IDisposable
{
public void Run(object parameter, Action<object> next)
{
}

public void Dispose()
{
}
}
#endregion

[Fact]
public void Resolve_ResolvesParameterlessConstructorMiddleware()
{
var serviceProvider = new ServiceCollection()
.AddTransient<ParameterlessConstructorMiddleware>()
.BuildServiceProvider(validateScopes: true);
var resolver = new ServiceProviderMiddlewareResolver(serviceProvider);

var resolverResult = resolver.Resolve(typeof(ParameterlessConstructorMiddleware));

Assert.NotNull(resolverResult.Middleware);
Assert.False(resolverResult.IsDisposable);
}

[Fact]
public void Resolve_ResolvesTransientMiddleware()
{
var serviceProvider = new ServiceCollection()
.AddTransient<TransientMiddleware>()
.AddTransient<ITransientService, TransientService>()
.BuildServiceProvider(validateScopes: true);
var resolver = new ServiceProviderMiddlewareResolver(serviceProvider);

var resolverResult = resolver.Resolve(typeof(TransientMiddleware));

Assert.NotNull(resolverResult.Middleware);
Assert.False(resolverResult.IsDisposable);
}

[Fact]
public void Resolve_ResolvesScopedMiddleware()
{
var serviceProvider = new ServiceCollection()
.AddScoped<ScopedMiddleware>()
.AddScoped<IScopedService, ScopedService>()
.BuildServiceProvider(validateScopes: true);
var scope = serviceProvider.CreateScope();
var resolver = new ServiceProviderMiddlewareResolver(scope.ServiceProvider);

var resolverResult = resolver.Resolve(typeof(ScopedMiddleware));

Assert.NotNull(resolverResult.Middleware);
Assert.False(resolverResult.IsDisposable);
}

[Fact]
public void Resolve_ResolvesSingletonMiddleware()
{
var serviceProvider = new ServiceCollection()
.AddSingleton<SingletonMiddleware>()
.AddSingleton<ISingletonService, SingletonService>()
.BuildServiceProvider(validateScopes: true);
var resolver = new ServiceProviderMiddlewareResolver(serviceProvider);

var resolverResult = resolver.Resolve(typeof(SingletonMiddleware));

Assert.NotNull(resolverResult.Middleware);
Assert.False(resolverResult.IsDisposable);
}

[Fact]
public void Resolve_ResolvesDisposableMiddleware()
{
var serviceProvider = new ServiceCollection()
.AddSingleton<DisposableMiddleware>()
.AddSingleton<ISingletonService, SingletonService>()
.BuildServiceProvider(validateScopes: true);
var resolver = new ServiceProviderMiddlewareResolver(serviceProvider);

var resolverResult = resolver.Resolve(typeof(DisposableMiddleware));

Assert.NotNull(resolverResult.Middleware);
Assert.False(resolverResult.IsDisposable);
}

[Fact]
public void Resolve_ThrowsInvalidOperationExceptionWhenServiceIsNotRegistered()
{
var serviceProvider = new ServiceCollection()
.BuildServiceProvider(validateScopes: true);
var resolver = new ServiceProviderMiddlewareResolver(serviceProvider);

Assert.Throws<InvalidOperationException>(() =>
resolver.Resolve(typeof(TransientMiddleware)));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@ namespace PipelineNet.ServiceProvider.MiddlewareResolver
{
/// <summary>
/// An implementation of <see cref="IMiddlewareResolver"/> that creates
/// instances using the <see cref="Microsoft.Extensions.DependencyInjection.ActivatorUtilities"/>.
/// instances using the <see cref="ActivatorUtilities"/>.
/// </summary>
public class ActivatorUtilitiesMiddlewareResolver : IMiddlewareResolver
{
private readonly IServiceProvider _serviceProvider;

/// <summary>
/// Creates a new <see cref="ActivatorUtilitiesMiddlewareResolver"/>.
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/>.</param>
public ActivatorUtilitiesMiddlewareResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException("serviceProvider",
"An instance of IServiceProvider must be provided."); ;
}

/// <inheritdoc/>
public MiddlewareResolverResult Resolve(Type type)
{
var middleware = ActivatorUtilities.CreateInstance(_serviceProvider, type);
Expand Down
Loading

0 comments on commit 452aeb8

Please sign in to comment.