Add caching to API calls.

This commit is contained in:
Robert Marshall 2020-04-18 13:08:41 +01:00
parent fcdec66861
commit ed8468105d
14 changed files with 228 additions and 56 deletions

View file

@ -1,17 +1,24 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
namespace Website.Data {
public abstract class ApiClient {
private readonly HttpClient _client;
private readonly IMemoryCache _cache;
private readonly CacheDurations _cacheDurations;
protected ApiClient(HttpClient client) {
protected ApiClient(HttpClient client, IMemoryCache cache, CacheDurations cacheDurations) {
_client = client;
_cache = cache;
_cacheDurations = cacheDurations;
}
private IDictionary<string, string> ParseQueryParameters(object query) {
@ -20,28 +27,48 @@ namespace Website.Data {
return props.ToDictionary(info => info.Name, info => info.GetValue(query, null).ToString());
}
private async Task<T> Send<T>(HttpMethod method, string url, object query, HttpContent content = null) {
if (query != null)
url = QueryHelpers.AddQueryString(url, ParseQueryParameters(query));
private async Task<TResult> DoCachedCall<TResult>(HttpMethod method, string callerName, IDictionary<string, string> query, Func<Task<TResult>> call) {
if (method == HttpMethod.Post)
return await call();
using var httpRequest = new HttpRequestMessage(method, url) { Content = content };
var response = await _client.SendAsync(httpRequest);
var baseKey = GetType().Name + ":" + callerName;
var queryString = query != null ? "?" + string.Join('&', query.Select(pair => pair.Key + "=" + pair.Value)) : string.Empty;
var key = baseKey + queryString;
if (!response.IsSuccessStatusCode)
throw new ApiCallException(response);
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(json);
return await _cache.GetOrCreate(key, async entry => {
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_cacheDurations[baseKey]);
return await call();
});
}
protected async Task<T> Post<T>(string url, object value, object query = null) {
private async Task<T> Send<T>(HttpMethod method, string url, object query, string callerName, HttpContent content = null) {
IDictionary<string, string> queryParameters = null;
if (query != null) {
queryParameters = ParseQueryParameters(query);
url = QueryHelpers.AddQueryString(url, queryParameters);
}
return await DoCachedCall(method, callerName, queryParameters, async () => {
using var httpRequest = new HttpRequestMessage(method, url) { Content = content };
var response = await _client.SendAsync(httpRequest);
if (!response.IsSuccessStatusCode)
throw new ApiCallException(response);
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(json);
});
}
protected async Task<T> Post<T>(string url, object value, object query = null, [CallerMemberName] string callerName = "") {
var json = JsonConvert.SerializeObject(value);
using var requestBody = new StringContent(json, Encoding.UTF8, "application/json");
return await Send<T>(HttpMethod.Post, url, query, requestBody);
return await Send<T>(HttpMethod.Post, url, query, callerName, requestBody);
}
protected async Task<T> Get<T>(string url, object query = null) {
return await Send<T>(HttpMethod.Get, url, query);
protected async Task<T> Get<T>(string url, object query = null, [CallerMemberName] string callerName = "") {
return await Send<T>(HttpMethod.Get, url, query, callerName);
}
}

View file

@ -1,10 +1,11 @@
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Website.Models.Auth;
namespace Website.Data {
public class AuthenticationProvider:ApiClient, IAuthenticationProvider {
public AuthenticationProvider(HttpClient client) : base(client) {
public class AuthenticationProvider : ApiClient, IAuthenticationProvider {
public AuthenticationProvider(HttpClient client, IMemoryCache cache, CacheDurations cacheDurations) : base(client, cache, cacheDurations) {
}
public async Task<User> Authenticate(LoginRequest request) {

View file

@ -1,12 +1,13 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Net.Http;
using Microsoft.Extensions.Caching.Memory;
using Website.Models.Blog;
namespace Website.Data
{
public class BlogApi : ApiClient, IBlogApi {
public BlogApi(HttpClient client) : base(client) {
public BlogApi(HttpClient client, IMemoryCache cache, CacheDurations cacheDurations) : base(client, cache, cacheDurations) {
}
public async Task<BlogPost> GetPostByUrlAsync(string url) => await Get<BlogPost>("get/" + url);

View file

@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace Website.Data {
public class CacheDurations : Dictionary<string, Dictionary<string, int>> {
const string DefaultKey = "default";
public new int this[string key] {
get {
var keyParts = key.Split(':', 2);
if (ContainsKey(keyParts[0]))
if (base[keyParts[0]].ContainsKey(keyParts[1]))
return base[keyParts[0]][keyParts[1]];
else if (base[keyParts[0]].ContainsKey(DefaultKey))
return base[keyParts[0]][DefaultKey];
return ContainsKey(DefaultKey) ? base[DefaultKey][DefaultKey] : 0;
}
}
}
}

View file

@ -1,12 +1,13 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Website.Models.Git;
namespace Website.Data {
public class GitApi : ApiClient, IGitApi {
public GitApi(HttpClient client) : base(client) {
public GitApi(HttpClient client, IMemoryCache cache, CacheDurations cacheDurations) : base(client, cache, cacheDurations) {
}
public async Task<IEnumerable<Repository>> GetRepositories(string user) => await Get<IEnumerable<Repository>>("repositories", new { user });

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
@ -32,6 +33,8 @@ namespace Website
services.AddSingleton(Configuration);
services.AddSingleton(Configuration.GetSection("cacheDurations").Get<CacheDurations>());
services.AddHttpClient<IGitApi, GitApi>(client => client.BaseAddress = new Uri(Configuration["gitApiEndpoint"]))
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler {ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true});

View file

@ -8,5 +8,19 @@
},
"blogApiEndpoint": "",
"gitApiEndpoint": "",
"authApiEndpoint": ""
"authApiEndpoint": "",
"cacheDurations": {
"default": {
"default": 30
},
"BlogApi": {
"default": 21600, // 6 hours
"GetPostByUrlAsync": 86400, // 1 day
"GetPostByIdAsync": 86400 // 1 day
},
"GitApi": {
"default": 3600, //1 hour
"GetCommit": 86400 // 1 day
}
}
}

View file

@ -14,5 +14,19 @@
"Url": "http://0.0.0.0:5000"
}
}
},
"cacheDurations": {
"default": {
"default": 30
},
"BlogApi": {
"default": 21600, // 6 hours
"GetPostByUrlAsync": 86400, // 1 day
"GetPostByIdAsync": 86400 // 1 day
},
"GitApi": {
"default": 3600, //1 hour
"GetCommit": 86400 // 1 day
}
}
}