From a7b89c70a115548c0a660c38cab7a7642529aabf Mon Sep 17 00:00:00 2001 From: Matthew Adams Date: Thu, 29 Sep 2022 11:15:39 +0100 Subject: [PATCH 1/9] Initial parameter cache optimization. --- .../UriTemplateParameterExtraction.cs | 11 +++++++++++ .../JsonElementExtensions.cs | 9 +++------ .../Corvus.UriTemplates/ParameterCache.cs | 19 +++++++------------ .../TavisApi/UriExtensions.cs | 3 +-- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterExtraction.cs b/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterExtraction.cs index b6e803f..0ea06ad 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterExtraction.cs +++ b/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterExtraction.cs @@ -66,6 +66,17 @@ public void ExtractParametersCorvusTavis() this.corvusTavisTemplate!.GetParameters(TavisUri); } + /// + /// Extract parameters from a URI template using the Corvus implementation of the Tavis API. + /// + [Benchmark] + public void ExtractParametersCorvusTavisWithParameterCache() + { + var cache = ParameterCache.Rent(5); + this.corvusTemplate!.ParseUri(Uri, ParameterCache.HandleParameters, ref cache); + cache.Return(); + } + /// /// Extract parameters from a URI template using Corvus types. /// diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonElementExtensions.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonElementExtensions.cs index d7e5e3a..990adea 100644 --- a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonElementExtensions.cs +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonElementExtensions.cs @@ -136,8 +136,7 @@ private static bool ProcessRawText(ReadOnlySpan rawInput, { if (sourceArray != null) { - sourceUnescaped.Clear(); - ArrayPool.Shared.Return(sourceArray); + ArrayPool.Shared.Return(sourceArray, true); } } } @@ -169,8 +168,7 @@ private static bool ProcessRawText(ReadOnlySpan rawInput, { if (pooledName != null) { - utf8Unescaped.Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } } } @@ -203,8 +201,7 @@ private static bool ProcessDecodedText(ReadOnlySpan decod if (sourceTranscodedArray != null) { - sourceTranscoded.Clear(); - ArrayPool.Shared.Return(sourceTranscodedArray); + ArrayPool.Shared.Return(sourceTranscodedArray, true); } return success; diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs index 304df15..996838b 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs @@ -76,13 +76,10 @@ public void Reset() /// The callback to recieve the enumerated parameters. public void EnumerateParameters(ParameterCacheCallback callback) { - if (this.items is not null) + for (int i = 0; i < this.written; ++i) { - for (int i = 0; i < this.written; ++i) - { - CacheEntry item = this.items[i]; - callback(item.Name.AsSpan(0, item.NameLength), item.Value.AsSpan(0, item.ValueLength)); - } + CacheEntry item = this.items[i]; + callback(item.Name.AsSpan(0, item.NameLength), item.Value.AsSpan(0, item.ValueLength)); } } @@ -103,7 +100,7 @@ public void Add(ReadOnlySpan name, ReadOnlySpan value) ArrayPool.Shared.Resize(ref this.items, this.items.Length + this.bufferIncrement); } - this.items[this.written] = (nameArray, name.Length, valueArray, value.Length); + this.items[this.written] = new(nameArray, name.Length, valueArray, value.Length); this.written++; } @@ -127,21 +124,19 @@ private void ResetItems() CacheEntry item = this.items[i]; if (item.Name.Length > 0) { - item.Name.AsSpan().Clear(); - ArrayPool.Shared.Return(item.Name); + ArrayPool.Shared.Return(item.Name, true); } if (item.Value.Length > 0) { - item.Value.AsSpan().Clear(); - ArrayPool.Shared.Return(item.Value); + ArrayPool.Shared.Return(item.Value, true); } } } private readonly struct CacheEntry { - public CacheEntry(char[] name, int nameLength, char[] value, int valueLength) + public CacheEntry(in char[] name, int nameLength, in char[] value, int valueLength) { this.Name = name; this.NameLength = nameLength; diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/UriExtensions.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/UriExtensions.cs index b977a1f..c30b281 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/UriExtensions.cs +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/TavisApi/UriExtensions.cs @@ -168,8 +168,7 @@ static void ExecuteCallback(QueryStringParameterCallback callback, ReadO { if (pooledArray is not null) { - lowerName.Clear(); - ArrayPool.Shared.Return(pooledArray); + ArrayPool.Shared.Return(pooledArray, true); } } } From f02a1d343a486574374dbe55411953de178ef1f6 Mon Sep 17 00:00:00 2001 From: Matthew Adams Date: Thu, 29 Sep 2022 11:28:14 +0100 Subject: [PATCH 2/9] Reduced the overhead of clearing the sensitive data. --- .../JsonPropertyExtensions.cs | 39 +++++++------------ .../Corvus.UriTemplates/JsonReaderHelper.cs | 9 ++--- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonPropertyExtensions.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonPropertyExtensions.cs index c22434a..689aeca 100644 --- a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonPropertyExtensions.cs +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonPropertyExtensions.cs @@ -206,8 +206,7 @@ private static bool ProcessRawText(ReadOnlySpan rawInput, { if (sourceArray != null) { - sourceUnescaped.Clear(); - ArrayPool.Shared.Return(sourceArray); + ArrayPool.Shared.Return(sourceArray, true); } } } @@ -239,8 +238,7 @@ private static bool ProcessRawText(ReadOnlySpan rawInput, { if (pooledName != null) { - utf8Unescaped.Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } } } @@ -273,8 +271,7 @@ private static bool ProcessDecodedText(ReadOnlySpan decod if (sourceTranscodedArray != null) { - sourceTranscoded.Clear(); - ArrayPool.Shared.Return(sourceTranscodedArray); + ArrayPool.Shared.Return(sourceTranscodedArray, true); } return success; @@ -343,14 +340,12 @@ private static bool ProcessEncodedNameAndEncodedValue(ReadOnlyS { if (pooledValue is not null) { - utf8UnescapedValue.Clear(); - ArrayPool.Shared.Return(pooledValue); + ArrayPool.Shared.Return(pooledValue, true); } if (pooledName is not null) { - utf8UnescapedName.Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } } } @@ -377,8 +372,7 @@ private static bool ProcessDecodedNameAndEncodedValue(ReadOnlyS { if (pooledValue != null) { - utf8UnescapedValue.Clear(); - ArrayPool.Shared.Return(pooledValue); + ArrayPool.Shared.Return(pooledValue, true); } } } @@ -405,8 +399,7 @@ private static bool ProcessEncodedNameAndDecodedValue(ReadOnlyS { if (pooledName != null) { - utf8UnescapedName.Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } } } @@ -467,14 +460,12 @@ private static bool ProcessEncodedNameAndEncodedValue(ReadOnlyS { if (pooledValue is not null) { - utf8UnescapedValue.Clear(); - ArrayPool.Shared.Return(pooledValue); + ArrayPool.Shared.Return(pooledValue, true); } if (pooledName is not null) { - utf8UnescapedName.Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } } } @@ -501,8 +492,7 @@ private static bool ProcessDecodedNameAndEncodedValue(ReadOnlyS { if (pooledValue != null) { - utf8UnescapedValue.Clear(); - ArrayPool.Shared.Return(pooledValue); + ArrayPool.Shared.Return(pooledValue, true); } } } @@ -529,8 +519,7 @@ private static bool ProcessEncodedNameAndDecodedValue(ReadOnlyS { if (pooledName != null) { - utf8UnescapedName.Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } } } @@ -566,14 +555,12 @@ private static bool ProcessDecodedNameAndValue(ReadOnlySpan.Shared.Return(transcodedNameArray); + ArrayPool.Shared.Return(transcodedNameArray, true); } if (transcodedValueArray != null) { - transcodedValue.Clear(); - ArrayPool.Shared.Return(transcodedValueArray); + ArrayPool.Shared.Return(transcodedValueArray, true); } return success; diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonReaderHelper.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonReaderHelper.cs index 331c6b4..48d8994 100644 --- a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonReaderHelper.cs +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonReaderHelper.cs @@ -34,8 +34,7 @@ public static bool CanDecodeBase64(ReadOnlySpan utf8Unescaped) { if (pooledArray != null) { - byteSpan.Clear(); - ArrayPool.Shared.Return(pooledArray); + ArrayPool.Shared.Return(pooledArray, true); } return false; @@ -44,8 +43,7 @@ public static bool CanDecodeBase64(ReadOnlySpan utf8Unescaped) if (pooledArray != null) { - byteSpan.Clear(); - ArrayPool.Shared.Return(pooledArray); + ArrayPool.Shared.Return(pooledArray, true); } return true; @@ -69,8 +67,7 @@ public static ReadOnlySpan GetUnescapedSpan(ReadOnlySpan utf8Source, if (pooledName != null) { - new Span(pooledName, 0, written).Clear(); - ArrayPool.Shared.Return(pooledName); + ArrayPool.Shared.Return(pooledName, true); } return propertyName; From 1c653cbf6a007396586977c47241bb466f8423c5 Mon Sep 17 00:00:00 2001 From: Matthew Adams Date: Thu, 29 Sep 2022 11:44:52 +0100 Subject: [PATCH 3/9] Refactored the parameter cache so it only allocates a single entry per name/value pair. --- .../Corvus.UriTemplates/ParameterCache.cs | 55 +++++++------------ 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs index 996838b..9b59834 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs @@ -79,7 +79,7 @@ public void EnumerateParameters(ParameterCacheCallback callback) for (int i = 0; i < this.written; ++i) { CacheEntry item = this.items[i]; - callback(item.Name.AsSpan(0, item.NameLength), item.Value.AsSpan(0, item.ValueLength)); + callback(item.Name, item.Value); } } @@ -90,17 +90,16 @@ public void EnumerateParameters(ParameterCacheCallback callback) /// The value of the parameter to add. public void Add(ReadOnlySpan name, ReadOnlySpan value) { - char[] nameArray = name.Length > 0 ? ArrayPool.Shared.Rent(name.Length) : Array.Empty(); - name.CopyTo(nameArray); - char[] valueArray = value.Length > 0 ? ArrayPool.Shared.Rent(value.Length) : Array.Empty(); - value.CopyTo(valueArray); + char[] entryArray = name.Length + value.Length > 0 ? ArrayPool.Shared.Rent(name.Length + value.Length) : Array.Empty(); + name.CopyTo(entryArray); + value.CopyTo(entryArray.AsSpan(name.Length)); if (this.written == this.items.Length) { ArrayPool.Shared.Resize(ref this.items, this.items.Length + this.bufferIncrement); } - this.items[this.written] = new(nameArray, name.Length, valueArray, value.Length); + this.items[this.written] = new(entryArray, name.Length, value.Length); this.written++; } @@ -121,45 +120,33 @@ private void ResetItems() { for (int i = 0; i < this.written; ++i) { - CacheEntry item = this.items[i]; - if (item.Name.Length > 0) - { - ArrayPool.Shared.Return(item.Name, true); - } - - if (item.Value.Length > 0) - { - ArrayPool.Shared.Return(item.Value, true); - } + this.items[i].Return(); } } private readonly struct CacheEntry { - public CacheEntry(in char[] name, int nameLength, in char[] value, int valueLength) + private readonly char[] entry; + private readonly int nameLength; + private readonly int valueLength; + + public CacheEntry(in char[] entry, int nameLength, int valueLength) { - this.Name = name; - this.NameLength = nameLength; - this.Value = value; - this.ValueLength = valueLength; + this.entry = entry; + this.nameLength = nameLength; + this.valueLength = valueLength; } - public char[] Name { get; } + public Span Name => this.nameLength > 0 ? this.entry.AsSpan(0, this.nameLength) : Span.Empty; - public int NameLength { get; } + public Span Value => this.valueLength > 0 ? this.entry.AsSpan(this.nameLength) : Span.Empty; - public char[] Value { get; } - - public int ValueLength { get; } - - public static implicit operator (char[] Name, int NameLength, char[] Value, int ValueLength)(CacheEntry value) + public void Return() { - return (value.Name, value.NameLength, value.Value, value.ValueLength); - } - - public static implicit operator CacheEntry((char[] Name, int NameLength, char[] Value, int ValueLength) value) - { - return new CacheEntry(value.Name, value.NameLength, value.Value, value.ValueLength); + if (this.entry.Length > 0) + { + ArrayPool.Shared.Return(this.entry); + } } } } \ No newline at end of file From 9ee66a840ca7e6e30f5c82b32d70d0c146175178 Mon Sep 17 00:00:00 2001 From: Matthew Adams Date: Fri, 7 Oct 2022 10:27:59 +0100 Subject: [PATCH 4/9] Simplified the API for use of ParameterCache. --- .../UriTemplateParameterExtraction.cs | 35 +++++++-- .../Corvus.UriTemplates/ParameterCache.cs | 73 +++++++++++-------- .../UriTemplateParserExtensions.cs | 36 +++++++++ 3 files changed, 105 insertions(+), 39 deletions(-) create mode 100644 Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateParserExtensions.cs diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterExtraction.cs b/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterExtraction.cs index 0ea06ad..69b98ff 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterExtraction.cs +++ b/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterExtraction.cs @@ -63,7 +63,7 @@ public void ExtractParametersTavis() [Benchmark] public void ExtractParametersCorvusTavis() { - this.corvusTavisTemplate!.GetParameters(TavisUri); + IDictionary? result = this.corvusTavisTemplate!.GetParameters(TavisUri); } /// @@ -72,9 +72,21 @@ public void ExtractParametersCorvusTavis() [Benchmark] public void ExtractParametersCorvusTavisWithParameterCache() { - var cache = ParameterCache.Rent(5); - this.corvusTemplate!.ParseUri(Uri, ParameterCache.HandleParameters, ref cache); - cache.Return(); + int state = 0; + + if (this.corvusTemplate!.EnumerateParameters(Uri, HandleParameters, ref state)) + { + // We can use the state + } + else + { + // We can't use the state + } + + static void HandleParameters(ReadOnlySpan name, ReadOnlySpan value, ref int state) + { + state++; + } } /// @@ -83,12 +95,19 @@ public void ExtractParametersCorvusTavisWithParameterCache() [Benchmark] public void ExtractParametersCorvus() { - object? state = default; - this.corvusTemplate!.ParseUri(Uri, HandleParameters, ref state); + int state = 0; + this.corvusTemplate!.ParseUri(Uri, HandleParameterMatching, ref state); - static void HandleParameters(bool reset, ReadOnlySpan name, ReadOnlySpan value, ref object? state) + static void HandleParameterMatching(bool reset, ReadOnlySpan name, ReadOnlySpan value, ref int state) { - // NOP + if (reset) + { + state = 0; + } + else + { + state++; + } } } } \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs index 9b59834..56d9cdf 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs @@ -10,7 +10,7 @@ namespace Corvus.UriTemplates; /// /// A cache for parameters extracted from a URI template. /// -public struct ParameterCache +internal struct ParameterCache { private readonly int bufferIncrement; private CacheEntry[] items; @@ -25,11 +25,34 @@ private ParameterCache(int initialCapacity) } /// - /// A callback for enumerating parameters from the cache. + /// Enumerate the parameters in the parser. /// - /// The name of the parameter. - /// The value of the parameter. - public delegate void ParameterCacheCallback(ReadOnlySpan name, ReadOnlySpan value); + /// The type of the state for the callback. + /// The parser to use. + /// The uri to parse. + /// The initial cache size, which should be greater than or equal to the expected number of parameters. + /// It also provides the increment for the cache size should it be exceeded. + /// The callback to recieve the enumerated parameters. + /// The state for the callback. + /// if the parser was successful, otherwise . + public static bool EnumerateParameters(IUriTemplateParser parser, ReadOnlySpan uri, int initialCapacity, EnumerateParametersCallback callback, ref TState state) + { + ParameterCache cache = Rent(initialCapacity); + if (parser.ParseUri(uri, HandleParameters, ref cache)) + { + for (int i = 0; i < cache.written; ++i) + { + CacheEntry item = cache.items[i]; + callback(item.Name, item.Value, ref state); + } + + cache.Return(); + return true; + } + + cache.Return(); + return false; + } /// /// Rent an instance of a parameter cache. @@ -37,7 +60,7 @@ private ParameterCache(int initialCapacity) /// The initial capacity of the cache. /// An instance of a parameter cache. /// When you have finished with the cache, call to relinquish any internal resources. - public static ParameterCache Rent(int initialCapacity) + private static ParameterCache Rent(int initialCapacity) { return new(initialCapacity); } @@ -49,7 +72,8 @@ public static ParameterCache Rent(int initialCapacity) /// The name of the parameter. /// The value of the parameter. /// The parameter cache. - public static void HandleParameters(bool reset, ReadOnlySpan name, ReadOnlySpan value, ref ParameterCache state) + /// Pass this to , as the callback. + private static void HandleParameters(bool reset, ReadOnlySpan name, ReadOnlySpan value, ref ParameterCache state) { if (!reset) { @@ -61,34 +85,12 @@ public static void HandleParameters(bool reset, ReadOnlySpan name, ReadOnl } } - /// - /// Reset the items written. - /// - public void Reset() - { - this.ResetItems(); - this.written = 0; - } - - /// - /// Enumerate the parameters in the cache. - /// - /// The callback to recieve the enumerated parameters. - public void EnumerateParameters(ParameterCacheCallback callback) - { - for (int i = 0; i < this.written; ++i) - { - CacheEntry item = this.items[i]; - callback(item.Name, item.Value); - } - } - /// /// Add a parameter to the cache. /// /// The name of the parameter to add. /// The value of the parameter to add. - public void Add(ReadOnlySpan name, ReadOnlySpan value) + private void Add(ReadOnlySpan name, ReadOnlySpan value) { char[] entryArray = name.Length + value.Length > 0 ? ArrayPool.Shared.Rent(name.Length + value.Length) : Array.Empty(); name.CopyTo(entryArray); @@ -103,10 +105,19 @@ public void Add(ReadOnlySpan name, ReadOnlySpan value) this.written++; } + /// + /// Reset the items written. + /// + private void Reset() + { + this.ResetItems(); + this.written = 0; + } + /// /// Return the resources used by the cache. /// - public void Return() + private void Return() { if (!this.returned) { diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateParserExtensions.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateParserExtensions.cs new file mode 100644 index 0000000..b66e35a --- /dev/null +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/UriTemplateParserExtensions.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Corvus.UriTemplates; + +/// +/// A callback for enumerating parameters from the cache. +/// +/// The type of the state for the callback. +/// The name of the parameter. +/// The value of the parameter. +/// The state for the callback. +public delegate void EnumerateParametersCallback(ReadOnlySpan name, ReadOnlySpan value, ref TState state); + +/// +/// Extension methods for . +/// +public static class UriTemplateParserExtensions +{ + /// + /// Enumerate the parameters in the parser. + /// + /// The type of the state for the callback. + /// The parser to use. + /// The uri to parse. + /// The callback to recieve the enumerated parameters. + /// The state for the callback. + /// The initial cache size, which should be greater than or equal to the expected number of parameters. + /// It also provides the increment for the cache size should it be exceeded. + /// if the parser was successful, otherwise . + public static bool EnumerateParameters(this IUriTemplateParser parser, ReadOnlySpan uri, EnumerateParametersCallback callback, ref TState state, int initialCapacity = 10) + { + return ParameterCache.EnumerateParameters(parser, uri, initialCapacity, callback, ref state); + } +} \ No newline at end of file From 1c749ae61d447a8c93cdc63e9b90c08ac4f56c66 Mon Sep 17 00:00:00 2001 From: Matthew Adams Date: Fri, 7 Oct 2022 10:43:21 +0100 Subject: [PATCH 5/9] Fixed bug: value length not being respected correctly. --- .../Corvus.UriTemplate.Benchmarking.csproj | 1 + .../UriTemplateParameterSetting.cs | 25 ++++++++++++---- .../packages.lock.json | 30 +++++++++++++++++++ .../UsageTests.cs | 27 +++++------------ .../Corvus.UriTemplates/ParameterCache.cs | 2 +- 5 files changed, 60 insertions(+), 25 deletions(-) diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/Corvus.UriTemplate.Benchmarking.csproj b/Solutions/Corvus.UriTemplate.Benchmarking/Corvus.UriTemplate.Benchmarking.csproj index 5f26a56..56f6935 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/Corvus.UriTemplate.Benchmarking.csproj +++ b/Solutions/Corvus.UriTemplate.Benchmarking/Corvus.UriTemplate.Benchmarking.csproj @@ -12,6 +12,7 @@ + diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterSetting.cs b/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterSetting.cs index 56ed73a..fae0bdd 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterSetting.cs +++ b/Solutions/Corvus.UriTemplate.Benchmarking/UriTemplateParameterSetting.cs @@ -14,9 +14,10 @@ namespace Corvus.UriTemplates.Benchmarking; public class UriTemplateParameterSetting { private const string UriTemplate = "http://example.org/location{?value*}"; - private static readonly Dictionary Values = new() { { "foo", "bar" }, { "bar", "baz" }, { "baz", "bob" } }; + private static readonly Dictionary Value = new() { { "foo", "bar" }, { "bar", "baz" }, { "baz", "bob" } }; + private static readonly Dictionary Parameters = new() { { "value", Value } }; - private readonly JsonDocument jsonValues = JsonDocument.Parse("{ \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }"); + private readonly JsonDocument jsonValues = JsonDocument.Parse("{\"value\": { \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }}"); private Tavis.UriTemplates.UriTemplate? tavisTemplate; private TavisApi.UriTemplate? corvusTavisTemplate; @@ -49,7 +50,7 @@ public Task GlobalCleanup() [Benchmark(Baseline = true)] public void ResolveUriTavis() { - this.tavisTemplate!.SetParameter("value", Values); + this.tavisTemplate!.SetParameter("value", Value); this.tavisTemplate!.Resolve(); } @@ -59,7 +60,7 @@ public void ResolveUriTavis() [Benchmark] public void ResolveUriCorvusTavis() { - this.corvusTavisTemplate!.SetParameter("value", Values); + this.corvusTavisTemplate!.SetParameter("value", Value); this.corvusTavisTemplate!.Resolve(); } @@ -67,7 +68,7 @@ public void ResolveUriCorvusTavis() /// Resolve a URI from a template and parameter values using Corvus.UriTemplateResolver. /// [Benchmark] - public void ResolveUriCorvus() + public void ResolveUriCorvusJson() { object? nullState = default; JsonUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, this.jsonValues.RootElement, HandleResult, ref nullState); @@ -76,4 +77,18 @@ static void HandleResult(ReadOnlySpan resolvedTemplate, ref object? state) // NOP } } + + /// + /// Resolve a URI from a template and parameter values using Corvus.UriTemplateResolver. + /// + [Benchmark] + public void ResolveUriCorvusDictionary() + { + object? nullState = default; + DictionaryUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, Parameters, HandleResult, ref nullState); + static void HandleResult(ReadOnlySpan resolvedTemplate, ref object? state) + { + // NOP + } + } } \ No newline at end of file diff --git a/Solutions/Corvus.UriTemplate.Benchmarking/packages.lock.json b/Solutions/Corvus.UriTemplate.Benchmarking/packages.lock.json index 2b2a00e..30db83a 100644 --- a/Solutions/Corvus.UriTemplate.Benchmarking/packages.lock.json +++ b/Solutions/Corvus.UriTemplate.Benchmarking/packages.lock.json @@ -42,6 +42,21 @@ "Microsoft.SourceLink.GitHub": "1.1.1" } }, + "Roslynator.Analyzers": { + "type": "Direct", + "requested": "[4.1.1, )", + "resolved": "4.1.1", + "contentHash": "3cPVlrB1PytlO1ztZZBOExDKQWpMZgI15ZDa0BqLu0l6xv+xIRfEpqjNRcpvUy3aLxWTkPgSKZbbaO+VoFEJ1g==" + }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.435, )", + "resolved": "1.2.0-beta.435", + "contentHash": "TADk7vdGXtfTnYCV7GyleaaRTQjfoSfZXprQrVMm7cSJtJbFc1QIbWPyLvrgrfGdfHbGmUPvaN4ODKNxg2jgPQ==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.435" + } + }, "Tavis.UriTemplates": { "type": "Direct", "requested": "[1.1.1, )", @@ -345,6 +360,11 @@ "Microsoft.NETCore.Targets": "1.0.1" } }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.435", + "contentHash": "ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg==" + }, "System.AppContext": { "type": "Transitive", "resolved": "4.1.0", @@ -1107,6 +1127,16 @@ "System.Collections.Immutable": "[7.0.0-rc.1.22426.10, )" } }, + "corvus.uritemplates.resolvers.dictionaryofobject": { + "type": "Project", + "dependencies": { + "CommunityToolkit.HighPerformance": "[8.0.0, )", + "Corvus.UriTemplates": "[1.0.0, )", + "Microsoft.Extensions.ObjectPool": "[7.0.0-rc.1.22427.2, )", + "System.Buffers": "[4.5.1, )", + "System.Collections.Immutable": "[7.0.0-rc.1.22426.10, )" + } + }, "corvus.uritemplates.resolvers.json": { "type": "Project", "dependencies": { diff --git a/Solutions/Corvus.UriTemplate.TavisApi.Tests/UsageTests.cs b/Solutions/Corvus.UriTemplate.TavisApi.Tests/UsageTests.cs index fcc0484..437eb6c 100644 --- a/Solutions/Corvus.UriTemplate.TavisApi.Tests/UsageTests.cs +++ b/Solutions/Corvus.UriTemplate.TavisApi.Tests/UsageTests.cs @@ -11,20 +11,15 @@ public class UsageTests [Fact] public void ShouldRetrieveParameters() { - var state = ParameterCache.Rent(5); IUriTemplateParser corvusTemplate = UriTemplateParserFactory.CreateParser("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}"); - corvusTemplate!.ParseUri("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=123232323&hash=23ADE34FAE&callback=http%3A%2F%2Fexample.com%2Fcallback", ParameterCache.HandleParameters, ref state); + int state = 0; - int count = 0; + corvusTemplate!.EnumerateParameters("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=123232323&hash=23ADE34FAE&callback=http%3A%2F%2Fexample.com%2Fcallback", Callback, ref state); - state.EnumerateParameters(Callback); + Assert.Equal(3, state); - Assert.Equal(3, count); - - state.Return(); - - void Callback(ReadOnlySpan name, ReadOnlySpan value) + void Callback(ReadOnlySpan name, ReadOnlySpan value, ref int count) { if (name.SequenceEqual("parentRequestId")) { @@ -51,20 +46,14 @@ void Callback(ReadOnlySpan name, ReadOnlySpan value) [Fact] public void ShouldRetrieveParametersWhenReallocationIsRequired() { - var state = ParameterCache.Rent(1); IUriTemplateParser corvusTemplate = UriTemplateParserFactory.CreateParser("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}"); - corvusTemplate!.ParseUri("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=123232323&hash=23ADE34FAE&callback=http%3A%2F%2Fexample.com%2Fcallback", ParameterCache.HandleParameters, ref state); - - int count = 0; - - state.EnumerateParameters(Callback); - - Assert.Equal(3, count); + int state = 0; + corvusTemplate!.EnumerateParameters("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=123232323&hash=23ADE34FAE&callback=http%3A%2F%2Fexample.com%2Fcallback", Callback, ref state, 1); - state.Return(); + Assert.Equal(3, state); - void Callback(ReadOnlySpan name, ReadOnlySpan value) + void Callback(ReadOnlySpan name, ReadOnlySpan value, ref int count) { if (name.SequenceEqual("parentRequestId")) { diff --git a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs index 56d9cdf..9d24937 100644 --- a/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs +++ b/Solutions/Corvus.UriTemplates/Corvus.UriTemplates/ParameterCache.cs @@ -150,7 +150,7 @@ public CacheEntry(in char[] entry, int nameLength, int valueLength) public Span Name => this.nameLength > 0 ? this.entry.AsSpan(0, this.nameLength) : Span.Empty; - public Span Value => this.valueLength > 0 ? this.entry.AsSpan(this.nameLength) : Span.Empty; + public Span Value => this.valueLength > 0 ? this.entry.AsSpan(this.nameLength, this.valueLength) : Span.Empty; public void Return() { From 9f4de13635b6b77ed173d7a4a5c8e75eebd7ab80 Mon Sep 17 00:00:00 2001 From: Matthew Adams Date: Fri, 7 Oct 2022 11:36:31 +0100 Subject: [PATCH 6/9] Added a sandbox console app. --- Solutions/Corvus.UriTemplates.sln | 10 ++++++++-- Solutions/Sandbox/Program.cs | 15 +++++++++++++++ Solutions/Sandbox/Sandbox.csproj | 15 +++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 Solutions/Sandbox/Program.cs create mode 100644 Solutions/Sandbox/Sandbox.csproj diff --git a/Solutions/Corvus.UriTemplates.sln b/Solutions/Corvus.UriTemplates.sln index 33f97f6..7eca243 100644 --- a/Solutions/Corvus.UriTemplates.sln +++ b/Solutions/Corvus.UriTemplates.sln @@ -9,9 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.UriTemplates", "Corv EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UriTemplateTests", "Corvus.UriTemplate.TavisApi.Tests\UriTemplateTests.csproj", "{32E0D248-3274-4C77-B1D4-7035E184958B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.UriTemplates.Resolvers.Json", "Corvus.UriTemplates.Resolvers.Json\Corvus.UriTemplates.Resolvers.Json.csproj", "{69C2149D-21D8-48FA-B201-7AFD59EBB5EA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.UriTemplates.Resolvers.Json", "Corvus.UriTemplates.Resolvers.Json\Corvus.UriTemplates.Resolvers.Json.csproj", "{69C2149D-21D8-48FA-B201-7AFD59EBB5EA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Corvus.UriTemplates.Resolvers.DictionaryOfObject", "Corvus.UriTemplates.Resolvers.DictionaryOfObject\Corvus.UriTemplates.Resolvers.DictionaryOfObject.csproj", "{C77AE102-65C4-4C64-8B53-B2476EC80280}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.UriTemplates.Resolvers.DictionaryOfObject", "Corvus.UriTemplates.Resolvers.DictionaryOfObject\Corvus.UriTemplates.Resolvers.DictionaryOfObject.csproj", "{C77AE102-65C4-4C64-8B53-B2476EC80280}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sandbox", "Sandbox\Sandbox.csproj", "{302F8634-2171-43DB-B5DD-5B01BE6026C2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -39,6 +41,10 @@ Global {C77AE102-65C4-4C64-8B53-B2476EC80280}.Debug|Any CPU.Build.0 = Debug|Any CPU {C77AE102-65C4-4C64-8B53-B2476EC80280}.Release|Any CPU.ActiveCfg = Release|Any CPU {C77AE102-65C4-4C64-8B53-B2476EC80280}.Release|Any CPU.Build.0 = Release|Any CPU + {302F8634-2171-43DB-B5DD-5B01BE6026C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {302F8634-2171-43DB-B5DD-5B01BE6026C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {302F8634-2171-43DB-B5DD-5B01BE6026C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {302F8634-2171-43DB-B5DD-5B01BE6026C2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Solutions/Sandbox/Program.cs b/Solutions/Sandbox/Program.cs new file mode 100644 index 0000000..6e4d792 --- /dev/null +++ b/Solutions/Sandbox/Program.cs @@ -0,0 +1,15 @@ +using System.Text.Json; +using Corvus.UriTemplates; + +const string uriTemplate = "http://example.org/location{?value*}"; + +using JsonDocument jsonValues = JsonDocument.Parse("{\"value\": { \"foo\": \"bar\", \"bar\": 3.4, \"baz\": \"bob\" }}"); + +object? nullState = default; + +JsonUriTemplateResolver.TryResolveResult(uriTemplate.AsSpan(), false, jsonValues.RootElement, HandleResult, ref nullState); + +static void HandleResult(ReadOnlySpan resolvedTemplate, ref object? state) +{ + Console.WriteLine(resolvedTemplate.ToString()); +} diff --git a/Solutions/Sandbox/Sandbox.csproj b/Solutions/Sandbox/Sandbox.csproj new file mode 100644 index 0000000..2aac6ca --- /dev/null +++ b/Solutions/Sandbox/Sandbox.csproj @@ -0,0 +1,15 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + From 844c95293beb9e3a00bf18e5d34feead9e933c2e Mon Sep 17 00:00:00 2001 From: Matthew Adams Date: Fri, 7 Oct 2022 12:20:54 +0100 Subject: [PATCH 7/9] Optimized "always false" condition. --- .../Corvus.UriTemplates/JsonTemplateParameterProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonTemplateParameterProvider.cs b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonTemplateParameterProvider.cs index 8a9b470..755777a 100644 --- a/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonTemplateParameterProvider.cs +++ b/Solutions/Corvus.UriTemplates.Resolvers.Json/Corvus.UriTemplates/JsonTemplateParameterProvider.cs @@ -54,7 +54,7 @@ public static VariableProcessingState ProcessVariable(ref VariableSpecification { if (variableSpecification.OperatorInfo.Named && !variableSpecification.Explode) //// exploding will prefix with list name { - AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, value.GetArrayLength() == 0); + AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, false); } AppendArray(output, variableSpecification.OperatorInfo, variableSpecification.Explode, variableSpecification.VarName, value); @@ -68,7 +68,7 @@ public static VariableProcessingState ProcessVariable(ref VariableSpecification if (variableSpecification.OperatorInfo.Named && !variableSpecification.Explode) //// exploding will prefix with list name { - AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, !HasProperties(value)); + AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, false); } AppendObject(output, variableSpecification.OperatorInfo, variableSpecification.Explode, value); @@ -88,7 +88,7 @@ public static VariableProcessingState ProcessVariable(ref VariableSpecification { if (variableSpecification.OperatorInfo.Named) { - AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, IsNullOrUndefined(value)); + AppendName(output, variableSpecification.VarName, variableSpecification.OperatorInfo.IfEmpty, false); } AppendValue(output, value, variableSpecification.PrefixLength, variableSpecification.OperatorInfo.AllowReserved); From 9e37310c8b810180a33ed3f645253dba5736689e Mon Sep 17 00:00:00 2001 From: Matthew Adams Date: Fri, 7 Oct 2022 12:26:47 +0100 Subject: [PATCH 8/9] Added some examples of using Json and Dictionary parameter providers. --- Solutions/Sandbox/Program.cs | 5 ++++- Solutions/Sandbox/Sandbox.csproj | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Solutions/Sandbox/Program.cs b/Solutions/Sandbox/Program.cs index 6e4d792..0461860 100644 --- a/Solutions/Sandbox/Program.cs +++ b/Solutions/Sandbox/Program.cs @@ -3,11 +3,14 @@ const string uriTemplate = "http://example.org/location{?value*}"; -using JsonDocument jsonValues = JsonDocument.Parse("{\"value\": { \"foo\": \"bar\", \"bar\": 3.4, \"baz\": \"bob\" }}"); +using var jsonValues = JsonDocument.Parse("{\"value\": { \"foo\": \"bar\", \"bar\": 3.4, \"baz\": null }}"); +Dictionary value = new() { { "foo", "bar" }, { "bar", "baz" }, { "baz", "bob" } }; +Dictionary parameters = new() { { "value", value } }; object? nullState = default; JsonUriTemplateResolver.TryResolveResult(uriTemplate.AsSpan(), false, jsonValues.RootElement, HandleResult, ref nullState); +DictionaryUriTemplateResolver.TryResolveResult(uriTemplate.AsSpan(), false, parameters, HandleResult, ref nullState); static void HandleResult(ReadOnlySpan resolvedTemplate, ref object? state) { diff --git a/Solutions/Sandbox/Sandbox.csproj b/Solutions/Sandbox/Sandbox.csproj index 2aac6ca..d7d47fb 100644 --- a/Solutions/Sandbox/Sandbox.csproj +++ b/Solutions/Sandbox/Sandbox.csproj @@ -8,6 +8,7 @@ + From f2b5891d8f26f4c6068281184acacf814932876f Mon Sep 17 00:00:00 2001 From: Matthew Adams Date: Fri, 7 Oct 2022 12:45:09 +0100 Subject: [PATCH 9/9] Updated the README from the blog post. --- README.md | 145 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 76 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 0d13c8d..270d801 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,8 @@ As you can see, there is a significant benefit to using the Corvus implementatio | ExtractParametersCorvusTavis | 495.2 ns | NA | 0.50 | 0.1450 | 608 B | 0.55 | | ExtractParametersCorvus | 174.6 ns | NA | 0.18 | - | - | 0.00 | -## Parameter Extraction - -### Using the Tavis API +## Tavis API +### Parameter Extraction ```csharp UriTemplate template = new("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}"); @@ -43,52 +42,7 @@ Uri uri = new ("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=12 IDictionary? result = template.GetParameters(uri); ``` -### Using the low-allocation API directly - -The lowest-level access makes use of a callback, which is fed the parameters as they are found. - -If the `reset` flag is set, you should disregard any parameters that have previously been sent, and start again. (This is typically the case where a partial match fails, and is restarted.) - -In order to manage the cache/reset process for you, we provide a `ParameterCache` type. You can rent an instance, and use it to accumulate the results for you. You can then enumerate the result set, and return the resource that have been rented for you. - -```csharp -var state = ParameterCache.Rent(5); -IUriTemplateParser corvusTemplate = UriTemplateParserFactory.CreateParser("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}"); - -corvusTemplate!.ParseUri("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=123232323&hash=23ADE34FAE&callback=http%3A%2F%2Fexample.com%2Fcallback", ParameterCache.HandleParameters, ref state); - -state.EnumerateParameters(HandleFinalParameterSet); - -state.Return(); - -void HandleFinalParameterSet(ReadOnlySpan name, ReadOnlySpan value) -{ - if (name.SequenceEqual("parentRequestId")) - { - Assert.True(value.SequenceEqual("123232323"), $"parentRequestId was {value}"); - count++; - } - else if (name.SequenceEqual("hash")) - { - Assert.True(value.SequenceEqual("23ADE34FAE"), $"hash was {value}"); - count++; - } - else if (name.SequenceEqual("callback")) - { - Assert.True(value.SequenceEqual("http%3A%2F%2Fexample.com%2Fcallback"), $"callback was {value}"); - count++; - } - else - { - Assert.True(false, $"Unexpected parameter: (name: '{name}', value: '{value}')"); - } -} -``` - - -## URI Resolution - -### Using the Tavis API +### URI Resolution Replacing a path segment parameter, @@ -186,47 +140,100 @@ public void TestExtremeEncoding() } ``` -### Using the low-allocation API directly +Our `Corvus.UriTemplates.TavisApi` implementation is built over an underlying low-allocation API. + +## Low allocation API + +### Extracting parameter values from a URI by matching it to a URI template + +To create an instance of a parser for a URI template, call one of the `CreateParser()` overloads, passing it your URI template. -The low-allocation library provides a generic class `UriTemplateResolver` for URI template resolution. +```csharp +IUriTemplateParser UriTemplateParserFactory.CreateParser(string uriTemplate); +``` -The `TParameterProvider` is a type which implements `ITemplateParameterProvider`, to process a parameter payload according to a variable specification. +or -This allows you to process parameters as efficiently as possible, based on the types you need to support. +```csharp +IUriTemplateParser UriTemplateParserFactory.CreateParser(ReadOnlySpan uriTemplate); +``` -The package `Corvus.UriTemplates.Resolvers.Json` contains a `JsonTemplateResolver` that takes a parameter set based on a `System.Text.Json.JsonElement` of `JsonValueKind.Object`. Its properties become the named parameters. +You would typically have some initialization code that is called once to build your parsers from your templates (either derived statically or from some configuration) ```csharp -using JsonDocument jsonValues = JsonDocument.Parse("{ \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }"); -object? nullState = default; -JsonUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, jsonValues.RootElement, HandleResult, ref nullState); +private const string UriTemplate = "http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}"; -static void HandleResult(ReadOnlySpan resolvedTemplate, ref object? state) +private static readonly IUriTemplateParser CorvusTemplate = CreateParser(); + +private static IUriTemplateParser CreateParser() { - // Do what you want with the resolved template - // (Typically, you use the state you have passed in to pprovide the resolved template - // to the outside world in some form.) + return + UriTemplateParserFactory.CreateParser(UriTemplate) } ``` -There are also overloads of `TryResolveResult` which will write to an `IBufferWriter` instead of providing the `ReadOnlySpan` to a callback. +You can then make use of that parser to extract parameter values from a URI. + +The parser uses a callback model to deliver the parameters to you (to avoid allocations). If you are used to low allocation code, you will probably recognize the pattern. + +You call `EnumerateParmaeters()`, passing the URI you wish to parse (as a `ReadOnlySpan`), a callback, and the initial value of a state object, which will be passed to that callback. + +The callback itself is called by the parser each time a matched parameter is discovered. -Similarly, a resolver that takes parameters from a `Dictionary` can be found in the package `Corvus.UriTemplates.Resolvers.DictionaryOfObject`. +It is given `ReadOnlySpan` instances for the name and value pairs, along with the current version of the state object. This state is passed by `ref`, so you can update its value to keep track of whatever processing you are doing with the parameters you have been passed. + +Here's an example that just counts the parameters it has seen. ```csharp -using JsonDocument jsonValues = JsonDocument.Parse("{ \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }"); +int state = 0; + +CorvusTemplate.EnumerateParameters(Uri, HandleParameters, ref state); + +static void HandleParameters(ReadOnlySpan name, ReadOnlySpan value, ref int state) +{ + state++; +} +``` + +> There is a defaulted optional parameter to this method that lets you specific an initial capacity for the cache; if you know how many parameters you are going to match, you can tune this to minimize the amount of re-allocation required. + +### Resolving a template by substituting parameter values and producing a URI + +The other basic scenario is injecting parameter values into a URI template to produce a URI (or another URI template if we haven't replaced all the parameters in the template). + +The underlying type that does the work is called `UriTemplateResolver`. + +The `TParameterProvider` is an `ITemplateParameterProvider` - an interface implemented by types which convert from a source of parameter values (the `TParameterPayload`), on behalf of the `UriTemplateResolver`. + +We offer two of these providers "out of the box" - the `JsonTemplateParameterProvider` (which adapts to a `JsonElement`) and the `DictionaryTemplateParameterProvider` (which adapts to an `IDictionary` and is used by the underlying Tavis-compatible API). + +To save you having to work directly with the `UriTemplateResolver` plugging in all the necessary generic parameters, most `ITemplateParameterProvider` implements will offer a convenience type, and these are no exception. + +`JsonUriTemplateResolver` and `DictionaryUriTemplateResolver` give you strongly typed `TryResolveResult` and `TryGetParameterNames` methods which you can use in your code. + +Here's an example. + +```csharp +const string uriTemplate = "http://example.org/location{?value*}"; + +using var jsonValues = JsonDocument.Parse("{\"value\": { \"foo\": \"bar\", \"bar\": 3.4, \"baz\": null }}"); +Dictionary value = new() { { "foo", "bar" }, { "bar", "baz" }, { "baz", "bob" } }; +Dictionary parameters = new() { { "value", value } }; + object? nullState = default; -JsonUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, jsonValues.RootElement, HandleResult, ref nullState); + +JsonUriTemplateResolver.TryResolveResult(uriTemplate.AsSpan(), false, jsonValues.RootElement, HandleResult, ref nullState); +DictionaryUriTemplateResolver.TryResolveResult(uriTemplate.AsSpan(), false, parameters, HandleResult, ref nullState); static void HandleResult(ReadOnlySpan resolvedTemplate, ref object? state) { - // Do what you want with the resolved template - // (Typically, you use the state you have passed in to pprovide the resolved template - // to the outside world in some form.) + Console.WriteLine(resolvedTemplate.ToString()); } ``` -You should examine the implementations of those types if you wish to implement your own low-allocation parameter providers. +Notice how we can use the exact same callback that receives the resolved template, for both resolvers - the callback is not dependent on the particular parameter provider. + +> The Dictionary provider is somewhat faster than the JSON provider, largely because it has less work to do to extract parameter names and values. However, the JSON parameter provider offers direct support for all JSON value kinds (including encoding serialized "deeply nested" JSON values). ## Build and test