Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 87 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# MitMediator.InMemoryCache

## An attribute-driven in-memory caching extension for the [MitMediator](https://github.com/dzmprt/MitMediator)

[![Build and Test](https://github.com/dzmprt/MitMediator.InMemoryCache/actions/workflows/dotnet.yml/badge.svg)](https://github.com/dzmprt/MitMediator.InMemoryCache/actions/workflows/dotnet.yml)
![NuGet](https://img.shields.io/nuget/v/MitMediator.InMemoryCache)
![.NET 9.0](https://img.shields.io/badge/Version-.NET%209.0-informational?style=flat&logo=dotnet)
Expand All @@ -9,23 +10,24 @@

## Installation

### 1. Install the package
### 1. Add package

```sh
dotnet add package MitMediator.InMemoryCache -v 9.0.0
dotnet add package MitMediator.InMemoryCache -v 9.0.0-alfa-2
```

### 2. Use extension for `IServiceCollection`
### 2. Register services

```csharp
// Register handlers and IMediator
builder.Services.AddMitMediator();

// Register MemoryCache and InMemoryCacheBehavior
// Read information about all IRequest<>
// Register MemoryCache, InMemoryCacheBehavior,
// scan information about all IRequest<>
builder.Services.AddRequestsInMemoryCache()
```
⚠️⚠️⚠️ **Make sure to register `.AddRequestsInMemoryCache()` as the last `IPipelineBehavior`. Cached responses will prevent further pipeline execution**

⚠️ **Important: Make sure `.AddRequestsInMemoryCache()` is registered as the last `IPipelineBehavior`. Cached responses will short-circuit the pipeline and prevent further execution**

To customize `MemoryCache` options and specify assemblies to scan:

Expand All @@ -35,40 +37,104 @@ builder.Services.AddRequestsInMemoryCache(
new []{typeof(GetQuery).Assembly});`
```

> For `ICollection` types, the cache entry size is `response.Count`; for all other types, it is 1

## Usage

Decorate your request classes with caching attributes:
Decorate your request classes with attribute `[CacheResponse]`

Requests decorated with the `[CacheResponse]` attribute will have their responses cached in memory. You can control expiration, entry size, and define which requests should invalidate the cache

### CacheResponseAttribute params

| Name | Description |
|------------------------|-------------------------------------------------------------------------------------------------------|
| `expirationSeconds` | Absolute expiration time in seconds, relative to the current moment. Set null for indefinitely |
| `entrySize` | The size of the cache entry item. For collections, size is calculated per element. Default value is 1 |
| `requestsToClearCache` | Types of requests that will trigger cache invalidation |

Use `IMediator` extension methods to clear cached responses:

Clear cache for specific request data:
```csharp
using MitMediator.InMemoryCache;
mediator.ClearResponseCacheAsync(request, ct);
```

[CacheForever]
Clear all cached responses for a request type
```csharp
mediator.ClearAllResponseCacheAsync<GetBookQuery>(ct);
```

## Example usage

Cache indefinitely:
```csharp
[CacheResponse]
public struct GetGenresQuery : IRequest<Genre[]>;
```

> Default `entrySize` is 1

[CacheForSeconds(10)]
Cache for 10 seconds:
```csharp
[CacheResponse(10)]
public struct GetBookQuery : IRequest<Book>
{
public int BookId { get; set; }
}
```

[CacheUntilSent(typeof(DeleteAuthorCommand), typeof(UpdateAuthorCommand))]
public struct GetAuthorQuery : IRequest<Author>
Invalidate cache on specific requests:
```csharp
[CacheResponse(typeof(DeleteAuthorCommand), typeof(UpdateAuthorCommand))]
public struct GetAuthorsByFilterQuery : IRequest<Author[]>
{
public int? Limit { get; init; }

public int? Offset { get; init; }

public string? FreeText { get; init; }
}
```

Set custom entry size and time (30s)
```csharp
[CacheResponse(30, 4)]
public struct GetBooksByFilterQuery : IRequest<Book[]>
{
public int AuthorId { get; init; }
public int? Limit { get; init; }

public int? Offset { get; init; }

public string? FreeText { get; init; }
}
```
## Available Attributes

- `[CacheForever]` - caches the response indefinitely
- `[CacheForSeconds(int seconds)]` - caches the response for the specified seconds
- `[CacheUntilSent(params Type[] triggers)]` - caches the response until one of the specified request types is sent
> For `ICollection` types, the cache entry size is `response.Count * entrySize`

Clear cache after updating data:
```csharp
public async ValueTask<Book> HandleAsync(UpdateBookTitleCommand command, CancellationToken cancellationToken)
{
var book = await _booksRepository.FirstOrDefaultAsync(b => b.BookId == command.BookId, cancellationToken);

book.SetTitle(command.Title);

await _booksRepository.UpdateAsync(book, cancellationToken);

// Clear cached response for the updated book
await _mediator.ClearResponseCacheAsync(new GetBookQuery() { BookId = command.BookId }, cancellationToken);
return book;
}
```

> Responses are cached per unique request data.
For example, GetAuthorQuery will cache a separate response for each AuthorId
> Responses are cached per unique request data.
> For example, `GetBookQuery` caches a separate response for each `BookId`

## See [samples](./samples)

## License

MIT




Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Books.Application.Abstractions.Infrastructure;
using Books.Application.Exceptions;
using Books.Application.UseCase.Authors.Queries.GetAuthor;
using Books.Domain;
using MitMediator;
using MitMediator.InMemoryCache;

namespace Books.Application.UseCase.Authors.Commands.DeleteAuthor;

Expand All @@ -11,16 +13,18 @@ namespace Books.Application.UseCase.Authors.Commands.DeleteAuthor;
internal sealed class DeleteAuthorCommandHandler : IRequestHandler<DeleteAuthorCommand>
{
private readonly IBaseRepository<Author> _authorRepository;

private readonly IMediator _mediator;

/// <summary>
/// Initializes a new instance of the <see cref="DeleteAuthorCommandHandler"/>.
/// </summary>
/// <param name="authorRepository">Author repository.</param>
public DeleteAuthorCommandHandler(IBaseRepository<Author> authorRepository)
public DeleteAuthorCommandHandler(IBaseRepository<Author> authorRepository, IMediator mediator)
{
_authorRepository = authorRepository;
_mediator = mediator;
}

/// <inheritdoc/>
public async ValueTask<Unit> HandleAsync(DeleteAuthorCommand command, CancellationToken cancellationToken)
{
Expand All @@ -29,7 +33,9 @@ public async ValueTask<Unit> HandleAsync(DeleteAuthorCommand command, Cancellati
{
throw new NotFoundException();
}

await _authorRepository.RemoveAsync(author, cancellationToken);
await _mediator.ClearResponseCacheAsync(new GetAuthorQuery { AuthorId = command.AuthorId }, cancellationToken);
return Unit.Value;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Books.Application.Abstractions.Infrastructure;
using Books.Application.Exceptions;
using Books.Application.UseCase.Authors.Queries.GetAuthor;
using Books.Domain;
using MitMediator;
using MitMediator.InMemoryCache;

namespace Books.Application.UseCase.Authors.Commands.UpdateAuthor;

Expand All @@ -11,28 +13,33 @@ namespace Books.Application.UseCase.Authors.Commands.UpdateAuthor;
internal sealed class UpdateAuthorCommandHandler : IRequestHandler<UpdateAuthorCommand, Author>
{
private readonly IBaseRepository<Author> _authorRepository;
private readonly IMediator _mediator;

/// <summary>
/// Initializes a new instance of the <see cref="UpdateAuthorCommandHandler"/>.
/// </summary>
/// <param name="authorRepository">Author repository.</param>
public UpdateAuthorCommandHandler(IBaseRepository<Author> authorRepository)
public UpdateAuthorCommandHandler(IBaseRepository<Author> authorRepository, IMediator mediator)
{
_authorRepository = authorRepository;
_mediator = mediator;
}

/// <inheritdoc/>
/// <returns>The updated author.</returns>
public async ValueTask<Author> HandleAsync(UpdateAuthorCommand command, CancellationToken cancellationToken)
{
var author = await _authorRepository.FirstOrDefaultAsync(q => q.AuthorId == command.AuthorId, cancellationToken);
var author =
await _authorRepository.FirstOrDefaultAsync(q => q.AuthorId == command.AuthorId, cancellationToken);
if (author is null)
{
throw new NotFoundException();
}

author.UpdateFirstName(command.FirstName);
author.UpdateLastName(command.LastName);
await _authorRepository.UpdateAsync(author, cancellationToken);
await _mediator.ClearResponseCacheAsync(new GetAuthorQuery { AuthorId = command.AuthorId }, cancellationToken);
return author;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using Books.Application.UseCase.Authors.Commands.DeleteAuthor;
using Books.Application.UseCase.Authors.Commands.UpdateAuthor;
using Books.Domain;
using MitMediator;
using MitMediator.InMemoryCache;
Expand All @@ -9,7 +7,7 @@ namespace Books.Application.UseCase.Authors.Queries.GetAuthor;
/// <summary>
/// Get author query.
/// </summary>
[CacheUntilSent(typeof(DeleteAuthorCommand), typeof(UpdateAuthorCommand))]
[CacheResponse]
public struct GetAuthorQuery : IRequest<Author>
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Books.Application.UseCase.Authors.Commands.CreateAuthor;
using Books.Application.UseCase.Authors.Commands.DeleteAuthor;
using Books.Application.UseCase.Authors.Commands.UpdateAuthor;
using Books.Domain;
Expand All @@ -10,7 +9,7 @@ namespace Books.Application.UseCase.Authors.Queries.GetAuthorsByFilter;
/// <summary>
/// Get authors query.
/// </summary>
[CacheUntilSent(typeof(CreateAuthorCommand), typeof(DeleteAuthorCommand), typeof(UpdateAuthorCommand))]
[CacheResponse(typeof(DeleteAuthorCommand), typeof(UpdateAuthorCommand))]
public struct GetAuthorsByFilterQuery : IRequest<Author[]>
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Books.Application.UseCase.Authors.Commands.DeleteAuthor;
using Books.Application.Abstractions.Infrastructure;
using Books.Application.Exceptions;
using Books.Application.UseCase.Books.Queries.GetBook;
using Books.Domain;
using MitMediator;
using MitMediator.InMemoryCache;

namespace Books.Application.UseCase.Books.Commands.DeleteBook;

Expand All @@ -12,14 +14,16 @@ namespace Books.Application.UseCase.Books.Commands.DeleteBook;
internal sealed class DeleteBookCommandHandler : IRequestHandler<DeleteBookCommand>
{
private readonly IBaseRepository<Book> _booksRepository;

private readonly IMediator _mediator;

/// <summary>
/// Initializes a new instance of the <see cref="DeleteBookCommandHandler"/>.
/// </summary>
/// <param name="booksRepository">Books repository.</param>
public DeleteBookCommandHandler(IBaseRepository<Book> booksRepository)
public DeleteBookCommandHandler(IBaseRepository<Book> booksRepository, IMediator mediator)
{
_booksRepository = booksRepository;
_mediator = mediator;
}

/// <inheritdoc/>
Expand All @@ -31,6 +35,7 @@ public async ValueTask<Unit> HandleAsync(DeleteBookCommand command, Cancellation
throw new NotFoundException();
}
await _booksRepository.RemoveAsync(book, cancellationToken);
await _mediator.ClearResponseCacheAsync(new GetBookQuery() { BookId = command.BookId }, cancellationToken);
return Unit.Value;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Books.Application.Abstractions.Infrastructure;
using Books.Application.Exceptions;
using Books.Application.UseCase.Books.Commands.CreateBook;
using Books.Application.UseCase.Books.Queries.GetBook;
using Books.Domain;
using MitMediator;
using MitMediator.InMemoryCache;

namespace Books.Application.UseCase.Books.Commands.UpdateBook;

Expand All @@ -16,6 +18,8 @@ public class UpdateBookCommandHandler : IRequestHandler<UpdateBookCommand, Book>
private readonly IBaseRepository<Author> _authorsRepository;

private readonly IBaseRepository<Genre> _genresRepository;

private readonly IMediator _mediator;

/// <summary>
/// Initializes a new instance of the <see cref="CreateBookCommandHandler"/>.
Expand All @@ -26,38 +30,42 @@ public class UpdateBookCommandHandler : IRequestHandler<UpdateBookCommand, Book>
public UpdateBookCommandHandler(
IBaseRepository<Book> booksRepository,
IBaseRepository<Author> authorsRepository,
IBaseRepository<Genre> genresRepository)
IBaseRepository<Genre> genresRepository,
IMediator mediator)
{
_booksRepository = booksRepository;
_authorsRepository = authorsRepository;
_genresRepository = genresRepository;
_mediator = mediator;
}

/// <inheritdoc/>
public async ValueTask<Book> HandleAsync(UpdateBookCommand request, CancellationToken cancellationToken)
public async ValueTask<Book> HandleAsync(UpdateBookCommand command, CancellationToken cancellationToken)
{
var book = await _booksRepository.FirstOrDefaultAsync(b => b.BookId == request.BookId, cancellationToken);
var book = await _booksRepository.FirstOrDefaultAsync(b => b.BookId == command.BookId, cancellationToken);
if (book is null)
{
throw new NotFoundException();
}

var author = await _authorsRepository.FirstOrDefaultAsync(a => a.AuthorId == request.AuthorId, cancellationToken);
var author = await _authorsRepository.FirstOrDefaultAsync(a => a.AuthorId == command.AuthorId, cancellationToken);
if (author is null)
{
throw new BadOperationException("Author not found");
}
var genre = await _genresRepository.FirstOrDefaultAsync(g => g.GenreName == request.GenreName.Trim().ToUpperInvariant(), cancellationToken);
var genre = await _genresRepository.FirstOrDefaultAsync(g => g.GenreName == command.GenreName.Trim().ToUpperInvariant(), cancellationToken);
if (genre is null)
{
throw new BadOperationException("Genre not found");
}

book.SetTitle(request.Title);
book.SetTitle(command.Title);
book.SetAuthor(author);
book.SetGenre(genre);

await _booksRepository.UpdateAsync(book, cancellationToken);
await _mediator.ClearResponseCacheAsync(new GetBookQuery() { BookId = command.BookId }, cancellationToken);
await _mediator.ClearAllResponseCacheAsync<GetBookQuery>(cancellationToken);
return book;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Books.Application.UseCase.Books.Queries.GetBook;
/// <summary>
/// Get book query.
/// </summary>
[CacheForSeconds(10)]
[CacheResponse]
public struct GetBookQuery : IRequest<Book>
{
/// <summary>
Expand Down
Loading
Loading