A library for implementing the Command Pattern, providing of a set of base classes and an invoker class. Useful for abstracting data access and API calls as either queries (for read operations) or commands (for write operations).
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
samples
src
test/Magneto.Tests
.editorconfig
.gitattributes
.gitignore
CODEOFCONDUCT.md
CONTRIBUTING.md
Directory.Build.props
LICENSE
Magneto.png
Magneto.sln
Magneto.sln.DotSettings
NuGet.config
README.md

README.md

Build status Join the chat at https://gitter.im/magneto-project License

NuGet NuGet

Magneto

A library for implementing the Command Pattern, providing of a set of base classes for operations and a mediator/invoker/dispatcher class for invoking them. Useful for abstracting data access and API calls as either queries (for read operations) or commands (for write operations). Query and Command classes declare the type of context they require for execution, and the type of the result, and optionally the type of cache options they require for caching the result. Parameters are modelled as properties on the Query and Command classes.

Define a query object:

public class PostById : AsyncQuery<HttpClient, Post>
{
    // The context here is an HttpClient, but it could be an IDbConnection or anything you want
    public override async Task<Post> ExecuteAsync(HttpClient context, CancellationToken cancellationToken = default)
    {
        var response = await context.GetAsync($"https://jsonplaceholder.typicode.com/posts/{Id}", cancellationToken);
        return await response.Content.ReadAsAsync<Post>(cancellationToken);
    }
    
    // We could also add a constructor and make this a readonly property
    public int Id { get; set; }
}

Invoke it:

// Allow IMagneto to fetch the appropriate context from the ServiceProvider
var post = await _magneto.QueryAsync(new PostById { Id = 1 });
// Or pass the context yourself by using IMediary
var post = await _mediary.QueryAsync(new PostById { Id = 1 }, _httpClient);

When using the provided base classes for queries and commands, they behave like value objects which allows easier mocking in unit tests (just make sure to model all the parameters as public properties):

// Moq
magnetoMock.Setup(x => x.QueryAsync(new PostById { Id = 1 })).ReturnsAsync(new Post());
// NSubstitute
magneto.QueryAsync(new PostById { Id = 1 }).Returns(new Post());

Leverage built-in caching by deriving from a base class and specifying the type of cache options:

public class CommentsByPostId : AsyncCachedQuery<HttpClient, MemoryCacheEntryOptions, Comment[]>
{
    // Here we get to specify which parameters comprise the cache key
    protected override void ConfigureCache(ICacheConfig cacheConfig) => cacheConfig.VaryBy = PostId;
    
    // Here we get to specify the caching policy (absolute/sliding)
    protected override MemoryCacheEntryOptions GetCacheEntryOptions(HttpClient context) =>
        new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(30));

    protected override async Task<Comment[]> QueryAsync(HttpClient context, CancellationToken cancellationToken = default)
    {
        var response = await context.GetAsync($"https://jsonplaceholder.typicode.com/posts/{PostId}/comments", cancellationToken);
        return await response.Content.ReadAsAsync<Comment[]>(cancellationToken);
    }
    
    public int PostId { get; set; }
}

Cache intermediate results and transform to a final result:

public class UserById : AsyncTransformedCachedQuery<HttpClient, DistributedCacheEntryOptions, User[], User>
{
    protected override DistributedCacheEntryOptions GetCacheEntryOptions(HttpClient context) =>
        new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(30));
    
    protected override async Task<User[]> QueryAsync(HttpClient context, CancellationToken cancellationToken = default)
    {
        var response = await context.GetAsync("https://jsonplaceholder.typicode.com/users", cancellationToken);
        return await response.Content.ReadAsAsync<User[]>(cancellationToken);
    }
    
    protected override Task<User> TransformCachedResultAsync(User[] cachedResult, CancellationToken cancellationToken = default) =>
        Task.FromResult(cachedResult.Single(x => x.Id == Id));
    
    public int Id { get; set; }
}

Easily skip reading from the cache if required, by passing the optional argument CacheOption.Refresh (the fresh result will be written to the cache):

var postComments = await _magneto.QueryAsync(new CommentsByPostId { PostId = id }, CacheOption.Refresh);

Easily evict a previously cached result for a query:

var commentsByPostById = new CommentsByPostId { PostId = 1 };
var comments = await _magneto.QueryAsync(commentsByPostById);
...
await _magneto.EvictCachedResultAsync(commentsByPostById);

When using a distributed cache store, changes to a previously cached result for a query can be updated:

var commentsByPostById = new CommentsByPostId { PostId = 1 };
var comments = await _magneto.QueryAsync(commentsByPostById);
...
comments[0].Votes++;
await _magneto.UpdateCachedResultAsync(commentsByPostById);

Register a decorator to apply cross-cutting concerns:

// In application startup
...
services.AddSingleton<IDecorator, ApplicationInsightsDecorator>();
...

// Decorator implementation
public class ApplicationInsightsDecorator : IDecorator
{
    readonly TelemetryClient _telemetryClient;

    public ApplicationInsightsDecorator(TelemetryClient telemetryClient) => _telemetryClient = telemetryClient;

    public TResult Decorate<TResult>(string operationName, Func<TResult> invoke)
    {
        try
        {
            var stopwatch = Stopwatch.StartNew();
            var result = invoke();
            var elapsed = stopwatch.Elapsed.TotalMilliseconds;
            _telemetryClient.TrackMetric(operationName, elapsed);
            return result;
        }
        catch (Exception e)
        {
            _telemetryClient.TrackException(e);
            throw;
        }
    }
    ...
}

See the bundled sample application for further examples of usage.