Skip to content

Commit

Permalink
Add ValueTupleServiceProvider and update samples to show usage of Val…
Browse files Browse the repository at this point in the history
…ueTuple contexts and more advanced scenarios around cache eviction/update
  • Loading branch information
shaynevanasperen committed Sep 19, 2019
1 parent 7bf7d18 commit 56afbf6
Show file tree
Hide file tree
Showing 14 changed files with 189 additions and 65 deletions.
37 changes: 30 additions & 7 deletions samples/Samples/Controllers/PostsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Threading.Tasks;
using Magneto;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Samples.Domain;
using Samples.Models;
Expand All @@ -14,10 +15,7 @@ public class PostsController : Controller
{
readonly IMagneto _magneto;

public PostsController(IMagneto magneto)
{
_magneto = magneto;
}
public PostsController(IMagneto magneto) => _magneto = magneto;

[HttpGet("")]
public async Task<IActionResult> Index()
Expand All @@ -33,16 +31,34 @@ public async Task<IActionResult> Index(int id)
var postComments = await _magneto.QueryAsync(new CommentsByPostId { PostId = id });
return View("Post", new PostViewModel { Post = post, Comments = postComments });
}

[HttpPost("{id:int}")]
public async Task<IActionResult> Post(Post model)
{
var postById = new PostById { Id = model.Id };
var post = await _magneto.QueryAsync(postById);
post.Body = model.Body;
await _magneto.CommandAsync(new SavePost { Post = post });
// When using a distributed cache, we should either evict or update the entry, since we know it to be stale now
await _magneto.UpdateCachedResultAsync(postById);
//await _magneto.EvictCachedResultAsync(postById);

return RedirectToAction(nameof(Index), new { post.Id });
}
}

public class AllPosts : AsyncQuery<JsonPlaceHolderHttpClient, Post[]>
{
public override Task<Post[]> ExecuteAsync(JsonPlaceHolderHttpClient context, CancellationToken cancellationToken = default) => context.GetAsync<Post[]>("/posts", cancellationToken);
protected override Task<Post[]> QueryAsync(JsonPlaceHolderHttpClient context, CancellationToken cancellationToken = default) => context.GetAsync<Post[]>("/posts", cancellationToken);
}

public class PostById : AsyncQuery<JsonPlaceHolderHttpClient, Post>
public class PostById : AsyncCachedQuery<JsonPlaceHolderHttpClient, DistributedCacheEntryOptions, Post>
{
public override Task<Post> ExecuteAsync(JsonPlaceHolderHttpClient context, CancellationToken cancellationToken = default) => context.GetAsync<Post>($"/posts/{Id}", cancellationToken);
protected override void ConfigureCache(ICacheConfig cacheConfig) => cacheConfig.VaryBy(Id);

protected override DistributedCacheEntryOptions GetCacheEntryOptions(JsonPlaceHolderHttpClient context) => new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(30));

protected override Task<Post> QueryAsync(JsonPlaceHolderHttpClient context, CancellationToken cancellationToken = default) => context.GetAsync<Post>($"/posts/{Id}", cancellationToken);

public int Id { get; set; }
}
Expand All @@ -57,4 +73,11 @@ public class CommentsByPostId : AsyncCachedQuery<JsonPlaceHolderHttpClient, Memo

public int PostId { get; set; }
}

public class SavePost : AsyncCommand<JsonPlaceHolderHttpClient>
{
public override Task ExecuteAsync(JsonPlaceHolderHttpClient context, CancellationToken cancellationToken = default) => context.PostAsync($"/posts/{Post.Id}", Post, cancellationToken);

public Post Post { get; set; }
}
}
42 changes: 20 additions & 22 deletions samples/Samples/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
Expand All @@ -8,8 +7,8 @@
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Samples.Domain;
using Samples.Models;

Expand Down Expand Up @@ -62,26 +61,26 @@ public class UserById : AsyncTransformedCachedQuery<JsonPlaceHolderHttpClient, D

protected override Task<User[]> QueryAsync(JsonPlaceHolderHttpClient context, CancellationToken cancellationToken = default) => User.AllUsersAsync(context, cancellationToken);

