Skip to content

Commit fe0b4e9

Browse files
Merge pull request #206 from ronaldbarendse/feature/cachekey-abstraction
Abstract cache key creation to ICacheKey
2 parents 41e0645 + eee0259 commit fe0b4e9

20 files changed

Lines changed: 761 additions & 163 deletions

samples/ImageSharp.Web.Sample/Startup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public void ConfigureServices(IServiceCollection services)
5454
provider.GetRequiredService<IOptions<ImageSharpMiddlewareOptions>>(),
5555
provider.GetRequiredService<FormatUtilities>());
5656
})
57+
.SetCacheKey<UriRelativeLowerInvariantCacheKey>()
5758
.SetCacheHash<CacheHash>()
5859
.AddProvider<PhysicalFileSystemProvider>()
5960
.AddProcessor<ResizeWebProcessor>()
@@ -129,6 +130,7 @@ private void ConfigureCustomServicesAndCustomOptions(IServiceCollection services
129130
provider.GetRequiredService<IOptions<ImageSharpMiddlewareOptions>>(),
130131
provider.GetRequiredService<FormatUtilities>());
131132
})
133+
.SetCacheKey<UriRelativeLowerInvariantCacheKey>()
132134
.SetCacheHash<CacheHash>()
133135
.ClearProviders()
134136
.AddProvider<PhysicalFileSystemProvider>()
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Buffers;
6+
using System.Runtime.CompilerServices;
7+
using Microsoft.AspNetCore.Http;
8+
9+
namespace SixLabors.ImageSharp.Web.Caching
10+
{
11+
/// <summary>
12+
/// Optimized helper methods for generating cache keys from URI components. Much of this code has been adapted from the MIT licensed .NET runtime.
13+
/// </summary>
14+
internal static class CacheKeyHelper
15+
{
16+
private static readonly SpanAction<char, (bool LowerInvariant, string Host, string PathBase, string Path, string Query)> InitializeAbsoluteUriStringSpanAction = new(InitializeAbsoluteUriString);
17+
18+
public enum CaseHandling
19+
{
20+
None,
21+
LowerInvariant
22+
}
23+
24+
/// <summary>
25+
/// Combines the given URI components into a string that is properly encoded for use in HTTP headers.
26+
/// </summary>
27+
/// <param name="handling">Determines case handling for the result. <paramref name="query"/> is always converted to invariant lowercase.</param>
28+
/// <param name="pathBase">The first portion of the request path associated with application root.</param>
29+
/// <param name="path">The portion of the request path that identifies the requested resource.</param>
30+
/// <param name="query">The query, if any.</param>
31+
/// <returns>The combined URI components, properly encoded for use in HTTP headers.</returns>
32+
public static string BuildRelativeKey(
33+
CaseHandling handling,
34+
PathString pathBase = default,
35+
PathString path = default,
36+
QueryString query = default)
37+
38+
// Take any potential performance hit vs concatination for code reading sanity.
39+
=> BuildAbsoluteKey(handling, default, pathBase, path, query);
40+
41+
/// <summary>
42+
/// Combines the given URI components into a string that is properly encoded for use in HTTP headers.
43+
/// Note that unicode in the HostString will be encoded as punycode.
44+
/// </summary>
45+
/// <param name="handling">Determines case handling for the result. <paramref name="query"/> is always converted to invariant lowercase.</param>
46+
/// <param name="host">The host portion of the uri normally included in the Host header. This may include the port.</param>
47+
/// <param name="pathBase">The first portion of the request path associated with application root.</param>
48+
/// <param name="path">The portion of the request path that identifies the requested resource.</param>
49+
/// <param name="query">The query, if any.</param>
50+
/// <returns>The combined URI components, properly encoded for use in HTTP headers.</returns>
51+
public static string BuildAbsoluteKey(
52+
CaseHandling handling,
53+
HostString host,
54+
PathString pathBase = default,
55+
PathString path = default,
56+
QueryString query = default)
57+
{
58+
string hostText = host.ToString();
59+
string pathBaseText = pathBase.ToString();
60+
string pathText = path.ToString();
61+
string queryText = query.ToString();
62+
63+
// PERF: Calculate string length to allocate correct buffer size for string.Create.
64+
int length =
65+
hostText.Length +
66+
pathBaseText.Length +
67+
pathText.Length +
68+
queryText.Length;
69+
70+
if (string.IsNullOrEmpty(pathText))
71+
{
72+
if (string.IsNullOrEmpty(pathBaseText))
73+
{
74+
pathText = "/";
75+
length++;
76+
}
77+
}
78+
else if (pathBaseText.EndsWith('/'))
79+
{
80+
// If the path string has a trailing slash and the other string has a leading slash, we need
81+
// to trim one of them.
82+
// Just decrement the total length, for now.
83+
length--;
84+
}
85+
86+
return string.Create(length, (handling == CaseHandling.LowerInvariant, hostText, pathBaseText, pathText, queryText), InitializeAbsoluteUriStringSpanAction);
87+
}
88+
89+
/// <summary>
90+
/// Copies the specified <paramref name="text"/> to the specified <paramref name="buffer"/> starting at the specified <paramref name="index"/>.
91+
/// </summary>
92+
/// <param name="buffer">The buffer to copy text to.</param>
93+
/// <param name="index">The buffer start index.</param>
94+
/// <param name="text">The text to copy.</param>
95+
/// <returns>The <see cref="int"/> representing the combined text length.</returns>
96+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
97+
private static int CopyTextToBuffer(Span<char> buffer, int index, ReadOnlySpan<char> text)
98+
{
99+
text.CopyTo(buffer.Slice(index, text.Length));
100+
return index + text.Length;
101+
}
102+
103+
/// <summary>
104+
/// Copies the specified <paramref name="text"/> to the specified <paramref name="buffer"/> starting at the specified <paramref name="index"/>
105+
/// converting each character to lowercase, using the casing rules of the invariant culture.
106+
/// </summary>
107+
/// <param name="buffer">The buffer to copy text to.</param>
108+
/// <param name="index">The buffer start index.</param>
109+
/// <param name="text">The text to copy.</param>
110+
/// <returns>The <see cref="int"/> representing the combined text length.</returns>
111+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
112+
private static int CopyTextToBufferLowerInvariant(Span<char> buffer, int index, ReadOnlySpan<char> text)
113+
=> index + text.ToLowerInvariant(buffer.Slice(index, text.Length));
114+
115+
/// <summary>
116+
/// Initializes the URI <see cref="string"/> for <see cref="BuildAbsoluteKey(CaseHandling, HostString, PathString, PathString, QueryString)"/>.
117+
/// </summary>
118+
/// <param name="buffer">The URI <see cref="string"/>'s <see cref="char"/> buffer.</param>
119+
/// <param name="uriParts">The URI parts.</param>
120+
private static void InitializeAbsoluteUriString(Span<char> buffer, (bool Lower, string Host, string PathBase, string Path, string Query) uriParts)
121+
{
122+
int index = 0;
123+
ReadOnlySpan<char> pathBaseSpan = uriParts.PathBase.AsSpan();
124+
125+
if (uriParts.Path.Length > 0 && pathBaseSpan.Length > 0 && pathBaseSpan[pathBaseSpan.Length - 1] == '/')
126+
{
127+
// If the path string has a trailing slash and the other string has a leading slash, we need
128+
// to trim one of them.
129+
// Trim the last slash from pathBase. The total length was decremented before the call to string.Create.
130+
pathBaseSpan = pathBaseSpan.Slice(0, pathBaseSpan.Length - 1);
131+
}
132+
133+
if (uriParts.Lower)
134+
{
135+
index = CopyTextToBufferLowerInvariant(buffer, index, uriParts.Host.AsSpan());
136+
index = CopyTextToBufferLowerInvariant(buffer, index, pathBaseSpan);
137+
index = CopyTextToBufferLowerInvariant(buffer, index, uriParts.Path.AsSpan());
138+
}
139+
else
140+
{
141+
index = CopyTextToBuffer(buffer, index, uriParts.Host.AsSpan());
142+
index = CopyTextToBuffer(buffer, index, pathBaseSpan);
143+
index = CopyTextToBuffer(buffer, index, uriParts.Path.AsSpan());
144+
}
145+
146+
// Querystring is always copied as lower invariant.
147+
_ = CopyTextToBufferLowerInvariant(buffer, index, uriParts.Query.AsSpan());
148+
}
149+
}
150+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using SixLabors.ImageSharp.Web.Commands;
6+
7+
namespace SixLabors.ImageSharp.Web.Caching
8+
{
9+
/// <summary>
10+
/// Defines a contract that allows the creation of cache keys (used by <see cref="ICacheHash"/> to create hashed file names for storing cached images).
11+
/// </summary>
12+
public interface ICacheKey
13+
{
14+
/// <summary>
15+
/// Creates the cache key based on the specified context and commands.
16+
/// </summary>
17+
/// <param name="context">The HTTP context.</param>
18+
/// <param name="commands">The commands.</param>
19+
/// <returns>
20+
/// The cache key.
21+
/// </returns>
22+
string Create(HttpContext context, CommandCollection commands);
23+
}
24+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System.Text;
5+
using Microsoft.AspNetCore.Http;
6+
using SixLabors.ImageSharp.Web.Commands;
7+
8+
namespace SixLabors.ImageSharp.Web.Caching
9+
{
10+
/// <summary>
11+
/// Maintained for compatibility purposes only this cache key implementation generates the same
12+
/// out as the V1 middleware. If possible, it is recommended to use the <see cref="UriRelativeCacheKey"/>.
13+
/// </summary>
14+
public class LegacyV1CacheKey : ICacheKey
15+
{
16+
/// <inheritdoc/>
17+
public string Create(HttpContext context, CommandCollection commands)
18+
{
19+
var sb = new StringBuilder(context.Request.Host.ToString());
20+
21+
string pathBase = context.Request.PathBase.ToString();
22+
if (!string.IsNullOrWhiteSpace(pathBase))
23+
{
24+
sb.AppendFormat("{0}/", pathBase);
25+
}
26+
27+
string path = context.Request.Path.ToString();
28+
if (!string.IsNullOrWhiteSpace(path))
29+
{
30+
sb.Append(path);
31+
}
32+
33+
sb.Append(QueryString.Create(commands));
34+
35+
return sb.ToString().ToLowerInvariant();
36+
}
37+
}
38+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using SixLabors.ImageSharp.Web.Commands;
6+
7+
namespace SixLabors.ImageSharp.Web.Caching
8+
{
9+
/// <summary>
10+
/// Creates a cache key based on the request host, path and commands.
11+
/// </summary>
12+
public class UriAbsoluteCacheKey : ICacheKey
13+
{
14+
/// <inheritdoc/>
15+
public string Create(HttpContext context, CommandCollection commands)
16+
=> CacheKeyHelper.BuildAbsoluteKey(CacheKeyHelper.CaseHandling.None, context.Request.Host, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using SixLabors.ImageSharp.Web.Commands;
6+
7+
namespace SixLabors.ImageSharp.Web.Caching
8+
{
9+
/// <summary>
10+
/// Creates a case insensitive cache key based on the request host, path and commands.
11+
/// </summary>
12+
public class UriAbsoluteLowerInvariantCacheKey : ICacheKey
13+
{
14+
/// <inheritdoc/>
15+
public string Create(HttpContext context, CommandCollection commands)
16+
=> CacheKeyHelper.BuildAbsoluteKey(CacheKeyHelper.CaseHandling.LowerInvariant, context.Request.Host, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using SixLabors.ImageSharp.Web.Commands;
6+
7+
namespace SixLabors.ImageSharp.Web.Caching
8+
{
9+
/// <summary>
10+
/// Creates a cache key based on the request path and commands.
11+
/// </summary>
12+
public class UriRelativeCacheKey : ICacheKey
13+
{
14+
/// <inheritdoc/>
15+
public string Create(HttpContext context, CommandCollection commands)
16+
=> CacheKeyHelper.BuildRelativeKey(CacheKeyHelper.CaseHandling.None, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using SixLabors.ImageSharp.Web.Commands;
6+
7+
namespace SixLabors.ImageSharp.Web.Caching
8+
{
9+
/// <summary>
10+
/// Creates a case insensitive cache key based on the request path and commands.
11+
/// </summary>
12+
public class UriRelativeLowerInvariantCacheKey : ICacheKey
13+
{
14+
/// <inheritdoc/>
15+
public string Create(HttpContext context, CommandCollection commands)
16+
=> CacheKeyHelper.BuildRelativeKey(CacheKeyHelper.CaseHandling.LowerInvariant, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
17+
}
18+
}

src/ImageSharp.Web/DependencyInjection/ImageSharpBuilderExtensions.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,33 @@ public static IImageSharpBuilder SetCache(this IImageSharpBuilder builder, Func<
9696
return builder;
9797
}
9898

99+
/// <summary>
100+
/// Sets the given <see cref="ICacheKey"/> adding it to the service collection.
101+
/// </summary>
102+
/// <typeparam name="T">The type of class implementing <see cref="ICacheKey"/> to add.</typeparam>
103+
/// <param name="builder">The core builder.</param>
104+
/// <returns>The <see cref="IImageSharpBuilder"/>.</returns>
105+
public static IImageSharpBuilder SetCacheKey<T>(this IImageSharpBuilder builder)
106+
where T : class, ICacheKey
107+
{
108+
var descriptor = new ServiceDescriptor(typeof(ICacheKey), typeof(T), ServiceLifetime.Singleton);
109+
builder.Services.Replace(descriptor);
110+
return builder;
111+
}
112+
113+
/// <summary>
114+
/// Sets the given <see cref="ICacheKey"/> adding it to the service collection.
115+
/// </summary>
116+
/// <param name="builder">The core builder.</param>
117+
/// <param name="implementationFactory">The factory method for returning a <see cref="ICacheKey"/>.</param>
118+
/// <returns>The <see cref="IImageSharpBuilder"/>.</returns>
119+
public static IImageSharpBuilder SetCacheKey(this IImageSharpBuilder builder, Func<IServiceProvider, ICacheKey> implementationFactory)
120+
{
121+
var descriptor = new ServiceDescriptor(typeof(ICacheKey), implementationFactory, ServiceLifetime.Singleton);
122+
builder.Services.Replace(descriptor);
123+
return builder;
124+
}
125+
99126
/// <summary>
100127
/// Sets the given <see cref="ICacheHash"/> adding it to the service collection.
101128
/// </summary>

src/ImageSharp.Web/DependencyInjection/ServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ private static void AddDefaultServices(
6464

6565
builder.SetCache<PhysicalFileSystemCache>();
6666

67+
builder.SetCacheKey<UriRelativeLowerInvariantCacheKey>();
68+
6769
builder.SetCacheHash<CacheHash>();
6870

6971
builder.AddProvider<PhysicalFileSystemProvider>();

0 commit comments

Comments
 (0)