Skip to content

Commit

Permalink
add HybridCache, misc refactors
Browse files Browse the repository at this point in the history
  • Loading branch information
pwelter34 committed Oct 19, 2024
1 parent 591efeb commit 6f566be
Show file tree
Hide file tree
Showing 33 changed files with 429 additions and 178 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,8 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
7.0.x
8.0.x
9.0.x
- name: Restore Dependencies
run: dotnet restore
Expand Down
28 changes: 0 additions & 28 deletions src/MediatR.CommandQuery.Cosmos/DomainServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,34 +46,6 @@ public static IServiceCollection AddEntityQueries<TRepository, TEntity, TReadMod
return services;
}

public static IServiceCollection AddEntityQueryMemoryCache<TRepository, TEntity, TReadModel>(this IServiceCollection services)
where TRepository : ICosmosRepository<TEntity>
where TEntity : class, IHaveIdentifier<string>, new()
{
ArgumentNullException.ThrowIfNull(services);

services.AddTransient<IPipelineBehavior<EntityIdentifierQuery<string, TReadModel>, TReadModel>, MemoryCacheQueryBehavior<EntityIdentifierQuery<string, TReadModel>, TReadModel>>();
services.AddTransient<IPipelineBehavior<EntityIdentifiersQuery<string, TReadModel>, IReadOnlyCollection<TReadModel>>, MemoryCacheQueryBehavior<EntityIdentifiersQuery<string, TReadModel>, IReadOnlyCollection<TReadModel>>>();
services.AddTransient<IPipelineBehavior<EntityPagedQuery<TReadModel>, EntityPagedResult<TReadModel>>, MemoryCacheQueryBehavior<EntityPagedQuery<TReadModel>, EntityPagedResult<TReadModel>>>();
services.AddTransient<IPipelineBehavior<EntitySelectQuery<TReadModel>, IReadOnlyCollection<TReadModel>>, MemoryCacheQueryBehavior<EntitySelectQuery<TReadModel>, IReadOnlyCollection<TReadModel>>>();

return services;
}

public static IServiceCollection AddEntityQueryDistributedCache<TRepository, TEntity, TReadModel>(this IServiceCollection services)
where TRepository : ICosmosRepository<TEntity>
where TEntity : class, IHaveIdentifier<string>, new()
{
ArgumentNullException.ThrowIfNull(services);

services.AddTransient<IPipelineBehavior<EntityIdentifierQuery<string, TReadModel>, TReadModel>, DistributedCacheQueryBehavior<EntityIdentifierQuery<string, TReadModel>, TReadModel>>();
services.AddTransient<IPipelineBehavior<EntityIdentifiersQuery<string, TReadModel>, IReadOnlyCollection<TReadModel>>, DistributedCacheQueryBehavior<EntityIdentifiersQuery<string, TReadModel>, IReadOnlyCollection<TReadModel>>>();
services.AddTransient<IPipelineBehavior<EntityPagedQuery<TReadModel>, EntityPagedResult<TReadModel>>, DistributedCacheQueryBehavior<EntityPagedQuery<TReadModel>, EntityPagedResult<TReadModel>>>();
services.AddTransient<IPipelineBehavior<EntitySelectQuery<TReadModel>, IReadOnlyCollection<TReadModel>>, DistributedCacheQueryBehavior<EntitySelectQuery<TReadModel>, IReadOnlyCollection<TReadModel>>>();

return services;
}


public static IServiceCollection AddEntityCommands<TRepository, TEntity, TReadModel, TCreateModel, TUpdateModel>(this IServiceCollection services)
where TRepository : ICosmosRepository<TEntity>
Expand Down
5 changes: 4 additions & 1 deletion src/MediatR.CommandQuery.Endpoints/DispatcherEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ public void AddRoutes(IEndpointRouteBuilder app)

group
.MapPost(_dispatcherOptions.SendRoute, Send)
.ExcludeFromDescription();
.WithTags("Dispatcher")
.WithName($"Send")
.WithSummary("Send Mediator command")
.WithDescription("Send Mediator command");
}

