Skip to content

Commit

Permalink
Merge pull request #3 from Stillpoint-Software/feature/2-feature-crea…
Browse files Browse the repository at this point in the history
…te-extension-to-support-caching-values

[FEATURE]: Create Extension to Support Caching values
  • Loading branch information
MattEdwardsWaggleBee authored May 3, 2024
2 parents 802c6d6 + e33b75e commit 4ee2d99
Show file tree
Hide file tree
Showing 9 changed files with 584 additions and 2 deletions.
17 changes: 17 additions & 0 deletions Hyperbee.Pipeline.sln
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hyperbee.Pipeline.Tests", "
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{884A8242-351E-4363-9B34-E8C202CF7787}"
ProjectSection(SolutionItems) = preProject
docs\childPipeline.md = docs\childPipeline.md
docs\dependencyInjection.md = docs\dependencyInjection.md
docs\execution.md = docs\execution.md
docs\middleware.md = docs\middleware.md
docs\todo.md = docs\todo.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hyperbee.Pipeline.Caching", "src\Hyperbee.Pipline.Caching\Hyperbee.Pipeline.Caching.csproj", "{833A7497-542F-4B88-A76A-DA520E000F6F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hyperbee.Pipeline.Caching.Tests", "test\Hyperbee.PipelineCaching.Tests\Hyperbee.Pipeline.Caching.Tests.csproj", "{B7E5FBB3-AF2A-4E48-8E6A-10887DC6C4C0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -51,6 +59,14 @@ Global
{17DA1657-DF82-440F-B1F1-D888BFA9626B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17DA1657-DF82-440F-B1F1-D888BFA9626B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17DA1657-DF82-440F-B1F1-D888BFA9626B}.Release|Any CPU.Build.0 = Release|Any CPU
{833A7497-542F-4B88-A76A-DA520E000F6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{833A7497-542F-4B88-A76A-DA520E000F6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{833A7497-542F-4B88-A76A-DA520E000F6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{833A7497-542F-4B88-A76A-DA520E000F6F}.Release|Any CPU.Build.0 = Release|Any CPU
{B7E5FBB3-AF2A-4E48-8E6A-10887DC6C4C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B7E5FBB3-AF2A-4E48-8E6A-10887DC6C4C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B7E5FBB3-AF2A-4E48-8E6A-10887DC6C4C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B7E5FBB3-AF2A-4E48-8E6A-10887DC6C4C0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -60,6 +76,7 @@ Global
{4DBDB7F5-3F66-4572-80B5-3322449C77A4} = {1FA7CE2A-C9DA-4DC3-A242-5A7EAF8EE4FC}
{17DA1657-DF82-440F-B1F1-D888BFA9626B} = {F9B24CD9-E06B-4834-84CB-8C29E5F10BE0}
{884A8242-351E-4363-9B34-E8C202CF7787} = {870D9301-BE3D-44EA-BF9C-FCC2E87FE4CD}
{B7E5FBB3-AF2A-4E48-8E6A-10887DC6C4C0} = {F9B24CD9-E06B-4834-84CB-8C29E5F10BE0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {32874F5B-B467-4F28-A8E2-82C2536FB228}
Expand Down
8 changes: 6 additions & 2 deletions src/Hyperbee.Pipeline/Context/PipelineContextFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ public IPipelineContext Create( ILogger logger )

public static IPipelineContextFactory Instance { get; private set; }

public static IPipelineContextFactory CreateFactory( IServiceProvider serviceProvider = null )
public static IPipelineContextFactory CreateFactory( IServiceProvider serviceProvider = null, bool resetFactory = false )
{
// get-or-create
if ( resetFactory )
{
return Instance = new PipelineContextFactory( serviceProvider );
}

return Instance ??= new PipelineContextFactory( serviceProvider );
}
}
45 changes: 45 additions & 0 deletions src/Hyperbee.Pipline.Caching/Hyperbee.Pipeline.Caching.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>true</IsPackable>

<Authors>Stillpoint Software, Inc.</Authors>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>pipeline;caching</PackageTags>
<PackageIcon>icon.png</PackageIcon>
<PackageProjectUrl>https://github.com/Stillpoint-Software/Hyperbee.Pipeline/</PackageProjectUrl>
<PackageReleaseNotes>https://github.com/Stillpoint-Software/hyperbee.pipeline/releases/latest</PackageReleaseNotes>
<TargetFrameworks>net8.0</TargetFrameworks>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<Copyright>Stillpoint Software, Inc.</Copyright>
<Title>Hyperbee Piplines Caching</Title>
<Description>Caching for Hyperbee.Pipelines async pipelines</Description>
<RepositoryUrl>https://github.com/Stillpoint-Software/Hyperbee.Pipeline</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageReleaseNotes>https://github.com/Stillpoint-Software/hyperbee.pipeline/releases/latest</PackageReleaseNotes>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Hyperbee.Pipeline\Hyperbee.Pipeline.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\assets\icon.png" Pack="true" Visible="false" PackagePath="/" />
<None Include="..\..\LICENSE">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
</ItemGroup>

</Project>
136 changes: 136 additions & 0 deletions src/Hyperbee.Pipline.Caching/PipelineCacheExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using Hyperbee.Pipeline.Extensions.Implementation;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Hyperbee.Pipeline.Caching;

public static class PipelineCacheExtensions
{
public static IPipelineBuilder<TInput, TNext> PipeCache<TInput, TOutput, TNext>(
this IPipelineBuilder<TInput, TOutput> builder,
Func<IPipelineStartBuilder<TOutput, TOutput>, IPipelineBuilder<TOutput, TNext>> nestedBuilder,
Func<TOutput, PipelineMemoryCacheOptions, PipelineMemoryCacheOptions> optionsFunc = null )
{
ArgumentNullException.ThrowIfNull( nestedBuilder );

var block = PipelineFactory.Start<TOutput>();
var function = nestedBuilder( block ).GetPipelineFunction();

return builder.PipeCacheAsync( function.Function, optionsFunc );
}

public static IPipelineBuilder<TInput, TNext> PipeCacheAsync<TInput, TOutput, TNext>(
this IPipelineBuilder<TInput, TOutput> builder,
Func<IPipelineStartBuilder<TOutput, TOutput>, IPipelineBuilder<TOutput, TNext>> nestedBuilder,
Func<TOutput, PipelineMemoryCacheOptions, PipelineMemoryCacheOptions> optionsFunc = null )
{
ArgumentNullException.ThrowIfNull( nestedBuilder );

var block = PipelineFactory.Start<TOutput>();
var function = nestedBuilder( block ).GetPipelineFunction();

return builder.PipeCacheAsync( function.Function, optionsFunc );
}

public static IPipelineBuilder<TInput, TNext> PipeCache<TInput, TOutput, TNext>(
this IPipelineBuilder<TInput, TOutput> builder,
Function<TOutput, TNext> next,
Func<TOutput, PipelineMemoryCacheOptions, PipelineMemoryCacheOptions> optionsFunc = null )
{

// default to using input as key
optionsFunc ??= ( output, options ) =>
{
options.Key = output;
return options;
};


return builder.Pipe( ( context, argument ) =>
{
var cache = context
.ServiceProvider
.GetService<IMemoryCache>();

if ( cache == null )
{
context.Logger?.LogWarning( "Cache not configured." );
return next( context, argument );
}

var defaultCacheOption = context
.ServiceProvider
.GetService<IOptions<PipelineMemoryCacheOptions>>();

var cacheOption = optionsFunc( argument, defaultCacheOption?.Value ?? new PipelineMemoryCacheOptions() );

if ( cacheOption?.Key != null )
{
return cache.GetOrCreate( cacheOption.Key, entry =>
{
context.Logger?.LogDebug( "Creating cache entry for {Key} not configured", cacheOption.Key );
entry.SetOptions( cacheOption );
return next( context, argument );
} ) ?? default;
}

context.Logger?.LogError( "Cache entries must have a valid key." );
context.Exception = new InvalidOperationException( "Cache entries must have a valid key." );
context.CancelAfter();
return default;

} );
}

public static IPipelineBuilder<TInput, TNext> PipeCacheAsync<TInput, TOutput, TNext>(
this IPipelineBuilder<TInput, TOutput> builder,
FunctionAsync<TOutput, TNext> next,
Func<TOutput, PipelineMemoryCacheOptions, PipelineMemoryCacheOptions> optionsFunc = null )
{

// default to using input as key
optionsFunc ??= ( output, options ) =>
{
options.Key = output;
return options;
};


return builder.PipeAsync( async ( context, argument ) =>
{
var cache = context
.ServiceProvider
.GetService<IMemoryCache>();

if ( cache == null )
{
context.Logger?.LogWarning( "Cache not configured." );
return await next( context, argument );
}

var defaultCacheOption = context
.ServiceProvider
.GetService<IOptions<PipelineMemoryCacheOptions>>();

var cacheOption = optionsFunc( argument, defaultCacheOption?.Value ?? new PipelineMemoryCacheOptions() );

if ( cacheOption?.Key != null )
{
return await cache.GetOrCreateAsync( cacheOption.Key, entry =>
{
context.Logger?.LogDebug( "Creating cache entry for {Key} not configured", cacheOption.Key );
entry.SetOptions( cacheOption );
return next( context, argument );
} ) ?? default;
}

context.Logger?.LogError( "Cache entries must have a valid key." );
context.Exception = new InvalidOperationException( "Cache entries must have a valid key." );
context.CancelAfter();
return default;

} );
}
}
10 changes: 10 additions & 0 deletions src/Hyperbee.Pipline.Caching/PipelineMemoryCacheOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;

namespace Hyperbee.Pipeline.Caching;

public class PipelineMemoryCacheOptions : MemoryCacheEntryOptions, IOptions<PipelineMemoryCacheOptions>
{
public object Key { get; set; }
public PipelineMemoryCacheOptions Value => this;
}
76 changes: 76 additions & 0 deletions src/Hyperbee.Pipline.Caching/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Hyperbee.Pipeline.Caching

The `Hyperbee.Pipeline.Caching` library is a set of extentsions to `Hyperbee.Pipeline` that adds support for caching within a pipeline using `Microsoft.Extensions.Caching` libraries.

## Examples
For simple pipelines the previous step's return value can be used as the key:

```csharp
// Takes a string and returns a number
var command = PipelineFactory
.Start<string>()
.PipeCacheAsync( CharacterCountAsync )
.Build();

var result = await command( factory.Create( logger ), "test" );

Assert.AreEqual( 4, result );
```

Or for more complex the options callback can be used to customize how the results will be cached.

```csharp
// Takes a string and returns a number
var command = PipelineFactory
.Start<string>()
.PipeCacheAsync( CharacterCountAsync,
( input, options ) =>
{
options.Key = $"custom/{input}";
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours( 1 );
return options;
} )
.Build();

var result = await command( factory.Create( logger ), "test" );

Assert.AreEqual( 4, result );
```

When a set of steps should cache the overload that takes a builder can be used. In this case the inital input will be used at the key and the final step of the nested pipeline will be cached

```csharp
// Takes a string and returns a number
var command = PipelineFactory
.Start<string>()
.PipeCacheAsync( b => b
.PipeAsync( CharacterCountAsync )
.Pipe( (ctx, arg) => arg + 100 ))
.Build();

var result = await command( factory.Create( logger ), "test" );

Assert.AreEqual( 104, result );
```

## Dependacy Injection

Because this uses the existing DI built into pipelines, caching can be configured with an existing cache:

```csharp
// Add Memory Cache
services.AddMemoryCache();

// Share with the pipelines
services.AddPipeline( includeAllServices: true );
```

Or defined seperately as part of the container use for the pipelines:

```csharp
services.AddPipeline( (factoryServices, rootProvider) =>
{
factoryServices.AddMemoryCache();
factoryServices.AddPipelineDefaultCacheSettings( absoluteExpirationRelativeToNow: TimeSpan.FromHours( 1 ) )
} );
```
26 changes: 26 additions & 0 deletions src/Hyperbee.Pipline.Caching/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Hyperbee.Pipeline.Caching;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;

// ReSharper disable CheckNamespace
namespace Hyperbee.Pipeline;

public static class ServiceCollectionExtensions
{
public static IServiceCollection AddPipelineDefaultCacheSettings(
this IServiceCollection services,
DateTimeOffset? absoluteExpiration = null,
TimeSpan? absoluteExpirationRelativeToNow = null,
CacheItemPriority priority = CacheItemPriority.Normal,
PostEvictionCallbackRegistration callbackRegistration = null
)
{
return services.AddTransient( ( _ ) => new PipelineMemoryCacheOptions
{
AbsoluteExpiration = absoluteExpiration,
AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow,
Priority = priority,
PostEvictionCallbacks = { callbackRegistration }
} );
}
}
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.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Hyperbee.Pipeline\Hyperbee.Pipeline.csproj" />
<ProjectReference Include="..\..\src\Hyperbee.Pipline.Caching\Hyperbee.Pipeline.Caching.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>

</Project>
Loading

0 comments on commit 4ee2d99

Please sign in to comment.