Skip to content

Commit

Permalink
add convenience ServiceCollectionExtensions
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusz96 committed Aug 22, 2024
1 parent 62dbd4b commit c4b2c5b
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 51 deletions.
77 changes: 26 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<Bitmap> pipelineFactory = new AsyncPipelineFactory<Bitmap>(new ActivatorMiddlewareResovler());
Expand Down Expand Up @@ -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<IMyService, MyService>();

public interface IMyService
{
Task DoSomething();
Expand All @@ -243,12 +251,12 @@ public class MyService : IMyService
{
private readonly IAsyncPipelineFactory<Bitmap> _pipelineFactory;

public MyPipelineFactory(IAsyncPipelineFactory<Bitmap> pipelineFactory)
public MyService(IAsyncPipelineFactory<Bitmap> pipelineFactory)
{
_pipelineFactory = pipelineFactory;
}

public Task DoSomething()
public async Task DoSomething()
{
IAsyncPipeline<Bitmap> pipeline = _pipelineFactory.Create()
.Add<RoudCornersAsyncMiddleware>()
Expand All @@ -265,7 +273,6 @@ public class RoudCornersAsyncMiddleware : IAsyncMiddleware<Bitmap>
{
private readonly ILogger<RoudCornersAsyncMiddleware> _logger;

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

Register it using `IHttpContextAccessor` (recommended):
Alternatively, you can instantiate `ServiceProviderMiddlewareResolver` directly:
```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<,>));
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<Bitmap> pipeline = new AsyncPipeline<Bitmap>(new ServiceProviderMiddlewareResolver(serviceProvider))
.Add<RoudCornersAsyncMiddleware>()
.Add<AddTransparencyAsyncMiddleware>()
.Add<AddWatermarkAsyncMiddleware>();

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<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 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Bitmap> _pipelineFactory;

public MyService(IAsyncPipelineFactory<Bitmap> pipelineFactory)
{
_pipelineFactory = pipelineFactory;
}

public async Task DoSomething()
{
IAsyncPipeline<Bitmap> pipeline = _pipelineFactory.Create()
.Add<RoudCornersAsyncMiddleware>()
.Add<AddTransparencyAsyncMiddleware>()
.Add<AddWatermarkAsyncMiddleware>();

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

await pipeline.Execute(image);
}
}
#endregion

#region Middleware definitions
public class RoudCornersAsyncMiddleware : IAsyncMiddleware<Bitmap>
{
private readonly ILogger<RoudCornersAsyncMiddleware> _logger;

public RoudCornersAsyncMiddleware(ILogger<RoudCornersAsyncMiddleware> logger)
{
_logger = logger;
}

public async Task Run(Bitmap parameter, Func<Bitmap, Task> next)
{
_logger.LogInformation("Running RoudCornersAsyncMiddleware.");
// Handle somehow
await next(parameter);
}
}

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

public AddTransparencyAsyncMiddleware(ILogger<AddTransparencyAsyncMiddleware> logger)
{
_logger = logger;
}

public async Task Run(Bitmap parameter, Func<Bitmap, Task> next)
{
_logger.LogInformation("Running AddTransparencyAsyncMiddleware.");
// Handle somehow
await next(parameter);
}
}

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

public AddWatermarkAsyncMiddleware(ILogger<AddWatermarkAsyncMiddleware> logger)
{
_logger = logger;
}

public async Task Run(Bitmap parameter, Func<Bitmap, Task> next)
{
_logger.LogInformation("Running AddWatermarkAsyncMiddleware.");
// Handle somehow
await next(parameter);
}
}
#endregion

#region Logger definitions
public class TestLoggerProvider : ILoggerProvider
{
private readonly List<string> _messages;

public TestLoggerProvider(List<string> messages)
{
_messages = messages;
}

public ILogger CreateLogger(string categoryName) =>
new TestLogger(_messages);

public void Dispose()
{
}
}

public class TestLogger : ILogger
{
private readonly List<string> _messages;

public TestLogger(List<string> messages)
{
_messages = messages;
}

public IDisposable? BeginScope<TState>(TState state) where TState : notnull =>
new NullScope();

public bool IsEnabled(LogLevel logLevel) => true;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> 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<string>();
var serviceProvider = new ServiceCollection()
.AddLogging(bulider => bulider.Services.AddSingleton<ILoggerProvider>(new TestLoggerProvider(messages)))
.AddPipelineNet(typeof(RoudCornersAsyncMiddleware).Assembly)
.AddScoped<IMyService, MyService>()
.BuildServiceProvider();
var scope = serviceProvider.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<IMyService>();

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]);
}
}
}
68 changes: 68 additions & 0 deletions src/PipelineNet.ServiceProvider/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,6 +17,70 @@ namespace PipelineNet.ServiceProvider
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds core PipelineNet services. Adds all middleware from assemblies.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="assemblies">Assemblies to scan.</param>
/// <param name="lifetime">The lifetime of the registered services and middleware.</param>
/// <returns>The service collection.</returns>
public static IServiceCollection AddPipelineNet(
this IServiceCollection services,
IEnumerable<Assembly> 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;
}

/// <summary>
/// Adds core PipelineNet services. Adds all middleware from the assembly.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="assembly">The assembly to scan.</param>
/// <param name="lifetime">The lifetime of the registered services and middleware.</param>
/// <returns>The service collection.</returns>
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;
}

/// <summary>
/// Adds core PipelineNet services.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="lifetime">The lifetime of the registered services.</param>
/// <returns>The service collection.</returns>
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;
}

/// <summary>
/// Adds all middleware from assemblies.
/// </summary>
Expand Down

0 comments on commit c4b2c5b

Please sign in to comment.