protected override Task<User> TransformCachedResultAsync(User[] cachedResult, CancellationToken cancellationToken = default) => Task.FromResult(cachedResult.Single(x => x.Id == Id));
protected override Task<User> TransformCachedResultAsync(User[] cachedResult, CancellationToken cancellationToken = default) => Task.FromResult(cachedResult.SingleOrDefault(x => x.Id == Id));

public int Id { get; set; }
}

public class AlbumsByUserId : SyncTransformedCachedQuery<IFileProvider, MemoryCacheEntryOptions, Album[], Album[]>
public class AlbumsByUserId : SyncTransformedCachedQuery<(IFileProvider, ILogger<AlbumsByUserId>), MemoryCacheEntryOptions, Album[], Album[]>
{
const string Filename = "albums.json";

protected override MemoryCacheEntryOptions GetCacheEntryOptions(IFileProvider context) =>
new MemoryCacheEntryOptions()
.AddExpirationToken(context.Watch(Filename))
.RegisterPostEvictionCallback((echoKey, value, reason, state) =>
{
Console.WriteLine($"{echoKey} : {value} was evicted due to {reason}");
});
protected override MemoryCacheEntryOptions GetCacheEntryOptions((IFileProvider, ILogger<AlbumsByUserId>) context)
{
var (fileProvider, logger) = context;
return new MemoryCacheEntryOptions()
.AddExpirationToken(fileProvider.Watch(Album.AllAlbumsFilename))
.RegisterPostEvictionCallback((key, value, reason, state) =>
logger.LogInformation($"{key} : {value} was evicted due to {reason}"));
}

protected override Album[] Query(IFileProvider context)
protected override Album[] Query((IFileProvider, ILogger<AlbumsByUserId>) context)
{
using (var streamReader = new StreamReader(context.GetFileInfo(Filename).CreateReadStream()))
var (fileProvider, _) = context;
using (var streamReader = new StreamReader(fileProvider.GetFileInfo(Album.AllAlbumsFilename).CreateReadStream()))
{
var json = streamReader.ReadToEnd();
return JsonConvert.DeserializeObject<Album[]>(json);
Expand All @@ -93,15 +92,14 @@ protected override Album[] Query(IFileProvider context)
public int UserId { get; set; }
}

public class SaveAlbum : SyncCommand<IFileProvider>
public class SaveAlbum : SyncCommand<(IFileProvider, JsonSerializerSettings)>
{
const string Filename = "albums.json";

public override void Execute(IFileProvider context)
public override void Execute((IFileProvider, JsonSerializerSettings) context)
{
lock (context)
var (fileProvider, jsonSerializerSettings) = context;
lock (fileProvider)
{
var fileInfo = context.GetFileInfo(Filename);
var fileInfo = fileProvider.GetFileInfo(Album.AllAlbumsFilename);
Album[] albums;
using (var streamReader = new StreamReader(fileInfo.CreateReadStream()))
{
Expand All @@ -110,7 +108,7 @@ public override void Execute(IFileProvider context)
Album.Id = existingAlbums.Max(x => x.Id) + 1;
albums = existingAlbums.Concat(new[] { Album }).ToArray();
}
File.WriteAllText(fileInfo.PhysicalPath, JsonConvert.SerializeObject(albums, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
File.WriteAllText(fileInfo.PhysicalPath, JsonConvert.SerializeObject(albums, jsonSerializerSettings));
}
}

Expand Down
2 changes: 2 additions & 0 deletions samples/Samples/Domain/Entities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class Album
public int UserId { get; set; }
public int Id { get; set; }
public string Title { get; set; }

public const string AllAlbumsFilename = "albums.json";
}

public class Comment
Expand Down
4 changes: 4 additions & 0 deletions samples/Samples/Domain/JsonPlaceHolderHttpClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -20,5 +21,8 @@ public async Task<T> GetAsync<T>(string requestUri, CancellationToken cancellati
var response = await _httpClient.GetAsync(requestUri, cancellationToken);
return await response.Content.ReadAsAsync<T>(cancellationToken);
}

public Task<HttpResponseMessage> PostAsync<T>(string requestUri, T data, CancellationToken cancellationToken = default) =>
Task.FromResult(new HttpResponseMessage(HttpStatusCode.NoContent));
}
}
5 changes: 1 addition & 4 deletions samples/Samples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ namespace Samples
{
public sealed class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static void Main(string[] args) => CreateWebHostBuilder(args).Build().Run();

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
Expand Down
4 changes: 2 additions & 2 deletions samples/Samples/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:49808/",
"applicationUrl": "http://localhost:47808/",
"sslPort": 0
}
},
Expand All @@ -21,7 +21,7 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:49809"
"applicationUrl": "http://localhost:47809"
}
}
}
35 changes: 18 additions & 17 deletions samples/Samples/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Polly;
using Samples.Domain;
using Samples.Infrastructure;
Expand All @@ -18,26 +20,20 @@ namespace Samples
{
public class Startup
{
public Startup(IHostingEnvironment env)
public Startup(IHostingEnvironment environment, IConfiguration configuration)
{
Environment = env;
Environment = environment;
Configuration = configuration;
InitializeAlbums();
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}

void InitializeAlbums()
{
const string filename = "albums.json";
using (var streamReader = new StreamReader(Environment.ContentRootFileProvider.GetFileInfo(filename).CreateReadStream()))
File.WriteAllText(Path.Combine(Environment.WebRootPath, filename), streamReader.ReadToEnd());
using (var streamReader = new StreamReader(Environment.ContentRootFileProvider.GetFileInfo(Album.AllAlbumsFilename).CreateReadStream()))
File.WriteAllText(Path.Combine(Environment.WebRootPath, Album.AllAlbumsFilename), streamReader.ReadToEnd());
}

public IConfigurationRoot Configuration { get; }
public IConfiguration Configuration { get; }

protected IHostingEnvironment Environment { get; }

Expand All @@ -57,10 +53,15 @@ public void ConfigureServices(IServiceCollection services)
// Here we add a decorator object which performs exception logging and timing telemetry for all our Magneto operations.
services.AddSingleton<IDecorator, ApplicationInsightsDecorator>();

// Here we add the two context objects with which our queries and commands are executed.
// Here we add the three context objects with which our queries and commands are executed. The first two are actually
// used together in a ValueTuple and are resolved as such by a special wrapper around IServiceProvider.
services.AddSingleton(Environment.WebRootFileProvider);
services
.AddHttpClient<JsonPlaceHolderHttpClient>()
services.AddSingleton(new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Formatting = Formatting.Indented
});
services.AddHttpClient<JsonPlaceHolderHttpClient>()
.AddHttpMessageHandler(() => new EnsureSuccessHandler())
.AddTransientHttpErrorPolicy(x => x.WaitAndRetryAsync(new[]
{
Expand All @@ -78,9 +79,9 @@ public void ConfigureServices(IServiceCollection services)
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public static void Configure(IApplicationBuilder app, IHostingEnvironment env)
public void Configure(IApplicationBuilder app)
{
if (env.IsDevelopment())
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
Expand Down
7 changes: 5 additions & 2 deletions samples/Samples/Views/Posts/Post.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
}

<h3>@Model.Post.Title</h3>
<div>@Model.Post.Body</div>
<form asp-action="Post" asp-route-id="@Model.Post.Id">
<textarea name="Body" type="input">@Model.Post.Body</textarea>
<button>Update</button>
</form>
<h4>Comments</h4>
<ul class="list-unstyled">
@foreach (var comment in Model.Comments)
Expand All @@ -15,4 +18,4 @@
<i class="info small">@comment.Name <a href="mailto:@comment.Email">@comment.Email</a></i>
</li>
}
</ul>
</ul>
2 changes: 1 addition & 1 deletion src/Magneto.Microsoft/Magneto.Microsoft.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta-63102-01" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-19367-01" PrivateAssets="All" />
<PackageReference Include="MinVer" Version="1.1.0" PrivateAssets="All" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
</ItemGroup>
Expand Down
22 changes: 19 additions & 3 deletions src/Magneto/Magneto.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
using System;
using System;
using System.Threading;
using System.Threading.Tasks;
#if NETSTANDARD
using Code.Extensions.ValueTuple.ServiceProvider;
#endif
using Magneto.Core;

namespace Magneto
{
#if NETSTANDARD
/// <summary>
/// The main entry point for consumers to execute queries and commands using context retrieved from the given <see cref="IServiceProvider"/>.
/// If using an IoC container, it's highly recommended that this be registered as a scoped service
/// so that the injected <see cref="IServiceProvider"/> is scoped appropriately.
/// If using an IoC container, it's highly recommended that this be registered as a scoped service so that the injected <see cref="IServiceProvider"/>
/// is scoped appropriately. A special wrapper is used for the <see cref="IServiceProvider"/> which can resolve instances of <see cref="ValueTuple"/>
/// having up to 8 items by resolving each item from the wrapped <see cref="IServiceProvider"/>.
/// </summary>
public class Magneto : IMagneto
#else
/// <summary>
/// The main entry point for consumers to execute queries and commands using context retrieved from the given <see cref="IServiceProvider"/>.
/// If using an IoC container, it's highly recommended that this be registered as a scoped service so that the injected <see cref="IServiceProvider"/>
/// is scoped appropriately.
/// </summary>
public class Magneto : IMagneto
#endif
{
/// <summary>
/// Creates a new instance of <see cref="Magneto"/>.
Expand All @@ -22,6 +35,9 @@ public Magneto(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
Mediary = ServiceProvider.GetService<IMediary>() ?? new Mediary(ServiceProvider);
#if NETSTANDARD
ServiceProvider = new ValueTupleServiceProvider(ServiceProvider);
#endif
}

/// <summary>
Expand Down
3 changes: 2 additions & 1 deletion src/Magneto/Magneto.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
<ItemGroup>
<PackageReference Include="Code.Extensions.Generic.QuasiEquals" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Code.Extensions.Object.Flatten" Version="1.0.3" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta-63102-01" PrivateAssets="All" />
<PackageReference Condition="'$(TargetFramework)' == 'netstandard2.0'" Include="Code.Extensions.ValueTuple.ServiceProvider" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-19367-01" PrivateAssets="All" />
<PackageReference Include="MinVer" Version="1.1.0" PrivateAssets="All" />
</ItemGroup>

Expand Down
15 changes: 11 additions & 4 deletions src/Magneto/Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,32 @@ namespace Magneto
{
/// <summary>
/// <para>A base class for synchronous queries.</para>
/// <para>Implementors must override <see cref="Execute"/> in order to define how the query is executed.</para>
/// <para>Implementors must override <see cref="Query"/> in order to define how the query is executed.</para>
/// </summary>
/// <typeparam name="TContext">The type of context with which the query is executed.</typeparam>
/// <typeparam name="TResult">The type of the query result.</typeparam>
public abstract class SyncQuery<TContext, TResult> : Operation, ISyncQuery<TContext, TResult>
{
/// <inheritdoc cref="ISyncQuery{TContext,TResult}.Execute"/>
public abstract TResult Execute(TContext context);
public TResult Execute(TContext context) => Query(context);

/// <inheritdoc cref="ISyncQuery{TContext,TResult}.Execute"/>
protected abstract TResult Query(TContext context);
}

/// <summary>
/// <para>A base class for asynchronous queries.</para>
/// <para>Implementors must override <see cref="ExecuteAsync"/> in order to define how the query is executed.</para>
/// <para>Implementors must override <see cref="QueryAsync"/> in order to define how the query is executed.</para>
/// </summary>
/// <typeparam name="TContext">The type of context with which the query is executed.</typeparam>
/// <typeparam name="TResult">The type of the query result.</typeparam>
public abstract class AsyncQuery<TContext, TResult> : Operation, IAsyncQuery<TContext, TResult>
{
/// <inheritdoc cref="IAsyncQuery{TContext,TResult}.ExecuteAsync"/>
public abstract Task<TResult> ExecuteAsync(TContext context, CancellationToken cancellationToken = default);
public Task<TResult> ExecuteAsync(TContext context, CancellationToken cancellationToken = default) =>
QueryAsync(context, cancellationToken);

/// <inheritdoc cref="IAsyncQuery{TContext,TResult}.ExecuteAsync"/>
protected abstract Task<TResult> QueryAsync(TContext context, CancellationToken cancellationToken = default);
}
}
Loading

0 comments on commit 56afbf6

Please sign in to comment.