From b85bc14fabb7513746ade1c2d7e1c8555ecaa709 Mon Sep 17 00:00:00 2001 From: Corey Floyd Date: Tue, 24 Apr 2018 13:09:27 -0500 Subject: [PATCH 01/11] #258 Adds new utility to provide substrings of a given string as Span given a char delimeter. Language version updated to 7.2 due to need for ref Struct to utilize Span as a field. --- .../Internal/SpanSplitter.cs | 64 +++++++++++ .../JsonApiDotNetCore.csproj | 7 ++ test/UnitTests/Internal/SpanSplitterTests.cs | 107 ++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 src/JsonApiDotNetCore/Internal/SpanSplitter.cs create mode 100644 test/UnitTests/Internal/SpanSplitterTests.cs diff --git a/src/JsonApiDotNetCore/Internal/SpanSplitter.cs b/src/JsonApiDotNetCore/Internal/SpanSplitter.cs new file mode 100644 index 0000000000..268bcab195 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/SpanSplitter.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using JsonApiDotNetCore.Extensions; + +namespace JsonApiDotNetCore.Internal +{ + public readonly ref struct SpanSplitter + { + private readonly ReadOnlySpan _span; + private readonly List _delimeterIndexes; + private readonly List> _substringIndexes; + + public int Count => _substringIndexes.Count(); + public ReadOnlySpan this[int index] => GetSpanForSubstring(index + 1); + + public SpanSplitter(ref string str, char delimeter) + { + _span = str.AsSpan(); + _delimeterIndexes = str.IndexesOf(delimeter).ToList(); + _substringIndexes = new List>(); + BuildSubstringIndexes(); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => throw new NotSupportedException(); + + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => throw new NotSupportedException(); + + [EditorBrowsable(EditorBrowsableState.Never)] + public override string ToString() => throw new NotSupportedException(); + + private ReadOnlySpan GetSpanForSubstring(int substringNumber) + { + if (substringNumber > Count) + { + throw new ArgumentOutOfRangeException($"There are only {Count} substrings given the delimeter and base string provided"); + } + + var indexes = _substringIndexes[substringNumber - 1]; + return _span.Slice(indexes.Item1, indexes.Item2); + } + + private void BuildSubstringIndexes() + { + var start = 0; + var end = 0; + foreach (var index in _delimeterIndexes) + { + end = index; + if (start > end) break; + _substringIndexes.Add(new Tuple(start, end - start)); + start = ++end; + } + + if (end <= _span.Length) + { + _substringIndexes.Add(new Tuple(start, _span.Length - start)); + } + } + } +} diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index c4b9a6d932..f13c10ead7 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -21,6 +21,7 @@ + @@ -31,6 +32,12 @@ bin\Release\netstandard2.0\JsonApiDotNetCore.xml + + 7.2 + + + 7.2 + diff --git a/test/UnitTests/Internal/SpanSplitterTests.cs b/test/UnitTests/Internal/SpanSplitterTests.cs new file mode 100644 index 0000000000..016ee8ec82 --- /dev/null +++ b/test/UnitTests/Internal/SpanSplitterTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using JsonApiDotNetCore.Internal; +using Xunit; + +namespace UnitTests.Internal +{ + public class SpanSplitterTests : SpanSplitterTestsBase + { + [Fact] + public void StringWithDelimeterSplitsIntoCorrectNumberSubstrings() + { + GivenMultipleCommaDelimetedString(); + WhenSplittingIntoSubstrings(); + AssertCorrectSubstringsReturned(); + } + + [Fact] + public void StringWithSingleDelimeterSplitsIntoCorrectNumberSubstrings() + { + GivenSingleCommaDelimetedString(); + WhenSplittingIntoSubstrings(); + AssertCorrectSubstringsReturned(); + } + + [Fact] + public void StringWithNoDelimeterSplitsIntoSingleSubstring() + { + GivenNonCommaDelimetedString(); + WhenSplittingIntoSubstrings(); + AssertCorrectSubstringsReturned(); + } + + [Fact] + public void StringWithDelimeterAtEndSplitsIntoCorrectSubstring() + { + GivenStringWithCommaDelimeterAtEnd(); + WhenSplittingIntoSubstrings(); + AssertCorrectSubstringsReturned(); + } + + [Fact] + public void StringWithDelimeterAtBeginningSplitsIntoCorrectSubstring() + { + GivenStringWithCommaDelimeterAtBeginning(); + WhenSplittingIntoSubstrings(); + AssertCorrectSubstringsReturned(); + } + } + + public abstract class SpanSplitterTestsBase + { + private string _baseString; + private char _delimeter; + private readonly List _substrings = new List(); + + protected void GivenMultipleCommaDelimetedString() + { + _baseString = "This,Is,A,TestString"; + _delimeter = ','; + } + + protected void GivenSingleCommaDelimetedString() + { + _baseString = "This,IsATestString"; + _delimeter = ','; + } + + protected void GivenNonCommaDelimetedString() + { + _baseString = "ThisIsATestString"; + } + + protected void GivenStringWithCommaDelimeterAtEnd() + { + _baseString = "This,IsATestString,"; + _delimeter = ','; + } + + protected void GivenStringWithCommaDelimeterAtBeginning() + { + _baseString = "/api/v1/articles"; + _delimeter = '/'; + } + + protected void WhenSplittingIntoSubstrings() + { + SpanSplitter spanSplitter; + spanSplitter = new SpanSplitter(ref _baseString, _delimeter); + for (var i = 0; i < spanSplitter.Count; i++) + { + var span = spanSplitter[i]; + _substrings.Add(span.ToString()); + } + } + + protected void AssertCorrectSubstringsReturned() + { + Assert.NotEmpty(_substrings); + var stringSplitArray = _baseString.Split(_delimeter); + Assert.Equal(stringSplitArray.Length, _substrings.Count); + Assert.True(stringSplitArray.SequenceEqual(_substrings)); + } + } +} From 99369397b7bd43062acbfd4a35b94409e9f4398d Mon Sep 17 00:00:00 2001 From: Corey Floyd Date: Tue, 24 Apr 2018 13:20:19 -0500 Subject: [PATCH 02/11] #258 Updates LinkBuilder, RelatedAttrFilterQuery, RequestMiddleware, JsonApiContext, and QueryParser to utilize new SpanSplitter library to avoid unnecessary intermediate string creation. --- src/JsonApiDotNetCore/Builders/LinkBuilder.cs | 23 ++++++++++--------- .../Extensions/StringExtensions.cs | 13 +++++++++++ .../Internal/Query/RelatedAttrFilterQuery.cs | 15 ++++++------ .../Middleware/RequestMiddleware.cs | 8 +++++-- .../Services/JsonApiContext.cs | 9 +++++--- src/JsonApiDotNetCore/Services/QueryParser.cs | 21 +++++++++++------ 6 files changed, 59 insertions(+), 30 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs index dced1225a9..8b4b30f948 100644 --- a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs @@ -1,3 +1,6 @@ +using System; +using System.Text; +using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; @@ -20,20 +23,18 @@ public string GetBasePath(HttpContext context, string entityName) : $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}"; } - private string GetNamespaceFromPath(string path, string entityName) + private static string GetNamespaceFromPath(string path, string entityName) { - var nSpace = string.Empty; - var segments = path.Split('/'); - - for (var i = 1; i < segments.Length; i++) + var sb = new StringBuilder(); + var entityNameSpan = entityName.AsSpan(); + var subSpans = new SpanSplitter(ref path, '/'); + for (var i = 1; i < subSpans.Count; i++) { - if (segments[i].ToLower() == entityName) - break; - - nSpace += $"/{segments[i]}"; + var span = subSpans[i]; + if (entityNameSpan.SequenceEqual(span)) break; + sb.Append($"/{span.ToString()}"); } - - return nSpace; + return sb.ToString(); } public string GetSelfRelationLink(string parent, string parentId, string child) diff --git a/src/JsonApiDotNetCore/Extensions/StringExtensions.cs b/src/JsonApiDotNetCore/Extensions/StringExtensions.cs index 24d5bc8d58..e962baae3d 100644 --- a/src/JsonApiDotNetCore/Extensions/StringExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/StringExtensions.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Text; namespace JsonApiDotNetCore.Extensions @@ -50,5 +52,16 @@ public static string Dasherize(this string str) } return str; } + + public static IEnumerable IndexesOf(this string str, char delimeter) + { + var indexes = new List(); + for (var i = str.IndexOf(delimeter); i > -1 ; i = str.IndexOf(delimeter, i+1)) + { + indexes.Add(i); + } + return indexes; + } + } } diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index 5ec658873d..f4d8114a70 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -8,20 +8,21 @@ namespace JsonApiDotNetCore.Internal.Query public class RelatedAttrFilterQuery : BaseFilterQuery { private readonly IJsonApiContext _jsonApiContext; - + public RelatedAttrFilterQuery( IJsonApiContext jsonApiCopntext, FilterQuery filterQuery) { _jsonApiContext = jsonApiCopntext; - - var relationshipArray = filterQuery.Attribute.Split('.'); - - var relationship = GetRelationship(relationshipArray[0]); + var filterQueryAttribute = filterQuery.Attribute; + var relationshipSubSpans = new SpanSplitter(ref filterQueryAttribute, '.'); + var relationship1 = relationshipSubSpans[0].ToString(); + var relationship2 = relationshipSubSpans[1].ToString(); + var relationship = GetRelationship(relationshipSubSpans[0].ToString()); if (relationship == null) - throw new JsonApiException(400, $"{relationshipArray[1]} is not a valid relationship on {relationshipArray[0]}."); + throw new JsonApiException(400, $"{relationship2} is not a valid relationship on {relationship1}."); - var attribute = GetAttribute(relationship, relationshipArray[1]); + var attribute = GetAttribute(relationship, relationship2); if (attribute == null) throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute."); diff --git a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs index 6e2612c9a6..54a116489d 100644 --- a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using JsonApiDotNetCore.Internal; using Microsoft.AspNetCore.Http; @@ -54,8 +55,11 @@ private static bool IsValidAcceptHeader(HttpContext context) private static bool ContainsMediaTypeParameters(string mediaType) { - var mediaTypeArr = mediaType.Split(';'); - return (mediaTypeArr[0] == Constants.ContentType && mediaTypeArr.Length == 2); + const char delimeter = ';'; + var sliceLength = mediaType.IndexOf(delimeter); + if (sliceLength < 0) return false; + var mediaTypeSlice = mediaType.AsSpan().Slice(0, sliceLength); + return mediaTypeSlice.Length == 2 && mediaTypeSlice.SequenceEqual(Constants.ContentType.AsSpan()); } private static void FlushResponse(HttpContext context, int statusCode) diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index 1ebf5aeea1..0f07624a4e 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -64,7 +64,7 @@ public IJsonApiContext ApplyContext(object controller) throw new JsonApiException(500, $"A resource has not been properly defined for type '{typeof(T)}'. Ensure it has been registered on the ContextGraph."); var context = _httpContextAccessor.HttpContext; - var path = context.Request.Path.Value.Split('/'); + var requestPath = context.Request.Path.Value; if (context.Request.Query.Count > 0) { @@ -75,10 +75,13 @@ public IJsonApiContext ApplyContext(object controller) var linkBuilder = new LinkBuilder(this); BasePath = linkBuilder.GetBasePath(context, _controllerContext.RequestEntity.EntityName); PageManager = GetPageManager(); - IsRelationshipPath = path[path.Length - 2] == "relationships"; + + var pathSpans = new SpanSplitter(ref requestPath, '/'); + IsRelationshipPath = pathSpans[pathSpans.Count - 2].ToString() == "relationships"; + return this; } - + private PageManager GetPageManager() { if (Options.DefaultPageSize == 0 && (QuerySet == null || QuerySet.PageQuery.PageSize == 0)) diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index 5e705f4bc9..f988664c62 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -3,6 +3,7 @@ using System.Linq; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; @@ -93,16 +94,16 @@ protected virtual List ParseFilterQuery(string key, string value) // expected input = filter[id]=1 // expected input = filter[id]=eq:1 var queries = new List(); + var openBracketIndex = key.IndexOf(OPEN_BRACKET); + var closedBracketIndex = key.IndexOf(CLOSE_BRACKET); + var propertyNameSlice = key.AsSpan().Slice(openBracketIndex + 1, closedBracketIndex - openBracketIndex - 1); + var propertyName = propertyNameSlice.ToString(); - var propertyName = key.Split(OPEN_BRACKET, CLOSE_BRACKET)[1]; - - var values = value.Split(COMMA); - foreach (var val in values) + var spanSplitter = new SpanSplitter(ref value, COMMA); + for (var i = 0; i < spanSplitter.Count; i++) { - (var operation, var filterValue) = ParseFilterOperation(val); - queries.Add(new FilterQuery(propertyName, filterValue, operation)); + queries.Add(BuildFilterQuery(spanSplitter[i], propertyName)); } - return queries; } @@ -235,5 +236,11 @@ protected virtual AttrAttribute GetAttribute(string propertyName) throw new JsonApiException(400, $"Attribute '{propertyName}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e); } } + + private FilterQuery BuildFilterQuery(ReadOnlySpan query, string propertyName) + { + var (operation, filterValue) = ParseFilterOperation(query.ToString()); + return new FilterQuery(propertyName, filterValue, operation); + } } } From a48fc4ca925210a4d11f6b9ac6bb630a29102e58 Mon Sep 17 00:00:00 2001 From: Corey Floyd Date: Tue, 24 Apr 2018 13:41:51 -0500 Subject: [PATCH 03/11] #258 Fixes Error in ContainsMediaTypeParameters method's logic. ContentNegotion tests pass. --- src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs index 54a116489d..a7bbebb7f4 100644 --- a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs @@ -56,10 +56,9 @@ private static bool IsValidAcceptHeader(HttpContext context) private static bool ContainsMediaTypeParameters(string mediaType) { const char delimeter = ';'; - var sliceLength = mediaType.IndexOf(delimeter); - if (sliceLength < 0) return false; - var mediaTypeSlice = mediaType.AsSpan().Slice(0, sliceLength); - return mediaTypeSlice.Length == 2 && mediaTypeSlice.SequenceEqual(Constants.ContentType.AsSpan()); + var subSpans = new SpanSplitter(ref mediaType, delimeter); + if (subSpans.Count == 0) return false; + return subSpans.Count == 2 && subSpans[0].ToString() == Constants.ContentType; } private static void FlushResponse(HttpContext context, int statusCode) From cfa785fdd582c9d890cebbb24aad3d3a81501531 Mon Sep 17 00:00:00 2001 From: Corey Floyd Date: Tue, 24 Apr 2018 20:31:10 -0500 Subject: [PATCH 04/11] #258 Removes extra ToString() on the span and fixes naming to be more accurate --- .../Internal/Query/RelatedAttrFilterQuery.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index f4d8114a70..8602a24f30 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -15,14 +15,14 @@ public RelatedAttrFilterQuery( { _jsonApiContext = jsonApiCopntext; var filterQueryAttribute = filterQuery.Attribute; - var relationshipSubSpans = new SpanSplitter(ref filterQueryAttribute, '.'); - var relationship1 = relationshipSubSpans[0].ToString(); - var relationship2 = relationshipSubSpans[1].ToString(); - var relationship = GetRelationship(relationshipSubSpans[0].ToString()); + var filterQuerySubSpans = new SpanSplitter(ref filterQueryAttribute, '.'); + var subSpan1 = filterQuerySubSpans[0].ToString(); + var subSpan2 = filterQuerySubSpans[1].ToString(); + var relationship = GetRelationship(subSpan1); if (relationship == null) - throw new JsonApiException(400, $"{relationship2} is not a valid relationship on {relationship1}."); + throw new JsonApiException(400, $"{subSpan2} is not a valid relationship on {subSpan1}."); - var attribute = GetAttribute(relationship, relationship2); + var attribute = GetAttribute(relationship, subSpan2); if (attribute == null) throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute."); From 85bb9f4603dbc776b29cb8af6d712618f0f33d7d Mon Sep 17 00:00:00 2001 From: Corey Floyd Date: Tue, 24 Apr 2018 20:52:15 -0500 Subject: [PATCH 05/11] #258 Adds extension method for splitting with spans. Updates usages and unit tests to use extension method. --- src/JsonApiDotNetCore/Builders/LinkBuilder.cs | 3 ++- src/JsonApiDotNetCore/Extensions/StringExtensions.cs | 6 ++++++ .../Internal/Query/RelatedAttrFilterQuery.cs | 3 ++- src/JsonApiDotNetCore/Internal/SpanSplitter.cs | 7 ++++++- src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs | 3 ++- src/JsonApiDotNetCore/Services/JsonApiContext.cs | 3 ++- src/JsonApiDotNetCore/Services/QueryParser.cs | 2 +- test/UnitTests/Internal/SpanSplitterTests.cs | 3 ++- 8 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs index 8b4b30f948..b2bfcae168 100644 --- a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Text; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; @@ -27,7 +28,7 @@ private static string GetNamespaceFromPath(string path, string entityName) { var sb = new StringBuilder(); var entityNameSpan = entityName.AsSpan(); - var subSpans = new SpanSplitter(ref path, '/'); + var subSpans = path.SpanSplit('/'); for (var i = 1; i < subSpans.Count; i++) { var span = subSpans[i]; diff --git a/src/JsonApiDotNetCore/Extensions/StringExtensions.cs b/src/JsonApiDotNetCore/Extensions/StringExtensions.cs index e962baae3d..c64795cae1 100644 --- a/src/JsonApiDotNetCore/Extensions/StringExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/StringExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using JsonApiDotNetCore.Internal; namespace JsonApiDotNetCore.Extensions { @@ -62,6 +63,11 @@ public static IEnumerable IndexesOf(this string str, char delimeter) } return indexes; } + + public static SpanSplitter SpanSplit(this string str, char delimeter) + { + return SpanSplitter.Split(str, delimeter); + } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index 8602a24f30..6415b0c575 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; @@ -15,7 +16,7 @@ public RelatedAttrFilterQuery( { _jsonApiContext = jsonApiCopntext; var filterQueryAttribute = filterQuery.Attribute; - var filterQuerySubSpans = new SpanSplitter(ref filterQueryAttribute, '.'); + var filterQuerySubSpans = filterQueryAttribute.SpanSplit('.'); var subSpan1 = filterQuerySubSpans[0].ToString(); var subSpan2 = filterQuerySubSpans[1].ToString(); var relationship = GetRelationship(subSpan1); diff --git a/src/JsonApiDotNetCore/Internal/SpanSplitter.cs b/src/JsonApiDotNetCore/Internal/SpanSplitter.cs index 268bcab195..0f9aecbb59 100644 --- a/src/JsonApiDotNetCore/Internal/SpanSplitter.cs +++ b/src/JsonApiDotNetCore/Internal/SpanSplitter.cs @@ -15,7 +15,7 @@ public readonly ref struct SpanSplitter public int Count => _substringIndexes.Count(); public ReadOnlySpan this[int index] => GetSpanForSubstring(index + 1); - public SpanSplitter(ref string str, char delimeter) + private SpanSplitter(ref string str, char delimeter) { _span = str.AsSpan(); _delimeterIndexes = str.IndexesOf(delimeter).ToList(); @@ -23,6 +23,11 @@ public SpanSplitter(ref string str, char delimeter) BuildSubstringIndexes(); } + public static SpanSplitter Split(string str, char delimeter) + { + return new SpanSplitter(ref str, delimeter); + } + [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object obj) => throw new NotSupportedException(); diff --git a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs index a7bbebb7f4..8b6111fef9 100644 --- a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; @@ -56,7 +57,7 @@ private static bool IsValidAcceptHeader(HttpContext context) private static bool ContainsMediaTypeParameters(string mediaType) { const char delimeter = ';'; - var subSpans = new SpanSplitter(ref mediaType, delimeter); + var subSpans = mediaType.SpanSplit(delimeter); if (subSpans.Count == 0) return false; return subSpans.Count == 2 && subSpans[0].ToString() == Constants.ContentType; } diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index 0f07624a4e..b45b68c97b 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -3,6 +3,7 @@ using System.Linq; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; @@ -76,7 +77,7 @@ public IJsonApiContext ApplyContext(object controller) BasePath = linkBuilder.GetBasePath(context, _controllerContext.RequestEntity.EntityName); PageManager = GetPageManager(); - var pathSpans = new SpanSplitter(ref requestPath, '/'); + var pathSpans = requestPath.SpanSplit('/'); IsRelationshipPath = pathSpans[pathSpans.Count - 2].ToString() == "relationships"; return this; diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index f988664c62..ddff31b5f1 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -99,7 +99,7 @@ protected virtual List ParseFilterQuery(string key, string value) var propertyNameSlice = key.AsSpan().Slice(openBracketIndex + 1, closedBracketIndex - openBracketIndex - 1); var propertyName = propertyNameSlice.ToString(); - var spanSplitter = new SpanSplitter(ref value, COMMA); + var spanSplitter = value.SpanSplit(COMMA); for (var i = 0; i < spanSplitter.Count; i++) { queries.Add(BuildFilterQuery(spanSplitter[i], propertyName)); diff --git a/test/UnitTests/Internal/SpanSplitterTests.cs b/test/UnitTests/Internal/SpanSplitterTests.cs index 016ee8ec82..61160e7462 100644 --- a/test/UnitTests/Internal/SpanSplitterTests.cs +++ b/test/UnitTests/Internal/SpanSplitterTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using Xunit; @@ -88,7 +89,7 @@ protected void GivenStringWithCommaDelimeterAtBeginning() protected void WhenSplittingIntoSubstrings() { SpanSplitter spanSplitter; - spanSplitter = new SpanSplitter(ref _baseString, _delimeter); + spanSplitter = _baseString.SpanSplit(_delimeter); for (var i = 0; i < spanSplitter.Count; i++) { var span = spanSplitter[i]; From 01735f01efb7d3cbc4a063d788d8caa2dd87efce Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sat, 28 Apr 2018 20:01:32 -0500 Subject: [PATCH 06/11] refactor(LinkBuilder): reduce allocations --- ...espaceFromPath_Benchmarks-report-github.md | 16 ++++ ...uilder_ GetNamespaceFromPath_Benchmarks.cs | 90 +++++++++++++++++++ benchmarks/Program.cs | 4 +- src/JsonApiDotNetCore/Builders/LinkBuilder.cs | 36 +++++--- 4 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.LinkBuilder.LinkBuilder_GetNamespaceFromPath_Benchmarks-report-github.md create mode 100644 benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs diff --git a/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.LinkBuilder.LinkBuilder_GetNamespaceFromPath_Benchmarks-report-github.md b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.LinkBuilder.LinkBuilder_GetNamespaceFromPath_Benchmarks-report-github.md new file mode 100644 index 0000000000..72951396e8 --- /dev/null +++ b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.LinkBuilder.LinkBuilder_GetNamespaceFromPath_Benchmarks-report-github.md @@ -0,0 +1,16 @@ +``` ini + +BenchmarkDotNet=v0.10.10, OS=Mac OS X 10.12 +Processor=Intel Core i5-5257U CPU 2.70GHz (Broadwell), ProcessorCount=4 +.NET Core SDK=2.1.4 + [Host] : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT + Job-XFMVNE : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT + +LaunchCount=3 TargetCount=20 WarmupCount=10 + +``` +| Method | Mean | Error | StdDev | Gen 0 | Allocated | +|--------------------------- |-----------:|----------:|----------:|-------:|----------:| +| UsingSplit | 1,197.6 ns | 11.929 ns | 25.933 ns | 0.9251 | 1456 B | +| UsingSpanWithStringBuilder | 1,542.0 ns | 15.249 ns | 33.792 ns | 0.9460 | 1488 B | +| UsingSpanWithNoAlloc | 272.6 ns | 2.265 ns | 5.018 ns | 0.0863 | 136 B | diff --git a/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs new file mode 100644 index 0000000000..0604a68083 --- /dev/null +++ b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs @@ -0,0 +1,90 @@ +using System; +using System.Diagnostics; +using System.Text; +using System.Threading; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes.Exporters; +using BenchmarkDotNet.Attributes.Jobs; +using JsonApiDotNetCore.Extensions; + +namespace Benchmarks.LinkBuilder +{ + [MarkdownExporter, SimpleJob(launchCount : 3, warmupCount : 10, targetCount : 20), MemoryDiagnoser] + public class LinkBuilder_GetNamespaceFromPath_Benchmarks + { + private const string PATH = "/api/some-really-long-namespace-path/resources/current/articles"; + private const string ENTITY_NAME = "articles"; + + [Benchmark] + public void UsingSplit() => GetNamespaceFromPath_BySplitting(PATH, ENTITY_NAME); + + [Benchmark] + public void UsingSpanWithStringBuilder() => GetNamespaceFromPath_Using_Span_With_StringBuilder(PATH, ENTITY_NAME); + + [Benchmark] + public void UsingSpanWithNoAlloc() => GetNamespaceFromPath_Using_Span_No_Alloc(PATH, ENTITY_NAME); + + public static string GetNamespaceFromPath_BySplitting(string path, string entityName) + { + var nSpace = string.Empty; + var segments = path.Split('/'); + + for (var i = 1; i < segments.Length; i++) + { + if (segments[i].ToLower() == entityName) + break; + + nSpace += $"/{segments[i]}"; + } + + return nSpace; + } + + public static string GetNamespaceFromPath_Using_Span_No_Alloc(string path, string entityName) + { + var entityNameSpan = entityName.AsSpan(); + var pathSpan = path.AsSpan(); + const char delimiter = '/'; + for (var i = 0; i < pathSpan.Length; i++) + { + if(pathSpan[i].Equals(delimiter)) + { + var nextPosition = i+1; + if(pathSpan.Length > i + entityNameSpan.Length) + { + var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length); + if (entityNameSpan.SequenceEqual(possiblePathSegment)) + { + // check to see if it's the last position in the string + // or if the next character is a / + var lastCharacterPosition = nextPosition + entityNameSpan.Length; + + if(lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition + 1].Equals(delimiter)) + { + return pathSpan.Slice(0, i).ToString(); + } + } + } + } + } + + return string.Empty; + } + + public static string GetNamespaceFromPath_Using_Span_With_StringBuilder(string path, string entityName) + { + var sb = new StringBuilder(); + var entityNameSpan = entityName.AsSpan(); + var subSpans = path.SpanSplit('/'); + for (var i = 1; i < subSpans.Count; i++) + { + var span = subSpans[i]; + if (entityNameSpan.SequenceEqual(span)) + break; + + sb.Append($"/{span.ToString()}"); + } + return sb.ToString(); + } + } +} diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs index 7665d5fb97..813153ecb5 100644 --- a/benchmarks/Program.cs +++ b/benchmarks/Program.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Running; +using Benchmarks.LinkBuilder; using Benchmarks.Query; using Benchmarks.Serialization; @@ -8,7 +9,8 @@ static void Main(string[] args) { var switcher = new BenchmarkSwitcher(new[] { typeof(JsonApiDeserializer_Benchmarks), typeof(JsonApiSerializer_Benchmarks), - typeof(QueryParser_Benchmarks) + typeof(QueryParser_Benchmarks), + typeof(LinkBuilder_GetNamespaceFromPath_Benchmarks) }); switcher.Run(args); } diff --git a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs index b2bfcae168..9c3d19b05b 100644 --- a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs @@ -1,7 +1,4 @@ using System; -using System.Text; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; @@ -20,22 +17,39 @@ public string GetBasePath(HttpContext context, string entityName) { var r = context.Request; return (_context.Options.RelativeLinks) - ? $"{GetNamespaceFromPath(r.Path, entityName)}" + ? GetNamespaceFromPath(r.Path, entityName) : $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}"; } private static string GetNamespaceFromPath(string path, string entityName) { - var sb = new StringBuilder(); var entityNameSpan = entityName.AsSpan(); - var subSpans = path.SpanSplit('/'); - for (var i = 1; i < subSpans.Count; i++) + var pathSpan = path.AsSpan(); + const char delimiter = '/'; + for (var i = 0; i < pathSpan.Length; i++) { - var span = subSpans[i]; - if (entityNameSpan.SequenceEqual(span)) break; - sb.Append($"/{span.ToString()}"); + if(pathSpan[i].Equals(delimiter)) + { + var nextPosition = i + 1; + if(pathSpan.Length > i + entityNameSpan.Length) + { + var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length); + if (entityNameSpan.SequenceEqual(possiblePathSegment)) + { + // check to see if it's the last position in the string + // or if the next character is a / + var lastCharacterPosition = nextPosition + entityNameSpan.Length; + + if(lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter)) + { + return pathSpan.Slice(0, i).ToString(); + } + } + } + } } - return sb.ToString(); + + return string.Empty; } public string GetSelfRelationLink(string parent, string parentId, string child) From 4803fd0f5e8c3a34494e93e1b70c7c33a51854b3 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 1 May 2018 10:46:39 -0500 Subject: [PATCH 07/11] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b8a1d74643..7ac38bba9f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ services: before_script: - psql -c 'create database JsonApiDotNetCoreExample;' -U postgres mono: none -dotnet: 2.0.3 # https://www.microsoft.com/net/download/linux +dotnet: 2.1.105 # https://www.microsoft.com/net/download/linux branches: only: - master From 2f0e4818cef4d605dd7e9517069eb42c796850d9 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 1 May 2018 20:33:00 -0500 Subject: [PATCH 08/11] benchamrk: don't duplicate the final version's definition --- ...uilder_ GetNamespaceFromPath_Benchmarks.cs | 36 ++----------------- src/JsonApiDotNetCore/Builders/LinkBuilder.cs | 2 +- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs index 0604a68083..d53294e749 100644 --- a/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs +++ b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs @@ -1,7 +1,5 @@ using System; -using System.Diagnostics; using System.Text; -using System.Threading; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes.Exporters; using BenchmarkDotNet.Attributes.Jobs; @@ -22,7 +20,7 @@ public class LinkBuilder_GetNamespaceFromPath_Benchmarks public void UsingSpanWithStringBuilder() => GetNamespaceFromPath_Using_Span_With_StringBuilder(PATH, ENTITY_NAME); [Benchmark] - public void UsingSpanWithNoAlloc() => GetNamespaceFromPath_Using_Span_No_Alloc(PATH, ENTITY_NAME); + public void Current() => GetNameSpaceFromPath_Current(PATH, ENTITY_NAME); public static string GetNamespaceFromPath_BySplitting(string path, string entityName) { @@ -40,36 +38,8 @@ public static string GetNamespaceFromPath_BySplitting(string path, string entity return nSpace; } - public static string GetNamespaceFromPath_Using_Span_No_Alloc(string path, string entityName) - { - var entityNameSpan = entityName.AsSpan(); - var pathSpan = path.AsSpan(); - const char delimiter = '/'; - for (var i = 0; i < pathSpan.Length; i++) - { - if(pathSpan[i].Equals(delimiter)) - { - var nextPosition = i+1; - if(pathSpan.Length > i + entityNameSpan.Length) - { - var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length); - if (entityNameSpan.SequenceEqual(possiblePathSegment)) - { - // check to see if it's the last position in the string - // or if the next character is a / - var lastCharacterPosition = nextPosition + entityNameSpan.Length; - - if(lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition + 1].Equals(delimiter)) - { - return pathSpan.Slice(0, i).ToString(); - } - } - } - } - } - - return string.Empty; - } + public static string GetNameSpaceFromPath_Current(string path, string entityName) + => JsonApiDotNetCore.Builders.LinkBuilder.GetNamespaceFromPath(path, entityName); public static string GetNamespaceFromPath_Using_Span_With_StringBuilder(string path, string entityName) { diff --git a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs index 9c3d19b05b..c3769eccbc 100644 --- a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs @@ -21,7 +21,7 @@ public string GetBasePath(HttpContext context, string entityName) : $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}"; } - private static string GetNamespaceFromPath(string path, string entityName) + internal static string GetNamespaceFromPath(string path, string entityName) { var entityNameSpan = entityName.AsSpan(); var pathSpan = path.AsSpan(); From 6568c37734451544c69cf654c57dc1521e2be678 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 1 May 2018 21:26:19 -0500 Subject: [PATCH 09/11] add benchmarks to RequestMiddleware --- ...TypeParameters_Benchmarks-report-github.md | 14 ++++++++++ benchmarks/Program.cs | 4 ++- .../ContainsMediaTypeParameters_Benchmarks.cs | 26 +++++++++++++++++++ .../Middleware/RequestMiddleware.cs | 22 +++++++++++----- 4 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.RequestMiddleware.ContainsMediaTypeParameters_Benchmarks-report-github.md create mode 100644 benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs diff --git a/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.RequestMiddleware.ContainsMediaTypeParameters_Benchmarks-report-github.md b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.RequestMiddleware.ContainsMediaTypeParameters_Benchmarks-report-github.md new file mode 100644 index 0000000000..066e7b2036 --- /dev/null +++ b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.RequestMiddleware.ContainsMediaTypeParameters_Benchmarks-report-github.md @@ -0,0 +1,14 @@ +``` ini + +BenchmarkDotNet=v0.10.10, OS=Mac OS X 10.12 +Processor=Intel Core i5-5257U CPU 2.70GHz (Broadwell), ProcessorCount=4 +.NET Core SDK=2.1.4 + [Host] : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT + DefaultJob : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT + + +``` +| Method | Mean | Error | StdDev | Gen 0 | Allocated | +|----------- |----------:|----------:|----------:|-------:|----------:| +| UsingSplit | 157.28 ns | 2.9689 ns | 5.8602 ns | 0.2134 | 336 B | +| Current | 39.96 ns | 0.6489 ns | 0.6070 ns | - | 0 B | diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs index 813153ecb5..989272c7f1 100644 --- a/benchmarks/Program.cs +++ b/benchmarks/Program.cs @@ -1,6 +1,7 @@ using BenchmarkDotNet.Running; using Benchmarks.LinkBuilder; using Benchmarks.Query; +using Benchmarks.RequestMiddleware; using Benchmarks.Serialization; namespace Benchmarks { @@ -10,7 +11,8 @@ static void Main(string[] args) { typeof(JsonApiDeserializer_Benchmarks), typeof(JsonApiSerializer_Benchmarks), typeof(QueryParser_Benchmarks), - typeof(LinkBuilder_GetNamespaceFromPath_Benchmarks) + typeof(LinkBuilder_GetNamespaceFromPath_Benchmarks), + typeof(ContainsMediaTypeParameters_Benchmarks) }); switcher.Run(args); } diff --git a/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs b/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs new file mode 100644 index 0000000000..9826bd158a --- /dev/null +++ b/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs @@ -0,0 +1,26 @@ +using System; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes.Exporters; +using JsonApiDotNetCore.Internal; + +namespace Benchmarks.RequestMiddleware +{ + [MarkdownExporter, MemoryDiagnoser] + public class ContainsMediaTypeParameters_Benchmarks + { + private const string MEDIA_TYPE = "application/vnd.api+json; version=1"; + + [Benchmark] + public void UsingSplit() => UsingSplitImpl(MEDIA_TYPE); + + [Benchmark] + public void Current() + => JsonApiDotNetCore.Middleware.RequestMiddleware.ContainsMediaTypeParameters(MEDIA_TYPE); + + private bool UsingSplitImpl(string mediaType) + { + var mediaTypeArr = mediaType.Split(';'); + return (mediaTypeArr[0] == Constants.ContentType && mediaTypeArr.Length == 2); + } + } +} diff --git a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs index 8b6111fef9..0ce54c8589 100644 --- a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; @@ -54,12 +53,23 @@ private static bool IsValidAcceptHeader(HttpContext context) return true; } - private static bool ContainsMediaTypeParameters(string mediaType) + internal static bool ContainsMediaTypeParameters(string mediaType) { - const char delimeter = ';'; - var subSpans = mediaType.SpanSplit(delimeter); - if (subSpans.Count == 0) return false; - return subSpans.Count == 2 && subSpans[0].ToString() == Constants.ContentType; + var incomingMediaTypeSpan = mediaType.AsSpan(); + + // if the content type is not application/vnd.api+json then continue on + if(incomingMediaTypeSpan.Length < Constants.ContentType.Length) + return false; + + var incomingContentType = incomingMediaTypeSpan.Slice(0, Constants.ContentType.Length); + if(incomingContentType.SequenceEqual(Constants.ContentType.AsSpan()) == false) + return false; + + // anything appended to "application/vnd.api+json;" will be considered a media type param + return ( + incomingMediaTypeSpan.Length >= Constants.ContentType.Length + 2 + && incomingMediaTypeSpan[Constants.ContentType.Length] == ';' + ); } private static void FlushResponse(HttpContext context, int statusCode) From fbe1d1bcfc585d1b0da6aa1c2d03c545bcb13119 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 1 May 2018 22:24:57 -0500 Subject: [PATCH 10/11] add benchmarks for IsRelationshipPath --- ...IsRelationship_Benchmarks-report-github.md | 12 +++++ .../PathIsRelationship_Benchmarks.cs | 24 ++++++++++ benchmarks/Program.cs | 4 +- .../ContainsMediaTypeParameters_Benchmarks.cs | 1 - .../Services/JsonApiContext.cs | 44 +++++++++++++++---- 5 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.JsonApiContext.PathIsRelationship_Benchmarks-report-github.md create mode 100644 benchmarks/JsonApiContext/PathIsRelationship_Benchmarks.cs diff --git a/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.JsonApiContext.PathIsRelationship_Benchmarks-report-github.md b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.JsonApiContext.PathIsRelationship_Benchmarks-report-github.md new file mode 100644 index 0000000000..6be58e241a --- /dev/null +++ b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.JsonApiContext.PathIsRelationship_Benchmarks-report-github.md @@ -0,0 +1,12 @@ +```ini +BenchmarkDotNet=v0.10.10, OS=Mac OS X 10.12 +Processor=Intel Core i5-5257U CPU 2.70GHz (Broadwell), ProcessorCount=4 +.NET Core SDK=2.1.4 + [Host] : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT + DefaultJob : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT +``` + +| Method | Mean | Error | StdDev | Gen 0 | Allocated | +| ---------- | --------: | ---------: | ---------: | -----: | --------: | +| UsingSplit | 421.08 ns | 19.3905 ns | 54.0529 ns | 0.4725 | 744 B | +| Current | 52.23 ns | 0.8052 ns | 0.7532 ns | - | 0 B | diff --git a/benchmarks/JsonApiContext/PathIsRelationship_Benchmarks.cs b/benchmarks/JsonApiContext/PathIsRelationship_Benchmarks.cs new file mode 100644 index 0000000000..83fe6fc53c --- /dev/null +++ b/benchmarks/JsonApiContext/PathIsRelationship_Benchmarks.cs @@ -0,0 +1,24 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes.Exporters; + +namespace Benchmarks.JsonApiContext +{ + [MarkdownExporter, MemoryDiagnoser] + public class PathIsRelationship_Benchmarks + { + private const string PATH = "https://example.com/api/v1/namespace/articles/relationships/author/"; + + [Benchmark] + public void Current() + => JsonApiDotNetCore.Services.JsonApiContext.PathIsRelationship(PATH); + + [Benchmark] + public void UsingSplit() => UsingSplitImpl(PATH); + + private bool UsingSplitImpl(string path) + { + var split = path.Split('/'); + return split[split.Length - 2] == "relationships"; + } + } +} diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs index 989272c7f1..9a2c45dffb 100644 --- a/benchmarks/Program.cs +++ b/benchmarks/Program.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Running; +using Benchmarks.JsonApiContext; using Benchmarks.LinkBuilder; using Benchmarks.Query; using Benchmarks.RequestMiddleware; @@ -12,7 +13,8 @@ static void Main(string[] args) { typeof(JsonApiSerializer_Benchmarks), typeof(QueryParser_Benchmarks), typeof(LinkBuilder_GetNamespaceFromPath_Benchmarks), - typeof(ContainsMediaTypeParameters_Benchmarks) + typeof(ContainsMediaTypeParameters_Benchmarks), + typeof(PathIsRelationship_Benchmarks) }); switcher.Run(args); } diff --git a/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs b/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs index 9826bd158a..ed64c98335 100644 --- a/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs +++ b/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs @@ -1,4 +1,3 @@ -using System; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes.Exporters; using JsonApiDotNetCore.Internal; diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index b45b68c97b..2665217fef 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; @@ -65,7 +63,6 @@ public IJsonApiContext ApplyContext(object controller) throw new JsonApiException(500, $"A resource has not been properly defined for type '{typeof(T)}'. Ensure it has been registered on the ContextGraph."); var context = _httpContextAccessor.HttpContext; - var requestPath = context.Request.Path.Value; if (context.Request.Query.Count > 0) { @@ -73,15 +70,46 @@ public IJsonApiContext ApplyContext(object controller) IncludedRelationships = QuerySet.IncludedRelationships; } - var linkBuilder = new LinkBuilder(this); - BasePath = linkBuilder.GetBasePath(context, _controllerContext.RequestEntity.EntityName); + BasePath = new LinkBuilder(this).GetBasePath(context, _controllerContext.RequestEntity.EntityName); PageManager = GetPageManager(); - - var pathSpans = requestPath.SpanSplit('/'); - IsRelationshipPath = pathSpans[pathSpans.Count - 2].ToString() == "relationships"; + IsRelationshipPath = PathIsRelationship(context.Request.Path.Value); return this; } + + internal static bool PathIsRelationship(string requestPath) + { + // while(!Debugger.IsAttached) { Thread.Sleep(1000); } + const string relationships = "relationships"; + const char pathSegmentDelimiter = '/'; + + var span = requestPath.AsSpan(); + + // we need to iterate over the string, from the end, + // checking whether or not the 2nd to last path segment + // is "relationships" + // -2 is chosen in case the path ends with '/' + for(var i = requestPath.Length - 2; i >= 0; i--) + { + // if there are not enough characters left in the path to + // contain "relationships" + if(i < relationships.Length) + return false; + + // we have found the first instance of '/' + if(span[i] == pathSegmentDelimiter) + { + // in the case of a "relationships" route, the next + // path segment will be "relationships" + return ( + span.Slice(i - relationships.Length, relationships.Length) + .SequenceEqual(relationships.AsSpan()) + ); + } + } + + return false; + } private PageManager GetPageManager() { From 369860c873a92b5395b4b62e6e842e472eabc0ec Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sun, 6 May 2018 16:56:19 -0500 Subject: [PATCH 11/11] clean up unused changes --- ...uilder_ GetNamespaceFromPath_Benchmarks.cs | 22 ---- .../Extensions/StringExtensions.cs | 19 --- .../Internal/Query/RelatedAttrFilterQuery.cs | 17 ++- .../Internal/SpanSplitter.cs | 69 ----------- src/JsonApiDotNetCore/Services/QueryParser.cs | 15 ++- test/UnitTests/Internal/SpanSplitterTests.cs | 108 ------------------ 6 files changed, 14 insertions(+), 236 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Internal/SpanSplitter.cs delete mode 100644 test/UnitTests/Internal/SpanSplitterTests.cs diff --git a/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs index d53294e749..05728321c3 100644 --- a/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs +++ b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs @@ -1,9 +1,6 @@ -using System; -using System.Text; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes.Exporters; using BenchmarkDotNet.Attributes.Jobs; -using JsonApiDotNetCore.Extensions; namespace Benchmarks.LinkBuilder { @@ -16,9 +13,6 @@ public class LinkBuilder_GetNamespaceFromPath_Benchmarks [Benchmark] public void UsingSplit() => GetNamespaceFromPath_BySplitting(PATH, ENTITY_NAME); - [Benchmark] - public void UsingSpanWithStringBuilder() => GetNamespaceFromPath_Using_Span_With_StringBuilder(PATH, ENTITY_NAME); - [Benchmark] public void Current() => GetNameSpaceFromPath_Current(PATH, ENTITY_NAME); @@ -40,21 +34,5 @@ public static string GetNamespaceFromPath_BySplitting(string path, string entity public static string GetNameSpaceFromPath_Current(string path, string entityName) => JsonApiDotNetCore.Builders.LinkBuilder.GetNamespaceFromPath(path, entityName); - - public static string GetNamespaceFromPath_Using_Span_With_StringBuilder(string path, string entityName) - { - var sb = new StringBuilder(); - var entityNameSpan = entityName.AsSpan(); - var subSpans = path.SpanSplit('/'); - for (var i = 1; i < subSpans.Count; i++) - { - var span = subSpans[i]; - if (entityNameSpan.SequenceEqual(span)) - break; - - sb.Append($"/{span.ToString()}"); - } - return sb.ToString(); - } } } diff --git a/src/JsonApiDotNetCore/Extensions/StringExtensions.cs b/src/JsonApiDotNetCore/Extensions/StringExtensions.cs index c64795cae1..24d5bc8d58 100644 --- a/src/JsonApiDotNetCore/Extensions/StringExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/StringExtensions.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Text; -using JsonApiDotNetCore.Internal; namespace JsonApiDotNetCore.Extensions { @@ -53,21 +50,5 @@ public static string Dasherize(this string str) } return str; } - - public static IEnumerable IndexesOf(this string str, char delimeter) - { - var indexes = new List(); - for (var i = str.IndexOf(delimeter); i > -1 ; i = str.IndexOf(delimeter, i+1)) - { - indexes.Add(i); - } - return indexes; - } - - public static SpanSplitter SpanSplit(this string str, char delimeter) - { - return SpanSplitter.Split(str, delimeter); - } - } } diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index 6415b0c575..d567de200a 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -11,20 +11,17 @@ public class RelatedAttrFilterQuery : BaseFilterQuery private readonly IJsonApiContext _jsonApiContext; public RelatedAttrFilterQuery( - IJsonApiContext jsonApiCopntext, + IJsonApiContext jsonApiContext, FilterQuery filterQuery) { - _jsonApiContext = jsonApiCopntext; - var filterQueryAttribute = filterQuery.Attribute; - var filterQuerySubSpans = filterQueryAttribute.SpanSplit('.'); - var subSpan1 = filterQuerySubSpans[0].ToString(); - var subSpan2 = filterQuerySubSpans[1].ToString(); - var relationship = GetRelationship(subSpan1); - if (relationship == null) - throw new JsonApiException(400, $"{subSpan2} is not a valid relationship on {subSpan1}."); + _jsonApiContext = jsonApiContext; - var attribute = GetAttribute(relationship, subSpan2); + var relationshipArray = filterQuery.Attribute.Split('.'); + var relationship = GetRelationship(relationshipArray[0]); + if (relationship == null) + throw new JsonApiException(400, $"{relationshipArray[1]} is not a valid relationship on {relationshipArray[0]}."); + var attribute = GetAttribute(relationship, relationshipArray[1]); if (attribute == null) throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute."); diff --git a/src/JsonApiDotNetCore/Internal/SpanSplitter.cs b/src/JsonApiDotNetCore/Internal/SpanSplitter.cs deleted file mode 100644 index 0f9aecbb59..0000000000 --- a/src/JsonApiDotNetCore/Internal/SpanSplitter.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using JsonApiDotNetCore.Extensions; - -namespace JsonApiDotNetCore.Internal -{ - public readonly ref struct SpanSplitter - { - private readonly ReadOnlySpan _span; - private readonly List _delimeterIndexes; - private readonly List> _substringIndexes; - - public int Count => _substringIndexes.Count(); - public ReadOnlySpan this[int index] => GetSpanForSubstring(index + 1); - - private SpanSplitter(ref string str, char delimeter) - { - _span = str.AsSpan(); - _delimeterIndexes = str.IndexesOf(delimeter).ToList(); - _substringIndexes = new List>(); - BuildSubstringIndexes(); - } - - public static SpanSplitter Split(string str, char delimeter) - { - return new SpanSplitter(ref str, delimeter); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public override bool Equals(object obj) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - public override int GetHashCode() => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - public override string ToString() => throw new NotSupportedException(); - - private ReadOnlySpan GetSpanForSubstring(int substringNumber) - { - if (substringNumber > Count) - { - throw new ArgumentOutOfRangeException($"There are only {Count} substrings given the delimeter and base string provided"); - } - - var indexes = _substringIndexes[substringNumber - 1]; - return _span.Slice(indexes.Item1, indexes.Item2); - } - - private void BuildSubstringIndexes() - { - var start = 0; - var end = 0; - foreach (var index in _delimeterIndexes) - { - end = index; - if (start > end) break; - _substringIndexes.Add(new Tuple(start, end - start)); - start = ++end; - } - - if (end <= _span.Length) - { - _substringIndexes.Add(new Tuple(start, _span.Length - start)); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index ddff31b5f1..34bf525e8e 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -3,7 +3,6 @@ using System.Linq; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; @@ -94,16 +93,16 @@ protected virtual List ParseFilterQuery(string key, string value) // expected input = filter[id]=1 // expected input = filter[id]=eq:1 var queries = new List(); - var openBracketIndex = key.IndexOf(OPEN_BRACKET); - var closedBracketIndex = key.IndexOf(CLOSE_BRACKET); - var propertyNameSlice = key.AsSpan().Slice(openBracketIndex + 1, closedBracketIndex - openBracketIndex - 1); - var propertyName = propertyNameSlice.ToString(); - var spanSplitter = value.SpanSplit(COMMA); - for (var i = 0; i < spanSplitter.Count; i++) + var propertyName = key.Split(OPEN_BRACKET, CLOSE_BRACKET)[1]; + + var values = value.Split(COMMA); + foreach (var val in values) { - queries.Add(BuildFilterQuery(spanSplitter[i], propertyName)); + (var operation, var filterValue) = ParseFilterOperation(val); + queries.Add(new FilterQuery(propertyName, filterValue, operation)); } + return queries; } diff --git a/test/UnitTests/Internal/SpanSplitterTests.cs b/test/UnitTests/Internal/SpanSplitterTests.cs deleted file mode 100644 index 61160e7462..0000000000 --- a/test/UnitTests/Internal/SpanSplitterTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using Xunit; - -namespace UnitTests.Internal -{ - public class SpanSplitterTests : SpanSplitterTestsBase - { - [Fact] - public void StringWithDelimeterSplitsIntoCorrectNumberSubstrings() - { - GivenMultipleCommaDelimetedString(); - WhenSplittingIntoSubstrings(); - AssertCorrectSubstringsReturned(); - } - - [Fact] - public void StringWithSingleDelimeterSplitsIntoCorrectNumberSubstrings() - { - GivenSingleCommaDelimetedString(); - WhenSplittingIntoSubstrings(); - AssertCorrectSubstringsReturned(); - } - - [Fact] - public void StringWithNoDelimeterSplitsIntoSingleSubstring() - { - GivenNonCommaDelimetedString(); - WhenSplittingIntoSubstrings(); - AssertCorrectSubstringsReturned(); - } - - [Fact] - public void StringWithDelimeterAtEndSplitsIntoCorrectSubstring() - { - GivenStringWithCommaDelimeterAtEnd(); - WhenSplittingIntoSubstrings(); - AssertCorrectSubstringsReturned(); - } - - [Fact] - public void StringWithDelimeterAtBeginningSplitsIntoCorrectSubstring() - { - GivenStringWithCommaDelimeterAtBeginning(); - WhenSplittingIntoSubstrings(); - AssertCorrectSubstringsReturned(); - } - } - - public abstract class SpanSplitterTestsBase - { - private string _baseString; - private char _delimeter; - private readonly List _substrings = new List(); - - protected void GivenMultipleCommaDelimetedString() - { - _baseString = "This,Is,A,TestString"; - _delimeter = ','; - } - - protected void GivenSingleCommaDelimetedString() - { - _baseString = "This,IsATestString"; - _delimeter = ','; - } - - protected void GivenNonCommaDelimetedString() - { - _baseString = "ThisIsATestString"; - } - - protected void GivenStringWithCommaDelimeterAtEnd() - { - _baseString = "This,IsATestString,"; - _delimeter = ','; - } - - protected void GivenStringWithCommaDelimeterAtBeginning() - { - _baseString = "/api/v1/articles"; - _delimeter = '/'; - } - - protected void WhenSplittingIntoSubstrings() - { - SpanSplitter spanSplitter; - spanSplitter = _baseString.SpanSplit(_delimeter); - for (var i = 0; i < spanSplitter.Count; i++) - { - var span = spanSplitter[i]; - _substrings.Add(span.ToString()); - } - } - - protected void AssertCorrectSubstringsReturned() - { - Assert.NotEmpty(_substrings); - var stringSplitArray = _baseString.Split(_delimeter); - Assert.Equal(stringSplitArray.Length, _substrings.Count); - Assert.True(stringSplitArray.SequenceEqual(_substrings)); - } - } -}