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).
Define a query object:
public class PostById : AsyncQuery<HttpClient, Post>
{
public override async Task<Post> ExecuteAsync(HttpClient context)
{
var response = await context.GetAsync($"https://jsonplaceholder.typicode.com/posts/{Id}");
return await response.Content.ReadAsAsync<Post>();
}
public int Id { get; set; }
}
Invoke it:
var post = await _invoker.QueryAsync(new PostById { Id = 1 });
Mock the result in a unit test:
// Moq
invokerMock.Setup(x => x.QueryAsync(new PostById { Id = 1 })).ReturnsAsync(new Post());
// NSubstitute
invokerMock.QueryAsync(new PostById { Id = 1 }).Returns(new Post());
Leverage built-in caching by deriving from a base class:
public class CommentsByPostId : AsyncCachedQuery<HttpClient, MemoryCacheEntryOptions, Comment[]>
{
protected override void ConfigureCache(ICacheConfig cacheConfig) => cacheConfig.VaryBy = PostId;
protected override MemoryCacheEntryOptions GetCacheEntryOptions(HttpClient context) =>
new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(30));
protected override async Task<Comment[]> QueryAsync(HttpClient context)
{
var response = await context.GetAsync($"https://jsonplaceholder.typicode.com/posts/{PostId}/comments");
return await response.Content.ReadAsAsync<Comment[]>();
}
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)
{
var response = await context.GetAsync("https://jsonplaceholder.typicode.com/users");
return await response.Content.ReadAsAsync<User[]>();
}
protected override Task<User> TransformCachedResultAsync(User[] cachedResult) =>
Task.FromResult(cachedResult.Single(x => x.Id == Id));
public int Id { get; set; }
}
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 T Decorate<T>(object operation, Func<T> invoke)
{
try
{
var stopwatch = Stopwatch.StartNew();
var result = invoke();
var elapsed = stopwatch.Elapsed.TotalMilliseconds;
_telemetryClient.TrackMetric(operation.GetType().FullName, elapsed);
return result;
}
catch (Exception e)
{
_telemetryClient.TrackException(e);
throw;
}
}