protected virtual async Task<IResult> Send(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public abstract class EntityQueryEndpointBase<TKey, TListModel, TReadModel>
protected EntityQueryEndpointBase(IMediator mediator, string entityName) : base(mediator)
{
EntityName = entityName;
RoutePrefix = $"/api/{EntityName}";
RoutePrefix = EntityName;
}

public string EntityName { get; }
Expand Down
11 changes: 7 additions & 4 deletions src/MediatR.CommandQuery.Endpoints/FeatureEndpointExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;

Expand All @@ -12,12 +13,14 @@ public static IServiceCollection AddFeatureEndpoints(this IServiceCollection ser
return services;
}

public static IEndpointRouteBuilder MapFeatureEndpoints(this IEndpointRouteBuilder builder)
public static IEndpointConventionBuilder MapFeatureEndpoints(this IEndpointRouteBuilder builder, string prefix = "/api")
{
var featureGroup = builder.MapGroup(prefix);

var features = builder.ServiceProvider.GetServices<IFeatureEndpoint>();
foreach (var feature in features)
feature.AddRoutes(builder);

return builder;
feature.AddRoutes(featureGroup);
return featureGroup;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ public static IServiceCollection AddEntityQueries<TContext, TEntity, TKey, TRead
where TEntity : class, IHaveIdentifier<TKey>, new()
where TReadModel : class
{
if (services is null)
throw new System.ArgumentNullException(nameof(services));
ArgumentNullException.ThrowIfNull(services);

CacheTagger.SetTag<TReadModel, TEntity>();

// standard queries
services.TryAddTransient<IRequestHandler<EntityIdentifierQuery<TKey, TReadModel>, TReadModel>, EntityIdentifierQueryHandler<TContext, TEntity, TKey, TReadModel>>();
Expand All @@ -46,47 +47,18 @@ public static IServiceCollection AddEntityQueries<TContext, TEntity, TKey, TRead
return services;
}

public static IServiceCollection AddEntityQueryMemoryCache<TContext, TEntity, TKey, TReadModel>(this IServiceCollection services)
where TContext : DbContext
where TEntity : class, IHaveIdentifier<TKey>, new()
{
if (services is null)
throw new System.ArgumentNullException(nameof(services));

services.AddTransient<IPipelineBehavior<EntityIdentifierQuery<TKey, TReadModel>, TReadModel>, MemoryCacheQueryBehavior<EntityIdentifierQuery<TKey, TReadModel>, TReadModel>>();
services.AddTransient<IPipelineBehavior<EntityIdentifiersQuery<TKey, TReadModel>, IReadOnlyCollection<TReadModel>>, MemoryCacheQueryBehavior<EntityIdentifiersQuery<TKey, TReadModel>, IReadOnlyCollection<TReadModel>>>();
services.AddTransient<IPipelineBehavior<EntityPagedQuery<TReadModel>, EntityPagedResult<TReadModel>>, MemoryCacheQueryBehavior<EntityPagedQuery<TReadModel>, EntityPagedResult<TReadModel>>>();
services.AddTransient<IPipelineBehavior<EntitySelectQuery<TReadModel>, IReadOnlyCollection<TReadModel>>, MemoryCacheQueryBehavior<EntitySelectQuery<TReadModel>, IReadOnlyCollection<TReadModel>>>();

return services;
}

public static IServiceCollection AddEntityQueryDistributedCache<TContext, TEntity, TKey, TReadModel>(this IServiceCollection services)
where TContext : DbContext
where TEntity : class, IHaveIdentifier<TKey>, new()
{
if (services is null)
throw new System.ArgumentNullException(nameof(services));

services.AddTransient<IPipelineBehavior<EntityIdentifierQuery<TKey, TReadModel>, TReadModel>, DistributedCacheQueryBehavior<EntityIdentifierQuery<TKey, TReadModel>, TReadModel>>();
services.AddTransient<IPipelineBehavior<EntityIdentifiersQuery<TKey, TReadModel>, IReadOnlyCollection<TReadModel>>, DistributedCacheQueryBehavior<EntityIdentifiersQuery<TKey, TReadModel>, IReadOnlyCollection<TReadModel>>>();
services.AddTransient<IPipelineBehavior<EntityPagedQuery<TReadModel>, EntityPagedResult<TReadModel>>, DistributedCacheQueryBehavior<EntityPagedQuery<TReadModel>, EntityPagedResult<TReadModel>>>();
services.AddTransient<IPipelineBehavior<EntitySelectQuery<TReadModel>, IReadOnlyCollection<TReadModel>>, DistributedCacheQueryBehavior<EntitySelectQuery<TReadModel>, IReadOnlyCollection<TReadModel>>>();

return services;
}


public static IServiceCollection AddEntityCommands<TContext, TEntity, TKey, TReadModel, TCreateModel, TUpdateModel>(this IServiceCollection services)
where TContext : DbContext
where TEntity : class, IHaveIdentifier<TKey>, new()
where TCreateModel : class
where TUpdateModel : class
{
if (services is null)
throw new System.ArgumentNullException(nameof(services));
ArgumentNullException.ThrowIfNull(services);

services.TryAddSingleton<IPrincipalReader, PrincipalReader>();
CacheTagger.SetTag<TReadModel, TEntity>();
CacheTagger.SetTag<TCreateModel, TEntity>();
CacheTagger.SetTag<TUpdateModel, TEntity>();

services
.AddEntityCreateCommand<TContext, TEntity, TKey, TReadModel, TCreateModel>()
Expand All @@ -104,8 +76,7 @@ public static IServiceCollection AddEntityCreateCommand<TContext, TEntity, TKey,
where TEntity : class, IHaveIdentifier<TKey>, new()
where TCreateModel : class
{
if (services is null)
throw new System.ArgumentNullException(nameof(services));
ArgumentNullException.ThrowIfNull(services);

// standard crud commands
services.TryAddTransient<IRequestHandler<EntityCreateCommand<TCreateModel, TReadModel>, TReadModel>, EntityCreateCommandHandler<TContext, TEntity, TKey, TCreateModel, TReadModel>>();
Expand Down Expand Up @@ -134,8 +105,7 @@ public static IServiceCollection AddEntityUpdateCommand<TContext, TEntity, TKey,
where TEntity : class, IHaveIdentifier<TKey>, new()
where TUpdateModel : class
{
if (services is null)
throw new System.ArgumentNullException(nameof(services));
ArgumentNullException.ThrowIfNull(services);

// allow query for update models
services.TryAddTransient<IRequestHandler<EntityIdentifierQuery<TKey, TUpdateModel>, TUpdateModel>, EntityIdentifierQueryHandler<TContext, TEntity, TKey, TUpdateModel>>();
Expand Down Expand Up @@ -168,8 +138,7 @@ public static IServiceCollection AddEntityUpsertCommand<TContext, TEntity, TKey,
where TEntity : class, IHaveIdentifier<TKey>, new()
where TUpdateModel : class
{
if (services is null)
throw new System.ArgumentNullException(nameof(services));
ArgumentNullException.ThrowIfNull(services);

// standard crud commands
services.TryAddTransient<IRequestHandler<EntityUpsertCommand<TKey, TUpdateModel, TReadModel>, TReadModel>, EntityUpsertCommandHandler<TContext, TEntity, TKey, TUpdateModel, TReadModel>>();
Expand Down Expand Up @@ -197,8 +166,7 @@ public static IServiceCollection AddEntityPatchCommand<TContext, TEntity, TKey,
where TContext : DbContext
where TEntity : class, IHaveIdentifier<TKey>, new()
{
if (services is null)
throw new System.ArgumentNullException(nameof(services));
ArgumentNullException.ThrowIfNull(services);

// standard crud commands
services.TryAddTransient<IRequestHandler<EntityPatchCommand<TKey, TReadModel>, TReadModel>, EntityPatchCommandHandler<TContext, TEntity, TKey, TReadModel>>();
Expand All @@ -213,8 +181,7 @@ public static IServiceCollection AddEntityDeleteCommand<TContext, TEntity, TKey,
where TContext : DbContext
where TEntity : class, IHaveIdentifier<TKey>, new()
{
if (services is null)
throw new System.ArgumentNullException(nameof(services));
ArgumentNullException.ThrowIfNull(services);

// standard crud commands
services.TryAddTransient<IRequestHandler<EntityDeleteCommand<TKey, TReadModel>, TReadModel>, EntityDeleteCommandHandler<TContext, TEntity, TKey, TReadModel>>();
Expand Down
34 changes: 6 additions & 28 deletions src/MediatR.CommandQuery.MongoDB/DomainServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public static IServiceCollection AddEntityQueries<TRepository, TEntity, TKey, TR
{
ArgumentNullException.ThrowIfNull(services);

CacheTagger.SetTag<TReadModel, TEntity>();

// standard queries
services.TryAddTransient<IRequestHandler<EntityIdentifierQuery<TKey, TReadModel>, TReadModel>, EntityIdentifierQueryHandler<TRepository, TEntity, TKey, TReadModel>>();
services.TryAddTransient<IRequestHandler<EntityIdentifiersQuery<TKey, TReadModel>, IReadOnlyCollection<TReadModel>>, EntityIdentifiersQueryHandler<TRepository, TEntity, TKey, TReadModel>>();
Expand All @@ -46,34 +48,6 @@ public static IServiceCollection AddEntityQueries<TRepository, TEntity, TKey, TR
return services;
}

public static IServiceCollection AddEntityQueryMemoryCache<TRepository, TEntity, TKey, TReadModel>(this IServiceCollection services)
where TRepository : IMongoRepository<TEntity, TKey>
where TEntity : class, IHaveIdentifier<TKey>, new()
{
ArgumentNullException.ThrowIfNull(services);

services.AddTransient<IPipelineBehavior<EntityIdentifierQuery<TKey, TReadModel>, TReadModel>, MemoryCacheQueryBehavior<EntityIdentifierQuery<TKey, TReadModel>, TReadModel>>();
services.AddTransient<IPipelineBehavior<EntityIdentifiersQuery<TKey, TReadModel>, IReadOnlyCollection<TReadModel>>, MemoryCacheQueryBehavior<EntityIdentifiersQuery<TKey, TReadModel>, IReadOnlyCollection<TReadModel>>>();
services.AddTransient<IPipelineBehavior<EntityPagedQuery<TReadModel>, EntityPagedResult<TReadModel>>, MemoryCacheQueryBehavior<EntityPagedQuery<TReadModel>, EntityPagedResult<TReadModel>>>();
services.AddTransient<IPipelineBehavior<EntitySelectQuery<TReadModel>, IReadOnlyCollection<TReadModel>>, MemoryCacheQueryBehavior<EntitySelectQuery<TReadModel>, IReadOnlyCollection<TReadModel>>>();

return services;
}

public static IServiceCollection AddEntityQueryDistributedCache<TRepository, TEntity, TKey, TReadModel>(this IServiceCollection services)
where TRepository : IMongoRepository<TEntity, TKey>
where TEntity : class, IHaveIdentifier<TKey>, new()
{
ArgumentNullException.ThrowIfNull(services);

services.AddTransient<IPipelineBehavior<EntityIdentifierQuery<TKey, TReadModel>, TReadModel>, DistributedCacheQueryBehavior<EntityIdentifierQuery<TKey, TReadModel>, TReadModel>>();
services.AddTransient<IPipelineBehavior<EntityIdentifiersQuery<TKey, TReadModel>, IReadOnlyCollection<TReadModel>>, DistributedCacheQueryBehavior<EntityIdentifiersQuery<TKey, TReadModel>, IReadOnlyCollection<TReadModel>>>();
services.AddTransient<IPipelineBehavior<EntityPagedQuery<TReadModel>, EntityPagedResult<TReadModel>>, DistributedCacheQueryBehavior<EntityPagedQuery<TReadModel>, EntityPagedResult<TReadModel>>>();
services.AddTransient<IPipelineBehavior<EntitySelectQuery<TReadModel>, IReadOnlyCollection<TReadModel>>, DistributedCacheQueryBehavior<EntitySelectQuery<TReadModel>, IReadOnlyCollection<TReadModel>>>();

return services;
}


public static IServiceCollection AddEntityCommands<TRepository, TEntity, TKey, TReadModel, TCreateModel, TUpdateModel>(this IServiceCollection services)
where TRepository : IMongoRepository<TEntity, TKey>
Expand All @@ -83,6 +57,10 @@ public static IServiceCollection AddEntityCommands<TRepository, TEntity, TKey, T
{
ArgumentNullException.ThrowIfNull(services);

CacheTagger.SetTag<TReadModel, TEntity>();
CacheTagger.SetTag<TCreateModel, TEntity>();
CacheTagger.SetTag<TUpdateModel, TEntity>();

services
.AddEntityCreateCommand<TRepository, TEntity, TKey, TReadModel, TCreateModel>()
.AddEntityUpdateCommand<TRepository, TEntity, TKey, TReadModel, TUpdateModel>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,23 @@ public DistributedCacheQueryBehavior(
IDistributedCacheSerializer distributedCacheSerializer)
: base(loggerFactory)
{
_distributedCache = distributedCache ?? throw new ArgumentNullException(nameof(distributedCache));
_distributedCacheSerializer = distributedCacheSerializer ?? throw new ArgumentNullException(nameof(distributedCacheSerializer));
ArgumentNullException.ThrowIfNull(distributedCache);
ArgumentNullException.ThrowIfNull(distributedCacheSerializer);

_distributedCache = distributedCache;
_distributedCacheSerializer = distributedCacheSerializer;
}

protected override async Task<TResponse> Process(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (next is null)
throw new ArgumentNullException(nameof(next));

if (next is null)
throw new ArgumentNullException(nameof(next));
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(next);

// cache only if implements interface
var cacheRequest = request as ICacheQueryResult;
var cacheRequest = request as ICacheResult;
if (cacheRequest?.IsCacheable() != true)
return await next().ConfigureAwait(false);

Expand Down Expand Up @@ -70,7 +70,8 @@ protected override async Task<TResponse> Process(
var options = new DistributedCacheEntryOptions
{
SlidingExpiration = cacheRequest.SlidingExpiration(),
AbsoluteExpiration = cacheRequest.AbsoluteExpiration()
AbsoluteExpiration = cacheRequest.AbsoluteExpiration(),

};

await _distributedCache
Expand Down
43 changes: 43 additions & 0 deletions src/MediatR.CommandQuery/Behaviors/HybridCacheExpireBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using MediatR.CommandQuery.Definitions;

using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Logging;

namespace MediatR.CommandQuery.Behaviors;

public partial class HybridCacheExpireBehavior<TRequest, TResponse> : PipelineBehaviorBase<TRequest, TResponse>
where TRequest : class, IRequest<TResponse>
{
private readonly HybridCache _hybridCache;

public HybridCacheExpireBehavior(
ILoggerFactory loggerFactory,
HybridCache hybridCache)
: base(loggerFactory)
{
ArgumentNullException.ThrowIfNull(hybridCache);

_hybridCache = hybridCache;
}

protected override async Task<TResponse> Process(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(next);

var response = await next().ConfigureAwait(false);

// expire cache
if (request is not ICacheExpire cacheRequest)
return response;

var cacheTag = cacheRequest.GetCacheTag();
if (!string.IsNullOrEmpty(cacheTag))
await _hybridCache.RemoveByTagAsync(cacheTag, cancellationToken);

return response;
}
}
50 changes: 50 additions & 0 deletions src/MediatR.CommandQuery/Behaviors/HybridCacheQueryBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using MediatR.CommandQuery.Definitions;

using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Logging;

namespace MediatR.CommandQuery.Behaviors;

public partial class HybridCacheQueryBehavior<TRequest, TResponse> : PipelineBehaviorBase<TRequest, TResponse>
where TRequest : class, IRequest<TResponse>
{
private readonly HybridCache _hybridCache;

public HybridCacheQueryBehavior(
ILoggerFactory loggerFactory,
HybridCache hybridCache)
: base(loggerFactory)
{
_hybridCache = hybridCache ?? throw new ArgumentNullException(nameof(hybridCache));
}

protected override async Task<TResponse> Process(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(next);

// cache only if implements interface
var cacheRequest = request as ICacheResult;
if (cacheRequest?.IsCacheable() != true)
return await next().ConfigureAwait(false);

var cacheKey = cacheRequest.GetCacheKey();
var cacheTag = cacheRequest.GetCacheTag();

var cacheOptions = new HybridCacheEntryOptions
{
Expiration = cacheRequest.SlidingExpiration(),
LocalCacheExpiration = cacheRequest.SlidingExpiration(),
};

return await _hybridCache.GetOrCreateAsync(
key: cacheKey,
factory: async token => await next().ConfigureAwait(false),
options: cacheOptions,
tags: string.IsNullOrEmpty(cacheTag) ? null : [cacheTag],
cancellationToken: cancellationToken);
}
}
Loading

0 comments on commit 6f566be

Please sign in to comment.