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 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/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/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/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/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs new file mode 100644 index 0000000000..05728321c3 --- /dev/null +++ b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs @@ -0,0 +1,38 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes.Exporters; +using BenchmarkDotNet.Attributes.Jobs; + +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 Current() => GetNameSpaceFromPath_Current(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_Current(string path, string entityName) + => JsonApiDotNetCore.Builders.LinkBuilder.GetNamespaceFromPath(path, entityName); + } +} diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs index 7665d5fb97..9a2c45dffb 100644 --- a/benchmarks/Program.cs +++ b/benchmarks/Program.cs @@ -1,5 +1,8 @@ using BenchmarkDotNet.Running; +using Benchmarks.JsonApiContext; +using Benchmarks.LinkBuilder; using Benchmarks.Query; +using Benchmarks.RequestMiddleware; using Benchmarks.Serialization; namespace Benchmarks { @@ -8,7 +11,10 @@ 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), + typeof(ContainsMediaTypeParameters_Benchmarks), + typeof(PathIsRelationship_Benchmarks) }); switcher.Run(args); } diff --git a/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs b/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs new file mode 100644 index 0000000000..ed64c98335 --- /dev/null +++ b/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs @@ -0,0 +1,25 @@ +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/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs index dced1225a9..c3769eccbc 100644 --- a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs @@ -1,3 +1,4 @@ +using System; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; @@ -16,24 +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 string GetNamespaceFromPath(string path, string entityName) + internal static string GetNamespaceFromPath(string path, string entityName) { - var nSpace = string.Empty; - var segments = path.Split('/'); - - for (var i = 1; i < segments.Length; i++) + var entityNameSpan = entityName.AsSpan(); + var pathSpan = path.AsSpan(); + const char delimiter = '/'; + for (var i = 0; i < pathSpan.Length; i++) { - if (segments[i].ToLower() == entityName) - break; + 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; - nSpace += $"/{segments[i]}"; + if(lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter)) + { + return pathSpan.Slice(0, i).ToString(); + } + } + } + } } - return nSpace; + return string.Empty; } public string GetSelfRelationLink(string parent, string parentId, string child) diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index 5ec658873d..d567de200a 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; @@ -8,21 +9,19 @@ namespace JsonApiDotNetCore.Internal.Query public class RelatedAttrFilterQuery : BaseFilterQuery { private readonly IJsonApiContext _jsonApiContext; - + public RelatedAttrFilterQuery( - IJsonApiContext jsonApiCopntext, + IJsonApiContext jsonApiContext, FilterQuery filterQuery) { - _jsonApiContext = jsonApiCopntext; + _jsonApiContext = jsonApiContext; 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/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/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs index 6e2612c9a6..0ce54c8589 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; @@ -52,10 +53,23 @@ private static bool IsValidAcceptHeader(HttpContext context) return true; } - private static bool ContainsMediaTypeParameters(string mediaType) + internal static bool ContainsMediaTypeParameters(string mediaType) { - var mediaTypeArr = mediaType.Split(';'); - return (mediaTypeArr[0] == Constants.ContentType && mediaTypeArr.Length == 2); + 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) diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index 1ebf5aeea1..2665217fef 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; @@ -64,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 path = context.Request.Path.Value.Split('/'); if (context.Request.Query.Count > 0) { @@ -72,13 +70,47 @@ 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(); - IsRelationshipPath = path[path.Length - 2] == "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() { 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..34bf525e8e 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -235,5 +235,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); + } } }