Skip to content

Commit

Permalink
add proper DI, FinallyThrow and improve the documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusz96 committed Aug 6, 2024
1 parent bd73c5c commit c1c5e7a
Show file tree
Hide file tree
Showing 43 changed files with 1,032 additions and 46 deletions.
74 changes: 49 additions & 25 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)
- [ASP.NET Core implementation](#aspnet-core-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,24 @@ 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 @@ -202,49 +223,54 @@ When configuring a pipeline/chain of responsibility you define the types of the
needs to be instantiated, so `IMiddlewareResolver` is responsible for that. Instantiated middleware are disposed automatically if they implement `IDisposable` or `IAsyncDisposable`. You can even create your own implementation, since the
`ActivatorMiddlewareResolver` only works for parametersless constructors.

### ServiceProvider implementation
### ASP.NET Core implementation

An implementation of the middleware resolver for `IServiceProvider` was provided by [@mariusz96](https://github.com/mariusz96). It is tested against Microsoft.Extensions.DependencyInjection `8.X.X`, but should work with any dependency injection container that implements `IServiceProvider`.
An implementation of the middleware resolver for `ASP.NET Core` was provided by [@mariusz96](https://github.com/mariusz96).

You can grab it from nuget with:

```
Install-Package PipelineNet.ServiceProvider
Install-Package PipelineNet.ServiceProvider.AspNetCore
```

Use it as follows:

It includes extension methods for registering factories and automatic middleware registration through assembly scanning:
```C#
services.AddScoped<IMyPipelineFactory, MyPipelineFactory>();
builder.Services.AddScoped<IMyService, MyService>();
builder.Services.AddControllers()
.AddPipelineNet(typeof(RoudCornersAsyncMiddleware).Assembly); // Add pipeline and chain of responsibility factories and all middleware from the assembly
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 MyService(IAsyncPipelineFactory<Bitmap> pipelineFactory)
{
_serviceProvider = serviceProvider;
_pipelineFactory = pipelineFactory;
}

public IAsyncPipeline<Bitmap> CreatePipeline()
public async 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 the IServiceProvider
public RoudCornersAsyncMiddleware(ILogger<RoudCornersAsyncMiddleware> logger)
{
_logger = logger;
Expand All @@ -259,9 +285,7 @@ public class RoudCornersAsyncMiddleware : IAsyncMiddleware<Bitmap>
}
```

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).
Note that `PipelineNet.ServiceProvider.AspNetCore` uses `IHttpContextAccessor` and thus by default, cannot be used outside of HTTP request. You can create your own implementation though.

### Unity implementation

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc;
using PipelineNet.Pipelines;
using PipelineNet.ServiceProvider.Pipelines.Factories;
using static PipelineNet.ServiceProvider.AspNetCore.Tests.ServiceCollectionExtensionsTests;

namespace PipelineNet.ServiceProvider.AspNetCore.Tests.Controllers
{
[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
private readonly IAsyncPipelineFactory<Bitmap> _pipelineFactory;

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

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

Bitmap image = new Bitmap();

await pipeline.Execute(image);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.7" />
<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" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\PipelineNet.ServiceProvider.AspNetCore\PipelineNet.ServiceProvider.AspNetCore.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
29 changes: 29 additions & 0 deletions src/PipelineNet.ServiceProvider.AspNetCore.Tests/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using PipelineNet.ServiceProvider.AspNetCore;
using static PipelineNet.ServiceProvider.AspNetCore.Tests.ServiceCollectionExtensionsTests;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddLogging(builder =>
builder.Services.AddSingleton<ILoggerProvider, TestOutputHelperLoggerProvider>());

builder.Services.AddControllers()
.AddPipelineNet(typeof(RoudCornersAsyncMiddleware).Assembly);

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

public partial class Program { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using PipelineNet.Middleware;
using Xunit.Abstractions;

namespace PipelineNet.ServiceProvider.AspNetCore.Tests
{
public class ServiceCollectionExtensionsTests
: IClassFixture<WebApplicationFactory<Program>>
{
#region Service defintions
public class Bitmap
{
}

public class TestOutputHelperLogger : ILogger
{
private readonly ITestOutputHelper _output;

public TestOutputHelperLogger(ITestOutputHelper output)
{
_output = output;
}

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) =>
_output.WriteLine($"{logLevel}:{eventId}:{formatter(state, exception)}");

private class NullScope : IDisposable
{
public void Dispose()
{
}
}
}

public class TestOutputHelperLoggerProvider : ILoggerProvider
{
private readonly ITestOutputHelper _output;

public TestOutputHelperLoggerProvider(ITestOutputHelper output)
{
_output = output;
}

public ILogger CreateLogger(string categoryName) =>
new TestOutputHelperLogger(_output);

public void Dispose()
{
}
}
#endregion

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

// The following constructor arguments will be provided by the IServiceProvider
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.");
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.");
await next(parameter);
}
}
#endregion

private readonly WebApplicationFactory<Program> _factory;
private readonly ITestOutputHelper _output;

public ServiceCollectionExtensionsTests(WebApplicationFactory<Program> factory, ITestOutputHelper output)
{
_factory = factory;
_output = output;
}

[Fact]
public async Task AcceptanceTest()
{
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton(_output); // Add ITestOutputHelper for TestOutputHelperLoggerProvider.
});
})
.CreateClient();

await client.PostAsync("/My", null);
}
}
}
Loading

0 comments on commit c1c5e7a

Please sign in to comment.