diff --git a/benchmarks/BenchmarkResource.cs b/benchmarks/BenchmarkResource.cs index 0353078601..50c132c8a7 100644 --- a/benchmarks/BenchmarkResource.cs +++ b/benchmarks/BenchmarkResource.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace Benchmarks { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class BenchmarkResource : Identifiable { [Attr(PublicName = BenchmarkResourcePublicNames.NameAttr)] @@ -11,16 +13,4 @@ public sealed class BenchmarkResource : Identifiable [HasOne] public SubResource Child { get; set; } } - - public class SubResource : Identifiable - { - [Attr] - public string Value { get; set; } - } - - internal static class BenchmarkResourcePublicNames - { - public const string NameAttr = "full-name"; - public const string Type = "simple-types"; - } } diff --git a/benchmarks/BenchmarkResourcePublicNames.cs b/benchmarks/BenchmarkResourcePublicNames.cs new file mode 100644 index 0000000000..b97db1ea64 --- /dev/null +++ b/benchmarks/BenchmarkResourcePublicNames.cs @@ -0,0 +1,8 @@ +namespace Benchmarks +{ + internal static class BenchmarkResourcePublicNames + { + public const string NameAttr = "full-name"; + public const string Type = "simple-types"; + } +} \ No newline at end of file diff --git a/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs b/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs index 60b5f31ce1..3486ce30d9 100644 --- a/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs +++ b/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs @@ -4,6 +4,7 @@ namespace Benchmarks.LinkBuilder { + // ReSharper disable once ClassCanBeSealed.Global [MarkdownExporter, SimpleJob(launchCount: 3, warmupCount: 10, targetCount: 20), MemoryDiagnoser] public class LinkBuilderGetNamespaceFromPathBenchmarks { @@ -17,7 +18,7 @@ public class LinkBuilderGetNamespaceFromPathBenchmarks [Benchmark] public void UsingReadOnlySpan() => GetNamespaceFromPathUsingReadOnlySpan(RequestPath, ResourceName); - public static string GetNamespaceFromPathUsingStringSplit(string path, string resourceName) + private static void GetNamespaceFromPathUsingStringSplit(string path, string resourceName) { StringBuilder namespaceBuilder = new StringBuilder(path.Length); string[] segments = path.Split('/'); @@ -33,10 +34,10 @@ public static string GetNamespaceFromPathUsingStringSplit(string path, string re namespaceBuilder.Append(segments[index]); } - return namespaceBuilder.ToString(); + _ = namespaceBuilder.ToString(); } - public static string GetNamespaceFromPathUsingReadOnlySpan(string path, string resourceName) + private static void GetNamespaceFromPathUsingReadOnlySpan(string path, string resourceName) { ReadOnlySpan resourceNameSpan = resourceName.AsSpan(); ReadOnlySpan pathSpan = path.AsSpan(); @@ -57,14 +58,12 @@ public static string GetNamespaceFromPathUsingReadOnlySpan(string path, string r bool hasDelimiterAfterSegment = pathSpan.Length >= lastCharacterIndex + 1 && pathSpan[lastCharacterIndex].Equals(PathDelimiter); if (isAtEnd || hasDelimiterAfterSegment) { - return pathSpan.Slice(0, index).ToString(); + _ = pathSpan.Slice(0, index).ToString(); } } } } } - - return string.Empty; } } } diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs index 396f8786cb..963b0322e8 100644 --- a/benchmarks/Program.cs +++ b/benchmarks/Program.cs @@ -5,7 +5,7 @@ namespace Benchmarks { - internal class Program + internal static class Program { private static void Main(string[] args) { diff --git a/benchmarks/Query/QueryParserBenchmarks.cs b/benchmarks/Query/QueryParserBenchmarks.cs index 990d2a1995..f34f37f919 100644 --- a/benchmarks/Query/QueryParserBenchmarks.cs +++ b/benchmarks/Query/QueryParserBenchmarks.cs @@ -1,7 +1,7 @@ using System; -using System.Collections.Generic; using System.ComponentModel.Design; using BenchmarkDotNet.Attributes; +using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.QueryStrings; @@ -13,6 +13,7 @@ namespace Benchmarks.Query { + // ReSharper disable once ClassCanBeSealed.Global [MarkdownExporter, SimpleJob(launchCount: 3, warmupCount: 10, targetCount: 20), MemoryDiagnoser] public class QueryParserBenchmarks { @@ -43,11 +44,8 @@ private static QueryStringReader CreateQueryParameterDiscoveryForSort(IResourceG JsonApiRequest request, IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor) { var sortReader = new SortQueryStringParameterReader(request, resourceGraph); - - var readers = new List - { - sortReader - }; + + var readers = sortReader.AsEnumerable(); return new QueryStringReader(options, queryStringAccessor, readers, NullLoggerFactory.Instance); } @@ -65,10 +63,7 @@ private static QueryStringReader CreateQueryParameterDiscoveryForAll(IResourceGr var defaultsReader = new DefaultsQueryStringParameterReader(options); var nullsReader = new NullsQueryStringParameterReader(options); - var readers = new List - { - includeReader, filterReader, sortReader, sparseFieldSetReader, paginationReader, defaultsReader, nullsReader - }; + var readers = ArrayFactory.Create(includeReader, filterReader, sortReader, sparseFieldSetReader, paginationReader, defaultsReader, nullsReader); return new QueryStringReader(options, queryStringAccessor, readers, NullLoggerFactory.Instance); } @@ -94,15 +89,21 @@ public void DescendingSort() [Benchmark] public void ComplexQuery() => Run(100, () => { - var queryString = $"?filter[{BenchmarkResourcePublicNames.NameAttr}]=abc,eq:abc&sort=-{BenchmarkResourcePublicNames.NameAttr}&include=child&page[size]=1&fields[{BenchmarkResourcePublicNames.Type}]={BenchmarkResourcePublicNames.NameAttr}"; + const string resourceName = BenchmarkResourcePublicNames.Type; + const string attrName = BenchmarkResourcePublicNames.NameAttr; + + var queryString = $"?filter[{attrName}]=abc,eq:abc&sort=-{attrName}&include=child&page[size]=1&fields[{resourceName}]={attrName}"; _queryStringAccessor.SetQueryString(queryString); _queryStringReaderForAll.ReadAll(null); }); - private void Run(int iterations, Action action) { + private void Run(int iterations, Action action) + { for (int i = 0; i < iterations; i++) + { action(); + } } private sealed class FakeRequestQueryStringAccessor : IRequestQueryStringAccessor diff --git a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs index 5de48d8604..3d43a1fe87 100644 --- a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs @@ -12,6 +12,7 @@ namespace Benchmarks.Serialization { + // ReSharper disable once ClassCanBeSealed.Global [MarkdownExporter] public class JsonApiDeserializerBenchmarks { @@ -23,10 +24,7 @@ public class JsonApiDeserializerBenchmarks Id = "1", Attributes = new Dictionary { - { - "name", - Guid.NewGuid().ToString() - } + ["name"] = Guid.NewGuid().ToString() } } }); @@ -39,7 +37,10 @@ public JsonApiDeserializerBenchmarks() IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options); var targetedFields = new TargetedFields(); var request = new JsonApiRequest(); - _jsonApiDeserializer = new RequestDeserializer(resourceGraph, new ResourceFactory(new ServiceContainer()), targetedFields, new HttpContextAccessor(), request, options); + var resourceFactory = new ResourceFactory(new ServiceContainer()); + var httpContextAccessor = new HttpContextAccessor(); + + _jsonApiDeserializer = new RequestDeserializer(resourceGraph, resourceFactory, targetedFields, httpContextAccessor, request, options); } [Benchmark] diff --git a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs index f9f294c26b..c3f613ab56 100644 --- a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs @@ -11,10 +11,11 @@ namespace Benchmarks.Serialization { + // ReSharper disable once ClassCanBeSealed.Global [MarkdownExporter] public class JsonApiSerializerBenchmarks { - private static readonly BenchmarkResource _content = new BenchmarkResource + private static readonly BenchmarkResource Content = new BenchmarkResource { Id = 123, Name = Guid.NewGuid().ToString() @@ -53,6 +54,6 @@ private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resource } [Benchmark] - public object SerializeSimpleObject() => _jsonApiSerializer.Serialize(_content); + public object SerializeSimpleObject() => _jsonApiSerializer.Serialize(Content); } } diff --git a/benchmarks/SubResource.cs b/benchmarks/SubResource.cs new file mode 100644 index 0000000000..73536a87ae --- /dev/null +++ b/benchmarks/SubResource.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace Benchmarks +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class SubResource : Identifiable + { + [Attr] + public string Value { get; set; } + } +} diff --git a/src/Examples/GettingStarted/Data/SampleDbContext.cs b/src/Examples/GettingStarted/Data/SampleDbContext.cs index 95781423b6..b54011ff14 100644 --- a/src/Examples/GettingStarted/Data/SampleDbContext.cs +++ b/src/Examples/GettingStarted/Data/SampleDbContext.cs @@ -1,8 +1,10 @@ using GettingStarted.Models; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace GettingStarted.Data { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public class SampleDbContext : DbContext { public DbSet Books { get; set; } diff --git a/src/Examples/GettingStarted/Models/Book.cs b/src/Examples/GettingStarted/Models/Book.cs index 9e4ae01b50..9f15d3e3c9 100644 --- a/src/Examples/GettingStarted/Models/Book.cs +++ b/src/Examples/GettingStarted/Models/Book.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace GettingStarted.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Book : Identifiable { [Attr] diff --git a/src/Examples/GettingStarted/Models/Person.cs b/src/Examples/GettingStarted/Models/Person.cs index 35551bb311..495a4fe27b 100644 --- a/src/Examples/GettingStarted/Models/Person.cs +++ b/src/Examples/GettingStarted/Models/Person.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace GettingStarted.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Person : Identifiable { [Attr] diff --git a/src/Examples/GettingStarted/Program.cs b/src/Examples/GettingStarted/Program.cs index c7d55fcf4d..fad81e1bba 100644 --- a/src/Examples/GettingStarted/Program.cs +++ b/src/Examples/GettingStarted/Program.cs @@ -3,14 +3,14 @@ namespace GettingStarted { - public class Program + internal static class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } - public static IHostBuilder CreateHostBuilder(string[] args) => + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { diff --git a/src/Examples/GettingStarted/Startup.cs b/src/Examples/GettingStarted/Startup.cs index b97618f823..8d7eacdeea 100644 --- a/src/Examples/GettingStarted/Startup.cs +++ b/src/Examples/GettingStarted/Startup.cs @@ -1,5 +1,6 @@ using GettingStarted.Data; using GettingStarted.Models; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; @@ -27,6 +28,7 @@ public void ConfigureServices(IServiceCollection services) } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + [UsedImplicitly] public void Configure(IApplicationBuilder app, SampleDbContext context) { context.Database.EnsureDeleted(); diff --git a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs index 8c063c42d7..a438f44831 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs @@ -1,8 +1,12 @@ +using JetBrains.Annotations; using JsonApiDotNetCoreExample.Models; using Microsoft.EntityFrameworkCore; +// @formatter:wrap_chained_method_calls chop_always + namespace JsonApiDotNetCoreExample.Data { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class AppDbContext : DbContext { public DbSet TodoItems { get; set; } @@ -37,7 +41,7 @@ protected override void OnModelCreating(ModelBuilder builder) .OnDelete(DeleteBehavior.Cascade); builder.Entity() - .HasMany(t => t.ChildrenTodos) + .HasMany(t => t.ChildTodoItems) .WithOne(t => t.ParentTodo); builder.Entity() diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs index 9c91823669..6d4bc02cdb 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Hooks.Internal.Execution; @@ -10,7 +11,8 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class ArticleHooksDefinition : ResourceHooksDefinition
+ [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class ArticleHooksDefinition : ResourceHooksDefinition
{ public ArticleHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableHooksDefinition.cs index 19c0708ba2..845fc61eea 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableHooksDefinition.cs @@ -25,7 +25,8 @@ protected void DisallowLocked(IEnumerable resources) { throw new JsonApiException(new Error(HttpStatusCode.Forbidden) { - Title = $"You are not allowed to update fields or relationships of locked resource of type '{_resourceGraph.GetResourceContext().PublicName}'." + Title = "You are not allowed to update fields or relationships of " + + $"locked resource of type '{_resourceGraph.GetResourceContext().PublicName}'." }); } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs index 5ccfcf2447..8d73f9eaec 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Hooks.Internal.Execution; @@ -9,7 +10,8 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class PassportHooksDefinition : LockableHooksDefinition + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class PassportHooksDefinition : LockableHooksDefinition { public PassportHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs index 42c1b405e4..28e1d51955 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs @@ -1,12 +1,14 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExample.Definitions { - public class PersonHooksDefinition : LockableHooksDefinition + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class PersonHooksDefinition : LockableHooksDefinition { public PersonHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs index 4ff4508bf7..2ae1a543c6 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCore.Resources; @@ -7,15 +8,11 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class TagHooksDefinition : ResourceHooksDefinition + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class TagHooksDefinition : ResourceHooksDefinition { public TagHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } - public override IEnumerable BeforeCreate(IResourceHashSet affected, ResourcePipeline pipeline) - { - return base.BeforeCreate(affected, pipeline); - } - public override IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline) { return resources.Where(t => t.Name != "This should not be included"); diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemHooksDefinition.cs index f9e15f6547..892b12c7a8 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemHooksDefinition.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Hooks.Internal.Execution; @@ -9,7 +10,8 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class TodoItemHooksDefinition : LockableHooksDefinition + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class TodoItemHooksDefinition : LockableHooksDefinition { public TodoItemHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } @@ -26,8 +28,8 @@ public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = fal public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { - List todos = resourcesByRelationship.GetByRelationship().SelectMany(kvp => kvp.Value).ToList(); - DisallowLocked(todos); + List todoItems = resourcesByRelationship.GetByRelationship().SelectMany(kvp => kvp.Value).ToList(); + DisallowLocked(todoItems); } public override IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline) diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs index 65addba5a3..84de22fcdf 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Article : Identifiable { [Attr] diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/ArticleTag.cs b/src/Examples/JsonApiDotNetCoreExample/Models/ArticleTag.cs index b0e4d59435..7edc32ef75 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/ArticleTag.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/ArticleTag.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCoreExample.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ArticleTag { public int ArticleId { get; set; } diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs index cd023ba729..0b5b1d1acd 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Author : Identifiable { [Attr] diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/IIsLockable.cs b/src/Examples/JsonApiDotNetCoreExample/Models/IIsLockable.cs index fe7d07ad34..d59f5b8f4d 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/IIsLockable.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/IIsLockable.cs @@ -1,7 +1,7 @@ -namespace JsonApiDotNetCoreExample.Models +namespace JsonApiDotNetCoreExample.Models { public interface IIsLockable { - bool IsLocked { get; set; } + bool IsLocked { get; } } -} \ No newline at end of file +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/IdentifiableArticleTag.cs b/src/Examples/JsonApiDotNetCoreExample/Models/IdentifiableArticleTag.cs index 1d64298d7d..ba44bf56ad 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/IdentifiableArticleTag.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/IdentifiableArticleTag.cs @@ -1,9 +1,11 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { - public class IdentifiableArticleTag : Identifiable + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class IdentifiableArticleTag : Identifiable { public int ArticleId { get; set; } [HasOne] diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs index 0945d47938..23d4012dd9 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs @@ -1,9 +1,11 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { - public class Passport : Identifiable, IIsLockable + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Passport : Identifiable, IIsLockable { [Attr] public bool IsLocked { get; set; } diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs index 17f6a26473..45015c437f 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Person : Identifiable, IIsLockable { public bool IsLocked { get; set; } diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs index 995adea17e..b1b446d7f2 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs @@ -1,9 +1,11 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { - public class Tag : Identifiable + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Tag : Identifiable { [Attr] public string Name { get; set; } diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs index bc06420ad7..b9f0277b61 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { - public class TodoItem : Identifiable, IIsLockable + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class TodoItem : Identifiable, IIsLockable { public bool IsLocked { get; set; } @@ -28,6 +30,6 @@ public class TodoItem : Identifiable, IIsLockable public TodoItem ParentTodo { get; set; } [HasMany] - public IList ChildrenTodos { get; set; } + public IList ChildTodoItems { get; set; } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/User.cs b/src/Examples/JsonApiDotNetCoreExample/Models/User.cs index f61cf6e7ef..fd7852ec3b 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/User.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/User.cs @@ -1,9 +1,11 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { - public class User : Identifiable + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class User : Identifiable { [Attr] public string UserName { get; set; } diff --git a/src/Examples/JsonApiDotNetCoreExample/Program.cs b/src/Examples/JsonApiDotNetCoreExample/Program.cs index 8a892fcfe5..e2866c25ec 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Program.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Program.cs @@ -1,16 +1,17 @@ +using JsonApiDotNetCoreExample.Startups; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; namespace JsonApiDotNetCoreExample { - public class Program + internal static class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } - public static IHostBuilder CreateHostBuilder(string[] args) => + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs index 18d40e9ccd..0ee7454da6 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace JsonApiDotNetCoreExample +namespace JsonApiDotNetCoreExample.Startups { /// /// Empty startup class, required for integration tests. @@ -11,10 +10,6 @@ namespace JsonApiDotNetCoreExample /// public abstract class EmptyStartup { - protected EmptyStartup(IConfiguration configuration) - { - } - public virtual void ConfigureServices(IServiceCollection services) { } diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs index 553ad27999..fa7580865d 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -10,15 +10,14 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace JsonApiDotNetCoreExample +namespace JsonApiDotNetCoreExample.Startups { - public class Startup : EmptyStartup + public sealed class Startup : EmptyStartup { - private static readonly Version _postgresCiBuildVersion = new Version(9, 6); + private static readonly Version PostgresCiBuildVersion = new Version(9, 6); private readonly string _connectionString; public Startup(IConfiguration configuration) - : base(configuration) { string postgresPassword = Environment.GetEnvironmentVariable("PGPASSWORD") ?? "postgres"; _connectionString = configuration["Data:DefaultConnection"].Replace("###", postgresPassword); @@ -31,13 +30,13 @@ public override void ConfigureServices(IServiceCollection services) services.AddDbContext(options => { options.EnableSensitiveDataLogging(); - options.UseNpgsql(_connectionString, postgresOptions => postgresOptions.SetPostgresVersion(_postgresCiBuildVersion)); + options.UseNpgsql(_connectionString, postgresOptions => postgresOptions.SetPostgresVersion(PostgresCiBuildVersion)); }); services.AddJsonApi(ConfigureJsonApiOptions, discovery => discovery.AddCurrentAssembly()); } - protected void ConfigureJsonApiOptions(JsonApiOptions options) + private void ConfigureJsonApiOptions(JsonApiOptions options) { options.IncludeExceptionStackTraceInErrors = true; options.Namespace = "api/v1"; diff --git a/src/Examples/MultiDbContextExample/Data/DbContextA.cs b/src/Examples/MultiDbContextExample/Data/DbContextA.cs index 72fb233d5b..cb6000e051 100644 --- a/src/Examples/MultiDbContextExample/Data/DbContextA.cs +++ b/src/Examples/MultiDbContextExample/Data/DbContextA.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using MultiDbContextExample.Models; namespace MultiDbContextExample.Data { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class DbContextA : DbContext { public DbSet ResourceAs { get; set; } diff --git a/src/Examples/MultiDbContextExample/Data/DbContextB.cs b/src/Examples/MultiDbContextExample/Data/DbContextB.cs index 4b6c5e7690..b3e4e6e47f 100644 --- a/src/Examples/MultiDbContextExample/Data/DbContextB.cs +++ b/src/Examples/MultiDbContextExample/Data/DbContextB.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using MultiDbContextExample.Models; namespace MultiDbContextExample.Data { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class DbContextB : DbContext { public DbSet ResourceBs { get; set; } diff --git a/src/Examples/MultiDbContextExample/Models/ResourceA.cs b/src/Examples/MultiDbContextExample/Models/ResourceA.cs index 104611cffc..85cbf2b89a 100644 --- a/src/Examples/MultiDbContextExample/Models/ResourceA.cs +++ b/src/Examples/MultiDbContextExample/Models/ResourceA.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace MultiDbContextExample.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ResourceA : Identifiable { [Attr] diff --git a/src/Examples/MultiDbContextExample/Models/ResourceB.cs b/src/Examples/MultiDbContextExample/Models/ResourceB.cs index 1fb41b6f96..dd1739ee49 100644 --- a/src/Examples/MultiDbContextExample/Models/ResourceB.cs +++ b/src/Examples/MultiDbContextExample/Models/ResourceB.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace MultiDbContextExample.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ResourceB : Identifiable { [Attr] diff --git a/src/Examples/MultiDbContextExample/Program.cs b/src/Examples/MultiDbContextExample/Program.cs index c6e698750c..5d138239c0 100644 --- a/src/Examples/MultiDbContextExample/Program.cs +++ b/src/Examples/MultiDbContextExample/Program.cs @@ -3,14 +3,14 @@ namespace MultiDbContextExample { - public class Program + internal static class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } - public static IHostBuilder CreateHostBuilder(string[] args) + private static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); diff --git a/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs b/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs index 574695700b..44174777f8 100644 --- a/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs +++ b/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Repositories; @@ -8,7 +9,8 @@ namespace MultiDbContextExample.Repositories { - public class DbContextARepository : EntityFrameworkCoreRepository + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class DbContextARepository : EntityFrameworkCoreRepository where TResource : class, IIdentifiable { public DbContextARepository(ITargetedFields targetedFields, DbContextResolver contextResolver, diff --git a/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs b/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs index 098a580579..eb2cc5fc2b 100644 --- a/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs +++ b/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Repositories; @@ -8,7 +9,8 @@ namespace MultiDbContextExample.Repositories { - public class DbContextBRepository : EntityFrameworkCoreRepository + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class DbContextBRepository : EntityFrameworkCoreRepository where TResource : class, IIdentifiable { public DbContextBRepository(ITargetedFields targetedFields, DbContextResolver contextResolver, diff --git a/src/Examples/MultiDbContextExample/Startup.cs b/src/Examples/MultiDbContextExample/Startup.cs index 09e120cd01..b1b7b6c57e 100644 --- a/src/Examples/MultiDbContextExample/Startup.cs +++ b/src/Examples/MultiDbContextExample/Startup.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -10,7 +11,7 @@ namespace MultiDbContextExample { - public class Startup + public sealed class Startup { // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) @@ -28,6 +29,7 @@ public void ConfigureServices(IServiceCollection services) } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + [UsedImplicitly] public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DbContextA dbContextA, DbContextB dbContextB) { diff --git a/src/Examples/NoEntityFrameworkExample/Data/AppDbContext.cs b/src/Examples/NoEntityFrameworkExample/Data/AppDbContext.cs index abf11869dd..336951eec3 100644 --- a/src/Examples/NoEntityFrameworkExample/Data/AppDbContext.cs +++ b/src/Examples/NoEntityFrameworkExample/Data/AppDbContext.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using NoEntityFrameworkExample.Models; namespace NoEntityFrameworkExample.Data { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class AppDbContext : DbContext { public DbSet WorkItems { get; set; } diff --git a/src/Examples/NoEntityFrameworkExample/Models/WorkItem.cs b/src/Examples/NoEntityFrameworkExample/Models/WorkItem.cs index a3a929bd5a..20d381a2ba 100644 --- a/src/Examples/NoEntityFrameworkExample/Models/WorkItem.cs +++ b/src/Examples/NoEntityFrameworkExample/Models/WorkItem.cs @@ -1,9 +1,11 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace NoEntityFrameworkExample.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class WorkItem : Identifiable { [Attr] diff --git a/src/Examples/NoEntityFrameworkExample/Program.cs b/src/Examples/NoEntityFrameworkExample/Program.cs index a4d260e5a6..c5b2eaa194 100755 --- a/src/Examples/NoEntityFrameworkExample/Program.cs +++ b/src/Examples/NoEntityFrameworkExample/Program.cs @@ -3,14 +3,14 @@ namespace NoEntityFrameworkExample { - public class Program + internal static class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } - public static IHostBuilder CreateHostBuilder(string[] args) => + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { diff --git a/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs b/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs index 363db95d4b..c0fdc425cf 100644 --- a/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs +++ b/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Dapper; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; using Microsoft.Extensions.Configuration; @@ -13,6 +14,7 @@ namespace NoEntityFrameworkExample.Services { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class WorkItemService : IResourceService { private readonly string _connectionString; @@ -25,16 +27,19 @@ public WorkItemService(IConfiguration configuration) public async Task> GetAsync(CancellationToken cancellationToken) { - return (await QueryAsync(async connection => - await connection.QueryAsync(new CommandDefinition(@"select * from ""WorkItems""", cancellationToken: cancellationToken)))).ToList(); + const string commandText = @"select * from ""WorkItems"""; + var commandDefinition = new CommandDefinition(commandText, cancellationToken: cancellationToken); + + return await QueryAsync(async connection => await connection.QueryAsync(commandDefinition)); } public async Task GetAsync(int id, CancellationToken cancellationToken) { - var query = await QueryAsync(async connection => - await connection.QueryAsync(new CommandDefinition(@"select * from ""WorkItems"" where ""Id""=@id", new {id}, cancellationToken: cancellationToken))); + const string commandText = @"select * from ""WorkItems"" where ""Id""=@id"; + var commandDefinition = new CommandDefinition(commandText, new {id}, cancellationToken: cancellationToken); - return query.Single(); + var workItems = await QueryAsync(async connection => await connection.QueryAsync(commandDefinition)); + return workItems.Single(); } public Task GetSecondaryAsync(int id, string relationshipName, CancellationToken cancellationToken) @@ -49,17 +54,16 @@ public Task GetRelationshipAsync(int id, string relationshipName, Cancel public async Task CreateAsync(WorkItem resource, CancellationToken cancellationToken) { - return (await QueryAsync(async connection => + const string commandText = @"insert into ""WorkItems"" (""Title"", ""IsBlocked"", ""DurationInHours"", ""ProjectId"") values " + + @"(@title, @isBlocked, @durationInHours, @projectId) returning ""Id"", ""Title"", ""IsBlocked"", ""DurationInHours"", ""ProjectId"""; + + var commandDefinition = new CommandDefinition(commandText, new { - var query = - @"insert into ""WorkItems"" (""Title"", ""IsBlocked"", ""DurationInHours"", ""ProjectId"") values " + - @"(@title, @isBlocked, @durationInHours, @projectId) returning ""Id"", ""Title"", ""IsBlocked"", ""DurationInHours"", ""ProjectId"""; - - return await connection.QueryAsync(new CommandDefinition(query, new - { - title = resource.Title, isBlocked = resource.IsBlocked, durationInHours = resource.DurationInHours, projectId = resource.ProjectId - }, cancellationToken: cancellationToken)); - })).SingleOrDefault(); + title = resource.Title, isBlocked = resource.IsBlocked, durationInHours = resource.DurationInHours, projectId = resource.ProjectId + }, cancellationToken: cancellationToken); + + var workItems = await QueryAsync(async connection => await connection.QueryAsync(commandDefinition)); + return workItems.Single(); } public Task AddToToManyRelationshipAsync(int primaryId, string relationshipName, ISet secondaryResourceIds, CancellationToken cancellationToken) @@ -79,8 +83,10 @@ public Task SetRelationshipAsync(int primaryId, string relationshipName, object public async Task DeleteAsync(int id, CancellationToken cancellationToken) { + const string commandText = @"delete from ""WorkItems"" where ""Id""=@id"; + await QueryAsync(async connection => - await connection.QueryAsync(new CommandDefinition(@"delete from ""WorkItems"" where ""Id""=@id", new {id}, cancellationToken: cancellationToken))); + await connection.QueryAsync(new CommandDefinition(commandText, new {id}, cancellationToken: cancellationToken))); } public Task RemoveFromToManyRelationshipAsync(int primaryId, string relationshipName, ISet secondaryResourceIds, CancellationToken cancellationToken) @@ -88,13 +94,13 @@ public Task RemoveFromToManyRelationshipAsync(int primaryId, string relationship throw new NotImplementedException(); } - private async Task> QueryAsync(Func>> query) + private async Task> QueryAsync(Func>> query) { - using IDbConnection dbConnection = GetConnection; + using IDbConnection dbConnection = new NpgsqlConnection(_connectionString); dbConnection.Open(); - return await query(dbConnection); - } - private IDbConnection GetConnection => new NpgsqlConnection(_connectionString); + IEnumerable resources = await query(dbConnection); + return resources.ToList(); + } } } diff --git a/src/Examples/NoEntityFrameworkExample/Startup.cs b/src/Examples/NoEntityFrameworkExample/Startup.cs index 4754f6003d..6699be3376 100644 --- a/src/Examples/NoEntityFrameworkExample/Startup.cs +++ b/src/Examples/NoEntityFrameworkExample/Startup.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Builder; @@ -11,7 +12,7 @@ namespace NoEntityFrameworkExample { - public class Startup + public sealed class Startup { private readonly string _connectionString; @@ -39,6 +40,7 @@ public void ConfigureServices(IServiceCollection services) } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + [UsedImplicitly] public void Configure(IApplicationBuilder app, AppDbContext context) { context.Database.EnsureCreated(); diff --git a/src/Examples/ReportsExample/Models/Report.cs b/src/Examples/ReportsExample/Models/Report.cs index 8125f1f0ab..6635687a1d 100644 --- a/src/Examples/ReportsExample/Models/Report.cs +++ b/src/Examples/ReportsExample/Models/Report.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace ReportsExample.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Report : Identifiable { [Attr] diff --git a/src/Examples/ReportsExample/Models/ReportStatistics.cs b/src/Examples/ReportsExample/Models/ReportStatistics.cs index 85b1ffdf1f..53c2c2d2ee 100644 --- a/src/Examples/ReportsExample/Models/ReportStatistics.cs +++ b/src/Examples/ReportsExample/Models/ReportStatistics.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace ReportsExample.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ReportStatistics { public string ProgressIndication { get; set; } diff --git a/src/Examples/ReportsExample/Program.cs b/src/Examples/ReportsExample/Program.cs index d2d216af50..ee8edaa620 100644 --- a/src/Examples/ReportsExample/Program.cs +++ b/src/Examples/ReportsExample/Program.cs @@ -3,14 +3,14 @@ namespace ReportsExample { - public class Program + internal static class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } - public static IHostBuilder CreateHostBuilder(string[] args) => + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { diff --git a/src/Examples/ReportsExample/Services/ReportService.cs b/src/Examples/ReportsExample/Services/ReportService.cs index 6b13e1f91d..2fcb4367f0 100644 --- a/src/Examples/ReportsExample/Services/ReportService.cs +++ b/src/Examples/ReportsExample/Services/ReportService.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; using ReportsExample.Models; namespace ReportsExample.Services { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public class ReportService : IGetAllService { private readonly ILogger _logger; @@ -21,12 +22,12 @@ public Task> GetAsync(CancellationToken cancellation { _logger.LogInformation("GetAsync"); - IReadOnlyCollection reports = GetReports().ToList(); + var reports = GetReports(); return Task.FromResult(reports); } - private IEnumerable GetReports() + private IReadOnlyCollection GetReports() { return new List { diff --git a/src/JsonApiDotNetCore/ArgumentGuard.cs b/src/JsonApiDotNetCore/ArgumentGuard.cs new file mode 100644 index 0000000000..28640848a2 --- /dev/null +++ b/src/JsonApiDotNetCore/ArgumentGuard.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore +{ + internal static class ArgumentGuard + { + [AssertionMethod] + [ContractAnnotation("value: null => halt")] + public static void NotNull([CanBeNull] [NoEnumeration] T value, [NotNull] [InvokerParameterName] string name) + where T : class + { + if (value is null) + { + throw new ArgumentNullException(name); + } + } + + [AssertionMethod] + [ContractAnnotation("value: null => halt")] + public static void NotNullNorEmpty([CanBeNull] IEnumerable value, [NotNull] [InvokerParameterName] string name) + { + NotNull(value, name); + + if (!value.Any()) + { + throw new ArgumentException("Collection cannot be empty.", name); + } + } + } +} diff --git a/src/JsonApiDotNetCore/ArrayFactory.cs b/src/JsonApiDotNetCore/ArrayFactory.cs new file mode 100644 index 0000000000..44eb3ba3f6 --- /dev/null +++ b/src/JsonApiDotNetCore/ArrayFactory.cs @@ -0,0 +1,10 @@ +namespace JsonApiDotNetCore +{ + internal static class ArrayFactory + { + public static T[] Create(params T[] items) + { + return items; + } + } +} diff --git a/src/JsonApiDotNetCore/AtomicOperations/EntityFrameworkCoreTransaction.cs b/src/JsonApiDotNetCore/AtomicOperations/EntityFrameworkCoreTransaction.cs index f863a03d1e..edb1cdd45d 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/EntityFrameworkCoreTransaction.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/EntityFrameworkCoreTransaction.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; @@ -10,6 +11,7 @@ namespace JsonApiDotNetCore.AtomicOperations /// /// Represents an Entity Framework Core transaction in an atomic:operations request. /// + [PublicAPI] public sealed class EntityFrameworkCoreTransaction : IOperationsTransaction { private readonly IDbContextTransaction _transaction; @@ -20,8 +22,11 @@ public sealed class EntityFrameworkCoreTransaction : IOperationsTransaction public EntityFrameworkCoreTransaction(IDbContextTransaction transaction, DbContext dbContext) { - _transaction = transaction ?? throw new ArgumentNullException(nameof(transaction)); - _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + ArgumentGuard.NotNull(transaction, nameof(transaction)); + ArgumentGuard.NotNull(dbContext, nameof(dbContext)); + + _transaction = transaction; + _dbContext = dbContext; } /// diff --git a/src/JsonApiDotNetCore/AtomicOperations/EntityFrameworkCoreTransactionFactory.cs b/src/JsonApiDotNetCore/AtomicOperations/EntityFrameworkCoreTransactionFactory.cs index afde631c33..e115c133be 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/EntityFrameworkCoreTransactionFactory.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/EntityFrameworkCoreTransactionFactory.cs @@ -1,4 +1,3 @@ -using System; using System.Threading; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; @@ -17,8 +16,11 @@ public sealed class EntityFrameworkCoreTransactionFactory : IOperationsTransacti public EntityFrameworkCoreTransactionFactory(IDbContextResolver dbContextResolver, IJsonApiOptions options) { - _dbContextResolver = dbContextResolver ?? throw new ArgumentNullException(nameof(dbContextResolver)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + ArgumentGuard.NotNull(dbContextResolver, nameof(dbContextResolver)); + ArgumentGuard.NotNull(options, nameof(options)); + + _dbContextResolver = dbContextResolver; + _options = options; } /// diff --git a/src/JsonApiDotNetCore/AtomicOperations/IOperationProcessorAccessor.cs b/src/JsonApiDotNetCore/AtomicOperations/IOperationProcessorAccessor.cs index ca4aa424cf..3045e33f7d 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/IOperationProcessorAccessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/IOperationProcessorAccessor.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; using JsonApiDotNetCore.AtomicOperations.Processors; using JsonApiDotNetCore.Resources; @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.AtomicOperations { /// - /// Retrieves a instance from the D/I container and invokes a method on it. + /// Retrieves an instance from the D/I container and invokes a method on it. /// public interface IOperationProcessorAccessor { diff --git a/src/JsonApiDotNetCore/AtomicOperations/IOperationsTransaction.cs b/src/JsonApiDotNetCore/AtomicOperations/IOperationsTransaction.cs index 5e36b5ff03..16f7e6c4e5 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/IOperationsTransaction.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/IOperationsTransaction.cs @@ -1,12 +1,14 @@ using System; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; namespace JsonApiDotNetCore.AtomicOperations { /// /// Represents the overarching transaction in an atomic:operations request. /// + [PublicAPI] public interface IOperationsTransaction : IAsyncDisposable { /// diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs index ae475027c5..2c8b7ab53c 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs @@ -20,8 +20,8 @@ public void Reset() /// public void Declare(string localId, string resourceType) { - if (localId == null) throw new ArgumentNullException(nameof(localId)); - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(localId, nameof(localId)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); AssertIsNotDeclared(localId); @@ -43,9 +43,9 @@ private void AssertIsNotDeclared(string localId) /// public void Assign(string localId, string resourceType, string stringId) { - if (localId == null) throw new ArgumentNullException(nameof(localId)); - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); - if (stringId == null) throw new ArgumentNullException(nameof(stringId)); + ArgumentGuard.NotNull(localId, nameof(localId)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + ArgumentGuard.NotNull(stringId, nameof(stringId)); AssertIsDeclared(localId); @@ -64,8 +64,8 @@ public void Assign(string localId, string resourceType, string stringId) /// public string GetValue(string localId, string resourceType) { - if (localId == null) throw new ArgumentNullException(nameof(localId)); - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(localId, nameof(localId)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); AssertIsDeclared(localId); diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs index c5e2a09f03..95ce18da6d 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; @@ -10,6 +10,7 @@ namespace JsonApiDotNetCore.AtomicOperations /// /// Validates declaration, assignment and reference of local IDs within a list of operations. /// + [PublicAPI] public sealed class LocalIdValidator { private readonly ILocalIdTracker _localIdTracker; @@ -17,12 +18,17 @@ public sealed class LocalIdValidator public LocalIdValidator(ILocalIdTracker localIdTracker, IResourceContextProvider resourceContextProvider) { - _localIdTracker = localIdTracker ?? throw new ArgumentNullException(nameof(localIdTracker)); - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + + _localIdTracker = localIdTracker; + _resourceContextProvider = resourceContextProvider; } public void Validate(IEnumerable operations) { + ArgumentGuard.NotNull(operations, nameof(operations)); + _localIdTracker.Reset(); int operationIndex = 0; @@ -31,28 +37,10 @@ public void Validate(IEnumerable operations) { foreach (var operation in operations) { - if (operation.Kind == OperationKind.CreateResource) - { - DeclareLocalId(operation.Resource); - } - else - { - AssertLocalIdIsAssigned(operation.Resource); - } - - foreach (var secondaryResource in operation.GetSecondaryResources()) - { - AssertLocalIdIsAssigned(secondaryResource); - } - - if (operation.Kind == OperationKind.CreateResource) - { - AssignLocalId(operation); - } + ValidateOperation(operation); operationIndex++; } - } catch (JsonApiException exception) { @@ -65,6 +53,28 @@ public void Validate(IEnumerable operations) } } + private void ValidateOperation(OperationContainer operation) + { + if (operation.Kind == OperationKind.CreateResource) + { + DeclareLocalId(operation.Resource); + } + else + { + AssertLocalIdIsAssigned(operation.Resource); + } + + foreach (var secondaryResource in operation.GetSecondaryResources()) + { + AssertLocalIdIsAssigned(secondaryResource); + } + + if (operation.Kind == OperationKind.CreateResource) + { + AssignLocalId(operation); + } + } + private void DeclareLocalId(IIdentifiable resource) { if (resource.LocalId != null) diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs index 31dea19969..551e564358 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.AtomicOperations.Processors; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; @@ -10,6 +11,7 @@ namespace JsonApiDotNetCore.AtomicOperations { /// + [PublicAPI] public class OperationProcessorAccessor : IOperationProcessorAccessor { private readonly IResourceContextProvider _resourceContextProvider; @@ -18,14 +20,17 @@ public class OperationProcessorAccessor : IOperationProcessorAccessor public OperationProcessorAccessor(IResourceContextProvider resourceContextProvider, IServiceProvider serviceProvider) { - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); + + _resourceContextProvider = resourceContextProvider; + _serviceProvider = serviceProvider; } /// public Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken) { - if (operation == null) throw new ArgumentNullException(nameof(operation)); + ArgumentGuard.NotNull(operation, nameof(operation)); var processor = ResolveProcessor(operation); return processor.ProcessAsync(operation, cancellationToken); @@ -45,19 +50,33 @@ private static Type GetProcessorInterface(OperationKind kind) switch (kind) { case OperationKind.CreateResource: + { return typeof(ICreateProcessor<,>); + } case OperationKind.UpdateResource: + { return typeof(IUpdateProcessor<,>); + } case OperationKind.DeleteResource: + { return typeof(IDeleteProcessor<,>); + } case OperationKind.SetRelationship: + { return typeof(ISetRelationshipProcessor<,>); + } case OperationKind.AddToRelationship: + { return typeof(IAddToRelationshipProcessor<,>); + } case OperationKind.RemoveFromRelationship: + { return typeof(IRemoveFromRelationshipProcessor<,>); + } default: + { throw new NotSupportedException($"Unknown operation kind '{kind}'."); + } } } } diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs index 80f2a2a454..4cbd1f938b 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; @@ -12,6 +13,7 @@ namespace JsonApiDotNetCore.AtomicOperations { /// + [PublicAPI] public class OperationsProcessor : IOperationsProcessor { private readonly IOperationProcessorAccessor _operationProcessorAccessor; @@ -26,12 +28,19 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso IOperationsTransactionFactory operationsTransactionFactory, ILocalIdTracker localIdTracker, IResourceContextProvider resourceContextProvider, IJsonApiRequest request, ITargetedFields targetedFields) { - _operationProcessorAccessor = operationProcessorAccessor ?? throw new ArgumentNullException(nameof(operationProcessorAccessor)); - _operationsTransactionFactory = operationsTransactionFactory ?? throw new ArgumentNullException(nameof(operationsTransactionFactory)); - _localIdTracker = localIdTracker ?? throw new ArgumentNullException(nameof(localIdTracker)); - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); - _request = request ?? throw new ArgumentNullException(nameof(request)); - _targetedFields = targetedFields ?? throw new ArgumentNullException(nameof(targetedFields)); + ArgumentGuard.NotNull(operationProcessorAccessor, nameof(operationProcessorAccessor)); + ArgumentGuard.NotNull(operationsTransactionFactory, nameof(operationsTransactionFactory)); + ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); + + _operationProcessorAccessor = operationProcessorAccessor; + _operationsTransactionFactory = operationsTransactionFactory; + _localIdTracker = localIdTracker; + _resourceContextProvider = resourceContextProvider; + _request = request; + _targetedFields = targetedFields; _localIdValidator = new LocalIdValidator(_localIdTracker, _resourceContextProvider); } @@ -39,7 +48,7 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso public virtual async Task> ProcessAsync(IList operations, CancellationToken cancellationToken) { - if (operations == null) throw new ArgumentNullException(nameof(operations)); + ArgumentGuard.NotNull(operations, nameof(operations)); _localIdValidator.Validate(operations); _localIdTracker.Reset(); @@ -55,7 +64,7 @@ public virtual async Task> ProcessAsync(IList> ProcessAsync(IList ProcessOperation(OperationContainer operation, + protected virtual async Task ProcessOperationAsync(OperationContainer operation, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/AddToRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/AddToRelationshipProcessor.cs index 8aeba25d8f..c988d8da00 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/AddToRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/AddToRelationshipProcessor.cs @@ -1,12 +1,13 @@ -using System; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.AtomicOperations.Processors { /// + [PublicAPI] public class AddToRelationshipProcessor : IAddToRelationshipProcessor where TResource : class, IIdentifiable { @@ -14,14 +15,16 @@ public class AddToRelationshipProcessor : IAddToRelationshipProc public AddToRelationshipProcessor(IAddToRelationshipService service) { - _service = service ?? throw new ArgumentNullException(nameof(service)); + ArgumentGuard.NotNull(service, nameof(service)); + + _service = service; } /// public virtual async Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken) { - if (operation == null) throw new ArgumentNullException(nameof(operation)); + ArgumentGuard.NotNull(operation, nameof(operation)); var primaryId = (TId) operation.Resource.GetTypedId(); var secondaryResourceIds = operation.GetSecondaryResources(); diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/CreateProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/CreateProcessor.cs index 062e096910..9c8547a676 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/CreateProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/CreateProcessor.cs @@ -1,6 +1,6 @@ -using System; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; @@ -8,6 +8,7 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors { /// + [PublicAPI] public class CreateProcessor : ICreateProcessor where TResource : class, IIdentifiable { @@ -18,16 +19,20 @@ public class CreateProcessor : ICreateProcessor public CreateProcessor(ICreateService service, ILocalIdTracker localIdTracker, IResourceContextProvider resourceContextProvider) { - _service = service ?? throw new ArgumentNullException(nameof(service)); - _localIdTracker = localIdTracker ?? throw new ArgumentNullException(nameof(localIdTracker)); - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + ArgumentGuard.NotNull(service, nameof(service)); + ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + + _service = service; + _localIdTracker = localIdTracker; + _resourceContextProvider = resourceContextProvider; } /// public virtual async Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken) { - if (operation == null) throw new ArgumentNullException(nameof(operation)); + ArgumentGuard.NotNull(operation, nameof(operation)); var newResource = await _service.CreateAsync((TResource) operation.Resource, cancellationToken); diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/DeleteProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/DeleteProcessor.cs index dea9ba72da..dd91f23384 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/DeleteProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/DeleteProcessor.cs @@ -1,12 +1,13 @@ -using System; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.AtomicOperations.Processors { /// + [PublicAPI] public class DeleteProcessor : IDeleteProcessor where TResource : class, IIdentifiable { @@ -14,14 +15,16 @@ public class DeleteProcessor : IDeleteProcessor public DeleteProcessor(IDeleteService service) { - _service = service ?? throw new ArgumentNullException(nameof(service)); + ArgumentGuard.NotNull(service, nameof(service)); + + _service = service; } /// public virtual async Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken) { - if (operation == null) throw new ArgumentNullException(nameof(operation)); + ArgumentGuard.NotNull(operation, nameof(operation)); var id = (TId) operation.Resource.GetTypedId(); await _service.DeleteAsync(id, cancellationToken); diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/IAddToRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/IAddToRelationshipProcessor.cs index 965c704e0a..113771ab4d 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/IAddToRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/IAddToRelationshipProcessor.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; +// ReSharper disable UnusedTypeParameter + namespace JsonApiDotNetCore.AtomicOperations.Processors { /// @@ -7,6 +10,7 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors /// /// The resource type. /// The resource identifier type. + [PublicAPI] public interface IAddToRelationshipProcessor : IOperationProcessor where TResource : class, IIdentifiable { diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/ICreateProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/ICreateProcessor.cs index 846d6fc39a..79ffaf4090 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/ICreateProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/ICreateProcessor.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; +// ReSharper disable UnusedTypeParameter + namespace JsonApiDotNetCore.AtomicOperations.Processors { /// @@ -7,6 +10,7 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors /// /// The resource type. /// The resource identifier type. + [PublicAPI] public interface ICreateProcessor : IOperationProcessor where TResource : class, IIdentifiable { diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/IDeleteProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/IDeleteProcessor.cs index bb3efc30b0..3c5fbff948 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/IDeleteProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/IDeleteProcessor.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; +// ReSharper disable UnusedTypeParameter + namespace JsonApiDotNetCore.AtomicOperations.Processors { /// @@ -7,6 +10,7 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors /// /// The resource type. /// The resource identifier type. + [PublicAPI] public interface IDeleteProcessor : IOperationProcessor where TResource : class, IIdentifiable { diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/IRemoveFromRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/IRemoveFromRelationshipProcessor.cs index ae5d80c55a..02ce98d21d 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/IRemoveFromRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/IRemoveFromRelationshipProcessor.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; +// ReSharper disable UnusedTypeParameter + namespace JsonApiDotNetCore.AtomicOperations.Processors { /// @@ -7,6 +10,7 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors /// /// /// + [PublicAPI] public interface IRemoveFromRelationshipProcessor : IOperationProcessor where TResource : class, IIdentifiable { diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/ISetRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/ISetRelationshipProcessor.cs index f99c91d672..4943cdf97f 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/ISetRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/ISetRelationshipProcessor.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; +// ReSharper disable UnusedTypeParameter + namespace JsonApiDotNetCore.AtomicOperations.Processors { /// @@ -7,6 +10,7 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors /// /// The resource type. /// The resource identifier type. + [PublicAPI] public interface ISetRelationshipProcessor : IOperationProcessor where TResource : class, IIdentifiable { diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/IUpdateProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/IUpdateProcessor.cs index f4f403c073..7c8967e8e7 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/IUpdateProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/IUpdateProcessor.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; +// ReSharper disable UnusedTypeParameter + namespace JsonApiDotNetCore.AtomicOperations.Processors { /// @@ -8,6 +11,7 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors /// /// The resource type. /// The resource identifier type. + [PublicAPI] public interface IUpdateProcessor : IOperationProcessor where TResource : class, IIdentifiable { diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/RemoveFromRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/RemoveFromRelationshipProcessor.cs index 2ed9152d93..5a3c339417 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/RemoveFromRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/RemoveFromRelationshipProcessor.cs @@ -1,12 +1,13 @@ -using System; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.AtomicOperations.Processors { /// + [PublicAPI] public class RemoveFromRelationshipProcessor : IRemoveFromRelationshipProcessor where TResource : class, IIdentifiable { @@ -14,14 +15,16 @@ public class RemoveFromRelationshipProcessor : IRemoveFromRelati public RemoveFromRelationshipProcessor(IRemoveFromRelationshipService service) { - _service = service ?? throw new ArgumentNullException(nameof(service)); + ArgumentGuard.NotNull(service, nameof(service)); + + _service = service; } /// public virtual async Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken) { - if (operation == null) throw new ArgumentNullException(nameof(operation)); + ArgumentGuard.NotNull(operation, nameof(operation)); var primaryId = (TId) operation.Resource.GetTypedId(); var secondaryResourceIds = operation.GetSecondaryResources(); diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs index 979f44237b..1f9e825537 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs @@ -1,7 +1,7 @@ -using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Services; @@ -9,6 +9,7 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors { /// + [PublicAPI] public class SetRelationshipProcessor : ISetRelationshipProcessor where TResource : class, IIdentifiable { @@ -16,14 +17,16 @@ public class SetRelationshipProcessor : ISetRelationshipProcesso public SetRelationshipProcessor(ISetRelationshipService service) { - _service = service ?? throw new ArgumentNullException(nameof(service)); + ArgumentGuard.NotNull(service, nameof(service)); + + _service = service; } /// public virtual async Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken) { - if (operation == null) throw new ArgumentNullException(nameof(operation)); + ArgumentGuard.NotNull(operation, nameof(operation)); var primaryId = (TId) operation.Resource.GetTypedId(); object rightValue = GetRelationshipRightValue(operation); diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/UpdateProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/UpdateProcessor.cs index 25f3232ffc..f1828caaaf 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/UpdateProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/UpdateProcessor.cs @@ -1,12 +1,13 @@ -using System; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.AtomicOperations.Processors { /// + [PublicAPI] public class UpdateProcessor : IUpdateProcessor where TResource : class, IIdentifiable { @@ -14,14 +15,16 @@ public class UpdateProcessor : IUpdateProcessor public UpdateProcessor(IUpdateService service) { - _service = service ?? throw new ArgumentNullException(nameof(service)); + ArgumentGuard.NotNull(service, nameof(service)); + + _service = service; } /// public virtual async Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken) { - if (operation == null) throw new ArgumentNullException(nameof(operation)); + ArgumentGuard.NotNull(operation, nameof(operation)); var resource = (TResource) operation.Resource; var updated = await _service.UpdateAsync(resource.Id, resource, cancellationToken); diff --git a/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs index b176df1ead..f0a4e6715e 100644 --- a/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs @@ -1,4 +1,3 @@ -using System; using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; @@ -22,8 +21,8 @@ public static class ApplicationBuilderExtensions /// public static void UseJsonApi(this IApplicationBuilder builder) { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - + ArgumentGuard.NotNull(builder, nameof(builder)); + using var scope = builder.ApplicationServices.GetRequiredService().CreateScope(); var inverseNavigationResolver = scope.ServiceProvider.GetRequiredService(); inverseNavigationResolver.Resolve(); diff --git a/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs b/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs index d421102c08..ca71ae0470 100644 --- a/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs +++ b/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs @@ -9,14 +9,16 @@ public sealed class GenericServiceFactory : IGenericServiceFactory public GenericServiceFactory(IRequestScopedServiceProvider serviceProvider) { - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); + + _serviceProvider = serviceProvider; } /// public TInterface Get(Type openGenericType, Type resourceType) { - if (openGenericType == null) throw new ArgumentNullException(nameof(openGenericType)); - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(openGenericType, nameof(openGenericType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); return GetInternal(openGenericType, resourceType); } @@ -24,9 +26,9 @@ public TInterface Get(Type openGenericType, Type resourceType) /// public TInterface Get(Type openGenericType, Type resourceType, Type keyType) { - if (openGenericType == null) throw new ArgumentNullException(nameof(openGenericType)); - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); - if (keyType == null) throw new ArgumentNullException(nameof(keyType)); + ArgumentGuard.NotNull(openGenericType, nameof(openGenericType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + ArgumentGuard.NotNull(keyType, nameof(keyType)); return GetInternal(openGenericType, resourceType, keyType); } diff --git a/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs b/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs index 52acdc14f2..63177fdfd2 100644 --- a/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs +++ b/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Configuration { @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Configuration /// Represents the Service Locator design pattern. Used to obtain object instances for types are not known until runtime. /// This is only used by resource hooks and subject to be removed in a future version. /// + [PublicAPI] public interface IGenericServiceFactory { /// diff --git a/src/JsonApiDotNetCore/Configuration/IInverseNavigationResolver.cs b/src/JsonApiDotNetCore/Configuration/IInverseNavigationResolver.cs index b726ccf71f..5a91192eed 100644 --- a/src/JsonApiDotNetCore/Configuration/IInverseNavigationResolver.cs +++ b/src/JsonApiDotNetCore/Configuration/IInverseNavigationResolver.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Configuration @@ -11,6 +12,7 @@ namespace JsonApiDotNetCore.Configuration /// that depend on the inverse navigation property (BeforeImplicitUpdateRelationship), /// you will need to override this service, or set explicitly. /// + [PublicAPI] public interface IInverseNavigationResolver { /// diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index b12fd6c497..ab46a78af2 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -187,6 +187,13 @@ public interface IJsonApiOptions /// JsonSerializerSettings SerializerSettings { get; } - internal DefaultContractResolver SerializerContractResolver => (DefaultContractResolver) SerializerSettings.ContractResolver; + internal NamingStrategy SerializerNamingStrategy + { + get + { + var contractResolver = SerializerSettings.ContractResolver as DefaultContractResolver; + return contractResolver?.NamingStrategy ?? JsonApiOptions.DefaultNamingStrategy; + } + } } } diff --git a/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs index 8a11d587f1..a02045257c 100644 --- a/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -9,6 +10,7 @@ namespace JsonApiDotNetCore.Configuration /// /// Enables retrieving the exposed resource fields (attributes and relationships) of resources registered in the resource graph. /// + [PublicAPI] public interface IResourceGraph : IResourceContextProvider { /// diff --git a/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs b/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs index bc93eff7e0..4c895a9251 100644 --- a/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs +++ b/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore; @@ -8,6 +8,7 @@ namespace JsonApiDotNetCore.Configuration { /// + [PublicAPI] public class InverseNavigationResolver : IInverseNavigationResolver { private readonly IResourceContextProvider _resourceContextProvider; @@ -16,8 +17,11 @@ public class InverseNavigationResolver : IInverseNavigationResolver public InverseNavigationResolver(IResourceContextProvider resourceContextProvider, IEnumerable dbContextResolvers) { - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); - _dbContextResolvers = dbContextResolvers ?? throw new ArgumentNullException(nameof(dbContextResolvers)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(dbContextResolvers, nameof(dbContextResolvers)); + + _resourceContextProvider = resourceContextProvider; + _dbContextResolvers = dbContextResolvers; } /// @@ -37,14 +41,19 @@ private void Resolve(DbContext dbContext) IEntityType entityType = dbContext.Model.FindEntityType(resourceContext.ResourceType); if (entityType != null) { - foreach (var relationship in resourceContext.Relationships) - { - if (!(relationship is HasManyThroughAttribute)) - { - INavigation inverseNavigation = entityType.FindNavigation(relationship.Property.Name)?.FindInverse(); - relationship.InverseNavigationProperty = inverseNavigation?.PropertyInfo; - } - } + ResolveRelationships(resourceContext.Relationships, entityType); + } + } + } + + private void ResolveRelationships(IReadOnlyCollection relationships, IEntityType entityType) + { + foreach (var relationship in relationships) + { + if (!(relationship is HasManyThroughAttribute)) + { + INavigation inverseNavigation = entityType.FindNavigation(relationship.Property.Name)?.FindInverse(); + relationship.InverseNavigationProperty = inverseNavigation?.PropertyInfo; } } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 25fe41bbd7..43c0413001 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -44,16 +44,19 @@ internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder, ID public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mvcBuilder) { - _services = services ?? throw new ArgumentNullException(nameof(services)); - _mvcBuilder = mvcBuilder ?? throw new ArgumentNullException(nameof(mvcBuilder)); - + ArgumentGuard.NotNull(services, nameof(services)); + ArgumentGuard.NotNull(mvcBuilder, nameof(mvcBuilder)); + + _services = services; + _mvcBuilder = mvcBuilder; _intermediateProvider = services.BuildServiceProvider(); + var loggerFactory = _intermediateProvider.GetRequiredService(); - + _resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory); _serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, loggerFactory); } - + /// /// Executes the action provided by the user to configure . /// @@ -225,26 +228,33 @@ private void AddQueryStringLayer() _services.AddScoped(); _services.AddScoped(); - _services.AddScoped(sp => sp.GetRequiredService()); - _services.AddScoped(sp => sp.GetRequiredService()); - _services.AddScoped(sp => sp.GetRequiredService()); - _services.AddScoped(sp => sp.GetRequiredService()); - _services.AddScoped(sp => sp.GetRequiredService()); - _services.AddScoped(sp => sp.GetRequiredService()); - _services.AddScoped(sp => sp.GetRequiredService()); - _services.AddScoped(sp => sp.GetRequiredService()); - - _services.AddScoped(sp => sp.GetRequiredService()); - _services.AddScoped(sp => sp.GetRequiredService()); - _services.AddScoped(sp => sp.GetRequiredService()); - _services.AddScoped(sp => sp.GetRequiredService()); - _services.AddScoped(sp => sp.GetRequiredService()); - _services.AddScoped(sp => sp.GetRequiredService()); + RegisterDependentService(); + RegisterDependentService(); + RegisterDependentService(); + RegisterDependentService(); + RegisterDependentService(); + RegisterDependentService(); + RegisterDependentService(); + RegisterDependentService(); + + RegisterDependentService(); + RegisterDependentService(); + RegisterDependentService(); + RegisterDependentService(); + RegisterDependentService(); + RegisterDependentService(); _services.AddScoped(); _services.AddSingleton(); } + private void RegisterDependentService() + where TCollectionElement : class + where TElementToAdd : TCollectionElement + { + _services.AddScoped(serviceProvider => serviceProvider.GetRequiredService()); + } + private void AddResourceHooks() { if (_options.EnableResourceHooks) diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiMetadataProvider.cs b/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs similarity index 94% rename from src/JsonApiDotNetCore/Configuration/JsonApiMetadataProvider.cs rename to src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs index b363227b65..e40935696d 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiMetadataProvider.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.Configuration /// /// Custom implementation of to support JSON:API partial patching. /// - internal class JsonApiModelMetadataProvider : DefaultModelMetadataProvider + internal sealed class JsonApiModelMetadataProvider : DefaultModelMetadataProvider { private readonly JsonApiValidationFilter _jsonApiValidationFilter; diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 7cf6866550..85811578fd 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -1,4 +1,5 @@ using System.Data; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -6,8 +7,11 @@ namespace JsonApiDotNetCore.Configuration { /// + [PublicAPI] public sealed class JsonApiOptions : IJsonApiOptions { + internal static readonly NamingStrategy DefaultNamingStrategy = new CamelCaseNamingStrategy(); + /// public string Namespace { get; set; } @@ -79,7 +83,7 @@ public sealed class JsonApiOptions : IJsonApiOptions { ContractResolver = new DefaultContractResolver { - NamingStrategy = new CamelCaseNamingStrategy() + NamingStrategy = DefaultNamingStrategy } }; diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs index c866a831f0..9baa1e3194 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs @@ -17,7 +17,9 @@ internal sealed class JsonApiValidationFilter : IPropertyValidationFilter public JsonApiValidationFilter(IRequestScopedServiceProvider serviceProvider) { - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); + + _serviceProvider = serviceProvider; } /// diff --git a/src/JsonApiDotNetCore/Configuration/PageNumber.cs b/src/JsonApiDotNetCore/Configuration/PageNumber.cs index bc94b3ca75..81561aefcb 100644 --- a/src/JsonApiDotNetCore/Configuration/PageNumber.cs +++ b/src/JsonApiDotNetCore/Configuration/PageNumber.cs @@ -1,7 +1,9 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Configuration { + [PublicAPI] public sealed class PageNumber : IEquatable { public static readonly PageNumber ValueOne = new PageNumber(1); diff --git a/src/JsonApiDotNetCore/Configuration/PageSize.cs b/src/JsonApiDotNetCore/Configuration/PageSize.cs index c1e3de877c..4533461502 100644 --- a/src/JsonApiDotNetCore/Configuration/PageSize.cs +++ b/src/JsonApiDotNetCore/Configuration/PageSize.cs @@ -1,7 +1,9 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Configuration { + [PublicAPI] public sealed class PageSize : IEquatable { public int Value { get; } diff --git a/src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs b/src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs index 1601e08c73..0bd2b71477 100644 --- a/src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs +++ b/src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs @@ -10,13 +10,15 @@ public sealed class RequestScopedServiceProvider : IRequestScopedServiceProvider public RequestScopedServiceProvider(IHttpContextAccessor httpContextAccessor) { - _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + ArgumentGuard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); + + _httpContextAccessor = httpContextAccessor; } /// public object GetService(Type serviceType) { - if (serviceType == null) throw new ArgumentNullException(nameof(serviceType)); + ArgumentGuard.NotNull(serviceType, nameof(serviceType)); if (_httpContextAccessor.HttpContext == null) { diff --git a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs index edde0bcc41..355c11e3ac 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Configuration @@ -8,6 +9,7 @@ namespace JsonApiDotNetCore.Configuration /// /// Provides metadata for a resource, such as its attributes and relationships. /// + [PublicAPI] public class ResourceContext { /// diff --git a/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs b/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs index 420b9015d6..70a14513ae 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs @@ -2,7 +2,7 @@ namespace JsonApiDotNetCore.Configuration { - internal class ResourceDescriptor + internal sealed class ResourceDescriptor { public Type ResourceType { get; } public Type IdType { get; } diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs index 90b5b0b83a..a52af699c1 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs @@ -2,20 +2,24 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Configuration { /// + [PublicAPI] public class ResourceGraph : IResourceGraph { private readonly IReadOnlyCollection _resources; - private static readonly Type _proxyTargetAccessorType = Type.GetType("Castle.DynamicProxy.IProxyTargetAccessor, Castle.Core"); + private static readonly Type ProxyTargetAccessorType = Type.GetType("Castle.DynamicProxy.IProxyTargetAccessor, Castle.Core"); public ResourceGraph(IReadOnlyCollection resources) { - _resources = resources ?? throw new ArgumentNullException(nameof(resources)); + ArgumentGuard.NotNull(resources, nameof(resources)); + + _resources = resources; } /// @@ -24,7 +28,7 @@ public ResourceGraph(IReadOnlyCollection resources) /// public ResourceContext GetResourceContext(string resourceName) { - if (resourceName == null) throw new ArgumentNullException(nameof(resourceName)); + ArgumentGuard.NotNull(resourceName, nameof(resourceName)); return _resources.SingleOrDefault(e => e.PublicName == resourceName); } @@ -32,7 +36,7 @@ public ResourceContext GetResourceContext(string resourceName) /// public ResourceContext GetResourceContext(Type resourceType) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); return IsLazyLoadingProxyForResourceType(resourceType) ? _resources.SingleOrDefault(e => e.ResourceType == resourceType.BaseType) @@ -64,7 +68,7 @@ public IReadOnlyCollection GetRelationships(Ex /// public IReadOnlyCollection GetFields(Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); + ArgumentGuard.NotNull(type, nameof(type)); return GetResourceContext(type).Fields; } @@ -72,7 +76,7 @@ public IReadOnlyCollection GetFields(Type type) /// public IReadOnlyCollection GetAttributes(Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); + ArgumentGuard.NotNull(type, nameof(type)); return GetResourceContext(type).Attributes; } @@ -80,7 +84,7 @@ public IReadOnlyCollection GetAttributes(Type type) /// public IReadOnlyCollection GetRelationships(Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); + ArgumentGuard.NotNull(type, nameof(type)); return GetResourceContext(type).Relationships; } @@ -88,7 +92,7 @@ public IReadOnlyCollection GetRelationships(Type type) /// public RelationshipAttribute GetInverseRelationship(RelationshipAttribute relationship) { - if (relationship == null) throw new ArgumentNullException(nameof(relationship)); + ArgumentGuard.NotNull(relationship, nameof(relationship)); if (relationship.InverseNavigationProperty == null) { @@ -104,14 +108,22 @@ private IReadOnlyCollection Getter(Expression { IReadOnlyCollection available; if (type == FieldFilterType.Attribute) + { available = GetResourceContext(typeof(TResource)).Attributes; + } else if (type == FieldFilterType.Relationship) + { available = GetResourceContext(typeof(TResource)).Relationships; + } else + { available = GetResourceContext(typeof(TResource)).Fields; + } if (selector == null) + { return available; + } var targeted = new List(); @@ -138,7 +150,9 @@ private IReadOnlyCollection Getter(Expression try { if (newExpression.Members == null) + { return targeted; + } foreach (var member in newExpression.Members) { @@ -159,13 +173,24 @@ private IReadOnlyCollection Getter(Expression } private bool IsLazyLoadingProxyForResourceType(Type resourceType) => - _proxyTargetAccessorType?.IsAssignableFrom(resourceType) ?? false; + ProxyTargetAccessorType?.IsAssignableFrom(resourceType) ?? false; private static Expression RemoveConvert(Expression expression) - => expression is UnaryExpression unaryExpression - && unaryExpression.NodeType == ExpressionType.Convert - ? RemoveConvert(unaryExpression.Operand) - : expression; + { + var innerExpression = expression; + + while (true) + { + if (innerExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression) + { + innerExpression = unaryExpression.Operand; + } + else + { + return innerExpression; + } + } + } private void ThrowNotExposedError(string memberName, FieldFilterType type) { diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index 998d153051..7b07847682 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using JetBrains.Annotations; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -12,6 +13,7 @@ namespace JsonApiDotNetCore.Configuration /// /// Builds and configures the . /// + [PublicAPI] public class ResourceGraphBuilder { private readonly IJsonApiOptions _options; @@ -20,9 +22,10 @@ public class ResourceGraphBuilder public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactory) { - if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + _options = options; _logger = loggerFactory.CreateLogger(); } @@ -80,21 +83,24 @@ public ResourceGraphBuilder Add(string publicName = null) where /// public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string publicName = null) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - if (_resources.All(e => e.ResourceType != resourceType)) + if (_resources.Any(e => e.ResourceType == resourceType)) { - if (TypeHelper.IsOrImplementsInterface(resourceType, typeof(IIdentifiable))) - { - publicName ??= FormatResourceName(resourceType); - idType ??= TypeLocator.TryGetIdType(resourceType); - var resourceContext = CreateResourceContext(publicName, resourceType, idType); - _resources.Add(resourceContext); - } - else - { - _logger.LogWarning($"Entity '{resourceType}' does not implement '{nameof(IIdentifiable)}'."); - } + return this; + } + + if (TypeHelper.IsOrImplementsInterface(resourceType, typeof(IIdentifiable))) + { + var effectivePublicName = publicName ?? FormatResourceName(resourceType); + var effectiveIdType = idType ?? TypeLocator.TryGetIdType(resourceType); + + var resourceContext = CreateResourceContext(effectivePublicName, resourceType, effectiveIdType); + _resources.Add(resourceContext); + } + else + { + _logger.LogWarning($"Entity '{resourceType}' does not implement '{nameof(IIdentifiable)}'."); } return this; @@ -112,7 +118,7 @@ public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string pu private IReadOnlyCollection GetAttributes(Type resourceType) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); var attributes = new List(); @@ -136,7 +142,9 @@ private IReadOnlyCollection GetAttributes(Type resourceType) } if (attribute == null) + { continue; + } attribute.PublicName ??= FormatPropertyName(property); attribute.Property = property; @@ -153,14 +161,17 @@ private IReadOnlyCollection GetAttributes(Type resourceType) private IReadOnlyCollection GetRelationships(Type resourceType) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); var attributes = new List(); var properties = resourceType.GetProperties(); foreach (var prop in properties) { var attribute = (RelationshipAttribute)prop.GetCustomAttribute(typeof(RelationshipAttribute)); - if (attribute == null) continue; + if (attribute == null) + { + continue; + } attribute.Property = prop; attribute.PublicName ??= FormatPropertyName(prop); @@ -172,11 +183,17 @@ private IReadOnlyCollection GetRelationships(Type resourc { var throughProperty = properties.SingleOrDefault(p => p.Name == hasManyThroughAttribute.ThroughPropertyName); if (throughProperty == null) - throw new InvalidConfigurationException($"Invalid {nameof(HasManyThroughAttribute)} on '{resourceType}.{attribute.Property.Name}': Resource does not contain a property named '{hasManyThroughAttribute.ThroughPropertyName}'."); + { + throw new InvalidConfigurationException($"Invalid {nameof(HasManyThroughAttribute)} on '{resourceType}.{attribute.Property.Name}': " + + $"Resource does not contain a property named '{hasManyThroughAttribute.ThroughPropertyName}'."); + } var throughType = TryGetThroughType(throughProperty); if (throughType == null) - throw new InvalidConfigurationException($"Invalid {nameof(HasManyThroughAttribute)} on '{resourceType}.{attribute.Property.Name}': Referenced property '{throughProperty.Name}' does not implement 'ICollection'."); + { + throw new InvalidConfigurationException($"Invalid {nameof(HasManyThroughAttribute)} on '{resourceType}.{attribute.Property.Name}': " + + $"Referenced property '{throughProperty.Name}' does not implement 'ICollection'."); + } // ICollection hasManyThroughAttribute.ThroughProperty = throughProperty; @@ -249,12 +266,13 @@ private Type TryGetThroughType(PropertyInfo throughProperty) private Type GetRelationshipType(RelationshipAttribute relationship, PropertyInfo property) { - if (relationship == null) throw new ArgumentNullException(nameof(relationship)); - if (property == null) throw new ArgumentNullException(nameof(property)); + ArgumentGuard.NotNull(relationship, nameof(relationship)); + ArgumentGuard.NotNull(property, nameof(property)); return relationship is HasOneAttribute ? property.PropertyType : property.PropertyType.GetGenericArguments()[0]; } + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local private IReadOnlyCollection GetEagerLoads(Type resourceType, int recursionDepth = 0) { if (recursionDepth >= 500) @@ -268,7 +286,10 @@ private IReadOnlyCollection GetEagerLoads(Type resourceType, foreach (var property in properties) { var attribute = (EagerLoadAttribute) property.GetCustomAttribute(typeof(EagerLoadAttribute)); - if (attribute == null) continue; + if (attribute == null) + { + continue; + } Type innerType = TypeOrElementType(property.PropertyType); attribute.Children = GetEagerLoads(innerType, recursionDepth + 1); @@ -290,13 +311,13 @@ private Type TypeOrElementType(Type type) private string FormatResourceName(Type resourceType) { - var formatter = new ResourceNameFormatter(_options); + var formatter = new ResourceNameFormatter(_options.SerializerNamingStrategy); return formatter.FormatResourceName(resourceType); } private string FormatPropertyName(PropertyInfo resourceProperty) { - return _options.SerializerContractResolver.NamingStrategy.GetPropertyName(resourceProperty.Name, false); + return _options.SerializerNamingStrategy.GetPropertyName(resourceProperty.Name, false); } } } diff --git a/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs b/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs index 85bf857a9a..70963bb1a3 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs @@ -10,9 +10,9 @@ internal sealed class ResourceNameFormatter { private readonly NamingStrategy _namingStrategy; - public ResourceNameFormatter(IJsonApiOptions options) + public ResourceNameFormatter(NamingStrategy namingStrategy) { - _namingStrategy = options.SerializerContractResolver.NamingStrategy; + _namingStrategy = namingStrategy; } /// diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index bfd3c5180f..ac2e28b22a 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using JetBrains.Annotations; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Serialization.Building; @@ -12,6 +13,7 @@ namespace JsonApiDotNetCore.Configuration { + [PublicAPI] public static class ServiceCollectionExtensions { /// @@ -24,7 +26,7 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, IMvcCoreBuilder mvcBuilder = null, ICollection dbContextTypes = null) { - if (services == null) throw new ArgumentNullException(nameof(services)); + ArgumentGuard.NotNull(services, nameof(services)); SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, dbContextTypes ?? Array.Empty()); @@ -42,7 +44,7 @@ public static IServiceCollection AddJsonApi(this IServiceCollection IMvcCoreBuilder mvcBuilder = null) where TDbContext : DbContext { - return AddJsonApi(services, options, discovery, resources, mvcBuilder, new[] {typeof(TDbContext)}); + return AddJsonApi(services, options, discovery, resources, mvcBuilder, typeof(TDbContext).AsArray()); } private static void SetupApplicationBuilder(IServiceCollection services, Action configureOptions, @@ -66,7 +68,7 @@ private static void SetupApplicationBuilder(IServiceCollection services, Action< /// public static IServiceCollection AddClientSerialization(this IServiceCollection services) { - if (services == null) throw new ArgumentNullException(nameof(services)); + ArgumentGuard.NotNull(services, nameof(services)); services.AddScoped(); services.AddScoped(sp => @@ -83,7 +85,7 @@ public static IServiceCollection AddClientSerialization(this IServiceCollection /// public static IServiceCollection AddResourceService(this IServiceCollection services) { - if (services == null) throw new ArgumentNullException(nameof(services)); + ArgumentGuard.NotNull(services, nameof(services)); RegisterForConstructedType(services, typeof(TService), ServiceDiscoveryFacade.ServiceInterfaces); @@ -96,7 +98,7 @@ public static IServiceCollection AddResourceService(this IServiceColle /// public static IServiceCollection AddResourceRepository(this IServiceCollection services) { - if (services == null) throw new ArgumentNullException(nameof(services)); + ArgumentGuard.NotNull(services, nameof(services)); RegisterForConstructedType(services, typeof(TRepository), ServiceDiscoveryFacade.RepositoryInterfaces); diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index b9a4926d74..bdcd027a84 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using JetBrains.Annotations; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; @@ -15,6 +16,7 @@ namespace JsonApiDotNetCore.Configuration /// /// Scans for types like resources, services, repositories and resource definitions in an assembly and registers them to the IoC container. /// + [PublicAPI] public class ServiceDiscoveryFacade { internal static readonly HashSet ServiceInterfaces = new HashSet { @@ -68,15 +70,15 @@ public class ServiceDiscoveryFacade public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder resourceGraphBuilder, IJsonApiOptions options, ILoggerFactory loggerFactory) { - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - + ArgumentGuard.NotNull(services, nameof(services)); + ArgumentGuard.NotNull(resourceGraphBuilder, nameof(resourceGraphBuilder)); + ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); + ArgumentGuard.NotNull(options, nameof(options)); + _logger = loggerFactory.CreateLogger(); - _services = services ?? throw new ArgumentNullException(nameof(services)); - _resourceGraphBuilder = resourceGraphBuilder ?? throw new ArgumentNullException(nameof(resourceGraphBuilder)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + _services = services; + _resourceGraphBuilder = resourceGraphBuilder; + _options = options; } /// @@ -89,11 +91,8 @@ public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder /// public ServiceDiscoveryFacade AddAssembly(Assembly assembly) { - if (assembly == null) - { - throw new ArgumentNullException(nameof(assembly)); - } - + ArgumentGuard.NotNull(assembly, nameof(assembly)); + _assemblyCache.RegisterAssembly(assembly); _logger.LogDebug($"Registering assembly '{assembly.FullName}' for discovery of resources and injectables."); @@ -102,12 +101,9 @@ public ServiceDiscoveryFacade AddAssembly(Assembly assembly) internal void DiscoverResources() { - foreach (var (_, resourceDescriptors) in _assemblyCache.GetResourceDescriptorsPerAssembly()) + foreach (var resourceDescriptor in _assemblyCache.GetResourceDescriptorsPerAssembly().SelectMany(tuple => tuple.resourceDescriptors)) { - foreach (var resourceDescriptor in resourceDescriptors) - { - AddResource(resourceDescriptor); - } + AddResource(resourceDescriptor); } } @@ -116,21 +112,25 @@ internal void DiscoverInjectables() foreach (var (assembly, resourceDescriptors) in _assemblyCache.GetResourceDescriptorsPerAssembly()) { AddDbContextResolvers(assembly); + AddInjectables(resourceDescriptors, assembly); + } + } + + private void AddInjectables(IReadOnlyCollection resourceDescriptors, Assembly assembly) + { + foreach (var resourceDescriptor in resourceDescriptors) + { + AddServices(assembly, resourceDescriptor); + AddRepositories(assembly, resourceDescriptor); + AddResourceDefinitions(assembly, resourceDescriptor); - foreach (var resourceDescriptor in resourceDescriptors) + if (_options.EnableResourceHooks) { - AddServices(assembly, resourceDescriptor); - AddRepositories(assembly, resourceDescriptor); - AddResourceDefinitions(assembly, resourceDescriptor); - - if (_options.EnableResourceHooks) - { - AddResourceHookDefinitions(assembly, resourceDescriptor); - } + AddResourceHookDefinitions(assembly, resourceDescriptor); } } } - + private void AddDbContextResolvers(Assembly assembly) { var dbContextTypes = TypeLocator.GetDerivedTypes(assembly, typeof(DbContext)); @@ -190,7 +190,10 @@ private void AddResourceDefinitions(Assembly assembly, ResourceDescriptor resour private void RegisterImplementations(Assembly assembly, Type interfaceType, ResourceDescriptor resourceDescriptor) { - var genericArguments = interfaceType.GetTypeInfo().GenericTypeParameters.Length == 2 ? new[] { resourceDescriptor.ResourceType, resourceDescriptor.IdType } : new[] { resourceDescriptor.ResourceType }; + var genericArguments = interfaceType.GetTypeInfo().GenericTypeParameters.Length == 2 + ? ArrayFactory.Create(resourceDescriptor.ResourceType, resourceDescriptor.IdType) + : ArrayFactory.Create(resourceDescriptor.ResourceType); + var result = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, genericArguments); if (result != null) { diff --git a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs index fd8b420b1b..a3684b77ca 100644 --- a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs +++ b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs @@ -50,35 +50,41 @@ public static ResourceDescriptor TryGetResourceDescriptor(Type type) /// public static (Type implementation, Type registrationInterface)? GetGenericInterfaceImplementation(Assembly assembly, Type openGenericInterface, params Type[] interfaceGenericTypeArguments) { - if (assembly == null) throw new ArgumentNullException(nameof(assembly)); - if (openGenericInterface == null) throw new ArgumentNullException(nameof(openGenericInterface)); - if (interfaceGenericTypeArguments == null) throw new ArgumentNullException(nameof(interfaceGenericTypeArguments)); + ArgumentGuard.NotNull(assembly, nameof(assembly)); + ArgumentGuard.NotNull(openGenericInterface, nameof(openGenericInterface)); + ArgumentGuard.NotNull(interfaceGenericTypeArguments, nameof(interfaceGenericTypeArguments)); if (!openGenericInterface.IsInterface || !openGenericInterface.IsGenericType || openGenericInterface != openGenericInterface.GetGenericTypeDefinition()) { - throw new ArgumentException($"Specified type '{openGenericInterface.FullName}' is not an open generic interface.", nameof(openGenericInterface)); + throw new ArgumentException( + $"Specified type '{openGenericInterface.FullName}' " + "is not an open generic interface.", + nameof(openGenericInterface)); } if (interfaceGenericTypeArguments.Length != openGenericInterface.GetGenericArguments().Length) { throw new ArgumentException( - $"Interface '{openGenericInterface.FullName}' requires {openGenericInterface.GetGenericArguments().Length} type parameters instead of {interfaceGenericTypeArguments.Length}.", - nameof(interfaceGenericTypeArguments)); + $"Interface '{openGenericInterface.FullName}' " + + $"requires {openGenericInterface.GetGenericArguments().Length} type parameters " + + $"instead of {interfaceGenericTypeArguments.Length}.", nameof(interfaceGenericTypeArguments)); } - foreach (var nextType in assembly.GetTypes()) + return assembly.GetTypes().Select(type => FindGenericInterfaceImplementationForType(type, openGenericInterface, interfaceGenericTypeArguments)) + .FirstOrDefault(result => result != null); + } + + private static (Type implementation, Type registrationInterface)? FindGenericInterfaceImplementationForType(Type nextType, Type openGenericInterface, Type[] interfaceGenericTypeArguments) + { + foreach (var nextGenericInterface in nextType.GetInterfaces().Where(type => type.IsGenericType)) { - foreach (var nextGenericInterface in nextType.GetInterfaces().Where(x => x.IsGenericType)) + var nextOpenGenericInterface = nextGenericInterface.GetGenericTypeDefinition(); + if (nextOpenGenericInterface == openGenericInterface) { - var nextOpenGenericInterface = nextGenericInterface.GetGenericTypeDefinition(); - if (nextOpenGenericInterface == openGenericInterface) + var nextGenericArguments = nextGenericInterface.GetGenericArguments(); + if (nextGenericArguments.Length == interfaceGenericTypeArguments.Length && nextGenericArguments.SequenceEqual(interfaceGenericTypeArguments)) { - var nextGenericArguments = nextGenericInterface.GetGenericArguments(); - if (nextGenericArguments.Length == interfaceGenericTypeArguments.Length && nextGenericArguments.SequenceEqual(interfaceGenericTypeArguments)) - { - return (nextType, nextOpenGenericInterface.MakeGenericType(interfaceGenericTypeArguments)); - } + return (nextType, nextOpenGenericInterface.MakeGenericType(interfaceGenericTypeArguments)); } } } @@ -118,7 +124,9 @@ public static IEnumerable GetDerivedTypes(Assembly assembly, Type inherite foreach (var type in assembly.GetTypes()) { if (inheritedType.IsAssignableFrom(type)) + { yield return type; + } } } } diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs index 6ed8711d97..c325d96d47 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.QueryStrings; namespace JsonApiDotNetCore.Controllers.Annotations @@ -16,6 +17,7 @@ namespace JsonApiDotNetCore.Controllers.Annotations /// [DisableQueryString("skipCache")] /// public class CustomersController : JsonApiController { } /// ]]> + [PublicAPI] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] public sealed class DisableQueryStringAttribute : Attribute { @@ -49,7 +51,7 @@ public DisableQueryStringAttribute(StandardQueryStringParameters parameters) /// public DisableQueryStringAttribute(string parameterNames) { - if (parameterNames == null) throw new ArgumentNullException(nameof(parameterNames)); + ArgumentGuard.NotNull(parameterNames, nameof(parameterNames)); ParameterNames = parameterNames.Split(",").ToList(); } diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/DisableRoutingConventionAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/DisableRoutingConventionAttribute.cs index ecd55377ab..4450aca1ce 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/DisableRoutingConventionAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/DisableRoutingConventionAttribute.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Controllers.Annotations { @@ -9,6 +10,7 @@ namespace JsonApiDotNetCore.Controllers.Annotations /// [DisableRoutingConvention, Route("some/custom/route/to/customers")] /// public class CustomersController : JsonApiController { } /// ]]> + [PublicAPI] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] public sealed class DisableRoutingConventionAttribute : Attribute { } diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/HttpReadOnlyAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/HttpReadOnlyAttribute.cs index 46b0f93f79..269086a586 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/HttpReadOnlyAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/HttpReadOnlyAttribute.cs @@ -1,3 +1,5 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCore.Controllers.Annotations { /// @@ -9,6 +11,7 @@ namespace JsonApiDotNetCore.Controllers.Annotations /// { /// } /// ]]> + [PublicAPI] public sealed class HttpReadOnlyAttribute : HttpRestrictAttribute { protected override string[] Methods { get; } = { "POST", "PATCH", "DELETE" }; diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/HttpRestrictAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/HttpRestrictAttribute.cs index dcb3fd88af..00f4dbc195 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/HttpRestrictAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/HttpRestrictAttribute.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Net.Http; using System.Threading.Tasks; @@ -15,8 +14,8 @@ public override async Task OnActionExecutionAsync( ActionExecutingContext context, ActionExecutionDelegate next) { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (next == null) throw new ArgumentNullException(nameof(next)); + ArgumentGuard.NotNull(context, nameof(context)); + ArgumentGuard.NotNull(next, nameof(next)); var method = context.HttpContext.Request.Method; diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpDeleteAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpDeleteAttribute.cs index 02a5fbdba1..e898dc0118 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpDeleteAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpDeleteAttribute.cs @@ -1,3 +1,5 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCore.Controllers.Annotations { /// @@ -9,6 +11,7 @@ namespace JsonApiDotNetCore.Controllers.Annotations /// { /// } /// ]]> + [PublicAPI] public sealed class NoHttpDeleteAttribute : HttpRestrictAttribute { protected override string[] Methods { get; } = { "DELETE" }; diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPatchAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPatchAttribute.cs index 7039356db2..01dc0e31ba 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPatchAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPatchAttribute.cs @@ -1,3 +1,5 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCore.Controllers.Annotations { /// @@ -9,6 +11,7 @@ namespace JsonApiDotNetCore.Controllers.Annotations /// { /// } /// ]]> + [PublicAPI] public sealed class NoHttpPatchAttribute : HttpRestrictAttribute { protected override string[] Methods { get; } = { "PATCH" }; diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPostAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPostAttribute.cs index a40f1a3574..34a0c8a83c 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPostAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPostAttribute.cs @@ -1,3 +1,5 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCore.Controllers.Annotations { /// @@ -9,6 +11,7 @@ namespace JsonApiDotNetCore.Controllers.Annotations /// { /// } /// ]]> + [PublicAPI] public sealed class NoHttpPostAttribute : HttpRestrictAttribute { protected override string[] Methods { get; } = { "POST" }; diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index 29ce0b6b8a..1d5640db00 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Net.Http; using System.Threading; @@ -72,9 +71,10 @@ protected BaseJsonApiController( IDeleteService delete = null, IRemoveFromRelationshipService removeFromRelationship = null) { - if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + _options = options; _traceWriter = new TraceLogWriter>(loggerFactory); _getAll = getAll; _getById = getById; @@ -96,7 +96,11 @@ public virtual async Task GetAsync(CancellationToken cancellation { _traceWriter.LogMethodStart(); - if (_getAll == null) throw new RequestMethodNotAllowedException(HttpMethod.Get); + if (_getAll == null) + { + throw new RequestMethodNotAllowedException(HttpMethod.Get); + } + var resources = await _getAll.GetAsync(cancellationToken); return Ok(resources); @@ -110,7 +114,11 @@ public virtual async Task GetAsync(TId id, CancellationToken canc { _traceWriter.LogMethodStart(new {id}); - if (_getById == null) throw new RequestMethodNotAllowedException(HttpMethod.Get); + if (_getById == null) + { + throw new RequestMethodNotAllowedException(HttpMethod.Get); + } + var resource = await _getById.GetAsync(id, cancellationToken); return Ok(resource); @@ -125,9 +133,14 @@ public virtual async Task GetAsync(TId id, CancellationToken canc public virtual async Task GetSecondaryAsync(TId id, string relationshipName, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {id, relationshipName}); - if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); - if (_getSecondary == null) throw new RequestMethodNotAllowedException(HttpMethod.Get); + ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); + + if (_getSecondary == null) + { + throw new RequestMethodNotAllowedException(HttpMethod.Get); + } + var relationship = await _getSecondary.GetSecondaryAsync(id, relationshipName, cancellationToken); return Ok(relationship); @@ -141,9 +154,14 @@ public virtual async Task GetSecondaryAsync(TId id, string relati public virtual async Task GetRelationshipAsync(TId id, string relationshipName, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {id, relationshipName}); - if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); - if (_getRelationship == null) throw new RequestMethodNotAllowedException(HttpMethod.Get); + ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); + + if (_getRelationship == null) + { + throw new RequestMethodNotAllowedException(HttpMethod.Get); + } + var rightResources = await _getRelationship.GetRelationshipAsync(id, relationshipName, cancellationToken); return Ok(rightResources); @@ -156,18 +174,22 @@ public virtual async Task GetRelationshipAsync(TId id, string rel public virtual async Task PostAsync([FromBody] TResource resource, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {resource}); - if (resource == null) throw new ArgumentNullException(nameof(resource)); + + ArgumentGuard.NotNull(resource, nameof(resource)); if (_create == null) + { throw new RequestMethodNotAllowedException(HttpMethod.Post); + } if (!_options.AllowClientGeneratedIds && resource.StringId != null) + { throw new ResourceIdInCreateResourceNotAllowedException(); + } if (_options.ValidateModelState && !ModelState.IsValid) { - var namingStrategy = _options.SerializerContractResolver.NamingStrategy; - throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, namingStrategy); + throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, _options.SerializerNamingStrategy); } var newResource = await _create.CreateAsync(resource, cancellationToken); @@ -195,10 +217,15 @@ public virtual async Task PostAsync([FromBody] TResource resource public virtual async Task PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {id, relationshipName, secondaryResourceIds}); - if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); - if (secondaryResourceIds == null) throw new ArgumentNullException(nameof(secondaryResourceIds)); - if (_addToRelationship == null) throw new RequestMethodNotAllowedException(HttpMethod.Post); + ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); + ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); + + if (_addToRelationship == null) + { + throw new RequestMethodNotAllowedException(HttpMethod.Post); + } + await _addToRelationship.AddToToManyRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); return NoContent(); @@ -212,14 +239,17 @@ public virtual async Task PostRelationshipAsync(TId id, string re public virtual async Task PatchAsync(TId id, [FromBody] TResource resource, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {id, resource}); - if (resource == null) throw new ArgumentNullException(nameof(resource)); - if (_update == null) throw new RequestMethodNotAllowedException(HttpMethod.Patch); + ArgumentGuard.NotNull(resource, nameof(resource)); + + if (_update == null) + { + throw new RequestMethodNotAllowedException(HttpMethod.Patch); + } if (_options.ValidateModelState && !ModelState.IsValid) { - var namingStrategy = _options.SerializerContractResolver.NamingStrategy; - throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, namingStrategy); + throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, _options.SerializerNamingStrategy); } var updated = await _update.UpdateAsync(id, resource, cancellationToken); @@ -238,9 +268,14 @@ public virtual async Task PatchAsync(TId id, [FromBody] TResource public virtual async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object secondaryResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {id, relationshipName, secondaryResourceIds}); - if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); - if (_setRelationship == null) throw new RequestMethodNotAllowedException(HttpMethod.Patch); + ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); + + if (_setRelationship == null) + { + throw new RequestMethodNotAllowedException(HttpMethod.Patch); + } + await _setRelationship.SetRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); return NoContent(); @@ -254,7 +289,11 @@ public virtual async Task DeleteAsync(TId id, CancellationToken c { _traceWriter.LogMethodStart(new {id}); - if (_delete == null) throw new RequestMethodNotAllowedException(HttpMethod.Delete); + if (_delete == null) + { + throw new RequestMethodNotAllowedException(HttpMethod.Delete); + } + await _delete.DeleteAsync(id, cancellationToken); return NoContent(); @@ -271,10 +310,15 @@ public virtual async Task DeleteAsync(TId id, CancellationToken c public virtual async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {id, relationshipName, secondaryResourceIds}); - if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); - if (secondaryResourceIds == null) throw new ArgumentNullException(nameof(secondaryResourceIds)); - if (_removeFromRelationship == null) throw new RequestMethodNotAllowedException(HttpMethod.Delete); + ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); + ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); + + if (_removeFromRelationship == null) + { + throw new RequestMethodNotAllowedException(HttpMethod.Delete); + } + await _removeFromRelationship.RemoveFromToManyRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); return NoContent(); diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs index f118d20e1f..719b281db6 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs @@ -3,12 +3,14 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.AtomicOperations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Controllers @@ -17,6 +19,7 @@ namespace JsonApiDotNetCore.Controllers /// Implements the foundational ASP.NET Core controller layer in the JsonApiDotNetCore architecture for handling atomic:operations requests. /// See https://jsonapi.org/ext/atomic/ for details. Delegates work to . /// + [PublicAPI] public abstract class BaseJsonApiOperationsController : CoreJsonApiController { private readonly IJsonApiOptions _options; @@ -28,12 +31,16 @@ public abstract class BaseJsonApiOperationsController : CoreJsonApiController protected BaseJsonApiOperationsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request, ITargetedFields targetedFields) { - if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); - - _options = options ?? throw new ArgumentNullException(nameof(options)); - _processor = processor ?? throw new ArgumentNullException(nameof(processor)); - _request = request ?? throw new ArgumentNullException(nameof(request)); - _targetedFields = targetedFields ?? throw new ArgumentNullException(nameof(targetedFields)); + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); + ArgumentGuard.NotNull(processor, nameof(processor)); + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); + + _options = options; + _processor = processor; + _request = request; + _targetedFields = targetedFields; _traceWriter = new TraceLogWriter(loggerFactory); } @@ -99,7 +106,8 @@ public virtual async Task PostOperationsAsync([FromBody] IList operat if (!validationContext.ModelState.IsValid) { - foreach (var (key, entry) in validationContext.ModelState) - { - foreach (var error in entry.Errors) - { - var violation = new ModelStateViolation($"/atomic:operations[{index}]/data/attributes/", key, operation.Resource.GetType(), error); - violations.Add(violation); - } - } + AddValidationErrors(validationContext.ModelState, operation.Resource.GetType(), index, violations); } } @@ -167,8 +168,26 @@ protected virtual void ValidateModelState(IEnumerable operat if (violations.Any()) { - var namingStrategy = _options.SerializerContractResolver.NamingStrategy; - throw new InvalidModelStateException(violations, _options.IncludeExceptionStackTraceInErrors, namingStrategy); + throw new InvalidModelStateException(violations, _options.IncludeExceptionStackTraceInErrors, _options.SerializerNamingStrategy); + } + } + + private static void AddValidationErrors(ModelStateDictionary modelState, Type resourceType, int operationIndex, List violations) + { + foreach (var (propertyName, entry) in modelState) + { + AddValidationErrors(entry, propertyName, resourceType, operationIndex, violations); + } + } + + private static void AddValidationErrors(ModelStateEntry entry, string propertyName, Type resourceType, int operationIndex, List violations) + { + foreach (var error in entry.Errors) + { + var prefix = $"/atomic:operations[{operationIndex}]/data/attributes/"; + var violation = new ModelStateViolation(prefix, propertyName, resourceType, error); + + violations.Add(violation); } } } diff --git a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs index 1c6853e61a..8406bdb55a 100644 --- a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc; @@ -12,14 +11,14 @@ public abstract class CoreJsonApiController : ControllerBase { protected IActionResult Error(Error error) { - if (error == null) throw new ArgumentNullException(nameof(error)); + ArgumentGuard.NotNull(error, nameof(error)); - return Error(new[] {error}); + return Error(error.AsEnumerable()); } protected IActionResult Error(IEnumerable errors) { - if (errors == null) throw new ArgumentNullException(nameof(errors)); + ArgumentGuard.NotNull(errors, nameof(errors)); var document = new ErrorDocument(errors); diff --git a/src/JsonApiDotNetCore/Controllers/ModelStateViolation.cs b/src/JsonApiDotNetCore/Controllers/ModelStateViolation.cs index b83453e919..8bda54c806 100644 --- a/src/JsonApiDotNetCore/Controllers/ModelStateViolation.cs +++ b/src/JsonApiDotNetCore/Controllers/ModelStateViolation.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace JsonApiDotNetCore.Controllers @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Controllers /// /// Represents the violation of a model state validation rule. /// + [PublicAPI] public sealed class ModelStateViolation { public string Prefix { get; } @@ -15,10 +17,15 @@ public sealed class ModelStateViolation public ModelStateViolation(string prefix, string propertyName, Type resourceType, ModelError error) { - Prefix = prefix ?? throw new ArgumentNullException(nameof(prefix)); - PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName)); - ResourceType = resourceType ?? throw new ArgumentNullException(nameof(resourceType)); - Error = error ?? throw new ArgumentNullException(nameof(error)); + ArgumentGuard.NotNull(prefix, nameof(prefix)); + ArgumentGuard.NotNull(propertyName, nameof(propertyName)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + ArgumentGuard.NotNull(error, nameof(error)); + + Prefix = prefix; + PropertyName = propertyName; + ResourceType = resourceType; + Error = error; } } } diff --git a/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs b/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs index cc5e9f036f..e0a647a8b2 100644 --- a/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs +++ b/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs @@ -1,4 +1,5 @@ using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Errors @@ -6,13 +7,15 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when a required relationship is cleared. /// + [PublicAPI] public sealed class CannotClearRequiredRelationshipException : JsonApiException { public CannotClearRequiredRelationshipException(string relationshipName, string resourceId, string resourceType) : base(new Error(HttpStatusCode.BadRequest) { Title = "Failed to clear a required relationship.", - Detail = $"The relationship '{relationshipName}' of resource type '{resourceType}' with ID '{resourceId}' cannot be cleared because it is a required relationship." + Detail = $"The relationship '{relationshipName}' of resource type '{resourceType}' " + + $"with ID '{resourceId}' cannot be cleared because it is a required relationship." }) { } diff --git a/src/JsonApiDotNetCore/Errors/InvalidConfigurationException.cs b/src/JsonApiDotNetCore/Errors/InvalidConfigurationException.cs index c838e2ca9d..884ed046a7 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidConfigurationException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidConfigurationException.cs @@ -1,10 +1,12 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Errors { /// /// The error that is thrown when configured usage of this library is invalid. /// + [PublicAPI] public sealed class InvalidConfigurationException : Exception { public InvalidConfigurationException(string message, Exception innerException = null) diff --git a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs index 7393a5596d..a3db7f492b 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Net; using System.Reflection; +using JetBrains.Annotations; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; @@ -14,6 +16,7 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when model state validation fails. /// + [PublicAPI] public class InvalidModelStateException : JsonApiException { public InvalidModelStateException(ModelStateDictionary modelState, Type resourceType, @@ -24,12 +27,26 @@ public InvalidModelStateException(ModelStateDictionary modelState, Type resource private static IEnumerable FromModelStateDictionary(ModelStateDictionary modelState, Type resourceType) { + ArgumentGuard.NotNull(modelState, nameof(modelState)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + + var violations = new List(); + foreach (var (propertyName, entry) in modelState) { - foreach (ModelError error in entry.Errors) - { - yield return new ModelStateViolation("/data/attributes/", propertyName, resourceType, error); - } + AddValidationErrors(entry, propertyName, resourceType, violations); + } + + return violations; + } + + private static void AddValidationErrors(ModelStateEntry entry, string propertyName, Type resourceType, + List violations) + { + foreach (ModelError error in entry.Errors) + { + var violation = new ModelStateViolation("/data/attributes/", propertyName, resourceType, error); + violations.Add(violation); } } @@ -42,25 +59,28 @@ public InvalidModelStateException(IEnumerable violations, private static IEnumerable FromModelStateViolations(IEnumerable violations, bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy) { - if (violations == null) throw new ArgumentNullException(nameof(violations)); - if (namingStrategy == null) throw new ArgumentNullException(nameof(namingStrategy)); + ArgumentGuard.NotNull(violations, nameof(violations)); + ArgumentGuard.NotNull(namingStrategy, nameof(namingStrategy)); - foreach (var violation in violations) + return violations.SelectMany(violation => FromModelStateViolation(violation, includeExceptionStackTraceInErrors, namingStrategy)); + } + + private static IEnumerable FromModelStateViolation(ModelStateViolation violation, bool includeExceptionStackTraceInErrors, + NamingStrategy namingStrategy) + { + if (violation.Error.Exception is JsonApiException jsonApiException) { - if (violation.Error.Exception is JsonApiException jsonApiException) + foreach (var error in jsonApiException.Errors) { - foreach (var error in jsonApiException.Errors) - { - yield return error; - } + yield return error; } - else - { - string attributeName = GetDisplayNameForProperty(violation.PropertyName, violation.ResourceType, namingStrategy); - var attributePath = violation.Prefix + attributeName; + } + else + { + string attributeName = GetDisplayNameForProperty(violation.PropertyName, violation.ResourceType, namingStrategy); + var attributePath = violation.Prefix + attributeName; - yield return FromModelError(violation.Error, attributePath, includeExceptionStackTraceInErrors); - } + yield return FromModelError(violation.Error, attributePath, includeExceptionStackTraceInErrors); } } diff --git a/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs b/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs index 3b68dfc931..b776d2c683 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Serialization.Objects; @@ -8,6 +9,7 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when translating a to Entity Framework Core fails. /// + [PublicAPI] public sealed class InvalidQueryException : JsonApiException { public InvalidQueryException(string reason, Exception exception) diff --git a/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs b/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs index 12e66112d0..f0e2b146d6 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Errors @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when processing the request fails due to an error in the request query string. /// + [PublicAPI] public sealed class InvalidQueryStringParameterException : JsonApiException { public string QueryParameterName { get; } diff --git a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs index e9f2bf0a75..b50101f6dc 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs @@ -1,6 +1,7 @@ using System; using System.Net; using System.Text; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Errors @@ -8,6 +9,7 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when deserializing the request body fails. /// + [PublicAPI] public sealed class InvalidRequestBodyException : JsonApiException { public InvalidRequestBodyException(string reason, string details, string requestBody, Exception innerException = null) diff --git a/src/JsonApiDotNetCore/Errors/JsonApiException.cs b/src/JsonApiDotNetCore/Errors/JsonApiException.cs index 7f0ef7be8b..278aa1f4ab 100644 --- a/src/JsonApiDotNetCore/Errors/JsonApiException.cs +++ b/src/JsonApiDotNetCore/Errors/JsonApiException.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; @@ -9,9 +10,10 @@ namespace JsonApiDotNetCore.Errors /// /// The base class for an that represents one or more JSON:API error objects in an unsuccessful response. /// + [PublicAPI] public class JsonApiException : Exception { - private static readonly JsonSerializerSettings _errorSerializerSettings = new JsonSerializerSettings + private static readonly JsonSerializerSettings ErrorSerializerSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.Indented @@ -22,15 +24,15 @@ public class JsonApiException : Exception public JsonApiException(Error error, Exception innerException = null) : base(null, innerException) { - if (error == null) throw new ArgumentNullException(nameof(error)); + ArgumentGuard.NotNull(error, nameof(error)); - Errors = new[] {error}; + Errors = error.AsArray(); } public JsonApiException(IEnumerable errors, Exception innerException = null) : base(null, innerException) { - if (errors == null) throw new ArgumentNullException(nameof(errors)); + ArgumentGuard.NotNull(errors, nameof(errors)); Errors = errors.ToList(); @@ -40,6 +42,6 @@ public JsonApiException(IEnumerable errors, Exception innerException = nu } } - public override string Message => "Errors = " + JsonConvert.SerializeObject(Errors, _errorSerializerSettings); + public override string Message => "Errors = " + JsonConvert.SerializeObject(Errors, ErrorSerializerSettings); } } diff --git a/src/JsonApiDotNetCore/Errors/MissingResourceInRelationship.cs b/src/JsonApiDotNetCore/Errors/MissingResourceInRelationship.cs index 421592abd8..95ef199932 100644 --- a/src/JsonApiDotNetCore/Errors/MissingResourceInRelationship.cs +++ b/src/JsonApiDotNetCore/Errors/MissingResourceInRelationship.cs @@ -1,7 +1,8 @@ -using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Errors { + [PublicAPI] public sealed class MissingResourceInRelationship { public string RelationshipName { get; } @@ -10,9 +11,13 @@ public sealed class MissingResourceInRelationship public MissingResourceInRelationship(string relationshipName, string resourceType, string resourceId) { - RelationshipName = relationshipName ?? throw new ArgumentNullException(nameof(relationshipName)); - ResourceType = resourceType ?? throw new ArgumentNullException(nameof(resourceType)); - ResourceId = resourceId ?? throw new ArgumentNullException(nameof(resourceId)); + ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + ArgumentGuard.NotNull(resourceId, nameof(resourceId)); + + RelationshipName = relationshipName; + ResourceType = resourceType; + ResourceId = resourceId; } } } diff --git a/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs b/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs index 088db9300d..980f6457fb 100644 --- a/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs +++ b/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs @@ -1,4 +1,5 @@ using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Errors @@ -7,13 +8,15 @@ namespace JsonApiDotNetCore.Errors /// The error that is thrown when accessing a repository that does not support transactions /// during an atomic:operations request. /// + [PublicAPI] public sealed class MissingTransactionSupportException : JsonApiException { public MissingTransactionSupportException(string resourceType) : base(new Error(HttpStatusCode.UnprocessableEntity) { Title = "Unsupported resource type in atomic:operations request.", - Detail = $"Operations on resources of type '{resourceType}' cannot be used because transaction support is unavailable." + Detail = $"Operations on resources of type '{resourceType}' " + + "cannot be used because transaction support is unavailable." }) { } diff --git a/src/JsonApiDotNetCore/Errors/NonSharedTransactionException.cs b/src/JsonApiDotNetCore/Errors/NonSharedTransactionException.cs index d0bcd69505..dbf699c301 100644 --- a/src/JsonApiDotNetCore/Errors/NonSharedTransactionException.cs +++ b/src/JsonApiDotNetCore/Errors/NonSharedTransactionException.cs @@ -1,4 +1,5 @@ using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Errors @@ -7,13 +8,15 @@ namespace JsonApiDotNetCore.Errors /// The error that is thrown when a repository does not participate in the overarching transaction /// during an atomic:operations request. /// + [PublicAPI] public sealed class NonSharedTransactionException : JsonApiException { public NonSharedTransactionException() : base(new Error(HttpStatusCode.UnprocessableEntity) { Title = "Unsupported combination of resource types in atomic:operations request.", - Detail = "All operations need to participate in a single shared transaction, which is not the case for this request." + Detail = "All operations need to participate in a single shared transaction, " + + "which is not the case for this request." }) { } diff --git a/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs b/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs index 313222f4a2..091ff99586 100644 --- a/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs @@ -1,4 +1,5 @@ using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Errors @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when a relationship does not exist. /// + [PublicAPI] public sealed class RelationshipNotFoundException : JsonApiException { public RelationshipNotFoundException(string relationshipName, string resourceType) : base(new Error(HttpStatusCode.NotFound) diff --git a/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs b/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs index 7444d6cc46..0a2db9e061 100644 --- a/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs +++ b/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Http; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Errors @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when a request is received that contains an unsupported HTTP verb. /// + [PublicAPI] public sealed class RequestMethodNotAllowedException : JsonApiException { public HttpMethod Method { get; } diff --git a/src/JsonApiDotNetCore/Errors/ResourceAlreadyExistsException.cs b/src/JsonApiDotNetCore/Errors/ResourceAlreadyExistsException.cs index dcc0a0a66b..34bc21dff5 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceAlreadyExistsException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceAlreadyExistsException.cs @@ -1,4 +1,5 @@ using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Errors @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when creating a resource with an ID that already exists. /// + [PublicAPI] public sealed class ResourceAlreadyExistsException : JsonApiException { public ResourceAlreadyExistsException(string resourceId, string resourceType) diff --git a/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs b/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs index fff90a6fb9..efe5d80c72 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs @@ -1,4 +1,5 @@ using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Errors @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when a resource creation request or operation is received that contains a client-generated ID. /// + [PublicAPI] public sealed class ResourceIdInCreateResourceNotAllowedException : JsonApiException { public ResourceIdInCreateResourceNotAllowedException(int? atomicOperationIndex = null) diff --git a/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs b/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs index b463bbee0d..b75366f102 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs @@ -1,4 +1,5 @@ using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Errors @@ -6,13 +7,15 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when the resource ID in the request body does not match the ID in the current endpoint URL. /// + [PublicAPI] public sealed class ResourceIdMismatchException : JsonApiException { public ResourceIdMismatchException(string bodyId, string endpointId, string requestPath) : base(new Error(HttpStatusCode.Conflict) { Title = "Resource ID mismatch between request body and endpoint URL.", - Detail = $"Expected resource ID '{endpointId}' in PATCH request body at endpoint '{requestPath}', instead of '{bodyId}'." + Detail = $"Expected resource ID '{endpointId}' in PATCH request body " + + $"at endpoint '{requestPath}', instead of '{bodyId}'." }) { } diff --git a/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs b/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs index a9d127ee59..4e63220269 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs @@ -1,4 +1,5 @@ using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Errors @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when a resource does not exist. /// + [PublicAPI] public sealed class ResourceNotFoundException : JsonApiException { public ResourceNotFoundException(string resourceId, string resourceType) diff --git a/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs b/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs index 5edec07bb3..78f490c593 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Http; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; @@ -8,13 +9,15 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when the resource type in the request body does not match the type expected at the current endpoint URL. /// + [PublicAPI] public sealed class ResourceTypeMismatchException : JsonApiException { public ResourceTypeMismatchException(HttpMethod method, string requestPath, ResourceContext expected, ResourceContext actual) : base(new Error(HttpStatusCode.Conflict) { Title = "Resource type mismatch between request body and endpoint URL.", - Detail = $"Expected resource of type '{expected.PublicName}' in {method} request body at endpoint '{requestPath}', instead of '{actual?.PublicName}'." + Detail = $"Expected resource of type '{expected.PublicName}' in {method} " + + $"request body at endpoint '{requestPath}', instead of '{actual?.PublicName}'." }) { } diff --git a/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs b/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs index cc91a119e3..450ae4cdd5 100644 --- a/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Errors @@ -8,6 +9,7 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when referencing one or more non-existing resources in one or more relationships. /// + [PublicAPI] public sealed class ResourcesInRelationshipsNotFoundException : JsonApiException { public ResourcesInRelationshipsNotFoundException(IEnumerable missingResources) diff --git a/src/JsonApiDotNetCore/Errors/ToManyRelationshipRequiredException.cs b/src/JsonApiDotNetCore/Errors/ToManyRelationshipRequiredException.cs index 5682ef9679..840965aed4 100644 --- a/src/JsonApiDotNetCore/Errors/ToManyRelationshipRequiredException.cs +++ b/src/JsonApiDotNetCore/Errors/ToManyRelationshipRequiredException.cs @@ -1,4 +1,5 @@ using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Errors @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when an attempt is made to update a to-one relationship from a to-many relationship endpoint. /// + [PublicAPI] public sealed class ToManyRelationshipRequiredException : JsonApiException { public ToManyRelationshipRequiredException(string relationshipName) diff --git a/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs b/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs index 7f8cd6fc9d..58c0219bf6 100644 --- a/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs +++ b/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs @@ -1,5 +1,5 @@ -using System; using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc; @@ -8,6 +8,7 @@ namespace JsonApiDotNetCore.Errors /// /// The error that is thrown when an with non-success status is returned from a controller method. /// + [PublicAPI] public sealed class UnsuccessfulActionResultException : JsonApiException { public UnsuccessfulActionResultException(HttpStatusCode status) @@ -25,8 +26,8 @@ public UnsuccessfulActionResultException(ProblemDetails problemDetails) private static Error ToError(ProblemDetails problemDetails) { - if (problemDetails == null) throw new ArgumentNullException(nameof(problemDetails)); - + ArgumentGuard.NotNull(problemDetails, nameof(problemDetails)); + var status = problemDetails.Status != null ? (HttpStatusCode) problemDetails.Status.Value : HttpStatusCode.InternalServerError; diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs index 95a16007ca..720363cc82 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCore.Resources; @@ -11,6 +12,7 @@ namespace JsonApiDotNetCore.Hooks.Internal.Discovery /// /// The default implementation for IHooksDiscovery /// + [PublicAPI] public class HooksDiscovery : IHooksDiscovery where TResource : class, IIdentifiable { private readonly Type _boundResourceDefinitionType = typeof(ResourceHooksDefinition); @@ -56,13 +58,15 @@ private void DiscoverImplementedHooks(Type containerType) var implementedHooks = new List(); // this hook can only be used with enabled database values - var databaseValuesEnabledHooks = new List { ResourceHook.BeforeImplicitUpdateRelationship }; + var databaseValuesEnabledHooks = ResourceHook.BeforeImplicitUpdateRelationship.AsList(); var databaseValuesDisabledHooks = new List(); foreach (var hook in _allHooks) { var method = containerType.GetMethod(hook.ToString("G")); - if (method.DeclaringType == _boundResourceDefinitionType) + if (method == null || method.DeclaringType == _boundResourceDefinitionType) + { continue; + } implementedHooks.Add(hook); var attr = method.GetCustomAttributes(true).OfType().SingleOrDefault(); diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs index 9473ddbb24..7fede4e57b 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs @@ -1,6 +1,8 @@ using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCore.Resources; +// ReSharper disable UnusedTypeParameter + namespace JsonApiDotNetCore.Hooks.Internal.Discovery { /// diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/LoadDatabaseValuesAttribute.cs b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/LoadDatabaseValuesAttribute.cs index 09a5393e3b..5fe3e71c39 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/LoadDatabaseValuesAttribute.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/LoadDatabaseValuesAttribute.cs @@ -1,15 +1,17 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Hooks.Internal.Discovery { + [PublicAPI] [AttributeUsage(AttributeTargets.Method)] public sealed class LoadDatabaseValuesAttribute : Attribute { - public readonly bool Value; + public bool Value { get; } - public LoadDatabaseValuesAttribute(bool mode = true) + public LoadDatabaseValuesAttribute(bool value = true) { - Value = mode; + Value = value; } } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs index d555e0d412..574966af47 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs @@ -4,12 +4,14 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using JetBrains.Annotations; using JsonApiDotNetCore.Hooks.Internal.Discovery; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Hooks.Internal.Execution { + [PublicAPI] public sealed class DiffableResourceHashSet : ResourceHashSet, IDiffableResourceHashSet where TResource : class, IIdentifiable { private readonly HashSet _databaseValues; @@ -35,14 +37,17 @@ internal DiffableResourceHashSet(IEnumerable requestResources, Dictionary relationships, ITargetedFields targetedFields) : this((HashSet)requestResources, (HashSet)databaseResources, TypeHelper.ConvertRelationshipDictionary(relationships), - TypeHelper.ConvertAttributeDictionary(targetedFields.Attributes, (HashSet)requestResources)) + targetedFields.Attributes == null ? null : TypeHelper.ConvertAttributeDictionary(targetedFields.Attributes, (HashSet)requestResources)) { } /// public IEnumerable> GetDiffs() { - if (!_databaseValuesLoaded) ThrowNoDbValuesError(); + if (!_databaseValuesLoaded) + { + ThrowNoDbValuesError(); + } foreach (var resource in this) { @@ -52,8 +57,10 @@ public IEnumerable> GetDiffs() } /// - public new HashSet GetAffected(Expression> navigationAction) + public override HashSet GetAffected(Expression> navigationAction) { + ArgumentGuard.NotNull(navigationAction, nameof(navigationAction)); + var propertyInfo = TypeHelper.ParseNavigationExpression(navigationAction); var propertyType = propertyInfo.PropertyType; if (TypeHelper.IsOrImplementsInterface(propertyType, typeof(IEnumerable))) @@ -66,11 +73,10 @@ public IEnumerable> GetDiffs() // the navigation action references a relationship. Redirect the call to the relationship dictionary. return base.GetAffected(navigationAction); } - else if (_updatedAttributes.TryGetValue(propertyInfo, out HashSet resources)) - { - return resources; - } - return new HashSet(); + + return _updatedAttributes.TryGetValue(propertyInfo, out HashSet resources) + ? resources + : new HashSet(); } private void ThrowNoDbValuesError() diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs index 486146ff6f..222c8754e9 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs @@ -49,11 +49,14 @@ public IResourceHookContainer GetResourceHookContainer(RightType targetResource, container = _genericProcessorFactory.Get(typeof(ResourceHooksDefinition<>), targetResource); _hookContainers[targetResource] = container; } - if (container == null) return null; + if (container == null) + { + return null; + } // if there was a container, first check if it implements the hook we // want to use it for. - List targetHooks; + IEnumerable targetHooks; if (hook == ResourceHook.None) { CheckForTargetHookExistence(); @@ -61,12 +64,15 @@ public IResourceHookContainer GetResourceHookContainer(RightType targetResource, } else { - targetHooks = new List { hook }; + targetHooks = hook.AsEnumerable(); } foreach (ResourceHook targetHook in targetHooks) { - if (ShouldExecuteHook(targetResource, targetHook)) return container; + if (ShouldExecuteHook(targetResource, targetHook)) + { + return container; + } } return null; } @@ -81,12 +87,16 @@ public IEnumerable LoadDbValues(LeftType resourceTypeForRepository, IEnumerable { var idType = TypeHelper.GetIdType(resourceTypeForRepository); var parameterizedGetWhere = GetType() - .GetMethod(nameof(GetWhereAndInclude), BindingFlags.NonPublic | BindingFlags.Instance) + .GetMethod(nameof(GetWhereAndInclude), BindingFlags.NonPublic | BindingFlags.Instance)! .MakeGenericMethod(resourceTypeForRepository, idType); var cast = ((IEnumerable)resources).Cast(); var ids = TypeHelper.CopyToList(cast.Select(i => i.GetTypedId()), idType); - var values = (IEnumerable)parameterizedGetWhere.Invoke(this, new object[] { ids, relationshipsToNextLayer }); - if (values == null) return null; + var values = (IEnumerable)parameterizedGetWhere.Invoke(this, ArrayFactory.Create(ids, relationshipsToNextLayer)); + if (values == null) + { + return null; + } + return (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(resourceTypeForRepository), TypeHelper.CopyToList(values, resourceTypeForRepository)); } @@ -94,7 +104,11 @@ public HashSet LoadDbValues(IEnumerable resourc { var resourceType = typeof(TResource); var dbValues = LoadDbValues(resourceType, resources, hook, relationships)?.Cast(); - if (dbValues == null) return null; + if (dbValues == null) + { + return null; + } + return new HashSet(dbValues); } @@ -102,9 +116,15 @@ public bool ShouldLoadDbValues(Type resourceType, ResourceHook hook) { var discovery = GetHookDiscovery(resourceType); if (discovery.DatabaseValuesDisabledHooks.Contains(hook)) + { return false; + } + if (discovery.DatabaseValuesEnabledHooks.Contains(hook)) + { return true; + } + return _options.LoadDatabaseValues; } @@ -117,8 +137,10 @@ private bool ShouldExecuteHook(RightType resourceType, ResourceHook hook) private void CheckForTargetHookExistence() { if (!_targetedHooksForRelatedResources.Any()) + { throw new InvalidOperationException("Something is not right in the breadth first traversal of resource hook: " + "trying to get meta information when no allowed hooks are set"); + } } private IHooksDiscovery GetHookDiscovery(Type resourceType) @@ -180,50 +202,69 @@ public Dictionary LoadImplicitlyAffected( Dictionary leftResourcesByRelation, IEnumerable existingRightResources = null) { + var existingRightResourceList = existingRightResources?.Cast().ToList(); + var implicitlyAffected = new Dictionary(); foreach (var kvp in leftResourcesByRelation) { - if (IsHasManyThrough(kvp, out var lefts, out var relationship)) continue; + if (IsHasManyThrough(kvp, out var lefts, out var relationship)) + { + continue; + } // note that we don't have to check if BeforeImplicitUpdate hook is implemented. If not, it wont ever get here. var includedLefts = LoadDbValues(relationship.LeftType, lefts, ResourceHook.BeforeImplicitUpdateRelationship, relationship); - foreach (IIdentifiable ip in includedLefts) + AddToImplicitlyAffected(includedLefts, relationship, existingRightResourceList, implicitlyAffected); + } + + return implicitlyAffected.ToDictionary(kvp => kvp.Key, kvp => TypeHelper.CreateHashSetFor(kvp.Key.RightType, kvp.Value)); + } + + private void AddToImplicitlyAffected(IEnumerable includedLefts, RelationshipAttribute relationship, List existingRightResourceList, + Dictionary implicitlyAffected) + { + foreach (IIdentifiable ip in includedLefts) + { + IList dbRightResourceList = TypeHelper.CreateListFor(relationship.RightType); + var relationshipValue = relationship.GetValue(ip); + if (!(relationshipValue is IEnumerable)) { - IList dbRightResourceList = TypeHelper.CreateListFor(relationship.RightType); - var relationshipValue = relationship.GetValue(ip); - if (!(relationshipValue is IEnumerable)) - { - if (relationshipValue != null) dbRightResourceList.Add(relationshipValue); - } - else + if (relationshipValue != null) { - foreach (var item in (IEnumerable) relationshipValue) - { - dbRightResourceList.Add(item); - } + dbRightResourceList.Add(relationshipValue); } + } + else + { + AddToList(dbRightResourceList, (IEnumerable)relationshipValue); + } - var dbRightResourceListCast = dbRightResourceList.Cast().ToList(); - if (existingRightResources != null) dbRightResourceListCast = dbRightResourceListCast.Except(existingRightResources.Cast(), _comparer).ToList(); + var dbRightResourceListCast = dbRightResourceList.Cast().ToList(); + if (existingRightResourceList != null) + { + dbRightResourceListCast = dbRightResourceListCast.Except(existingRightResourceList, _comparer).ToList(); + } - if (dbRightResourceListCast.Any()) + if (dbRightResourceListCast.Any()) + { + if (!implicitlyAffected.TryGetValue(relationship, out IEnumerable affected)) { - if (!implicitlyAffected.TryGetValue(relationship, out IEnumerable affected)) - { - affected = TypeHelper.CreateListFor(relationship.RightType); - implicitlyAffected[relationship] = affected; - } - - foreach (var item in dbRightResourceListCast) - { - ((IList)affected).Add(item); - } + affected = TypeHelper.CreateListFor(relationship.RightType); + implicitlyAffected[relationship] = affected; } + + AddToList((IList)affected, dbRightResourceListCast); } } + } - return implicitlyAffected.ToDictionary(kvp => kvp.Key, kvp => TypeHelper.CreateHashSetFor(kvp.Key.RightType, kvp.Value)); + private static void AddToList(IList list, IEnumerable itemsToAdd) + { + foreach (var item in itemsToAdd) + { + list.Add(item); + } } private bool IsHasManyThrough(KeyValuePair kvp, diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs index ca95ddef87..7f07fc083c 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs @@ -16,9 +16,9 @@ public interface IRelationshipGetters where TLeftResource : class /// Dictionary> GetByRelationship() where TRightResource : class, IIdentifiable; /// - /// Gets a dictionary of all resources that have an affected relationship to type + /// Gets a dictionary of all resources that have an affected relationship to type /// - Dictionary> GetByRelationship(Type relatedResourceType); + Dictionary> GetByRelationship(Type resourceType); /// /// Gets a collection of all the resources for the property within /// has been affected by the request diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs index 1ddd5cbc36..8de79a01ed 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs @@ -1,7 +1,20 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + namespace JsonApiDotNetCore.Hooks.Internal.Execution { /// /// A dummy interface used internally by the hook executor. /// public interface IRelationshipsDictionary { } -} \ No newline at end of file + + /// + /// A helper class that provides insights in which relationships have been updated for which resources. + /// + public interface IRelationshipsDictionary : + IRelationshipGetters, + IReadOnlyDictionary>, + IRelationshipsDictionary where TRightResource : class, IIdentifiable + { } +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs index f624b93aad..0aa1b9406d 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs @@ -3,27 +3,19 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Hooks.Internal.Execution { - /// - /// A helper class that provides insights in which relationships have been updated for which resources. - /// - public interface IRelationshipsDictionary : - IRelationshipGetters, - IReadOnlyDictionary>, - IRelationshipsDictionary where TRightResource : class, IIdentifiable - { } - - /// /// Implementation of IAffectedRelationships{TRightResource} /// /// It is practically a ReadOnlyDictionary{RelationshipAttribute, HashSet{TRightResource}} dictionary /// with the two helper methods defined on IAffectedRelationships{TRightResource}. /// + [PublicAPI] public class RelationshipsDictionary : Dictionary>, IRelationshipsDictionary where TResource : class, IIdentifiable @@ -47,14 +39,16 @@ public Dictionary> GetByRelationship - public Dictionary> GetByRelationship(Type relatedType) + public Dictionary> GetByRelationship(Type resourceType) { - return this.Where(p => p.Key.RightType == relatedType).ToDictionary(p => p.Key, p => p.Value); + return this.Where(p => p.Key.RightType == resourceType).ToDictionary(p => p.Key, p => p.Value); } /// public HashSet GetAffected(Expression> navigationAction) { + ArgumentGuard.NotNull(navigationAction, nameof(navigationAction)); + var property = TypeHelper.ParseNavigationExpression(navigationAction); return this.Where(p => p.Key.Property.Name == property.Name).Select(p => p.Value).SingleOrDefault(); } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs index a627c5d38b..fe52b4f9b3 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCore.Hooks.Internal.Execution @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution /// A wrapper that contains a resource that is affected by the request, /// matched to its current database value /// + [PublicAPI] public sealed class ResourceDiffPair where TResource : class, IIdentifiable { public ResourceDiffPair(TResource resource, TResource databaseValue) @@ -23,4 +25,4 @@ public ResourceDiffPair(TResource resource, TResource databaseValue) /// public TResource DatabaseValue { get; } } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs index 9b61f57d36..192bcdad21 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq.Expressions; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -15,6 +16,7 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution /// Also contains information about updated relationships through /// implementation of IRelationshipsDictionary> /// + [PublicAPI] public class ResourceHashSet : HashSet, IResourceHashSet where TResource : class, IIdentifiable { /// @@ -37,9 +39,9 @@ internal ResourceHashSet(IEnumerable resources, /// - public Dictionary> GetByRelationship(Type leftType) + public Dictionary> GetByRelationship(Type resourceType) { - return _relationships.GetByRelationship(leftType); + return _relationships.GetByRelationship(resourceType); } /// @@ -49,7 +51,7 @@ public Dictionary> GetByRelationship - public HashSet GetAffected(Expression> navigationAction) + public virtual HashSet GetAffected(Expression> navigationAction) { return _relationships.GetAffected(navigationAction); } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs index 9d8c906106..3c256f1ad4 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs @@ -1,4 +1,5 @@ using System.Threading; +using JetBrains.Annotations; using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.Hooks.Internal.Execution @@ -8,6 +9,7 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution /// is called from , it will be called /// with parameter pipeline = ResourceAction.GetSingle. /// + [PublicAPI] public enum ResourcePipeline { None, diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs index ecd62c521e..d2bfcb39a8 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCore.Hooks.Internal.Traversal; @@ -13,6 +14,8 @@ using LeftType = System.Type; using RightType = System.Type; +// ReSharper disable PossibleMultipleEnumeration + namespace JsonApiDotNetCore.Hooks.Internal { /// @@ -43,14 +46,20 @@ public void BeforeRead(ResourcePipeline pipeline, string stringId = n { var hookContainer = _executorHelper.GetResourceHookContainer(ResourceHook.BeforeRead); hookContainer?.BeforeRead(pipeline, false, stringId); - var calledContainers = new List { typeof(TResource) }; + var calledContainers = typeof(TResource).AsList(); + + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true var includes = _constraintProviders - .SelectMany(p => p.GetConstraints()) + .SelectMany(provider => provider.GetConstraints()) .Select(expressionInScope => expressionInScope.Expression) .OfType() .ToArray(); + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + foreach (var chain in includes.SelectMany(IncludeChainConverter.GetRelationshipChains)) { RecursiveBeforeRead(chain.Fields.Cast().ToList(), pipeline, calledContainers); @@ -128,7 +137,7 @@ public IEnumerable OnReturn(IEnumerable resourc Traverse(_traversalHelper.CreateNextLayer(node), ResourceHook.OnReturn, (nextContainer, nextNode) => { - var filteredUniqueSet = CallHook(nextContainer, ResourceHook.OnReturn, new object[] { nextNode.UniqueResources, pipeline }); + var filteredUniqueSet = CallHook(nextContainer, ResourceHook.OnReturn, ArrayFactory.Create(nextNode.UniqueResources, pipeline)); nextNode.UpdateUnique(filteredUniqueSet); nextNode.Reassign(); }); @@ -145,7 +154,7 @@ public void AfterRead(IEnumerable resources, ResourcePipel Traverse(_traversalHelper.CreateNextLayer(node), ResourceHook.AfterRead, (nextContainer, nextNode) => { - CallHook(nextContainer, ResourceHook.AfterRead, new object[] { nextNode.UniqueResources, pipeline, true }); + CallHook(nextContainer, ResourceHook.AfterRead, ArrayFactory.Create(nextNode.UniqueResources, pipeline, true)); }); } @@ -206,16 +215,32 @@ private bool GetHook(ResourceHook target, IEnumerable reso /// private void Traverse(NodeLayer currentLayer, ResourceHook target, Action action) { - if (!currentLayer.AnyResources()) return; - foreach (IResourceNode node in currentLayer) + var nextLayer = currentLayer; + + while (true) { - var resourceType = node.ResourceType; - var hookContainer = _executorHelper.GetResourceHookContainer(resourceType, target); - if (hookContainer == null) continue; - action(hookContainer, node); + if (!nextLayer.AnyResources()) + { + return; + } + + TraverseNextLayer(nextLayer, action, target); + + nextLayer = _traversalHelper.CreateNextLayer(nextLayer.ToList()); } + } + + private void TraverseNextLayer(NodeLayer nextLayer, Action action, ResourceHook target) + { + foreach (IResourceNode node in nextLayer) + { + var hookContainer = _executorHelper.GetResourceHookContainer(node.ResourceType, target); - Traverse(_traversalHelper.CreateNextLayer(currentLayer.ToList()), target, action); + if (hookContainer != null) + { + action(hookContainer, node); + } + } } /// @@ -225,17 +250,33 @@ private void Traverse(NodeLayer currentLayer, ResourceHook target, Action private void RecursiveBeforeRead(List relationshipChain, ResourcePipeline pipeline, List calledContainers) { - var relationship = relationshipChain.First(); - if (!calledContainers.Contains(relationship.RightType)) + while (true) { - calledContainers.Add(relationship.RightType); - var container = _executorHelper.GetResourceHookContainer(relationship.RightType, ResourceHook.BeforeRead); - if (container != null) - CallHook(container, ResourceHook.BeforeRead, new object[] { pipeline, true, null }); + var relationship = relationshipChain.First(); + + if (!calledContainers.Contains(relationship.RightType)) + { + calledContainers.Add(relationship.RightType); + var container = _executorHelper.GetResourceHookContainer(relationship.RightType, ResourceHook.BeforeRead); + + if (container != null) + { + CallHook(container, ResourceHook.BeforeRead, new object[] + { + pipeline, + true, + null + }); + } + } + + relationshipChain.RemoveAt(0); + + if (!relationshipChain.Any()) + { + break; + } } - relationshipChain.RemoveAt(0); - if (relationshipChain.Any()) - RecursiveBeforeRead(relationshipChain, pipeline, calledContainers); } /// @@ -277,7 +318,7 @@ private void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, NodeLayer la currentResourcesGroupedInverse = ReplaceKeysWithInverseRelationships(currentResourcesGrouped); var resourcesByRelationship = CreateRelationshipHelper(resourceType, currentResourcesGroupedInverse, dbValues); - var allowedIds = CallHook(nestedHookContainer, ResourceHook.BeforeUpdateRelationship, new object[] { GetIds(uniqueResources), resourcesByRelationship, pipeline }).Cast(); + var allowedIds = CallHook(nestedHookContainer, ResourceHook.BeforeUpdateRelationship, ArrayFactory.Create(GetIds(uniqueResources), resourcesByRelationship, pipeline)).Cast(); var updated = GetAllowedResources(uniqueResources, allowedIds); node.UpdateUnique(updated); node.Reassign(); @@ -313,7 +354,7 @@ private void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, NodeLayer la // RelationshipAttribute from owner to article, which is the // inverse of HasOneAttribute:owner currentResourcesGroupedInverse = ReplaceKeysWithInverseRelationships(currentResourcesGrouped); - // Note that currently in the JADNC implementation of hooks, + // Note that currently in the JsonApiDotNetCore implementation of hooks, // the root layer is ALWAYS homogenous, so we safely assume // that for every relationship to the previous layer, the // left type is the same. @@ -332,7 +373,7 @@ private Dictionary ReplaceKeysWithInverseRel { // when Article has one Owner (HasOneAttribute:owner) is set, there is no guarantee // that the inverse attribute was also set (Owner has one Article: HasOneAttr:article). - // If it isn't, JADNC currently knows nothing about this relationship pointing back, and it + // If it isn't, JsonApiDotNetCore currently knows nothing about this relationship pointing back, and it // currently cannot fire hooks for resources resolved through inverse relationships. var inversableRelationshipAttributes = resourcesByRelationship.Where(kvp => kvp.Key.InverseNavigationProperty != null); return inversableRelationshipAttributes.ToDictionary(kvp => _resourceGraph.GetInverseRelationship(kvp.Key), kvp => kvp.Value); @@ -345,12 +386,20 @@ private Dictionary ReplaceKeysWithInverseRel private void FireForAffectedImplicits(Type resourceTypeToInclude, Dictionary implicitsTarget, ResourcePipeline pipeline, IEnumerable existingImplicitResources = null) { var container = _executorHelper.GetResourceHookContainer(resourceTypeToInclude, ResourceHook.BeforeImplicitUpdateRelationship); - if (container == null) return; + if (container == null) + { + return; + } + var implicitAffected = _executorHelper.LoadImplicitlyAffected(implicitsTarget, existingImplicitResources); - if (!implicitAffected.Any()) return; + if (!implicitAffected.Any()) + { + return; + } + var inverse = implicitAffected.ToDictionary(kvp => _resourceGraph.GetInverseRelationship(kvp.Key), kvp => kvp.Value); var resourcesByRelationship = CreateRelationshipHelper(resourceTypeToInclude, inverse); - CallHook(container, ResourceHook.BeforeImplicitUpdateRelationship, new object[] { resourcesByRelationship, pipeline}); + CallHook(container, ResourceHook.BeforeImplicitUpdateRelationship, ArrayFactory.Create(resourcesByRelationship, pipeline)); } /// @@ -359,12 +408,13 @@ private void FireForAffectedImplicits(Type resourceTypeToInclude, Dictionary /// The collection returned from the hook /// The pipeline from which the hook was fired + [AssertionMethod] private void ValidateHookResponse(IEnumerable returnedList, ResourcePipeline pipeline = 0) { if (pipeline == ResourcePipeline.GetSingle && returnedList.Count() > 1) { - throw new ApplicationException("The returned collection from this hook may contain at most one item in the case of the" + - pipeline.ToString("G") + "pipeline"); + throw new InvalidOperationException("The returned collection from this hook may contain at most one item in the case of the " + + pipeline.ToString("G") + " pipeline"); } } @@ -377,7 +427,7 @@ private IEnumerable CallHook(IResourceHookContainer container, ResourceHook hook // note that some of the hooks return "void". When these hooks, the // are called reflectively with Invoke like here, the return value // is just null, so we don't have to worry about casting issues here. - return (IEnumerable)ThrowJsonApiExceptionOnError(() => method.Invoke(container, arguments)); + return (IEnumerable)ThrowJsonApiExceptionOnError(() => method?.Invoke(container, arguments)); } /// @@ -389,7 +439,7 @@ private object ThrowJsonApiExceptionOnError(Func action) { return action(); } - catch (TargetInvocationException tie) + catch (TargetInvocationException tie) when (tie.InnerException != null) { throw tie.InnerException; } @@ -402,8 +452,14 @@ private object ThrowJsonApiExceptionOnError(Func action) /// The relationship helper. private IRelationshipsDictionary CreateRelationshipHelper(RightType resourceType, Dictionary prevLayerRelationships, IEnumerable dbValues = null) { - if (dbValues != null) prevLayerRelationships = ReplaceWithDbValues(prevLayerRelationships, dbValues.Cast()); - return (IRelationshipsDictionary)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipsDictionary<>), resourceType, true, prevLayerRelationships); + var prevLayerRelationshipsWithDbValues = prevLayerRelationships; + + if (dbValues != null) + { + prevLayerRelationshipsWithDbValues = ReplaceWithDbValues(prevLayerRelationshipsWithDbValues, dbValues.Cast()); + } + + return (IRelationshipsDictionary)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipsDictionary<>), resourceType, true, prevLayerRelationshipsWithDbValues); } /// @@ -414,7 +470,10 @@ private Dictionary ReplaceWithDbValues(Dicti { foreach (var key in prevLayerRelationships.Keys.ToList()) { - var replaced = TypeHelper.CopyToList(prevLayerRelationships[key].Cast().Select(resource => dbValues.Single(dbResource => dbResource.StringId == resource.StringId)), key.LeftType); + var source = prevLayerRelationships[key].Cast().Select(resource => + dbValues.Single(dbResource => dbResource.StringId == resource.StringId)); + + var replaced = TypeHelper.CopyToList(source, key.LeftType); prevLayerRelationships[key] = TypeHelper.CreateHashSetFor(key.LeftType, replaced); } return prevLayerRelationships; @@ -443,7 +502,11 @@ private IEnumerable LoadDbValues(Type resourceType, IEnumerable uniqueResources, { // We only need to load database values if the target hook of this hook execution // cycle is compatible with displaying database values and has this option enabled. - if (!_executorHelper.ShouldLoadDbValues(resourceType, targetHook)) return null; + if (!_executorHelper.ShouldLoadDbValues(resourceType, targetHook)) + { + return null; + } + return _executorHelper.LoadDbValues(resourceType, uniqueResources, targetHook, relationshipsToNextLayer); } @@ -459,7 +522,7 @@ private void FireAfterUpdateRelationship(IResourceHookContainer container, IReso // For the nested hook we need to replace these attributes with their inverse. // See the FireNestedBeforeUpdateHooks method for a more detailed example. var resourcesByRelationship = CreateRelationshipHelper(node.ResourceType, ReplaceKeysWithInverseRelationships(currentResourcesGrouped)); - CallHook(container, ResourceHook.AfterUpdateRelationship, new object[] { resourcesByRelationship, pipeline }); + CallHook(container, ResourceHook.AfterUpdateRelationship, ArrayFactory.Create(resourcesByRelationship, pipeline)); } /// diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutorFacade.cs b/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutorFacade.cs index 2f41667a97..136666500f 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutorFacade.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutorFacade.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -19,9 +18,11 @@ internal sealed class ResourceHookExecutorFacade : IResourceHookExecutorFacade public ResourceHookExecutorFacade(IResourceHookExecutor resourceHookExecutor, IResourceFactory resourceFactory) { - _resourceHookExecutor = - resourceHookExecutor ?? throw new ArgumentNullException(nameof(resourceHookExecutor)); - _resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); + ArgumentGuard.NotNull(resourceHookExecutor, nameof(resourceHookExecutor)); + ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); + + _resourceHookExecutor = resourceHookExecutor; + _resourceFactory = resourceFactory; } public void BeforeReadSingle(TId id, ResourcePipeline pipeline) @@ -36,7 +37,7 @@ public void BeforeReadSingle(TId id, ResourcePipeline pipeline) public void AfterReadSingle(TResource resource, ResourcePipeline pipeline) where TResource : class, IIdentifiable { - _resourceHookExecutor.AfterRead(ToList(resource), pipeline); + _resourceHookExecutor.AfterRead(resource.AsList(), pipeline); } public void BeforeReadMany() @@ -54,37 +55,37 @@ public void AfterReadMany(IReadOnlyCollection resources) public void BeforeCreate(TResource resource) where TResource : class, IIdentifiable { - _resourceHookExecutor.BeforeCreate(ToList(resource), ResourcePipeline.Post); + _resourceHookExecutor.BeforeCreate(resource.AsList(), ResourcePipeline.Post); } public void AfterCreate(TResource resource) where TResource : class, IIdentifiable { - _resourceHookExecutor.AfterCreate(ToList(resource), ResourcePipeline.Post); + _resourceHookExecutor.AfterCreate(resource.AsList(), ResourcePipeline.Post); } public void BeforeUpdateResource(TResource resource) where TResource : class, IIdentifiable { - _resourceHookExecutor.BeforeUpdate(ToList(resource), ResourcePipeline.Patch); + _resourceHookExecutor.BeforeUpdate(resource.AsList(), ResourcePipeline.Patch); } public void AfterUpdateResource(TResource resource) where TResource : class, IIdentifiable { - _resourceHookExecutor.AfterUpdate(ToList(resource), ResourcePipeline.Patch); + _resourceHookExecutor.AfterUpdate(resource.AsList(), ResourcePipeline.Patch); } public void BeforeUpdateRelationship(TResource resource) where TResource : class, IIdentifiable { - _resourceHookExecutor.BeforeUpdate(ToList(resource), ResourcePipeline.PatchRelationship); + _resourceHookExecutor.BeforeUpdate(resource.AsList(), ResourcePipeline.PatchRelationship); } public void AfterUpdateRelationship(TResource resource) where TResource : class, IIdentifiable { - _resourceHookExecutor.AfterUpdate(ToList(resource), ResourcePipeline.PatchRelationship); + _resourceHookExecutor.AfterUpdate(resource.AsList(), ResourcePipeline.PatchRelationship); } public void BeforeDelete(TId id) @@ -93,7 +94,7 @@ public void BeforeDelete(TId id) var temporaryResource = _resourceFactory.CreateInstance(); temporaryResource.Id = id; - _resourceHookExecutor.BeforeDelete(ToList(temporaryResource), ResourcePipeline.Delete); + _resourceHookExecutor.BeforeDelete(temporaryResource.AsList(), ResourcePipeline.Delete); } public void AfterDelete(TId id) @@ -102,13 +103,13 @@ public void AfterDelete(TId id) var temporaryResource = _resourceFactory.CreateInstance(); temporaryResource.Id = id; - _resourceHookExecutor.AfterDelete(ToList(temporaryResource), ResourcePipeline.Delete, true); + _resourceHookExecutor.AfterDelete(temporaryResource.AsList(), ResourcePipeline.Delete, true); } public void OnReturnSingle(TResource resource, ResourcePipeline pipeline) where TResource : class, IIdentifiable { - _resourceHookExecutor.OnReturn(ToList(resource), pipeline); + _resourceHookExecutor.OnReturn(resource.AsList(), pipeline); } public IReadOnlyCollection OnReturnMany(IReadOnlyCollection resources) @@ -127,16 +128,11 @@ public object OnReturnRelationship(object resourceOrResources) if (resourceOrResources is IIdentifiable) { - var resources = ToList((dynamic)resourceOrResources); + var resources = ObjectExtensions.AsList((dynamic)resourceOrResources); return Enumerable.SingleOrDefault(_resourceHookExecutor.OnReturn(resources, ResourcePipeline.GetRelationship)); } return resourceOrResources; } - - private static List ToList(TResource resource) - { - return new List {resource}; - } } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs index f35540e7d4..8e0ee92706 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs @@ -13,10 +13,14 @@ namespace JsonApiDotNetCore.Hooks.Internal.Traversal internal sealed class ChildNode : IResourceNode where TResource : class, IIdentifiable { private readonly IdentifiableComparer _comparer = IdentifiableComparer.Instance; + private readonly RelationshipsFromPreviousLayer _relationshipsFromPreviousLayer; + /// public RightType ResourceType { get; } + /// public RelationshipProxy[] RelationshipsToNextLayer { get; } + /// public IEnumerable UniqueResources { @@ -29,8 +33,6 @@ public IEnumerable UniqueResources /// public IRelationshipsFromPreviousLayer RelationshipsFromPreviousLayer => _relationshipsFromPreviousLayer; - private readonly RelationshipsFromPreviousLayer _relationshipsFromPreviousLayer; - public ChildNode(RelationshipProxy[] nextLayerRelationships, RelationshipsFromPreviousLayer prevLayerRelationships) { ResourceType = typeof(TResource); @@ -39,7 +41,7 @@ public ChildNode(RelationshipProxy[] nextLayerRelationships, RelationshipsFromPr } /// - public void UpdateUnique(IEnumerable updated) + public void UpdateUnique(IEnumerable updated) { List cast = updated.Cast().ToList(); foreach (var group in _relationshipsFromPreviousLayer) @@ -59,22 +61,28 @@ public void Reassign(IEnumerable updated = null) var proxy = group.Proxy; var leftResources = group.LeftResources; - foreach (IIdentifiable left in leftResources) - { - var currentValue = proxy.GetValue(left); + Reassign(leftResources, proxy, unique); + } + } - if (currentValue is IEnumerable relationshipCollection) - { - var intersection = relationshipCollection.Intersect(unique, _comparer); - IEnumerable typedCollection = TypeHelper.CopyToTypedCollection(intersection, relationshipCollection.GetType()); - proxy.SetValue(left, typedCollection); - } - else if (currentValue is IIdentifiable relationshipSingle) + private void Reassign(IEnumerable leftResources, RelationshipProxy proxy, HashSet unique) + { + foreach (IIdentifiable left in leftResources) + { + var currentValue = proxy.GetValue(left); + + if (currentValue is IEnumerable relationshipCollection) + { + var intersection = relationshipCollection.Intersect(unique, _comparer); + IEnumerable typedCollection = + TypeHelper.CopyToTypedCollection(intersection, relationshipCollection.GetType()); + proxy.SetValue(left, typedCollection); + } + else if (currentValue is IIdentifiable relationshipSingle) + { + if (!unique.Intersect(new HashSet {relationshipSingle}, _comparer).Any()) { - if (!unique.Intersect(new HashSet { relationshipSingle }, _comparer).Any()) - { - proxy.SetValue(left, null); - } + proxy.SetValue(left, null); } } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ITraversalHelper.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ITraversalHelper.cs index d3e2e2ba6a..4ca6ddfac0 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ITraversalHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ITraversalHelper.cs @@ -15,7 +15,7 @@ internal interface ITraversalHelper NodeLayer CreateNextLayer(IEnumerable nodes); /// /// Creates a root node for breadth-first-traversal (BFS). Note that typically, in - /// JADNC, the root layer will be homogeneous. Also, because it is the first layer, + /// JsonApiDotNetCore, the root layer will be homogeneous. Also, because it is the first layer, /// there can be no relationships to previous layers, only to next layers. /// /// The root node. diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs index 054d4c155c..86677d1907 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs @@ -21,6 +21,8 @@ internal sealed class RelationshipProxy { private readonly bool _skipThroughType; + public Type LeftType => Attribute.LeftType; + /// /// The target type for this relationship attribute. /// For HasOne has HasMany this is trivial: just the right-hand side. @@ -28,10 +30,11 @@ internal sealed class RelationshipProxy /// Identifiable) or it is the right-hand side (when the through resource is not identifiable) /// public Type RightType { get; } - public Type LeftType => Attribute.LeftType; + public bool IsContextRelation { get; } - public RelationshipAttribute Attribute { get; set; } + public RelationshipAttribute Attribute { get; } + public RelationshipProxy(RelationshipAttribute attr, Type relatedType, bool isContextRelation) { RightType = relatedType; @@ -60,12 +63,19 @@ public object GetValue(IIdentifiable resource) } var collection = new List(); var throughResources = (IEnumerable)hasManyThrough.ThroughProperty.GetValue(resource); - if (throughResources == null) return null; + if (throughResources == null) + { + return null; + } foreach (var throughResource in throughResources) { var rightResource = (IIdentifiable)hasManyThrough.RightProperty.GetValue(throughResource); - if (rightResource == null) continue; + if (rightResource == null) + { + continue; + } + collection.Add(rightResource); } @@ -95,7 +105,7 @@ public void SetValue(IIdentifiable resource, object value) var filteredList = new List(); var rightResources = TypeHelper.CopyToList((IEnumerable)value, RightType); - foreach (var throughResource in throughResources) + foreach (var throughResource in throughResources ?? Array.Empty()) { if (rightResources.Contains(hasManyThrough.RightProperty.GetValue(throughResource))) { diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs index 99a3057841..166b98f0a2 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs @@ -24,7 +24,7 @@ public Dictionary> LeftsToN { return _allRelationshipsToNextLayer .GroupBy(proxy => proxy.RightType) - .ToDictionary(gdc => gdc.Key, gdc => gdc.ToDictionary(p => p.Attribute, p => UniqueResources)); + .ToDictionary(gdc => gdc.Key, gdc => gdc.ToDictionary(p => p.Attribute, _ => UniqueResources)); } /// @@ -32,7 +32,7 @@ public Dictionary> LeftsToN /// public Dictionary LeftsToNextLayer() { - return RelationshipsToNextLayer.ToDictionary(p => p.Attribute, p => UniqueResources); + return RelationshipsToNextLayer.ToDictionary(p => p.Attribute, _ => UniqueResources); } /// diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/TraversalHelper.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/TraversalHelper.cs index bf6a1234c5..b27c0762ea 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/TraversalHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/TraversalHelper.cs @@ -44,7 +44,7 @@ public TraversalHelper( /// /// Creates a root node for breadth-first-traversal. Note that typically, in - /// JADNC, the root layer will be homogeneous. Also, because it is the first layer, + /// JsonApiDotNetCore, the root layer will be homogeneous. Also, because it is the first layer, /// there can be no relationships to previous layers, only to next layers. /// /// The root node. @@ -67,7 +67,7 @@ public RootNode CreateRootNode(IEnumerable root /// Root node. public NodeLayer CreateNextLayer(IResourceNode rootNode) { - return CreateNextLayer(new[] { rootNode }); + return CreateNextLayer(rootNode.AsEnumerable()); } /// @@ -121,37 +121,18 @@ private Dictionary private (Dictionary>, Dictionary>) ExtractResources(IEnumerable leftNodes) { - var leftResourcesGrouped = new Dictionary>(); // RelationshipAttr_prevLayer->currentLayer => prevLayerResources - var rightResourcesGrouped = new Dictionary>(); // RelationshipAttr_prevLayer->currentLayer => currentLayerResources + // RelationshipAttr_prevLayer->currentLayer => prevLayerResources + var leftResourcesGrouped = new Dictionary>(); + + // RelationshipAttr_prevLayer->currentLayer => currentLayerResources + var rightResourcesGrouped = new Dictionary>(); foreach (var node in leftNodes) { var leftResources = node.UniqueResources; var relationships = node.RelationshipsToNextLayer; - foreach (IIdentifiable leftResource in leftResources) - { - foreach (var proxy in relationships) - { - var relationshipValue = proxy.GetValue(leftResource); - // skip this relationship if it's not populated - if (!proxy.IsContextRelation && relationshipValue == null) continue; - if (!(relationshipValue is IEnumerable rightResources)) - { - // in the case of a to-one relationship, the assigned value - // will not be a list. We therefore first wrap it in a list. - var list = TypeHelper.CreateListFor(proxy.RightType); - if (relationshipValue != null) list.Add(relationshipValue); - rightResources = list; - } - - var uniqueRightResources = UniqueInTree(rightResources.Cast(), proxy.RightType); - if (proxy.IsContextRelation || uniqueRightResources.Any()) - { - AddToRelationshipGroup(rightResourcesGrouped, proxy, uniqueRightResources); - AddToRelationshipGroup(leftResourcesGrouped, proxy, new[] { leftResource }); - } - } - } + + ExtractLeftResources(leftResources, relationships, rightResourcesGrouped, leftResourcesGrouped); } var processResourcesMethod = GetType().GetMethod(nameof(ProcessResources), BindingFlags.NonPublic | BindingFlags.Instance); @@ -159,12 +140,56 @@ private Dictionary(list)); } return (leftResourcesGrouped, rightResourcesGrouped); } + private void ExtractLeftResources(IEnumerable leftResources, RelationshipProxy[] relationships, Dictionary> rightResourcesGrouped, + Dictionary> leftResourcesGrouped) + { + foreach (IIdentifiable leftResource in leftResources) + { + ExtractLeftResource(leftResource, relationships, rightResourcesGrouped, leftResourcesGrouped); + } + } + + private void ExtractLeftResource(IIdentifiable leftResource, RelationshipProxy[] relationships, + Dictionary> rightResourcesGrouped, + Dictionary> leftResourcesGrouped) + { + foreach (var proxy in relationships) + { + var relationshipValue = proxy.GetValue(leftResource); + // skip this relationship if it's not populated + if (!proxy.IsContextRelation && relationshipValue == null) + { + continue; + } + + if (!(relationshipValue is IEnumerable rightResources)) + { + // in the case of a to-one relationship, the assigned value + // will not be a list. We therefore first wrap it in a list. + var list = TypeHelper.CreateListFor(proxy.RightType); + if (relationshipValue != null) + { + list.Add(relationshipValue); + } + + rightResources = list; + } + + var uniqueRightResources = UniqueInTree(rightResources.Cast(), proxy.RightType); + if (proxy.IsContextRelation || uniqueRightResources.Any()) + { + AddToRelationshipGroup(rightResourcesGrouped, proxy, uniqueRightResources); + AddToRelationshipGroup(leftResourcesGrouped, proxy, leftResource.AsEnumerable()); + } + } + } + /// /// Get all populated relationships known in the current tree traversal from a /// left type to any right type @@ -199,13 +224,21 @@ private void RegisterRelationshipProxies(RightType type) { foreach (RelationshipAttribute attr in _resourceGraph.GetRelationships(type)) { - if (!attr.CanInclude) continue; + if (!attr.CanInclude) + { + continue; + } + if (!_relationshipProxies.TryGetValue(attr, out _)) { RightType rightType = GetRightTypeFromRelationship(attr); bool isContextRelation = false; var relationshipsToUpdate = _targetedFields.Relationships; - if (relationshipsToUpdate != null) isContextRelation = relationshipsToUpdate.Contains(attr); + if (relationshipsToUpdate != null) + { + isContextRelation = relationshipsToUpdate.Contains(attr); + } + var proxy = new RelationshipProxy(attr, rightType, isContextRelation); _relationshipProxies[attr] = proxy; } @@ -290,7 +323,8 @@ private IResourceNode CreateNodeInstance(RightType nodeType, RelationshipProxy[] /// private IRelationshipsFromPreviousLayer CreateRelationshipsFromInstance(RightType nodeType, IEnumerable relationshipsFromPrev) { - var cast = TypeHelper.CopyToList(relationshipsFromPrev, relationshipsFromPrev.First().GetType()); + var relationshipsFromPrevList = relationshipsFromPrev.ToList(); + var cast = TypeHelper.CopyToList(relationshipsFromPrevList, relationshipsFromPrevList.First().GetType()); return (IRelationshipsFromPreviousLayer)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipsFromPreviousLayer<>), nodeType, cast); } diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 18954c1bac..33a5e9647e 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -23,6 +23,7 @@ + diff --git a/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs index d7c78a94a8..63d74d18c0 100644 --- a/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -12,8 +11,8 @@ public sealed class AsyncConvertEmptyActionResultFilter : IAsyncConvertEmptyActi /// public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (next == null) throw new ArgumentNullException(nameof(next)); + ArgumentGuard.NotNull(context, nameof(context)); + ArgumentGuard.NotNull(next, nameof(next)); if (context.HttpContext.IsJsonApiRequest()) { diff --git a/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs index 0334f3a9eb..9fa5df8366 100644 --- a/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs @@ -1,24 +1,27 @@ -using System; using System.Threading.Tasks; +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace JsonApiDotNetCore.Middleware { /// + [PublicAPI] public class AsyncJsonApiExceptionFilter : IAsyncJsonApiExceptionFilter { private readonly IExceptionHandler _exceptionHandler; public AsyncJsonApiExceptionFilter(IExceptionHandler exceptionHandler) { - _exceptionHandler = exceptionHandler ?? throw new ArgumentNullException(nameof(exceptionHandler)); + ArgumentGuard.NotNull(exceptionHandler, nameof(exceptionHandler)); + + _exceptionHandler = exceptionHandler; } /// public Task OnExceptionAsync(ExceptionContext context) { - if (context == null) throw new ArgumentNullException(nameof(context)); + ArgumentGuard.NotNull(context, nameof(context)); if (context.HttpContext.IsJsonApiRequest()) { diff --git a/src/JsonApiDotNetCore/Middleware/AsyncQueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncQueryStringActionFilter.cs index 4bb9f88c32..65a7ac14b1 100644 --- a/src/JsonApiDotNetCore/Middleware/AsyncQueryStringActionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/AsyncQueryStringActionFilter.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; using System.Threading.Tasks; using JsonApiDotNetCore.Controllers.Annotations; @@ -14,14 +13,16 @@ public sealed class AsyncQueryStringActionFilter : IAsyncQueryStringActionFilter public AsyncQueryStringActionFilter(IQueryStringReader queryStringReader) { - _queryStringReader = queryStringReader ?? throw new ArgumentNullException(nameof(queryStringReader)); + ArgumentGuard.NotNull(queryStringReader, nameof(queryStringReader)); + + _queryStringReader = queryStringReader; } /// public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (next == null) throw new ArgumentNullException(nameof(next)); + ArgumentGuard.NotNull(context, nameof(context)); + ArgumentGuard.NotNull(next, nameof(next)); if (context.HttpContext.IsJsonApiRequest()) { diff --git a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs index b85125a821..568cf6fed7 100644 --- a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs +++ b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Serialization.Objects; @@ -9,6 +10,7 @@ namespace JsonApiDotNetCore.Middleware { /// + [PublicAPI] public class ExceptionHandler : IExceptionHandler { private readonly IJsonApiOptions _options; @@ -16,15 +18,16 @@ public class ExceptionHandler : IExceptionHandler public ExceptionHandler(ILoggerFactory loggerFactory, IJsonApiOptions options) { - if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); + ArgumentGuard.NotNull(options, nameof(options)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + _options = options; _logger = loggerFactory.CreateLogger(); } public ErrorDocument HandleException(Exception exception) { - if (exception == null) throw new ArgumentNullException(nameof(exception)); + ArgumentGuard.NotNull(exception, nameof(exception)); Exception demystified = exception.Demystify(); @@ -43,7 +46,7 @@ private void LogException(Exception exception) protected virtual LogLevel GetLogLevel(Exception exception) { - if (exception == null) throw new ArgumentNullException(nameof(exception)); + ArgumentGuard.NotNull(exception, nameof(exception)); if (exception is OperationCanceledException) { @@ -60,33 +63,27 @@ protected virtual LogLevel GetLogLevel(Exception exception) protected virtual string GetLogMessage(Exception exception) { - if (exception == null) throw new ArgumentNullException(nameof(exception)); + ArgumentGuard.NotNull(exception, nameof(exception)); return exception.Message; } protected virtual ErrorDocument CreateErrorDocument(Exception exception) { - if (exception == null) throw new ArgumentNullException(nameof(exception)); + ArgumentGuard.NotNull(exception, nameof(exception)); var errors = exception is JsonApiException jsonApiException ? jsonApiException.Errors : exception is OperationCanceledException - ? new[] - { - new Error((HttpStatusCode) 499) + ? new Error((HttpStatusCode) 499) { Title = "Request execution was canceled." - } - } - : new[] - { - new Error(HttpStatusCode.InternalServerError) + }.AsArray() + : new Error(HttpStatusCode.InternalServerError) { Title = "An unhandled error occurred while processing this request.", Detail = exception.Message - } - }; + }.AsArray(); foreach (var error in errors) { diff --git a/src/JsonApiDotNetCore/Middleware/HttpContextExtensions.cs b/src/JsonApiDotNetCore/Middleware/HttpContextExtensions.cs index ccafcae28c..b77cf79a2a 100644 --- a/src/JsonApiDotNetCore/Middleware/HttpContextExtensions.cs +++ b/src/JsonApiDotNetCore/Middleware/HttpContextExtensions.cs @@ -1,28 +1,29 @@ -using System; +using JetBrains.Annotations; using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Middleware { + [PublicAPI] public static class HttpContextExtensions { - private const string _isJsonApiRequestKey = "JsonApiDotNetCore_IsJsonApiRequest"; + private const string IsJsonApiRequestKey = "JsonApiDotNetCore_IsJsonApiRequest"; /// /// Indicates whether the currently executing HTTP request is being handled by JsonApiDotNetCore. /// public static bool IsJsonApiRequest(this HttpContext httpContext) { - if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + ArgumentGuard.NotNull(httpContext, nameof(httpContext)); - string value = httpContext.Items[_isJsonApiRequestKey] as string; + string value = httpContext.Items[IsJsonApiRequestKey] as string; return value == bool.TrueString; } internal static void RegisterJsonApiRequest(this HttpContext httpContext) { - if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + ArgumentGuard.NotNull(httpContext, nameof(httpContext)); - httpContext.Items[_isJsonApiRequestKey] = bool.TrueString; + httpContext.Items[IsJsonApiRequestKey] = bool.TrueString; } } } diff --git a/src/JsonApiDotNetCore/Middleware/IAsyncConvertEmptyActionResultFilter.cs b/src/JsonApiDotNetCore/Middleware/IAsyncConvertEmptyActionResultFilter.cs index cc1983ae4f..9101c97c21 100644 --- a/src/JsonApiDotNetCore/Middleware/IAsyncConvertEmptyActionResultFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/IAsyncConvertEmptyActionResultFilter.cs @@ -1,12 +1,18 @@ +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.Filters; namespace JsonApiDotNetCore.Middleware { /// /// Converts action result without parameters into action result with null parameter. - /// For example: return NotFound() -> return NotFound(null) + /// + /// return NotFound(null) + /// ]]> + /// /// This ensures our formatter is invoked, where we'll build a JSON:API compliant response. /// For details, see: https://github.com/dotnet/aspnetcore/issues/16969 /// + [PublicAPI] public interface IAsyncConvertEmptyActionResultFilter : IAsyncAlwaysRunResultFilter { } } diff --git a/src/JsonApiDotNetCore/Middleware/IAsyncJsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/IAsyncJsonApiExceptionFilter.cs index f47a13bd58..67b1985816 100644 --- a/src/JsonApiDotNetCore/Middleware/IAsyncJsonApiExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/IAsyncJsonApiExceptionFilter.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.Filters; namespace JsonApiDotNetCore.Middleware @@ -5,5 +6,6 @@ namespace JsonApiDotNetCore.Middleware /// /// Application-wide exception filter that invokes for JSON:API requests. /// + [PublicAPI] public interface IAsyncJsonApiExceptionFilter : IAsyncExceptionFilter { } } diff --git a/src/JsonApiDotNetCore/Middleware/IAsyncQueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/IAsyncQueryStringActionFilter.cs index 2f1f844166..c2a4effcce 100644 --- a/src/JsonApiDotNetCore/Middleware/IAsyncQueryStringActionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/IAsyncQueryStringActionFilter.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.Filters; namespace JsonApiDotNetCore.Middleware @@ -5,5 +6,6 @@ namespace JsonApiDotNetCore.Middleware /// /// Application-wide entry point for processing JSON:API request query strings. /// + [PublicAPI] public interface IAsyncQueryStringActionFilter : IAsyncActionFilter { } } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs index 7a08e89277..1aeb803be6 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.Formatters; namespace JsonApiDotNetCore.Middleware @@ -5,5 +6,6 @@ namespace JsonApiDotNetCore.Middleware /// /// Application-wide entry point for reading JSON:API request bodies. /// + [PublicAPI] public interface IJsonApiInputFormatter : IInputFormatter { } } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs index b02192ae0a..1afd8683f9 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.Formatters; namespace JsonApiDotNetCore.Middleware @@ -5,5 +6,6 @@ namespace JsonApiDotNetCore.Middleware /// /// Application-wide entry point for writing JSON:API response bodies. /// + [PublicAPI] public interface IJsonApiOutputFormatter : IOutputFormatter { } } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs index ecabf6d8eb..84e56e1941 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs @@ -18,8 +18,10 @@ public interface IJsonApiRequest /// The request URL prefix. This may be an absolute or relative path, depending on . /// /// + /// /// string BasePath { get; } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiRoutingConvention.cs index 3d4df4d29c..e1c941414c 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiRoutingConvention.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.ApplicationModels; namespace JsonApiDotNetCore.Middleware @@ -6,5 +7,6 @@ namespace JsonApiDotNetCore.Middleware /// Service for specifying which routing convention to use. This can be overridden to customize /// the relation between controllers and mapped routes. /// + [PublicAPI] public interface IJsonApiRoutingConvention : IApplicationModelConvention, IControllerResourceMapping { } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs index ebab9cc134..fc5a1e2230 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using JsonApiDotNetCore.Serialization; using Microsoft.AspNetCore.Mvc.Formatters; @@ -12,7 +11,7 @@ public sealed class JsonApiInputFormatter : IJsonApiInputFormatter /// public bool CanRead(InputFormatterContext context) { - if (context == null) throw new ArgumentNullException(nameof(context)); + ArgumentGuard.NotNull(context, nameof(context)); return context.HttpContext.IsJsonApiRequest(); } @@ -20,7 +19,7 @@ public bool CanRead(InputFormatterContext context) /// public async Task ReadAsync(InputFormatterContext context) { - if (context == null) throw new ArgumentNullException(nameof(context)); + ArgumentGuard.NotNull(context, nameof(context)); var reader = context.HttpContext.RequestServices.GetRequiredService(); return await reader.ReadAsync(context); diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index a18b413010..46aded0e1f 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -6,6 +6,7 @@ using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization; @@ -21,10 +22,11 @@ namespace JsonApiDotNetCore.Middleware /// /// Intercepts HTTP requests to populate injected instance for JSON:API requests. /// + [PublicAPI] public sealed class JsonApiMiddleware { - private static readonly MediaTypeHeaderValue _mediaType = MediaTypeHeaderValue.Parse(HeaderConstants.MediaType); - private static readonly MediaTypeHeaderValue _atomicOperationsMediaType = MediaTypeHeaderValue.Parse(HeaderConstants.AtomicOperationsMediaType); + private static readonly MediaTypeHeaderValue MediaType = MediaTypeHeaderValue.Parse(HeaderConstants.MediaType); + private static readonly MediaTypeHeaderValue AtomicOperationsMediaType = MediaTypeHeaderValue.Parse(HeaderConstants.AtomicOperationsMediaType); private readonly RequestDelegate _next; @@ -33,17 +35,17 @@ public JsonApiMiddleware(RequestDelegate next) _next = next; } - public async Task Invoke(HttpContext httpContext, + public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options, IJsonApiRequest request, IResourceContextProvider resourceContextProvider) { - if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); - if (controllerResourceMapping == null) throw new ArgumentNullException(nameof(controllerResourceMapping)); - if (options == null) throw new ArgumentNullException(nameof(options)); - if (request == null) throw new ArgumentNullException(nameof(request)); - if (resourceContextProvider == null) throw new ArgumentNullException(nameof(resourceContextProvider)); + ArgumentGuard.NotNull(httpContext, nameof(httpContext)); + ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); var routeValues = httpContext.GetRouteData().Values; @@ -51,7 +53,7 @@ public async Task Invoke(HttpContext httpContext, if (primaryResourceContext != null) { if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerSettings) || - !await ValidateAcceptHeaderAsync(_mediaType, httpContext, options.SerializerSettings)) + !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerSettings)) { return; } @@ -63,7 +65,7 @@ public async Task Invoke(HttpContext httpContext, else if (IsOperationsRequest(routeValues)) { if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerSettings) || - !await ValidateAcceptHeaderAsync(_atomicOperationsMediaType, httpContext, options.SerializerSettings)) + !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerSettings)) { return; } @@ -100,7 +102,8 @@ private static async Task ValidateContentTypeHeaderAsync(string allowedCon await FlushResponseAsync(httpContext.Response, serializerSettings, new Error(HttpStatusCode.UnsupportedMediaType) { Title = "The specified Content-Type header value is not supported.", - Detail = $"Please specify '{allowedContentType}' instead of '{contentType}' for the Content-Type header value." + Detail = $"Please specify '{allowedContentType}' instead of '{contentType}' " + + "for the Content-Type header value." }); return false; } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs index 9f77fc58e7..bd66f66067 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using JsonApiDotNetCore.Serialization; using Microsoft.AspNetCore.Mvc.Formatters; @@ -12,7 +11,7 @@ public sealed class JsonApiOutputFormatter : IJsonApiOutputFormatter /// public bool CanWriteResult(OutputFormatterCanWriteContext context) { - if (context == null) throw new ArgumentNullException(nameof(context)); + ArgumentGuard.NotNull(context, nameof(context)); return context.HttpContext.IsJsonApiRequest(); } @@ -20,7 +19,7 @@ public bool CanWriteResult(OutputFormatterCanWriteContext context) /// public async Task WriteAsync(OutputFormatterWriteContext context) { - if (context == null) throw new ArgumentNullException(nameof(context)); + ArgumentGuard.NotNull(context, nameof(context)); var writer = context.HttpContext.RequestServices.GetRequiredService(); await writer.WriteAsync(context); diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs index b4776419e3..ce2704aadf 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs @@ -1,10 +1,12 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Middleware { /// + [PublicAPI] public sealed class JsonApiRequest : IJsonApiRequest { /// @@ -40,7 +42,7 @@ public sealed class JsonApiRequest : IJsonApiRequest /// public void CopyFrom(IJsonApiRequest other) { - if (other == null) throw new ArgumentNullException(nameof(other)); + ArgumentGuard.NotNull(other, nameof(other)); Kind = other.Kind; BasePath = other.BasePath; diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs index f751e2a6ec..73468d4ef3 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Controllers.Annotations; @@ -28,26 +29,28 @@ namespace JsonApiDotNetCore.Middleware /// /// public class SomeVeryCustomController : CoreJsonApiController { } // => /someVeryCustoms/relationship/relatedResource /// ]]> + [PublicAPI] public class JsonApiRoutingConvention : IJsonApiRoutingConvention { private readonly IJsonApiOptions _options; private readonly IResourceContextProvider _resourceContextProvider; private readonly HashSet _registeredTemplates = new HashSet(); - - private readonly Dictionary _registeredResources = - new Dictionary(); + private readonly Dictionary _registeredResources = new Dictionary(); public JsonApiRoutingConvention(IJsonApiOptions options, IResourceContextProvider resourceContextProvider) { - _options = options ?? throw new ArgumentNullException(nameof(options)); - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + + _options = options; + _resourceContextProvider = resourceContextProvider; } /// public Type GetResourceTypeForController(string controllerName) { - if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); - + ArgumentGuard.NotNull(controllerName, nameof(controllerName)); + if (_registeredResources.TryGetValue(controllerName, out var resourceContext)) { return resourceContext.ResourceType; @@ -59,7 +62,7 @@ public Type GetResourceTypeForController(string controllerName) /// public void Apply(ApplicationModel application) { - if (application == null) throw new ArgumentNullException(nameof(application)); + ArgumentGuard.NotNull(application, nameof(application)); foreach (var controller in application.Controllers) { @@ -79,7 +82,7 @@ public void Apply(ApplicationModel application) } } - if (!RoutingConventionDisabled(controller)) + if (!IsRoutingConventionEnabled(controller)) { continue; } @@ -95,14 +98,10 @@ public void Apply(ApplicationModel application) } } - /// - /// Verifies if routing convention should be enabled for this controller. - /// - private bool RoutingConventionDisabled(ControllerModel controller) + private bool IsRoutingConventionEnabled(ControllerModel controller) { - var type = controller.ControllerType; - var notDisabled = type.GetCustomAttribute() == null; - return notDisabled && type.IsSubclassOf(typeof(CoreJsonApiController)); + return controller.ControllerType.IsSubclassOf(typeof(CoreJsonApiController)) && + controller.ControllerType.GetCustomAttribute() == null; } /// @@ -127,9 +126,7 @@ private string TemplateFromResource(ControllerModel model) /// private string TemplateFromController(ControllerModel model) { - string controllerName = - _options.SerializerContractResolver.NamingStrategy.GetPropertyName(model.ControllerName, false); - + string controllerName = _options.SerializerNamingStrategy.GetPropertyName(model.ControllerName, false); var template = $"{_options.Namespace}/{controllerName}"; if (_registeredTemplates.Add(template)) diff --git a/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs b/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs index 6e03c521dc..7ba6490547 100644 --- a/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs +++ b/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs @@ -42,9 +42,9 @@ private static string FormatMessage(string memberName, object parameters) builder.Append("Entering "); builder.Append(memberName); - builder.Append("("); + builder.Append('('); WriteProperties(builder, parameters); - builder.Append(")"); + builder.Append(')'); return builder.ToString(); } @@ -82,9 +82,9 @@ private static void WriteProperty(StringBuilder builder, PropertyInfo property, } else if (value is string stringValue) { - builder.Append("\""); + builder.Append('"'); builder.Append(stringValue); - builder.Append("\""); + builder.Append('"'); } else { diff --git a/src/JsonApiDotNetCore/ObjectExtensions.cs b/src/JsonApiDotNetCore/ObjectExtensions.cs new file mode 100644 index 0000000000..7dfa76e02c --- /dev/null +++ b/src/JsonApiDotNetCore/ObjectExtensions.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace JsonApiDotNetCore +{ + internal static class ObjectExtensions + { + public static IEnumerable AsEnumerable(this T element) + { + yield return element; + } + + public static T[] AsArray(this T element) + { + return new[] {element}; + } + + public static List AsList(this T element) + { + return new List {element}; + } + } +} diff --git a/src/JsonApiDotNetCore/Queries/ExpressionInScope.cs b/src/JsonApiDotNetCore/Queries/ExpressionInScope.cs index b24257a3fc..56f56468c1 100644 --- a/src/JsonApiDotNetCore/Queries/ExpressionInScope.cs +++ b/src/JsonApiDotNetCore/Queries/ExpressionInScope.cs @@ -1,4 +1,4 @@ -using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; @@ -7,6 +7,7 @@ namespace JsonApiDotNetCore.Queries /// /// Represents an expression coming from query string. The scope determines at which depth in the to apply its expression. /// + [PublicAPI] public class ExpressionInScope { public ResourceFieldChainExpression Scope { get; } @@ -14,8 +15,10 @@ public class ExpressionInScope public ExpressionInScope(ResourceFieldChainExpression scope, QueryExpression expression) { + ArgumentGuard.NotNull(expression, nameof(expression)); + Scope = scope; - Expression = expression ?? throw new ArgumentNullException(nameof(expression)); + Expression = expression; } public override string ToString() diff --git a/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs index 66ceabe09c..47bdde4bda 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs @@ -1,4 +1,4 @@ -using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Queries.Internal.Parsing; namespace JsonApiDotNetCore.Queries.Expressions @@ -6,13 +6,16 @@ namespace JsonApiDotNetCore.Queries.Expressions /// /// Represents the "has" filter function, resulting from text such as: has(articles) /// + [PublicAPI] public class CollectionNotEmptyExpression : FilterExpression { public ResourceFieldChainExpression TargetCollection { get; } public CollectionNotEmptyExpression(ResourceFieldChainExpression targetCollection) { - TargetCollection = targetCollection ?? throw new ArgumentNullException(nameof(targetCollection)); + ArgumentGuard.NotNull(targetCollection, nameof(targetCollection)); + + TargetCollection = targetCollection; } public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs index 318d61fe68..16063c9c1f 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs @@ -1,11 +1,13 @@ using System; using Humanizer; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { /// /// Represents a comparison filter function, resulting from text such as: equals(name,'Joe') /// + [PublicAPI] public class ComparisonExpression : FilterExpression { public ComparisonOperator Operator { get; } @@ -14,9 +16,12 @@ public class ComparisonExpression : FilterExpression public ComparisonExpression(ComparisonOperator @operator, QueryExpression left, QueryExpression right) { + ArgumentGuard.NotNull(left, nameof(left)); + ArgumentGuard.NotNull(right, nameof(right)); + Operator = @operator; - Left = left ?? throw new ArgumentNullException(nameof(left)); - Right = right ?? throw new ArgumentNullException(nameof(right)); + Left = left; + Right = right; } public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs index 3f6b83bb8e..26459b943f 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs @@ -1,4 +1,4 @@ -using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Queries.Internal.Parsing; namespace JsonApiDotNetCore.Queries.Expressions @@ -6,13 +6,16 @@ namespace JsonApiDotNetCore.Queries.Expressions /// /// Represents the "count" function, resulting from text such as: count(articles) /// + [PublicAPI] public class CountExpression : FunctionExpression { public ResourceFieldChainExpression TargetCollection { get; } public CountExpression(ResourceFieldChainExpression targetCollection) { - TargetCollection = targetCollection ?? throw new ArgumentNullException(nameof(targetCollection)); + ArgumentGuard.NotNull(targetCollection, nameof(targetCollection)); + + TargetCollection = targetCollection; } public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs index 134d8cebdd..69076e2d39 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using JetBrains.Annotations; using JsonApiDotNetCore.Queries.Internal.Parsing; namespace JsonApiDotNetCore.Queries.Expressions @@ -9,6 +10,7 @@ namespace JsonApiDotNetCore.Queries.Expressions /// /// Represents the "any" filter function, resulting from text such as: any(name,'Jack','Joe') /// + [PublicAPI] public class EqualsAnyOfExpression : FilterExpression { public ResourceFieldChainExpression TargetAttribute { get; } @@ -17,13 +19,16 @@ public class EqualsAnyOfExpression : FilterExpression public EqualsAnyOfExpression(ResourceFieldChainExpression targetAttribute, IReadOnlyCollection constants) { - TargetAttribute = targetAttribute ?? throw new ArgumentNullException(nameof(targetAttribute)); - Constants = constants ?? throw new ArgumentNullException(nameof(constants)); + ArgumentGuard.NotNull(targetAttribute, nameof(targetAttribute)); + ArgumentGuard.NotNull(constants, nameof(constants)); if (constants.Count < 2) { throw new ArgumentException("At least two constants are required.", nameof(constants)); } + + TargetAttribute = targetAttribute; + Constants = constants; } public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs index b46759f85f..6814166c4f 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Resources.Annotations; @@ -16,6 +15,7 @@ internal static class IncludeChainConverter /// /// /// Input tree: + /// /// Output chains: + /// Blog, /// Article -> Revisions -> Author + /// ]]> /// public static IReadOnlyCollection GetRelationshipChains(IncludeExpression include) { - if (include == null) - { - throw new ArgumentNullException(nameof(include)); - } + ArgumentGuard.NotNull(include, nameof(include)); IncludeToChainsConverter converter = new IncludeToChainsConverter(); converter.Visit(include, null); @@ -47,10 +46,12 @@ public static IReadOnlyCollection GetRelationshipC /// /// /// Input chains: + /// Blog, /// Article -> Revisions -> Author - /// + /// ]]> /// Output tree: + /// GetRelationshipC /// Author /// } /// } + /// ]]> /// public static IncludeExpression FromRelationshipChains(IReadOnlyCollection chains) { - if (chains == null) - { - throw new ArgumentNullException(nameof(chains)); - } + ArgumentGuard.NotNull(chains, nameof(chains)); var elements = ConvertChainsToElements(chains); return elements.Any() ? new IncludeExpression(elements) : IncludeExpression.Empty; @@ -77,20 +76,25 @@ private static IReadOnlyCollection ConvertChainsToElem foreach (ResourceFieldChainExpression chain in chains) { - MutableIncludeNode currentNode = rootNode; + ConvertChainToElement(chain, rootNode); + } - foreach (var relationship in chain.Fields.OfType()) - { - if (!currentNode.Children.ContainsKey(relationship)) - { - currentNode.Children[relationship] = new MutableIncludeNode(relationship); - } + return rootNode.Children.Values.Select(child => child.ToExpression()).ToArray(); + } + + private static void ConvertChainToElement(ResourceFieldChainExpression chain, MutableIncludeNode rootNode) + { + MutableIncludeNode currentNode = rootNode; - currentNode = currentNode.Children[relationship]; + foreach (var relationship in chain.Fields.OfType()) + { + if (!currentNode.Children.ContainsKey(relationship)) + { + currentNode.Children[relationship] = new MutableIncludeNode(relationship); } - } - return rootNode.Children.Values.Select(child => child.ToExpression()).ToArray(); + currentNode = currentNode.Children[relationship]; + } } private sealed class IncludeToChainsConverter : QueryExpressionVisitor diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs index c24390bce1..2b9be3d95d 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Expressions @@ -9,6 +10,7 @@ namespace JsonApiDotNetCore.Queries.Expressions /// /// Represents an element in . /// + [PublicAPI] public class IncludeElementExpression : QueryExpression { public RelationshipAttribute Relationship { get; } @@ -21,8 +23,11 @@ public IncludeElementExpression(RelationshipAttribute relationship) public IncludeElementExpression(RelationshipAttribute relationship, IReadOnlyCollection children) { - Relationship = relationship ?? throw new ArgumentNullException(nameof(relationship)); - Children = children ?? throw new ArgumentNullException(nameof(children)); + ArgumentGuard.NotNull(relationship, nameof(relationship)); + ArgumentGuard.NotNull(children, nameof(children)); + + Relationship = relationship; + Children = children; } public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs index d580f6c18d..a96065db56 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { /// /// Represents an inclusion tree, resulting from text such as: owner,articles.revisions /// + [PublicAPI] public class IncludeExpression : QueryExpression { public IReadOnlyCollection Elements { get; } @@ -20,12 +22,14 @@ private IncludeExpression() public IncludeExpression(IReadOnlyCollection elements) { - Elements = elements ?? throw new ArgumentNullException(nameof(elements)); + ArgumentGuard.NotNull(elements, nameof(elements)); if (!elements.Any()) { throw new ArgumentException("Must have one or more elements.", nameof(elements)); } + + Elements = elements; } public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs index 21876cf1e0..e59299ba3d 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs @@ -1,17 +1,20 @@ -using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { /// /// Represents a non-null constant value, resulting from text such as: equals(firstName,'Jack') /// + [PublicAPI] public class LiteralConstantExpression : IdentifierExpression { public string Value { get; } public LiteralConstantExpression(string text) { - Value = text ?? throw new ArgumentNullException(nameof(text)); + ArgumentGuard.NotNull(text, nameof(text)); + + Value = text; } public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs index 474ed177d0..ef093e204f 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs @@ -3,12 +3,14 @@ using System.Linq; using System.Text; using Humanizer; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { /// /// Represents a logical filter function, resulting from text such as: and(equals(title,'Work'),has(articles)) /// + [PublicAPI] public class LogicalExpression : FilterExpression { public LogicalOperator Operator { get; } @@ -16,10 +18,7 @@ public class LogicalExpression : FilterExpression public LogicalExpression(LogicalOperator @operator, IReadOnlyCollection terms) { - if (terms == null) - { - throw new ArgumentNullException(nameof(terms)); - } + ArgumentGuard.NotNull(terms, nameof(terms)); if (terms.Count < 2) { diff --git a/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs index 099546c033..6c026d0258 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs @@ -1,12 +1,14 @@ using System; using System.Text; using Humanizer; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { /// /// Represents a text-matching filter function, resulting from text such as: startsWith(name,'A') /// + [PublicAPI] public class MatchTextExpression : FilterExpression { public ResourceFieldChainExpression TargetAttribute { get; } @@ -16,8 +18,11 @@ public class MatchTextExpression : FilterExpression public MatchTextExpression(ResourceFieldChainExpression targetAttribute, LiteralConstantExpression textValue, TextMatchKind matchKind) { - TargetAttribute = targetAttribute ?? throw new ArgumentNullException(nameof(targetAttribute)); - TextValue = textValue ?? throw new ArgumentNullException(nameof(textValue)); + ArgumentGuard.NotNull(targetAttribute, nameof(targetAttribute)); + ArgumentGuard.NotNull(textValue, nameof(textValue)); + + TargetAttribute = targetAttribute; + TextValue = textValue; MatchKind = matchKind; } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs index e9fd9bb06d..23a9637045 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs @@ -1,4 +1,4 @@ -using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Queries.Internal.Parsing; namespace JsonApiDotNetCore.Queries.Expressions @@ -6,13 +6,16 @@ namespace JsonApiDotNetCore.Queries.Expressions /// /// Represents the "not" filter function, resulting from text such as: not(equals(title,'Work')) /// + [PublicAPI] public class NotExpression : FilterExpression { public QueryExpression Child { get; } public NotExpression(QueryExpression child) { - Child = child ?? throw new ArgumentNullException(nameof(child)); + ArgumentGuard.NotNull(child, nameof(child)); + + Child = child; } public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs index 7344518ef1..1041be47dd 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Queries.Internal.Parsing; namespace JsonApiDotNetCore.Queries.Expressions @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Queries.Expressions /// /// Represents the constant null, resulting from text such as: equals(lastName,null) /// + [PublicAPI] public class NullConstantExpression : IdentifierExpression { public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs index 9c1bc03c0b..29dbf72d99 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs @@ -1,10 +1,12 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { /// /// Represents an element in . /// + [PublicAPI] public class PaginationElementQueryStringValueExpression : QueryExpression { public ResourceFieldChainExpression Scope { get; } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs index a035b25167..593e2ca1b8 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; namespace JsonApiDotNetCore.Queries.Expressions @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Queries.Expressions /// /// Represents a pagination, produced from . /// + [PublicAPI] public class PaginationExpression : QueryExpression { public PageNumber PageNumber { get; } @@ -13,7 +15,9 @@ public class PaginationExpression : QueryExpression public PaginationExpression(PageNumber pageNumber, PageSize pageSize) { - PageNumber = pageNumber ?? throw new ArgumentNullException(nameof(pageNumber)); + ArgumentGuard.NotNull(pageNumber, nameof(pageNumber)); + + PageNumber = pageNumber; PageSize = pageSize; } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs index 7bd17ce921..5e054d000e 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { /// /// Represents pagination in a query string, resulting from text such as: 1,articles:2 /// + [PublicAPI] public class PaginationQueryStringValueExpression : QueryExpression { public IReadOnlyCollection Elements { get; } @@ -14,12 +16,14 @@ public class PaginationQueryStringValueExpression : QueryExpression public PaginationQueryStringValueExpression( IReadOnlyCollection elements) { - Elements = elements ?? throw new ArgumentNullException(nameof(elements)); + ArgumentGuard.NotNull(elements, nameof(elements)); - if (!Elements.Any()) + if (!elements.Any()) { throw new ArgumentException("Must have one or more elements.", nameof(elements)); } + + Elements = elements; } public override TResult Accept(QueryExpressionVisitor visitor, diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs index 8a09340a11..6ff2e777aa 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; namespace JsonApiDotNetCore.Queries.Expressions @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.Queries.Expressions /// /// Building block for rewriting trees. It walks through nested expressions and updates parent on changes. /// + [PublicAPI] public class QueryExpressionRewriter : QueryExpressionVisitor { public override QueryExpression Visit(QueryExpression expression, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs index 309da19819..6b7da6210d 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs @@ -1,8 +1,11 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCore.Queries.Expressions { /// /// Implements the visitor design pattern that enables traversing a tree. /// + [PublicAPI] public abstract class QueryExpressionVisitor { public virtual TResult Visit(QueryExpression expression, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs index 8c9279259a..ca514c6638 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs @@ -1,10 +1,12 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { /// /// Represents the scope of a query string parameter, resulting from text such as: ?filter[articles]=... /// + [PublicAPI] public class QueryStringParameterScopeExpression : QueryExpression { public LiteralConstantExpression ParameterName { get; } @@ -12,7 +14,9 @@ public class QueryStringParameterScopeExpression : QueryExpression public QueryStringParameterScopeExpression(LiteralConstantExpression parameterName, ResourceFieldChainExpression scope) { - ParameterName = parameterName ?? throw new ArgumentNullException(nameof(parameterName)); + ArgumentGuard.NotNull(parameterName, nameof(parameterName)); + + ParameterName = parameterName; Scope = scope; } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs index 37bb6cde07..dd47117388 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs @@ -1,13 +1,15 @@ using System; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using Microsoft.Extensions.Primitives; namespace JsonApiDotNetCore.Queries.Expressions { /// - /// Holds a expression, used for custom query string handlers from s. + /// Holds an expression, used for custom query string handlers from s. /// + [PublicAPI] public class QueryableHandlerExpression : QueryExpression { private readonly object _queryableHandler; @@ -15,7 +17,9 @@ public class QueryableHandlerExpression : QueryExpression public QueryableHandlerExpression(object queryableHandler, StringValues parameterValue) { - _queryableHandler = queryableHandler ?? throw new ArgumentNullException(nameof(queryableHandler)); + ArgumentGuard.NotNull(queryableHandler, nameof(queryableHandler)); + + _queryableHandler = queryableHandler; _parameterValue = parameterValue; } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs index 03de34a8f0..40c30113d9 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Expressions @@ -8,28 +9,28 @@ namespace JsonApiDotNetCore.Queries.Expressions /// /// Represents a chain of fields (relationships and attributes), resulting from text such as: articles.revisions.author /// + [PublicAPI] public class ResourceFieldChainExpression : IdentifierExpression { public IReadOnlyCollection Fields { get; } public ResourceFieldChainExpression(ResourceFieldAttribute field) { - if (field == null) - { - throw new ArgumentNullException(nameof(field)); - } + ArgumentGuard.NotNull(field, nameof(field)); - Fields = new[] {field}; + Fields = field.AsArray(); } public ResourceFieldChainExpression(IReadOnlyCollection fields) { - Fields = fields ?? throw new ArgumentNullException(nameof(fields)); + ArgumentGuard.NotNull(fields, nameof(fields)); if (!fields.Any()) { throw new ArgumentException("Must have one or more fields.", nameof(fields)); } + + Fields = fields; } public override TResult Accept(QueryExpressionVisitor visitor, diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs index 186a85013f..4711bd0cdc 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs @@ -1,11 +1,13 @@ using System; using System.Text; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { /// /// Represents an element in . /// + [PublicAPI] public class SortElementExpression : QueryExpression { public ResourceFieldChainExpression TargetAttribute { get; } @@ -14,13 +16,17 @@ public class SortElementExpression : QueryExpression public SortElementExpression(ResourceFieldChainExpression targetAttribute, bool isAscending) { - TargetAttribute = targetAttribute ?? throw new ArgumentNullException(nameof(targetAttribute)); + ArgumentGuard.NotNull(targetAttribute, nameof(targetAttribute)); + + TargetAttribute = targetAttribute; IsAscending = isAscending; } public SortElementExpression(CountExpression count, in bool isAscending) { - Count = count ?? throw new ArgumentNullException(nameof(count)); + ArgumentGuard.NotNull(count, nameof(count)); + + Count = count; IsAscending = isAscending; } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs index 61683a95a6..43e25df7e3 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs @@ -1,24 +1,28 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { /// /// Represents a sorting, resulting from text such as: lastName,-lastModifiedAt /// + [PublicAPI] public class SortExpression : QueryExpression { public IReadOnlyCollection Elements { get; } public SortExpression(IReadOnlyCollection elements) { - Elements = elements ?? throw new ArgumentNullException(nameof(elements)); + ArgumentGuard.NotNull(elements, nameof(elements)); if (!elements.Any()) { throw new ArgumentException("Must have one or more elements.", nameof(elements)); } + + Elements = elements; } public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs index 9e8a930703..db55b1ee29 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Expressions @@ -8,18 +9,21 @@ namespace JsonApiDotNetCore.Queries.Expressions /// /// Represents a sparse fieldset, resulting from text such as: firstName,lastName,articles /// + [PublicAPI] public class SparseFieldSetExpression : QueryExpression { public IReadOnlyCollection Fields { get; } public SparseFieldSetExpression(IReadOnlyCollection fields) { - Fields = fields ?? throw new ArgumentNullException(nameof(fields)); + ArgumentGuard.NotNull(fields, nameof(fields)); if (!fields.Any()) { throw new ArgumentException("Must have one or more fields.", nameof(fields)); } + + Fields = fields; } public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs index 79b4b12781..7375326189 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs @@ -1,34 +1,31 @@ using System; using System.Linq; using System.Linq.Expressions; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { + [PublicAPI] public static class SparseFieldSetExpressionExtensions { public static SparseFieldSetExpression Including(this SparseFieldSetExpression sparseFieldSet, Expression> fieldSelector, IResourceGraph resourceGraph) where TResource : class, IIdentifiable { - if (fieldSelector == null) - { - throw new ArgumentNullException(nameof(fieldSelector)); - } + ArgumentGuard.NotNull(fieldSelector, nameof(fieldSelector)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - if (resourceGraph == null) - { - throw new ArgumentNullException(nameof(resourceGraph)); - } + var newSparseFieldSet = sparseFieldSet; foreach (var field in resourceGraph.GetFields(fieldSelector)) { - sparseFieldSet = IncludeField(sparseFieldSet, field); + newSparseFieldSet = IncludeField(newSparseFieldSet, field); } - return sparseFieldSet; + return newSparseFieldSet; } private static SparseFieldSetExpression IncludeField(SparseFieldSetExpression sparseFieldSet, ResourceFieldAttribute fieldToInclude) @@ -47,22 +44,17 @@ public static SparseFieldSetExpression Excluding(this SparseFieldSetE Expression> fieldSelector, IResourceGraph resourceGraph) where TResource : class, IIdentifiable { - if (fieldSelector == null) - { - throw new ArgumentNullException(nameof(fieldSelector)); - } + ArgumentGuard.NotNull(fieldSelector, nameof(fieldSelector)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - if (resourceGraph == null) - { - throw new ArgumentNullException(nameof(resourceGraph)); - } + var newSparseFieldSet = sparseFieldSet; foreach (var field in resourceGraph.GetFields(fieldSelector)) { - sparseFieldSet = ExcludeField(sparseFieldSet, field); + newSparseFieldSet = ExcludeField(newSparseFieldSet, field); } - return sparseFieldSet; + return newSparseFieldSet; } private static SparseFieldSetExpression ExcludeField(SparseFieldSetExpression sparseFieldSet, ResourceFieldAttribute fieldToExclude) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs index 468e66a998..fcbdd2fd82 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; namespace JsonApiDotNetCore.Queries.Expressions @@ -9,18 +10,21 @@ namespace JsonApiDotNetCore.Queries.Expressions /// /// Represents a lookup table of sparse fieldsets per resource type. /// + [PublicAPI] public class SparseFieldTableExpression : QueryExpression { public IReadOnlyDictionary Table { get; } public SparseFieldTableExpression(IReadOnlyDictionary table) { - Table = table ?? throw new ArgumentNullException(nameof(table)); + ArgumentGuard.NotNull(table, nameof(table)); if (!table.Any()) { throw new ArgumentException("Must have one or more entries.", nameof(table)); } + + Table = table; } public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) @@ -36,13 +40,13 @@ public override string ToString() { if (builder.Length > 0) { - builder.Append(","); + builder.Append(','); } builder.Append(resource.PublicName); - builder.Append("("); + builder.Append('('); builder.Append(fields); - builder.Append(")"); + builder.Append(')'); } return builder.ToString(); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs index 791089f00d..aeb5358249 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using Humanizer; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources; @@ -10,6 +11,7 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing { + [PublicAPI] public class FilterParser : QueryExpressionParser { private readonly IResourceFactory _resourceFactory; @@ -20,14 +22,19 @@ public FilterParser(IResourceContextProvider resourceContextProvider, IResourceF Action validateSingleFieldCallback = null) : base(resourceContextProvider) { - _resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); + ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); + + _resourceFactory = resourceFactory; _validateSingleFieldCallback = validateSingleFieldCallback; } public FilterExpression Parse(string source, ResourceContext resourceContextInScope) { + ArgumentGuard.NotNull(resourceContextInScope, nameof(resourceContextInScope)); + + _resourceContextInScope = resourceContextInScope; + Tokenize(source); - _resourceContextInScope = resourceContextInScope ?? throw new ArgumentNullException(nameof(resourceContextInScope)); var expression = ParseFilter(); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs index 5510509c97..d1b7d73321 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Internal.Parsing { + [PublicAPI] public class IncludeParser : QueryExpressionParser { private readonly Action _validateSingleRelationshipCallback; @@ -21,7 +23,10 @@ public IncludeParser(IResourceContextProvider resourceContextProvider, public IncludeExpression Parse(string source, ResourceContext resourceContextInScope, int? maximumDepth) { - _resourceContextInScope = resourceContextInScope ?? throw new ArgumentNullException(nameof(resourceContextInScope)); + ArgumentGuard.NotNull(resourceContextInScope, nameof(resourceContextInScope)); + + _resourceContextInScope = resourceContextInScope; + Tokenize(source); var expression = ParseInclude(maximumDepth); @@ -36,10 +41,7 @@ protected IncludeExpression ParseInclude(int? maximumDepth) ResourceFieldChainExpression firstChain = ParseFieldChain(FieldChainRequirements.IsRelationship, "Relationship name expected."); - var chains = new List - { - firstChain - }; + var chains = firstChain.AsList(); while (TokenStack.Any()) { diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/Keywords.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/Keywords.cs index c1933152e1..dabe52e605 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/Keywords.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/Keywords.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCore.Queries.Internal.Parsing { + [PublicAPI] public static class Keywords { public const string Null = "null"; diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs index 765f62e7b1..c4744876a7 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Internal.Parsing { + [PublicAPI] public class PaginationParser : QueryExpressionParser { private readonly Action _validateSingleFieldCallback; @@ -21,7 +23,10 @@ public PaginationParser(IResourceContextProvider resourceContextProvider, public PaginationQueryStringValueExpression Parse(string source, ResourceContext resourceContextInScope) { - _resourceContextInScope = resourceContextInScope ?? throw new ArgumentNullException(nameof(resourceContextInScope)); + ArgumentGuard.NotNull(resourceContextInScope, nameof(resourceContextInScope)); + + _resourceContextInScope = resourceContextInScope; + Tokenize(source); var expression = ParsePagination(); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs index 277d18f62d..15b189c7f7 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; @@ -13,6 +14,7 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing /// Uses a tokenizer to populate a stack of tokens, which is then manipulated from the various parsing routines for subexpressions. /// Implementations should throw on invalid input. /// + [PublicAPI] public abstract class QueryExpressionParser { private protected ResourceFieldChainResolver ChainResolver { get; } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryParseException.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryParseException.cs index 33a556e6c2..da2d85f25a 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryParseException.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryParseException.cs @@ -1,7 +1,9 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Internal.Parsing { + [PublicAPI] public sealed class QueryParseException : Exception { public QueryParseException(string message) : base(message) diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs index 39a00a3098..33965facd4 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Internal.Parsing { + [PublicAPI] public class QueryStringParameterScopeParser : QueryExpressionParser { private readonly FieldChainRequirements _chainRequirements; @@ -22,7 +24,10 @@ public QueryStringParameterScopeParser(IResourceContextProvider resourceContextP public QueryStringParameterScopeExpression Parse(string source, ResourceContext resourceContextInScope) { - _resourceContextInScope = resourceContextInScope ?? throw new ArgumentNullException(nameof(resourceContextInScope)); + ArgumentGuard.NotNull(resourceContextInScope, nameof(resourceContextInScope)); + + _resourceContextInScope = resourceContextInScope; + Tokenize(source); var expression = ParseQueryStringParameterScope(); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs index 25e7659001..0084fa7bc2 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs @@ -1,10 +1,11 @@ -using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Internal.Parsing { + [PublicAPI] public sealed class QueryTokenizer { public static readonly IReadOnlyDictionary SingleCharacterToTokenKinds = @@ -26,7 +27,9 @@ public sealed class QueryTokenizer public QueryTokenizer(string source) { - _source = source ?? throw new ArgumentNullException(nameof(source)); + ArgumentGuard.NotNull(source, nameof(source)); + + _source = source; } public IEnumerable EnumerateTokens() diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs index 4dafa05c1f..b0a4af334f 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs @@ -15,7 +15,9 @@ internal sealed class ResourceFieldChainResolver public ResourceFieldChainResolver(IResourceContextProvider resourceContextProvider) { - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + + _resourceContextProvider = resourceContextProvider; } /// @@ -27,21 +29,22 @@ public IReadOnlyCollection ResolveToManyChain(ResourceCo var chain = new List(); var publicNameParts = path.Split("."); + var nextResourceContext = resourceContext; foreach (string publicName in publicNameParts[..^1]) { - var relationship = GetRelationship(publicName, resourceContext, path); + var relationship = GetRelationship(publicName, nextResourceContext, path); - validateCallback?.Invoke(relationship, resourceContext, path); + validateCallback?.Invoke(relationship, nextResourceContext, path); chain.Add(relationship); - resourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); + nextResourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); } string lastName = publicNameParts[^1]; - var lastToManyRelationship = GetToManyRelationship(lastName, resourceContext, path); + var lastToManyRelationship = GetToManyRelationship(lastName, nextResourceContext, path); - validateCallback?.Invoke(lastToManyRelationship, resourceContext, path); + validateCallback?.Invoke(lastToManyRelationship, nextResourceContext, path); chain.Add(lastToManyRelationship); return chain; @@ -63,15 +66,16 @@ public IReadOnlyCollection ResolveRelationshipChain(Reso Action validateCallback = null) { var chain = new List(); + var nextResourceContext = resourceContext; foreach (string publicName in path.Split(".")) { - var relationship = GetRelationship(publicName, resourceContext, path); + var relationship = GetRelationship(publicName, nextResourceContext, path); - validateCallback?.Invoke(relationship, resourceContext, path); + validateCallback?.Invoke(relationship, nextResourceContext, path); chain.Add(relationship); - resourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); + nextResourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); } return chain; @@ -92,21 +96,22 @@ public IReadOnlyCollection ResolveToOneChainEndingInAttr List chain = new List(); var publicNameParts = path.Split("."); + var nextResourceContext = resourceContext; foreach (string publicName in publicNameParts[..^1]) { - var toOneRelationship = GetToOneRelationship(publicName, resourceContext, path); + var toOneRelationship = GetToOneRelationship(publicName, nextResourceContext, path); - validateCallback?.Invoke(toOneRelationship, resourceContext, path); + validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); chain.Add(toOneRelationship); - resourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); + nextResourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); } string lastName = publicNameParts[^1]; - var lastAttribute = GetAttribute(lastName, resourceContext, path); + var lastAttribute = GetAttribute(lastName, nextResourceContext, path); - validateCallback?.Invoke(lastAttribute, resourceContext, path); + validateCallback?.Invoke(lastAttribute, nextResourceContext, path); chain.Add(lastAttribute); return chain; @@ -127,22 +132,23 @@ public IReadOnlyCollection ResolveToOneChainEndingInToMa List chain = new List(); var publicNameParts = path.Split("."); + var nextResourceContext = resourceContext; foreach (string publicName in publicNameParts[..^1]) { - var toOneRelationship = GetToOneRelationship(publicName, resourceContext, path); + var toOneRelationship = GetToOneRelationship(publicName, nextResourceContext, path); - validateCallback?.Invoke(toOneRelationship, resourceContext, path); + validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); chain.Add(toOneRelationship); - resourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); + nextResourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); } string lastName = publicNameParts[^1]; - var toManyRelationship = GetToManyRelationship(lastName, resourceContext, path); + var toManyRelationship = GetToManyRelationship(lastName, nextResourceContext, path); - validateCallback?.Invoke(toManyRelationship, resourceContext, path); + validateCallback?.Invoke(toManyRelationship, nextResourceContext, path); chain.Add(toManyRelationship); return chain; @@ -163,34 +169,35 @@ public IReadOnlyCollection ResolveToOneChainEndingInAttr List chain = new List(); var publicNameParts = path.Split("."); + var nextResourceContext = resourceContext; foreach (string publicName in publicNameParts[..^1]) { - var toOneRelationship = GetToOneRelationship(publicName, resourceContext, path); + var toOneRelationship = GetToOneRelationship(publicName, nextResourceContext, path); - validateCallback?.Invoke(toOneRelationship, resourceContext, path); + validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); chain.Add(toOneRelationship); - resourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); + nextResourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); } string lastName = publicNameParts[^1]; - var lastField = GetField(lastName, resourceContext, path); + var lastField = GetField(lastName, nextResourceContext, path); if (lastField is HasManyAttribute) { throw new QueryParseException(path == lastName - ? $"Field '{lastName}' must be an attribute or a to-one relationship on resource '{resourceContext.PublicName}'." - : $"Field '{lastName}' in '{path}' must be an attribute or a to-one relationship on resource '{resourceContext.PublicName}'."); + ? $"Field '{lastName}' must be an attribute or a to-one relationship on resource '{nextResourceContext.PublicName}'." + : $"Field '{lastName}' in '{path}' must be an attribute or a to-one relationship on resource '{nextResourceContext.PublicName}'."); } - validateCallback?.Invoke(lastField, resourceContext, path); + validateCallback?.Invoke(lastField, nextResourceContext, path); chain.Add(lastField); return chain; } - public RelationshipAttribute GetRelationship(string publicName, ResourceContext resourceContext, string path) + private RelationshipAttribute GetRelationship(string publicName, ResourceContext resourceContext, string path) { var relationship = resourceContext.Relationships.FirstOrDefault(r => r.PublicName == publicName); @@ -204,7 +211,7 @@ public RelationshipAttribute GetRelationship(string publicName, ResourceContext return relationship; } - public RelationshipAttribute GetToManyRelationship(string publicName, ResourceContext resourceContext, string path) + private RelationshipAttribute GetToManyRelationship(string publicName, ResourceContext resourceContext, string path) { var relationship = GetRelationship(publicName, resourceContext, path); @@ -218,7 +225,7 @@ public RelationshipAttribute GetToManyRelationship(string publicName, ResourceCo return relationship; } - public RelationshipAttribute GetToOneRelationship(string publicName, ResourceContext resourceContext, string path) + private RelationshipAttribute GetToOneRelationship(string publicName, ResourceContext resourceContext, string path) { var relationship = GetRelationship(publicName, resourceContext, path); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs index d8beac4d8f..2b00f686ef 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Internal.Parsing { + [PublicAPI] public class SortParser : QueryExpressionParser { private readonly Action _validateSingleFieldCallback; @@ -21,7 +23,10 @@ public SortParser(IResourceContextProvider resourceContextProvider, public SortExpression Parse(string source, ResourceContext resourceContextInScope) { - _resourceContextInScope = resourceContextInScope ?? throw new ArgumentNullException(nameof(resourceContextInScope)); + ArgumentGuard.NotNull(resourceContextInScope, nameof(resourceContextInScope)); + + _resourceContextInScope = resourceContextInScope; + Tokenize(source); SortExpression expression = ParseSort(); @@ -35,10 +40,7 @@ protected SortExpression ParseSort() { SortElementExpression firstElement = ParseSortElement(); - var elements = new List - { - firstElement - }; + var elements = firstElement.AsList(); while (TokenStack.Any()) { diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs index 25d2a21e71..aea883f43e 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Internal.Parsing { + [PublicAPI] public class SparseFieldSetParser : QueryExpressionParser { private readonly Action _validateSingleFieldCallback; @@ -20,7 +22,10 @@ public SparseFieldSetParser(IResourceContextProvider resourceContextProvider, Ac public SparseFieldSetExpression Parse(string source, ResourceContext resourceContext) { - _resourceContext = resourceContext ?? throw new ArgumentNullException(nameof(resourceContext)); + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + + _resourceContext = resourceContext; + Tokenize(source); var expression = ParseSparseFieldSet(); @@ -56,7 +61,7 @@ protected override IReadOnlyCollection OnResolveFieldCha _validateSingleFieldCallback?.Invoke(field, _resourceContext, path); - return new[] {field}; + return field.AsArray(); } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs index f1fd91b18b..18fd966167 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Internal.Parsing { + [PublicAPI] public class SparseFieldTypeParser : QueryExpressionParser { private readonly IResourceContextProvider _resourceContextProvider; @@ -19,11 +21,11 @@ public ResourceContext Parse(string source) { Tokenize(source); - var expression = ParseSparseFieldTarget(); + var resourceContext = ParseSparseFieldTarget(); AssertTokenStackIsEmpty(); - return expression; + return resourceContext; } private ResourceContext ParseSparseFieldTarget() diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/Token.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/Token.cs index 93995233d2..c8c8623a67 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/Token.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/Token.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCore.Queries.Internal.Parsing { + [PublicAPI] public sealed class Token { public TokenKind Kind { get; } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index 62d1a8a485..eac149391a 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources; @@ -9,6 +10,7 @@ namespace JsonApiDotNetCore.Queries.Internal { /// + [PublicAPI] public class QueryLayerComposer : IQueryLayerComposer { private readonly IEnumerable _constraintProviders; @@ -27,13 +29,20 @@ public QueryLayerComposer( IPaginationContext paginationContext, ITargetedFields targetedFields) { - _constraintProviders = constraintProviders ?? throw new ArgumentNullException(nameof(constraintProviders)); - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); - _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - _paginationContext = paginationContext ?? throw new ArgumentNullException(nameof(paginationContext)); - _targetedFields = targetedFields ?? throw new ArgumentNullException(nameof(targetedFields)); - _sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); + ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(paginationContext, nameof(paginationContext)); + ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); + + _constraintProviders = constraintProviders; + _resourceContextProvider = resourceContextProvider; + _resourceDefinitionAccessor = resourceDefinitionAccessor; + _options = options; + _paginationContext = paginationContext; + _targetedFields = targetedFields; + _sparseFieldSetCache = new SparseFieldSetCache(_constraintProviders, resourceDefinitionAccessor); } /// @@ -41,19 +50,25 @@ public FilterExpression GetTopFilterFromConstraints(ResourceContext resourceCont { var constraints = _constraintProviders.SelectMany(provider => provider.GetConstraints()).ToArray(); + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var filtersInTopScope = constraints .Where(constraint => constraint.Scope == null) .Select(constraint => constraint.Expression) .OfType() .ToArray(); + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + return GetFilter(filtersInTopScope, resourceContext); } /// public QueryLayer ComposeFromConstraints(ResourceContext requestResource) { - if (requestResource == null) throw new ArgumentNullException(nameof(requestResource)); + ArgumentGuard.NotNull(requestResource, nameof(requestResource)); var constraints = _constraintProviders.SelectMany(provider => provider.GetConstraints()).ToArray(); @@ -65,11 +80,17 @@ public QueryLayer ComposeFromConstraints(ResourceContext requestResource) private QueryLayer ComposeTopLayer(IEnumerable constraints, ResourceContext resourceContext) { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var expressionsInTopScope = constraints .Where(constraint => constraint.Scope == null) .Select(constraint => constraint.Expression) .ToArray(); + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + var topPagination = GetPagination(expressionsInTopScope, resourceContext); if (topPagination != null) { @@ -88,12 +109,18 @@ private QueryLayer ComposeTopLayer(IEnumerable constraints, R private IncludeExpression ComposeChildren(QueryLayer topLayer, ICollection constraints) { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var include = constraints .Where(constraint => constraint.Scope == null) .Select(constraint => constraint.Expression) .OfType() .FirstOrDefault() ?? IncludeExpression.Empty; + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + var includeElements = ProcessIncludeSet(include.Elements, topLayer, new List(), constraints); @@ -105,11 +132,11 @@ private IncludeExpression ComposeChildren(QueryLayer topLayer, ICollection ProcessIncludeSet(IReadOnlyCollection includeElements, QueryLayer parentLayer, ICollection parentRelationshipChain, ICollection constraints) { - includeElements = GetIncludeElements(includeElements, parentLayer.ResourceContext) ?? Array.Empty(); + var includeElementsEvaluated = GetIncludeElements(includeElements, parentLayer.ResourceContext) ?? Array.Empty(); var updatesInChildren = new Dictionary>(); - foreach (var includeElement in includeElements) + foreach (var includeElement in includeElementsEvaluated) { parentLayer.Projection ??= new Dictionary(); @@ -120,12 +147,18 @@ private IReadOnlyCollection ProcessIncludeSet(IReadOnl includeElement.Relationship }; + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var expressionsInCurrentScope = constraints .Where(constraint => constraint.Scope != null && constraint.Scope.Fields.SequenceEqual(relationshipChain)) .Select(constraint => constraint.Expression) .ToArray(); + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + var resourceContext = _resourceContextProvider.GetResourceContext(includeElement.Relationship.RightType); @@ -153,13 +186,13 @@ private IReadOnlyCollection ProcessIncludeSet(IReadOnl } } - return !updatesInChildren.Any() ? includeElements : ApplyIncludeElementUpdates(includeElements, updatesInChildren); + return !updatesInChildren.Any() ? includeElementsEvaluated : ApplyIncludeElementUpdates(includeElementsEvaluated, updatesInChildren); } private static IReadOnlyCollection ApplyIncludeElementUpdates(IEnumerable includeElements, IDictionary> updatesInChildren) { - var newIncludeElements = new List(includeElements); + var newIncludeElements = includeElements.ToList(); foreach (var (existingElement, updatedChildren) in updatesInChildren) { @@ -173,14 +206,14 @@ private static IReadOnlyCollection ApplyIncludeElement /// public QueryLayer ComposeForGetById(TId id, ResourceContext resourceContext, TopFieldSelection fieldSelection) { - if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); var idAttribute = GetIdAttribute(resourceContext); var queryLayer = ComposeFromConstraints(resourceContext); queryLayer.Sort = null; queryLayer.Pagination = null; - queryLayer.Filter = CreateFilterByIds(new[] {id}, idAttribute, queryLayer.Filter); + queryLayer.Filter = CreateFilterByIds(id.AsArray(), idAttribute, queryLayer.Filter); if (fieldSelection == TopFieldSelection.OnlyIdAttribute) { @@ -204,7 +237,7 @@ public QueryLayer ComposeForGetById(TId id, ResourceContext resourceContext /// public QueryLayer ComposeSecondaryLayerForRelationship(ResourceContext secondaryResourceContext) { - if (secondaryResourceContext == null) throw new ArgumentNullException(nameof(secondaryResourceContext)); + ArgumentGuard.NotNull(secondaryResourceContext, nameof(secondaryResourceContext)); var secondaryLayer = ComposeFromConstraints(secondaryResourceContext); secondaryLayer.Projection = GetProjectionForRelationship(secondaryResourceContext); @@ -217,21 +250,21 @@ private IDictionary GetProjectionForRelation { var secondaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(secondaryResourceContext); - return secondaryAttributeSet.ToDictionary(key => (ResourceFieldAttribute)key, value => (QueryLayer)null); + return secondaryAttributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer)null); } /// public QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, ResourceContext primaryResourceContext, TId primaryId, RelationshipAttribute secondaryRelationship) { - if (secondaryLayer == null) throw new ArgumentNullException(nameof(secondaryLayer)); - if (primaryResourceContext == null) throw new ArgumentNullException(nameof(primaryResourceContext)); - if (secondaryRelationship == null) throw new ArgumentNullException(nameof(secondaryRelationship)); + ArgumentGuard.NotNull(secondaryLayer, nameof(secondaryLayer)); + ArgumentGuard.NotNull(primaryResourceContext, nameof(primaryResourceContext)); + ArgumentGuard.NotNull(secondaryRelationship, nameof(secondaryRelationship)); var innerInclude = secondaryLayer.Include; secondaryLayer.Include = null; var primaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(primaryResourceContext); - var primaryProjection = primaryAttributeSet.ToDictionary(key => (ResourceFieldAttribute)key, value => (QueryLayer)null); + var primaryProjection = primaryAttributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer)null); primaryProjection[secondaryRelationship] = secondaryLayer; var primaryFilter = GetFilter(Array.Empty(), primaryResourceContext); @@ -240,7 +273,7 @@ public QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, return new QueryLayer(primaryResourceContext) { Include = RewriteIncludeForSecondaryEndpoint(innerInclude, secondaryRelationship), - Filter = CreateFilterByIds(new[] {primaryId}, primaryIdAttribute, primaryFilter), + Filter = CreateFilterByIds(primaryId.AsArray(), primaryIdAttribute, primaryFilter), Projection = primaryProjection }; } @@ -251,7 +284,7 @@ private IncludeExpression RewriteIncludeForSecondaryEndpoint(IncludeExpression r ? new IncludeElementExpression(secondaryRelationship, relativeInclude.Elements) : new IncludeElementExpression(secondaryRelationship); - return new IncludeExpression(new[] {parentElement}); + return new IncludeExpression(parentElement.AsArray()); } private FilterExpression CreateFilterByIds(ICollection ids, AttrAttribute idAttribute, FilterExpression existingFilter) @@ -271,17 +304,21 @@ private FilterExpression CreateFilterByIds(ICollection ids, AttrAttrib filter = new EqualsAnyOfExpression(idChain, constants); } + // @formatter:keep_existing_linebreaks true + return filter == null ? existingFilter : existingFilter == null ? filter - : new LogicalExpression(LogicalOperator.And, new[] {filter, existingFilter}); + : new LogicalExpression(LogicalOperator.And, ArrayFactory.Create(filter, existingFilter)); + + // @formatter:keep_existing_linebreaks restore } /// public QueryLayer ComposeForUpdate(TId id, ResourceContext primaryResource) { - if (primaryResource == null) throw new ArgumentNullException(nameof(primaryResource)); + ArgumentGuard.NotNull(primaryResource, nameof(primaryResource)); var includeElements = _targetedFields.Relationships .Select(relationship => new IncludeElementExpression(relationship)).ToArray(); @@ -292,7 +329,7 @@ public QueryLayer ComposeForUpdate(TId id, ResourceContext primaryResource) primaryLayer.Include = includeElements.Any() ? new IncludeExpression(includeElements) : IncludeExpression.Empty; primaryLayer.Sort = null; primaryLayer.Pagination = null; - primaryLayer.Filter = CreateFilterByIds(new[] {id}, primaryIdAttribute, primaryLayer.Filter); + primaryLayer.Filter = CreateFilterByIds(id.AsArray(), primaryIdAttribute, primaryLayer.Filter); primaryLayer.Projection = null; return primaryLayer; @@ -301,6 +338,8 @@ public QueryLayer ComposeForUpdate(TId id, ResourceContext primaryResource) /// public IEnumerable<(QueryLayer, RelationshipAttribute)> ComposeForGetTargetedSecondaryResourceIds(IIdentifiable primaryResource) { + ArgumentGuard.NotNull(primaryResource, nameof(primaryResource)); + foreach (var relationship in _targetedFields.Relationships) { object rightValue = relationship.GetValue(primaryResource); @@ -317,6 +356,9 @@ public QueryLayer ComposeForUpdate(TId id, ResourceContext primaryResource) /// public QueryLayer ComposeForGetRelationshipRightIds(RelationshipAttribute relationship, ICollection rightResourceIds) { + ArgumentGuard.NotNull(relationship, nameof(relationship)); + ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); + var rightResourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); var rightIdAttribute = GetIdAttribute(rightResourceContext); @@ -339,6 +381,9 @@ public QueryLayer ComposeForGetRelationshipRightIds(RelationshipAttribute relati /// public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, TId leftId, ICollection rightResourceIds) { + ArgumentGuard.NotNull(hasManyRelationship, nameof(hasManyRelationship)); + ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); + var leftResourceContext = _resourceContextProvider.GetResourceContext(hasManyRelationship.LeftType); var leftIdAttribute = GetIdAttribute(leftResourceContext); @@ -346,12 +391,12 @@ public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, T var rightIdAttribute = GetIdAttribute(rightResourceContext); var rightTypedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray(); - var leftFilter = CreateFilterByIds(new[] {leftId}, leftIdAttribute, null); + var leftFilter = CreateFilterByIds(leftId.AsArray(), leftIdAttribute, null); var rightFilter = CreateFilterByIds(rightTypedIds, rightIdAttribute, null); return new QueryLayer(leftResourceContext) { - Include = new IncludeExpression(new[] {new IncludeElementExpression(hasManyRelationship)}), + Include = new IncludeExpression(new IncludeElementExpression(hasManyRelationship).AsArray()), Filter = leftFilter, Projection = new Dictionary { @@ -370,16 +415,15 @@ public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, T protected virtual IReadOnlyCollection GetIncludeElements(IReadOnlyCollection includeElements, ResourceContext resourceContext) { - if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); - includeElements = _resourceDefinitionAccessor.OnApplyIncludes(resourceContext.ResourceType, includeElements); - return includeElements; + return _resourceDefinitionAccessor.OnApplyIncludes(resourceContext.ResourceType, includeElements); } protected virtual FilterExpression GetFilter(IReadOnlyCollection expressionsInScope, ResourceContext resourceContext) { - if (expressionsInScope == null) throw new ArgumentNullException(nameof(expressionsInScope)); - if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + ArgumentGuard.NotNull(expressionsInScope, nameof(expressionsInScope)); + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); var filters = expressionsInScope.OfType().ToArray(); @@ -392,8 +436,8 @@ protected virtual FilterExpression GetFilter(IReadOnlyCollection expressionsInScope, ResourceContext resourceContext) { - if (expressionsInScope == null) throw new ArgumentNullException(nameof(expressionsInScope)); - if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + ArgumentGuard.NotNull(expressionsInScope, nameof(expressionsInScope)); + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); var sort = expressionsInScope.OfType().FirstOrDefault(); @@ -402,7 +446,7 @@ protected virtual SortExpression GetSort(IReadOnlyCollection ex if (sort == null) { var idAttribute = GetIdAttribute(resourceContext); - sort = new SortExpression(new[] {new SortElementExpression(new ResourceFieldChainExpression(idAttribute), true)}); + sort = new SortExpression(new SortElementExpression(new ResourceFieldChainExpression(idAttribute), true).AsArray()); } return sort; @@ -410,8 +454,8 @@ protected virtual SortExpression GetSort(IReadOnlyCollection ex protected virtual PaginationExpression GetPagination(IReadOnlyCollection expressionsInScope, ResourceContext resourceContext) { - if (expressionsInScope == null) throw new ArgumentNullException(nameof(expressionsInScope)); - if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + ArgumentGuard.NotNull(expressionsInScope, nameof(expressionsInScope)); + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); var pagination = expressionsInScope.OfType().FirstOrDefault(); @@ -424,7 +468,7 @@ protected virtual PaginationExpression GetPagination(IReadOnlyCollection GetProjectionForSparseAttributeSet(ResourceContext resourceContext) { - if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); var fieldSet = _sparseFieldSetCache.GetSparseFieldSetForQuery(resourceContext); if (!fieldSet.Any()) @@ -436,7 +480,7 @@ protected virtual IDictionary GetProjectionF var idAttribute = GetIdAttribute(resourceContext); attributeSet.Add(idAttribute); - return attributeSet.ToDictionary(key => (ResourceFieldAttribute)key, value => (QueryLayer)null); + return attributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer)null); } private static AttrAttribute GetIdAttribute(ResourceContext resourceContext) diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs index 09a6f42655..2571b4931f 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs @@ -1,7 +1,7 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; @@ -12,6 +12,7 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding /// /// Transforms into calls. /// + [PublicAPI] public class IncludeClauseBuilder : QueryClauseBuilder { private readonly Expression _source; @@ -22,17 +23,18 @@ public IncludeClauseBuilder(Expression source, LambdaScope lambdaScope, Resource IResourceContextProvider resourceContextProvider) : base(lambdaScope) { - _source = source ?? throw new ArgumentNullException(nameof(source)); - _resourceContext = resourceContext ?? throw new ArgumentNullException(nameof(resourceContext)); - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + ArgumentGuard.NotNull(source, nameof(source)); + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + + _source = source; + _resourceContext = resourceContext; + _resourceContextProvider = resourceContextProvider; } public Expression ApplyInclude(IncludeExpression include) { - if (include == null) - { - throw new ArgumentNullException(nameof(include)); - } + ArgumentGuard.NotNull(include, nameof(include)); return Visit(include, null); } @@ -43,43 +45,48 @@ public override Expression VisitInclude(IncludeExpression expression, object arg foreach (ResourceFieldChainExpression chain in IncludeChainConverter.GetRelationshipChains(expression)) { - string path = null; + source = ProcessRelationshipChain(chain, source); + } - foreach (var relationship in chain.Fields.Cast()) - { - path = path == null ? relationship.RelationshipPath : path + "." + relationship.RelationshipPath; + return source; + } - var resourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); - source = ApplyEagerLoads(source, resourceContext.EagerLoads, path); - } + private Expression ProcessRelationshipChain(ResourceFieldChainExpression chain, Expression source) + { + string path = null; + var result = source; - source = IncludeExtensionMethodCall(source, path); + foreach (var relationship in chain.Fields.Cast()) + { + path = path == null ? relationship.RelationshipPath : path + "." + relationship.RelationshipPath; + + var resourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); + result = ApplyEagerLoads(result, resourceContext.EagerLoads, path); } - return source; + return IncludeExtensionMethodCall(result, path); } private Expression ApplyEagerLoads(Expression source, IEnumerable eagerLoads, string pathPrefix) { + var result = source; + foreach (var eagerLoad in eagerLoads) { string path = pathPrefix != null ? pathPrefix + "." + eagerLoad.Property.Name : eagerLoad.Property.Name; - source = IncludeExtensionMethodCall(source, path); + result = IncludeExtensionMethodCall(result, path); - source = ApplyEagerLoads(source, eagerLoad.Children, path); + result = ApplyEagerLoads(result, eagerLoad.Children, path); } - return source; + return result; } private Expression IncludeExtensionMethodCall(Expression source, string navigationPropertyPath) { Expression navigationExpression = Expression.Constant(navigationPropertyPath); - return Expression.Call(typeof(EntityFrameworkQueryableExtensions), "Include", new[] - { - LambdaScope.Parameter.Type - }, source, navigationExpression); + return Expression.Call(typeof(EntityFrameworkQueryableExtensions), "Include", LambdaScope.Parameter.Type.AsArray(), source, navigationExpression); } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameFactory.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameFactory.cs index 341dd2a23e..40d8b241b3 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameFactory.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameFactory.cs @@ -1,22 +1,20 @@ -using System; using System.Collections.Generic; using Humanizer; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// /// Produces unique names for lambda parameters. /// + [PublicAPI] public sealed class LambdaParameterNameFactory { private readonly HashSet _namesInScope = new HashSet(); public LambdaParameterNameScope Create(string typeName) { - if (typeName == null) - { - throw new ArgumentNullException(nameof(typeName)); - } + ArgumentGuard.NotNull(typeName, nameof(typeName)); string parameterName = typeName.Camelize(); parameterName = EnsureNameIsUnique(parameterName); diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameScope.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameScope.cs index e443997ca1..178ed800b6 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameScope.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameScope.cs @@ -1,7 +1,9 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { + [PublicAPI] public sealed class LambdaParameterNameScope : IDisposable { private readonly LambdaParameterNameFactory _owner; @@ -10,8 +12,11 @@ public sealed class LambdaParameterNameScope : IDisposable public LambdaParameterNameScope(string name, LambdaParameterNameFactory owner) { - _owner = owner ?? throw new ArgumentNullException(nameof(owner)); - Name = name ?? throw new ArgumentNullException(nameof(name)); + ArgumentGuard.NotNull(name, nameof(name)); + ArgumentGuard.NotNull(owner, nameof(owner)); + + Name = name; + _owner = owner; } public void Dispose() diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScope.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScope.cs index 8555fffc43..208c1a9fb3 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScope.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScope.cs @@ -1,5 +1,6 @@ using System; using System.Linq.Expressions; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding /// /// Contains details on a lambda expression, such as the name of the selector "x" in "x => x.Name". /// + [PublicAPI] public sealed class LambdaScope : IDisposable { private readonly LambdaParameterNameScope _parameterNameScope; @@ -17,8 +19,8 @@ public sealed class LambdaScope : IDisposable public LambdaScope(LambdaParameterNameFactory nameFactory, Type elementType, Expression accessorExpression, HasManyThroughAttribute hasManyThrough) { - if (nameFactory == null) throw new ArgumentNullException(nameof(nameFactory)); - if (elementType == null) throw new ArgumentNullException(nameof(elementType)); + ArgumentGuard.NotNull(nameFactory, nameof(nameFactory)); + ArgumentGuard.NotNull(elementType, nameof(elementType)); _parameterNameScope = nameFactory.Create(elementType.Name); Parameter = Expression.Parameter(elementType, _parameterNameScope.Name); diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScopeFactory.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScopeFactory.cs index b0ae0467b8..d9dc5b6a19 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScopeFactory.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScopeFactory.cs @@ -1,9 +1,11 @@ using System; using System.Linq.Expressions; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { + [PublicAPI] public sealed class LambdaScopeFactory { private readonly LambdaParameterNameFactory _nameFactory; @@ -11,16 +13,15 @@ public sealed class LambdaScopeFactory public LambdaScopeFactory(LambdaParameterNameFactory nameFactory, HasManyThroughAttribute hasManyThrough = null) { - _nameFactory = nameFactory ?? throw new ArgumentNullException(nameof(nameFactory)); + ArgumentGuard.NotNull(nameFactory, nameof(nameFactory)); + + _nameFactory = nameFactory; _hasManyThrough = hasManyThrough; } public LambdaScope CreateScope(Type elementType, Expression accessorExpression = null) { - if (elementType == null) - { - throw new ArgumentNullException(nameof(elementType)); - } + ArgumentGuard.NotNull(elementType, nameof(elementType)); return new LambdaScope(_nameFactory, elementType, accessorExpression, _hasManyThrough); } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/OrderClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/OrderClauseBuilder.cs index 2d7d72331e..2ddfb60e47 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/OrderClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/OrderClauseBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using JetBrains.Annotations; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; @@ -10,6 +11,7 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding /// /// Transforms into calls. /// + [PublicAPI] public class OrderClauseBuilder : QueryClauseBuilder { private readonly Expression _source; @@ -18,16 +20,16 @@ public class OrderClauseBuilder : QueryClauseBuilder public OrderClauseBuilder(Expression source, LambdaScope lambdaScope, Type extensionType) : base(lambdaScope) { - _source = source ?? throw new ArgumentNullException(nameof(source)); - _extensionType = extensionType ?? throw new ArgumentNullException(nameof(extensionType)); + ArgumentGuard.NotNull(source, nameof(source)); + ArgumentGuard.NotNull(extensionType, nameof(extensionType)); + + _source = source; + _extensionType = extensionType; } public Expression ApplyOrderBy(SortExpression expression) { - if (expression == null) - { - throw new ArgumentNullException(nameof(expression)); - } + ArgumentGuard.NotNull(expression, nameof(expression)); return Visit(expression, null); } @@ -52,30 +54,38 @@ public override Expression VisitSortElement(SortElementExpression expression, Ex LambdaExpression lambda = Expression.Lambda(body, LambdaScope.Parameter); - string operationName = previousExpression == null ? - expression.IsAscending ? "OrderBy" : "OrderByDescending" : - expression.IsAscending ? "ThenBy" : "ThenByDescending"; + string operationName = GetOperationName(previousExpression != null, expression.IsAscending); return ExtensionMethodCall(previousExpression ?? _source, operationName, body.Type, lambda); } + private static string GetOperationName(bool hasPrecedingSort, bool isAscending) + { + if (hasPrecedingSort) + { + return isAscending ? "ThenBy" : "ThenByDescending"; + } + + return isAscending ? "OrderBy" : "OrderByDescending"; + } + private Expression ExtensionMethodCall(Expression source, string operationName, Type keyType, LambdaExpression keySelector) { - return Expression.Call(_extensionType, operationName, new[] - { - LambdaScope.Parameter.Type, - keyType - }, source, keySelector); + var typeArguments = ArrayFactory.Create(LambdaScope.Parameter.Type, keyType); + return Expression.Call(_extensionType, operationName, typeArguments, source, keySelector); } protected override MemberExpression CreatePropertyExpressionForFieldChain(IReadOnlyCollection chain, Expression source) { - var components = chain.Select(field => - // In case of a HasManyThrough access (from count() function), we only need to look at the number of entries in the join table. - field is HasManyThroughAttribute hasManyThrough ? hasManyThrough.ThroughProperty.Name : field.Property.Name).ToArray(); - + var components = chain.Select(GetPropertyName).ToArray(); return CreatePropertyExpressionFromComponents(LambdaScope.Accessor, components); } + + private static string GetPropertyName(ResourceFieldAttribute field) + { + // In case of a HasManyThrough access (from count() function), we only need to look at the number of entries in the join table. + return field is HasManyThroughAttribute hasManyThrough ? hasManyThrough.ThroughProperty.Name : field.Property.Name; + } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs index 701a867812..60909aa272 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs @@ -17,7 +17,9 @@ public abstract class QueryClauseBuilder : QueryExpressionVisitor /// Drives conversion from into system trees. /// + [PublicAPI] public class QueryableBuilder { private readonly Expression _source; @@ -28,19 +30,27 @@ public QueryableBuilder(Expression source, Type elementType, Type extensionType, IResourceFactory resourceFactory, IResourceContextProvider resourceContextProvider, IModel entityModel, LambdaScopeFactory lambdaScopeFactory = null) { - _source = source ?? throw new ArgumentNullException(nameof(source)); - _elementType = elementType ?? throw new ArgumentNullException(nameof(elementType)); - _extensionType = extensionType ?? throw new ArgumentNullException(nameof(extensionType)); - _nameFactory = nameFactory ?? throw new ArgumentNullException(nameof(nameFactory)); - _resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); - _entityModel = entityModel ?? throw new ArgumentNullException(nameof(entityModel)); + ArgumentGuard.NotNull(source, nameof(source)); + ArgumentGuard.NotNull(elementType, nameof(elementType)); + ArgumentGuard.NotNull(extensionType, nameof(extensionType)); + ArgumentGuard.NotNull(nameFactory, nameof(nameFactory)); + ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(entityModel, nameof(entityModel)); + + _source = source; + _elementType = elementType; + _extensionType = extensionType; + _nameFactory = nameFactory; + _resourceFactory = resourceFactory; + _resourceContextProvider = resourceContextProvider; + _entityModel = entityModel; _lambdaScopeFactory = lambdaScopeFactory ?? new LambdaScopeFactory(_nameFactory); } public virtual Expression ApplyQuery(QueryLayer layer) { - if (layer == null) throw new ArgumentNullException(nameof(layer)); + ArgumentGuard.NotNull(layer, nameof(layer)); Expression expression = _source; diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs index eb8f37e117..09f1d3c3ed 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Repositories; @@ -16,9 +17,10 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding /// /// Transforms into calls. /// + [PublicAPI] public class SelectClauseBuilder : QueryClauseBuilder { - private static readonly ConstantExpression _nullConstant = Expression.Constant(null); + private static readonly ConstantExpression NullConstant = Expression.Constant(null); private readonly Expression _source; private readonly IModel _entityModel; @@ -31,20 +33,24 @@ public SelectClauseBuilder(Expression source, LambdaScope lambdaScope, IModel en LambdaParameterNameFactory nameFactory, IResourceFactory resourceFactory, IResourceContextProvider resourceContextProvider) : base(lambdaScope) { - _source = source ?? throw new ArgumentNullException(nameof(source)); - _entityModel = entityModel ?? throw new ArgumentNullException(nameof(entityModel)); - _extensionType = extensionType ?? throw new ArgumentNullException(nameof(extensionType)); - _nameFactory = nameFactory ?? throw new ArgumentNullException(nameof(nameFactory)); - _resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + ArgumentGuard.NotNull(source, nameof(source)); + ArgumentGuard.NotNull(entityModel, nameof(entityModel)); + ArgumentGuard.NotNull(extensionType, nameof(extensionType)); + ArgumentGuard.NotNull(nameFactory, nameof(nameFactory)); + ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + + _source = source; + _entityModel = entityModel; + _extensionType = extensionType; + _nameFactory = nameFactory; + _resourceFactory = resourceFactory; + _resourceContextProvider = resourceContextProvider; } public Expression ApplySelect(IDictionary selectors, ResourceContext resourceContext) { - if (selectors == null) - { - throw new ArgumentNullException(nameof(selectors)); - } + ArgumentGuard.NotNull(selectors, nameof(selectors)); if (!selectors.Any()) { @@ -183,14 +189,11 @@ private Expression CreateCollectionInitializer(LambdaScope lambdaScope, Property Type enumerableOfElementType = typeof(IEnumerable<>).MakeGenericType(elementType); Type typedCollection = TypeHelper.ToConcreteCollectionType(collectionProperty.PropertyType); - ConstructorInfo typedCollectionConstructor = typedCollection.GetConstructor(new[] - { - enumerableOfElementType - }); + ConstructorInfo typedCollectionConstructor = typedCollection.GetConstructor(enumerableOfElementType.AsArray()); if (typedCollectionConstructor == null) { - throw new Exception( + throw new InvalidOperationException( $"Constructor on '{typedCollection.Name}' that accepts '{enumerableOfElementType.Name}' not found."); } @@ -203,25 +206,19 @@ private Expression CreateCollectionInitializer(LambdaScope lambdaScope, Property private static Expression TestForNull(Expression expressionToTest, Expression ifFalseExpression) { - BinaryExpression equalsNull = Expression.Equal(expressionToTest, _nullConstant); - return Expression.Condition(equalsNull, Expression.Convert(_nullConstant, expressionToTest.Type), ifFalseExpression); + BinaryExpression equalsNull = Expression.Equal(expressionToTest, NullConstant); + return Expression.Condition(equalsNull, Expression.Convert(NullConstant, expressionToTest.Type), ifFalseExpression); } private static Expression CopyCollectionExtensionMethodCall(Expression source, string operationName, Type elementType) { - return Expression.Call(typeof(Enumerable), operationName, new[] - { - elementType - }, source); + return Expression.Call(typeof(Enumerable), operationName, elementType.AsArray(), source); } private Expression SelectExtensionMethodCall(Expression source, Type elementType, Expression selectorBody) { - return Expression.Call(_extensionType, "Select", new[] - { - elementType, - elementType - }, source, selectorBody); + var typeArguments = ArrayFactory.Create(elementType, elementType); + return Expression.Call(_extensionType, "Select", typeArguments, source, selectorBody); } private sealed class PropertySelector @@ -232,18 +229,19 @@ private sealed class PropertySelector public PropertySelector(PropertyInfo property, QueryLayer nextLayer = null) { - Property = property ?? throw new ArgumentNullException(nameof(property)); + ArgumentGuard.NotNull(property, nameof(property)); + + Property = property; NextLayer = nextLayer; } public PropertySelector(ResourceFieldAttribute field, QueryLayer nextLayer = null) { - OriginatingField = field ?? throw new ArgumentNullException(nameof(field)); - NextLayer = nextLayer; + ArgumentGuard.NotNull(field, nameof(field)); - Property = field is HasManyThroughAttribute hasManyThrough - ? hasManyThrough.ThroughProperty - : field.Property; + OriginatingField = field; + NextLayer = nextLayer; + Property = field is HasManyThroughAttribute hasManyThrough ? hasManyThrough.ThroughProperty : field.Property; } public override string ToString() diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SkipTakeClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SkipTakeClauseBuilder.cs index 96c94a9cb6..70daaab698 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SkipTakeClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SkipTakeClauseBuilder.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Linq.Expressions; +using JetBrains.Annotations; using JsonApiDotNetCore.Queries.Expressions; namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding @@ -8,6 +9,7 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding /// /// Transforms into and calls. /// + [PublicAPI] public class SkipTakeClauseBuilder : QueryClauseBuilder { private readonly Expression _source; @@ -16,16 +18,16 @@ public class SkipTakeClauseBuilder : QueryClauseBuilder public SkipTakeClauseBuilder(Expression source, LambdaScope lambdaScope, Type extensionType) : base(lambdaScope) { - _source = source ?? throw new ArgumentNullException(nameof(source)); - _extensionType = extensionType ?? throw new ArgumentNullException(nameof(extensionType)); + ArgumentGuard.NotNull(source, nameof(source)); + ArgumentGuard.NotNull(extensionType, nameof(extensionType)); + + _source = source; + _extensionType = extensionType; } public Expression ApplySkipTake(PaginationExpression expression) { - if (expression == null) - { - throw new ArgumentNullException(nameof(expression)); - } + ArgumentGuard.NotNull(expression, nameof(expression)); return Visit(expression, null); } @@ -53,10 +55,7 @@ private Expression ExtensionMethodCall(Expression source, string operationName, { Expression constant = CreateTupleAccessExpressionForConstant(value, typeof(int)); - return Expression.Call(_extensionType, operationName, new[] - { - LambdaScope.Parameter.Type - }, source, constant); + return Expression.Call(_extensionType, operationName, LambdaScope.Parameter.Type.AsArray(), source, constant); } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs index 9ed848053f..25da976355 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using JetBrains.Annotations; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; @@ -12,9 +13,10 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding /// /// Transforms into calls. /// + [PublicAPI] public class WhereClauseBuilder : QueryClauseBuilder { - private static readonly ConstantExpression _nullConstant = Expression.Constant(null); + private static readonly ConstantExpression NullConstant = Expression.Constant(null); private readonly Expression _source; private readonly Type _extensionType; @@ -22,16 +24,16 @@ public class WhereClauseBuilder : QueryClauseBuilder public WhereClauseBuilder(Expression source, LambdaScope lambdaScope, Type extensionType) : base(lambdaScope) { - _source = source ?? throw new ArgumentNullException(nameof(source)); - _extensionType = extensionType ?? throw new ArgumentNullException(nameof(extensionType)); + ArgumentGuard.NotNull(source, nameof(source)); + ArgumentGuard.NotNull(extensionType, nameof(extensionType)); + + _source = source; + _extensionType = extensionType; } public Expression ApplyWhere(FilterExpression filter) { - if (filter == null) - { - throw new ArgumentNullException(nameof(filter)); - } + ArgumentGuard.NotNull(filter, nameof(filter)); Expression body = Visit(filter, null); LambdaExpression lambda = Expression.Lambda(body, LambdaScope.Parameter); @@ -41,10 +43,7 @@ public Expression ApplyWhere(FilterExpression filter) private Expression WhereExtensionMethodCall(LambdaExpression predicate) { - return Expression.Call(_extensionType, "Where", new[] - { - LambdaScope.Parameter.Type - }, _source, predicate); + return Expression.Call(_extensionType, "Where", LambdaScope.Parameter.Type.AsArray(), _source, predicate); } public override Expression VisitCollectionNotEmpty(CollectionNotEmptyExpression expression, Type argument) @@ -55,7 +54,7 @@ public override Expression VisitCollectionNotEmpty(CollectionNotEmptyExpression if (elementType == null) { - throw new Exception("Expression must be a collection."); + throw new InvalidOperationException("Expression must be a collection."); } return AnyExtensionMethodCall(elementType, property); @@ -63,10 +62,7 @@ public override Expression VisitCollectionNotEmpty(CollectionNotEmptyExpression private static MethodCallExpression AnyExtensionMethodCall(Type elementType, Expression source) { - return Expression.Call(typeof(Enumerable), "Any", new[] - { - elementType - }, source); + return Expression.Call(typeof(Enumerable), "Any", elementType.AsArray(), source); } public override Expression VisitMatchText(MatchTextExpression expression, Type argument) @@ -75,7 +71,7 @@ public override Expression VisitMatchText(MatchTextExpression expression, Type a if (property.Type != typeof(string)) { - throw new Exception("Expression must be a string."); + throw new InvalidOperationException("Expression must be a string."); } Expression text = Visit(expression.TextValue, property.Type); @@ -102,7 +98,7 @@ public override Expression VisitEqualsAnyOf(EqualsAnyOfExpression expression, Ty foreach (LiteralConstantExpression constant in expression.Constants) { object value = ConvertTextToTargetType(constant.Value, property.Type); - valueList.Add(value); + valueList!.Add(value); } ConstantExpression collection = Expression.Constant(valueList); @@ -111,10 +107,7 @@ public override Expression VisitEqualsAnyOf(EqualsAnyOfExpression expression, Ty private static Expression ContainsExtensionMethodCall(Expression collection, Expression value) { - return Expression.Call(typeof(Enumerable), "Contains", new[] - { - value.Type - }, collection, value); + return Expression.Call(typeof(Enumerable), "Contains", value.Type.AsArray(), collection, value); } public override Expression VisitLogical(LogicalExpression expression, Type argument) @@ -252,7 +245,7 @@ private static Expression WrapInConvert(Expression expression, Type targetType) public override Expression VisitNullConstant(NullConstantExpression expression, Type expressionType) { - return _nullConstant; + return NullConstant; } public override Expression VisitLiteralConstant(LiteralConstantExpression expression, Type expressionType) @@ -278,11 +271,14 @@ private static object ConvertTextToTargetType(string text, Type targetType) protected override MemberExpression CreatePropertyExpressionForFieldChain(IReadOnlyCollection chain, Expression source) { - var components = chain.Select(field => - // In case of a HasManyThrough access (from count() or has() function), we only need to look at the number of entries in the join table. - field is HasManyThroughAttribute hasManyThrough ? hasManyThrough.ThroughProperty.Name : field.Property.Name).ToArray(); - + var components = chain.Select(GetPropertyName).ToArray(); return CreatePropertyExpressionFromComponents(LambdaScope.Accessor, components); } + + private static string GetPropertyName(ResourceFieldAttribute field) + { + // In case of a HasManyThrough access (from count() or has() function), we only need to look at the number of entries in the join table. + return field is HasManyThroughAttribute hasManyThrough ? hasManyThrough.ThroughProperty.Name : field.Property.Name; + } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs b/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs index 19333c149c..9f355696d3 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources; @@ -11,6 +12,7 @@ namespace JsonApiDotNetCore.Queries.Internal /// /// Takes sparse fieldsets from s and invokes on them. /// + [PublicAPI] public sealed class SparseFieldSetCache { private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; @@ -19,47 +21,58 @@ public sealed class SparseFieldSetCache public SparseFieldSetCache(IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor) { - if (constraintProviders == null) throw new ArgumentNullException(nameof(constraintProviders)); - _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); + ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); + ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); + _resourceDefinitionAccessor = resourceDefinitionAccessor; _lazySourceTable = new Lazy>>(() => BuildSourceTable(constraintProviders)); _visitedTable = new Dictionary>(); } private static IDictionary> BuildSourceTable(IEnumerable constraintProviders) { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var sparseFieldTables = constraintProviders .SelectMany(provider => provider.GetConstraints()) .Where(constraint => constraint.Scope == null) .Select(constraint => constraint.Expression) .OfType() .Select(expression => expression.Table) + .SelectMany(table => table) .ToArray(); + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + var mergedTable = new Dictionary>(); - foreach (var sparseFieldTable in sparseFieldTables) + foreach (var (resourceContext, sparseFieldSet) in sparseFieldTables) { - foreach (var (resourceContext, sparseFieldSet) in sparseFieldTable) + if (!mergedTable.ContainsKey(resourceContext)) { - if (!mergedTable.ContainsKey(resourceContext)) - { - mergedTable[resourceContext] = new HashSet(); - } - - foreach (var field in sparseFieldSet.Fields) - { - mergedTable[resourceContext].Add(field); - } + mergedTable[resourceContext] = new HashSet(); } + + AddSparseFieldsToSet(sparseFieldSet.Fields, mergedTable[resourceContext]); } return mergedTable; } + private static void AddSparseFieldsToSet(IReadOnlyCollection sparseFieldsToAdd, + HashSet sparseFieldSet) + { + foreach (var field in sparseFieldsToAdd) + { + sparseFieldSet.Add(field); + } + } + public IReadOnlyCollection GetSparseFieldSetForQuery(ResourceContext resourceContext) { - if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); if (!_visitedTable.ContainsKey(resourceContext)) { @@ -81,10 +94,10 @@ public IReadOnlyCollection GetSparseFieldSetForQuery(Res public IReadOnlyCollection GetIdAttributeSetForRelationshipQuery(ResourceContext resourceContext) { - if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); var idAttribute = resourceContext.Attributes.Single(attr => attr.Property.Name == nameof(Identifiable.Id)); - var inputExpression = new SparseFieldSetExpression(new []{idAttribute}); + var inputExpression = new SparseFieldSetExpression(idAttribute.AsArray()); // Intentionally not cached, as we are fetching ID only (ignoring any sparse fieldset that came from query string). var outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, inputExpression); @@ -99,6 +112,8 @@ public IReadOnlyCollection GetIdAttributeSetForRelationshipQuery( public IReadOnlyCollection GetSparseFieldSetForSerializer(ResourceContext resourceContext) { + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + if (!_visitedTable.ContainsKey(resourceContext)) { var inputFields = _lazySourceTable.Value.ContainsKey(resourceContext) @@ -127,7 +142,7 @@ public IReadOnlyCollection GetSparseFieldSetForSerialize private HashSet GetResourceFields(ResourceContext resourceContext) { - if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); var fieldSet = new HashSet(); diff --git a/src/JsonApiDotNetCore/Queries/QueryLayer.cs b/src/JsonApiDotNetCore/Queries/QueryLayer.cs index 74d8d5f043..c50146a329 100644 --- a/src/JsonApiDotNetCore/Queries/QueryLayer.cs +++ b/src/JsonApiDotNetCore/Queries/QueryLayer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; @@ -11,6 +12,7 @@ namespace JsonApiDotNetCore.Queries /// /// A nested data structure that contains constraints per resource type. /// + [PublicAPI] public sealed class QueryLayer { public ResourceContext ResourceContext { get; } @@ -23,7 +25,9 @@ public sealed class QueryLayer public QueryLayer(ResourceContext resourceContext) { - ResourceContext = resourceContext ?? throw new ArgumentNullException(nameof(resourceContext)); + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + + ResourceContext = resourceContext; } public override string ToString() diff --git a/src/JsonApiDotNetCore/QueryStrings/IFilterQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/IFilterQueryStringParameterReader.cs index a9666d946c..49caf84786 100644 --- a/src/JsonApiDotNetCore/QueryStrings/IFilterQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/IFilterQueryStringParameterReader.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Queries; namespace JsonApiDotNetCore.QueryStrings @@ -5,6 +6,7 @@ namespace JsonApiDotNetCore.QueryStrings /// /// Reads the 'filter' query string parameter and produces a set of query constraints from it. /// + [PublicAPI] public interface IFilterQueryStringParameterReader : IQueryStringParameterReader, IQueryConstraintProvider { } diff --git a/src/JsonApiDotNetCore/QueryStrings/IIncludeQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/IIncludeQueryStringParameterReader.cs index b03feed61c..e348f3635c 100644 --- a/src/JsonApiDotNetCore/QueryStrings/IIncludeQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/IIncludeQueryStringParameterReader.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Queries; namespace JsonApiDotNetCore.QueryStrings @@ -5,6 +6,7 @@ namespace JsonApiDotNetCore.QueryStrings /// /// Reads the 'include' query string parameter and produces a set of query constraints from it. /// + [PublicAPI] public interface IIncludeQueryStringParameterReader : IQueryStringParameterReader, IQueryConstraintProvider { } diff --git a/src/JsonApiDotNetCore/QueryStrings/IPaginationQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/IPaginationQueryStringParameterReader.cs index 7d60dbec17..c41f417435 100644 --- a/src/JsonApiDotNetCore/QueryStrings/IPaginationQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/IPaginationQueryStringParameterReader.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Queries; namespace JsonApiDotNetCore.QueryStrings @@ -5,6 +6,7 @@ namespace JsonApiDotNetCore.QueryStrings /// /// Reads the 'page' query string parameter and produces a set of query constraints from it. /// + [PublicAPI] public interface IPaginationQueryStringParameterReader : IQueryStringParameterReader, IQueryConstraintProvider { } diff --git a/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs index 5443dd58e8..58e1f18d40 100644 --- a/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Resources; @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.QueryStrings /// Reads custom query string parameters for which handlers on are registered /// and produces a set of query constraints from it. /// + [PublicAPI] public interface IResourceDefinitionQueryableParameterReader : IQueryStringParameterReader, IQueryConstraintProvider { } diff --git a/src/JsonApiDotNetCore/QueryStrings/ISortQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/ISortQueryStringParameterReader.cs index 6b66b43839..1fe5f4cb51 100644 --- a/src/JsonApiDotNetCore/QueryStrings/ISortQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/ISortQueryStringParameterReader.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Queries; namespace JsonApiDotNetCore.QueryStrings @@ -5,6 +6,7 @@ namespace JsonApiDotNetCore.QueryStrings /// /// Reads the 'sort' query string parameter and produces a set of query constraints from it. /// + [PublicAPI] public interface ISortQueryStringParameterReader : IQueryStringParameterReader, IQueryConstraintProvider { } diff --git a/src/JsonApiDotNetCore/QueryStrings/ISparseFieldSetQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/ISparseFieldSetQueryStringParameterReader.cs index 7d48297e0a..7a307f1f40 100644 --- a/src/JsonApiDotNetCore/QueryStrings/ISparseFieldSetQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/ISparseFieldSetQueryStringParameterReader.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Queries; namespace JsonApiDotNetCore.QueryStrings @@ -5,6 +6,7 @@ namespace JsonApiDotNetCore.QueryStrings /// /// Reads the 'fields' query string parameter and produces a set of query constraints from it. /// + [PublicAPI] public interface ISparseFieldSetQueryStringParameterReader : IQueryStringParameterReader, IQueryConstraintProvider { } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs index 776b9559cb..bf88cfb9e5 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs @@ -1,4 +1,4 @@ -using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Errors; @@ -8,6 +8,7 @@ namespace JsonApiDotNetCore.QueryStrings.Internal { /// + [PublicAPI] public class DefaultsQueryStringParameterReader : IDefaultsQueryStringParameterReader { private readonly IJsonApiOptions _options; @@ -17,14 +18,16 @@ public class DefaultsQueryStringParameterReader : IDefaultsQueryStringParameterR public DefaultsQueryStringParameterReader(IJsonApiOptions options) { - _options = options ?? throw new ArgumentNullException(nameof(options)); + ArgumentGuard.NotNull(options, nameof(options)); + + _options = options; SerializerDefaultValueHandling = options.SerializerSettings.DefaultValueHandling; } /// public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); return _options.AllowQueryStringOverrideForSerializerDefaultValueHandling && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Defaults); diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs index e7e2711548..3565f9beb1 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Errors; @@ -14,9 +15,10 @@ namespace JsonApiDotNetCore.QueryStrings.Internal { + [PublicAPI] public class FilterQueryStringParameterReader : QueryStringParameterReader, IFilterQueryStringParameterReader { - private static readonly LegacyFilterNotationConverter _legacyConverter = new LegacyFilterNotationConverter(); + private static readonly LegacyFilterNotationConverter LegacyConverter = new LegacyFilterNotationConverter(); private readonly IJsonApiOptions _options; private readonly QueryStringParameterScopeParser _scopeParser; @@ -30,7 +32,9 @@ public FilterQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, IJsonApiOptions options) : base(request, resourceContextProvider) { - _options = options ?? throw new ArgumentNullException(nameof(options)); + ArgumentGuard.NotNull(options, nameof(options)); + + _options = options; _scopeParser = new QueryStringParameterScopeParser(resourceContextProvider, FieldChainRequirements.EndsInToMany); _filterParser = new FilterParser(resourceContextProvider, resourceFactory, ValidateSingleField); } @@ -47,7 +51,7 @@ protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext /// public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Filter); @@ -56,50 +60,52 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr /// public virtual bool CanRead(string parameterName) { + ArgumentGuard.NotNull(parameterName, nameof(parameterName)); + var isNested = parameterName.StartsWith("filter[", StringComparison.Ordinal) && parameterName.EndsWith("]", StringComparison.Ordinal); return parameterName == "filter" || isNested; } /// - public virtual void Read(string parameterName, StringValues parameterValues) + public virtual void Read(string parameterName, StringValues parameterValue) { _lastParameterName = parameterName; - foreach (string parameterValue in ExtractParameterValues(parameterName, parameterValues)) + foreach (string value in parameterValue.SelectMany(ExtractParameterValue)) { - ReadSingleValue(parameterName, parameterValue); + ReadSingleValue(parameterName, value); } } - private IEnumerable ExtractParameterValues(string parameterName, StringValues parameterValues) + private IEnumerable ExtractParameterValue(string parameterValue) { - foreach (string parameterValue in parameterValues) + if (_options.EnableLegacyFilterNotation) { - if (_options.EnableLegacyFilterNotation) - { - foreach (string condition in _legacyConverter.ExtractConditions(parameterName, parameterValue)) - { - yield return condition; - } - } - else + foreach (string condition in LegacyConverter.ExtractConditions(parameterValue)) { - yield return parameterValue; + yield return condition; } } + else + { + yield return parameterValue; + } } private void ReadSingleValue(string parameterName, string parameterValue) { try { + string name = parameterName; + string value = parameterValue; + if (_options.EnableLegacyFilterNotation) { - (parameterName, parameterValue) = _legacyConverter.Convert(parameterName, parameterValue); + (name, value) = LegacyConverter.Convert(name, value); } - ResourceFieldChainExpression scope = GetScope(parameterName); - FilterExpression filter = GetFilter(parameterValue, scope); + ResourceFieldChainExpression scope = GetScope(name); + FilterExpression filter = GetFilter(value, scope); StoreFilterInScope(filter, scope); } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs index 5b193140ff..f9dc9d30d7 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Errors; @@ -12,6 +12,7 @@ namespace JsonApiDotNetCore.QueryStrings.Internal { + [PublicAPI] public class IncludeQueryStringParameterReader : QueryStringParameterReader, IIncludeQueryStringParameterReader { private readonly IJsonApiOptions _options; @@ -23,7 +24,9 @@ public class IncludeQueryStringParameterReader : QueryStringParameterReader, IIn public IncludeQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider, IJsonApiOptions options) : base(request, resourceContextProvider) { - _options = options ?? throw new ArgumentNullException(nameof(options)); + ArgumentGuard.NotNull(options, nameof(options)); + + _options = options; _includeParser = new IncludeParser(resourceContextProvider, ValidateSingleRelationship); } @@ -42,7 +45,7 @@ protected void ValidateSingleRelationship(RelationshipAttribute relationship, Re /// public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Include); @@ -82,7 +85,7 @@ public virtual IReadOnlyCollection GetConstraints() ? new ExpressionInScope(null, _includeExpression) : new ExpressionInScope(null, IncludeExpression.Empty); - return new[] {expressionInScope}; + return expressionInScope.AsArray(); } } } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs index efc96e435f..ae8e7a2230 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Queries.Internal.Parsing; namespace JsonApiDotNetCore.QueryStrings.Internal { + [PublicAPI] public sealed class LegacyFilterNotationConverter { private const string ParameterNamePrefix = "filter["; @@ -15,7 +17,7 @@ public sealed class LegacyFilterNotationConverter private const string InPrefix = "in:"; private const string NotInPrefix = "nin:"; - private static readonly Dictionary _prefixConversionTable = new Dictionary + private static readonly Dictionary PrefixConversionTable = new Dictionary { ["eq:"] = Keywords.Equals, ["lt:"] = Keywords.LessThan, @@ -25,8 +27,10 @@ public sealed class LegacyFilterNotationConverter ["like:"] = Keywords.Contains }; - public IEnumerable ExtractConditions(string parameterName, string parameterValue) + public IEnumerable ExtractConditions(string parameterValue) { + ArgumentGuard.NotNull(parameterValue, nameof(parameterValue)); + if (parameterValue.StartsWith(ExpressionPrefix, StringComparison.Ordinal) || parameterValue.StartsWith(InPrefix, StringComparison.Ordinal) || parameterValue.StartsWith(NotInPrefix, StringComparison.Ordinal)) @@ -44,8 +48,8 @@ public IEnumerable ExtractConditions(string parameterName, string parame public (string parameterName, string parameterValue) Convert(string parameterName, string parameterValue) { - if (parameterName == null) throw new ArgumentNullException(nameof(parameterName)); - if (parameterValue == null) throw new ArgumentNullException(nameof(parameterValue)); + ArgumentGuard.NotNull(parameterName, nameof(parameterName)); + ArgumentGuard.NotNull(parameterValue, nameof(parameterValue)); if (parameterValue.StartsWith(ExpressionPrefix, StringComparison.Ordinal)) { @@ -55,7 +59,7 @@ public IEnumerable ExtractConditions(string parameterName, string parame var attributeName = ExtractAttributeName(parameterName); - foreach (var (prefix, keyword) in _prefixConversionTable) + foreach (var (prefix, keyword) in PrefixConversionTable) { if (parameterValue.StartsWith(prefix, StringComparison.Ordinal)) { diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs index f181f5777b..98658bd356 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs @@ -1,4 +1,4 @@ -using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Errors; @@ -8,6 +8,7 @@ namespace JsonApiDotNetCore.QueryStrings.Internal { /// + [PublicAPI] public class NullsQueryStringParameterReader : INullsQueryStringParameterReader { private readonly IJsonApiOptions _options; @@ -17,14 +18,16 @@ public class NullsQueryStringParameterReader : INullsQueryStringParameterReader public NullsQueryStringParameterReader(IJsonApiOptions options) { - _options = options ?? throw new ArgumentNullException(nameof(options)); + ArgumentGuard.NotNull(options, nameof(options)); + + _options = options; SerializerNullValueHandling = options.SerializerSettings.NullValueHandling; } /// public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); return _options.AllowQueryStringOverrideForSerializerNullValueHandling && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Nulls); diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs index 7196e9aca1..0eb3a71b49 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Errors; @@ -12,6 +13,7 @@ namespace JsonApiDotNetCore.QueryStrings.Internal { + [PublicAPI] public class PaginationQueryStringParameterReader : QueryStringParameterReader, IPaginationQueryStringParameterReader { private const string PageSizeParameterName = "page[size]"; @@ -26,14 +28,16 @@ public class PaginationQueryStringParameterReader : QueryStringParameterReader, public PaginationQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider, IJsonApiOptions options) : base(request, resourceContextProvider) { - _options = options ?? throw new ArgumentNullException(nameof(options)); + ArgumentGuard.NotNull(options, nameof(options)); + + _options = options; _paginationParser = new PaginationParser(resourceContextProvider); } /// public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Page); @@ -100,6 +104,7 @@ protected virtual void ValidatePageSize(PaginationQueryStringValueExpression con } } + [AssertionMethod] protected virtual void ValidatePageNumber(PaginationQueryStringValueExpression constraint) { if (_options.MaximumPageNumber != null && diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs index 2ba5387f34..496c179fcc 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; @@ -18,12 +17,10 @@ public abstract class QueryStringParameterReader protected QueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + _resourceContextProvider = resourceContextProvider; _isCollectionRequest = request.IsCollection; RequestResource = request.SecondaryResource ?? request.PrimaryResource; IsAtomicOperationsRequest = request.Kind == EndpointKind.AtomicOperations; diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs index 1716e451ca..b7528624d2 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs @@ -1,6 +1,6 @@ -using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Errors; @@ -9,6 +9,7 @@ namespace JsonApiDotNetCore.QueryStrings.Internal { /// + [PublicAPI] public class QueryStringReader : IQueryStringReader { private readonly IJsonApiOptions _options; @@ -19,19 +20,21 @@ public class QueryStringReader : IQueryStringReader public QueryStringReader(IJsonApiOptions options, IRequestQueryStringAccessor queryStringAccessor, IEnumerable parameterReaders, ILoggerFactory loggerFactory) { - if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); - - _options = options ?? throw new ArgumentNullException(nameof(options)); - _queryStringAccessor = queryStringAccessor ?? throw new ArgumentNullException(nameof(queryStringAccessor)); - _parameterReaders = parameterReaders ?? throw new ArgumentNullException(nameof(parameterReaders)); + ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(queryStringAccessor, nameof(queryStringAccessor)); + ArgumentGuard.NotNull(parameterReaders, nameof(parameterReaders)); + _options = options; + _queryStringAccessor = queryStringAccessor; + _parameterReaders = parameterReaders; _logger = loggerFactory.CreateLogger(); } /// public virtual void ReadAll(DisableQueryStringAttribute disableQueryStringAttribute) { - disableQueryStringAttribute ??= DisableQueryStringAttribute.Empty; + var disableQueryStringAttributeNotNull = disableQueryStringAttribute ?? DisableQueryStringAttribute.Empty; foreach (var (parameterName, parameterValue) in _queryStringAccessor.Query) { @@ -48,7 +51,7 @@ public virtual void ReadAll(DisableQueryStringAttribute disableQueryStringAttrib _logger.LogDebug( $"Query string parameter '{parameterName}' with value '{parameterValue}' was accepted by {reader.GetType().Name}."); - if (!reader.IsEnabled(disableQueryStringAttribute)) + if (!reader.IsEnabled(disableQueryStringAttributeNotNull)) { throw new InvalidQueryStringParameterException(parameterName, "Usage of one or more query string parameters is not allowed at the requested endpoint.", diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/RequestQueryStringAccessor.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/RequestQueryStringAccessor.cs index 5c2fd09c35..d53d01b054 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/RequestQueryStringAccessor.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/RequestQueryStringAccessor.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.QueryStrings.Internal @@ -8,12 +7,13 @@ internal sealed class RequestQueryStringAccessor : IRequestQueryStringAccessor { private readonly IHttpContextAccessor _httpContextAccessor; - public QueryString QueryString => _httpContextAccessor.HttpContext.Request.QueryString; public IQueryCollection Query => _httpContextAccessor.HttpContext.Request.Query; public RequestQueryStringAccessor(IHttpContextAccessor httpContextAccessor) { - _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + ArgumentGuard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); + + _httpContextAccessor = httpContextAccessor; } } } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs index 3fd55aabb1..868551fa26 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; @@ -11,6 +11,7 @@ namespace JsonApiDotNetCore.QueryStrings.Internal { /// + [PublicAPI] public class ResourceDefinitionQueryableParameterReader : IResourceDefinitionQueryableParameterReader { private readonly IJsonApiRequest _request; @@ -19,8 +20,11 @@ public class ResourceDefinitionQueryableParameterReader : IResourceDefinitionQue public ResourceDefinitionQueryableParameterReader(IJsonApiRequest request, IResourceDefinitionAccessor resourceDefinitionAccessor) { - _request = request ?? throw new ArgumentNullException(nameof(request)); - _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); + + _request = request; + _resourceDefinitionAccessor = resourceDefinitionAccessor; } /// diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs index 8fa2cba15d..e9ca858acf 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Errors; @@ -12,6 +13,7 @@ namespace JsonApiDotNetCore.QueryStrings.Internal { + [PublicAPI] public class SortQueryStringParameterReader : QueryStringParameterReader, ISortQueryStringParameterReader { private readonly QueryStringParameterScopeParser _scopeParser; @@ -38,7 +40,7 @@ protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext /// public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Sort); @@ -47,6 +49,8 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr /// public virtual bool CanRead(string parameterName) { + ArgumentGuard.NotNull(parameterName, nameof(parameterName)); + var isNested = parameterName.StartsWith("sort[", StringComparison.Ordinal) && parameterName.EndsWith("]", StringComparison.Ordinal); return parameterName == "sort" || isNested; } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs index d39fd5c774..b1cc88afdc 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Errors; @@ -13,6 +14,7 @@ namespace JsonApiDotNetCore.QueryStrings.Internal { + [PublicAPI] public class SparseFieldSetQueryStringParameterReader : QueryStringParameterReader, ISparseFieldSetQueryStringParameterReader { private readonly SparseFieldTypeParser _sparseFieldTypeParser; @@ -39,7 +41,7 @@ protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext /// public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Fields); @@ -48,6 +50,8 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr /// public virtual bool CanRead(string parameterName) { + ArgumentGuard.NotNull(parameterName, nameof(parameterName)); + return parameterName.StartsWith("fields[", StringComparison.Ordinal) && parameterName.EndsWith("]", StringComparison.Ordinal); } @@ -84,10 +88,7 @@ private SparseFieldSetExpression GetSparseFieldSet(string parameterValue, Resour public virtual IReadOnlyCollection GetConstraints() { return _sparseFieldTable.Any() - ? new[] - { - new ExpressionInScope(null, new SparseFieldTableExpression(_sparseFieldTable)) - } + ? new ExpressionInScope(null, new SparseFieldTableExpression(_sparseFieldTable)).AsArray() : Array.Empty(); } } diff --git a/src/JsonApiDotNetCore/Repositories/DataStoreUpdateException.cs b/src/JsonApiDotNetCore/Repositories/DataStoreUpdateException.cs index 9ce5d31c1c..80a1b85a77 100644 --- a/src/JsonApiDotNetCore/Repositories/DataStoreUpdateException.cs +++ b/src/JsonApiDotNetCore/Repositories/DataStoreUpdateException.cs @@ -1,10 +1,12 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Repositories { /// /// The error that is thrown when the underlying data store is unable to persist changes. /// + [PublicAPI] public sealed class DataStoreUpdateException : Exception { public DataStoreUpdateException(Exception exception) diff --git a/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs b/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs index 83a3f77bf3..5adfbc2c8f 100644 --- a/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs +++ b/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace JsonApiDotNetCore.Repositories { + [PublicAPI] public static class DbContextExtensions { /// @@ -14,8 +16,8 @@ public static class DbContextExtensions /// public static IIdentifiable GetTrackedOrAttach(this DbContext dbContext, IIdentifiable resource) { - if (dbContext == null) throw new ArgumentNullException(nameof(dbContext)); - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(dbContext, nameof(dbContext)); + ArgumentGuard.NotNull(resource, nameof(resource)); var trackedIdentifiable = (IIdentifiable)dbContext.GetTrackedIdentifiable(resource); if (trackedIdentifiable == null) @@ -32,25 +34,29 @@ public static IIdentifiable GetTrackedOrAttach(this DbContext dbContext, IIdenti /// public static object GetTrackedIdentifiable(this DbContext dbContext, IIdentifiable identifiable) { - if (dbContext == null) throw new ArgumentNullException(nameof(dbContext)); - if (identifiable == null) throw new ArgumentNullException(nameof(identifiable)); + ArgumentGuard.NotNull(dbContext, nameof(dbContext)); + ArgumentGuard.NotNull(identifiable, nameof(identifiable)); - var entityType = identifiable.GetType(); - var entityEntry = dbContext.ChangeTracker - .Entries() - .FirstOrDefault(entry => - entry.Entity.GetType() == entityType && - ((IIdentifiable) entry.Entity).StringId == identifiable.StringId); + var resourceType = identifiable.GetType(); + string stringId = identifiable.StringId; + + var entityEntry = dbContext.ChangeTracker.Entries() + .FirstOrDefault(entry => IsResource(entry, resourceType, stringId)); return entityEntry?.Entity; } + private static bool IsResource(EntityEntry entry, Type resourceType, string stringId) + { + return entry.Entity.GetType() == resourceType && ((IIdentifiable) entry.Entity).StringId == stringId; + } + /// /// Detaches all entities from the change tracker. /// public static void ResetChangeTracker(this DbContext dbContext) { - if (dbContext == null) throw new ArgumentNullException(nameof(dbContext)); + ArgumentGuard.NotNull(dbContext, nameof(dbContext)); List entriesWithChanges = dbContext.ChangeTracker.Entries().ToList(); diff --git a/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs b/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs index 6a4984b7ab..9b8f1327ce 100644 --- a/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs +++ b/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs @@ -1,9 +1,10 @@ -using System; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCore.Repositories { /// + [PublicAPI] public sealed class DbContextResolver : IDbContextResolver where TDbContext : DbContext { @@ -11,7 +12,9 @@ public sealed class DbContextResolver : IDbContextResolver public DbContextResolver(TDbContext context) { - _context = context ?? throw new ArgumentNullException(nameof(context)); + ArgumentGuard.NotNull(context, nameof(context)); + + _context = context; } public DbContext GetContext() => _context; diff --git a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs index 53c672f0c7..a87155cda1 100644 --- a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; @@ -22,6 +23,7 @@ namespace JsonApiDotNetCore.Repositories /// /// Implements the foundational Repository layer in the JsonApiDotNetCore architecture that uses Entity Framework Core. /// + [PublicAPI] public class EntityFrameworkCoreRepository : IResourceRepository, IRepositorySupportsTransaction where TResource : class, IIdentifiable { @@ -43,14 +45,17 @@ public EntityFrameworkCoreRepository( IEnumerable constraintProviders, ILoggerFactory loggerFactory) { - if (contextResolver == null) throw new ArgumentNullException(nameof(contextResolver)); - if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); - - _targetedFields = targetedFields ?? throw new ArgumentNullException(nameof(targetedFields)); - _resourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); - _resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); - _constraintProviders = constraintProviders ?? throw new ArgumentNullException(nameof(constraintProviders)); - + ArgumentGuard.NotNull(contextResolver, nameof(contextResolver)); + ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); + ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); + ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); + + _targetedFields = targetedFields; + _resourceGraph = resourceGraph; + _resourceFactory = resourceFactory; + _constraintProviders = constraintProviders; _dbContext = contextResolver.GetContext(); _traceWriter = new TraceLogWriter>(loggerFactory); } @@ -59,7 +64,8 @@ public EntityFrameworkCoreRepository( public virtual async Task> GetAsync(QueryLayer layer, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {layer}); - if (layer == null) throw new ArgumentNullException(nameof(layer)); + + ArgumentGuard.NotNull(layer, nameof(layer)); IQueryable query = ApplyQueryLayer(layer); return await query.ToListAsync(cancellationToken); @@ -83,23 +89,31 @@ public virtual async Task CountAsync(FilterExpression topFilter, Cancellati protected virtual IQueryable ApplyQueryLayer(QueryLayer layer) { _traceWriter.LogMethodStart(new {layer}); - if (layer == null) throw new ArgumentNullException(nameof(layer)); + ArgumentGuard.NotNull(layer, nameof(layer)); + + QueryLayer rewrittenLayer = layer; if (EntityFrameworkCoreSupport.Version.Major < 5) { var writer = new MemoryLeakDetectionBugRewriter(); - layer = writer.Rewrite(layer); + rewrittenLayer = writer.Rewrite(layer); } IQueryable source = GetAll(); + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var queryableHandlers = _constraintProviders - .SelectMany(p => p.GetConstraints()) + .SelectMany(provider => provider.GetConstraints()) .Where(expressionInScope => expressionInScope.Scope == null) .Select(expressionInScope => expressionInScope.Expression) .OfType() .ToArray(); + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + foreach (var queryableHandler in queryableHandlers) { source = queryableHandler.Apply(source); @@ -108,7 +122,7 @@ protected virtual IQueryable ApplyQueryLayer(QueryLayer layer) var nameFactory = new LambdaParameterNameFactory(); var builder = new QueryableBuilder(source.Expression, source.ElementType, typeof(Queryable), nameFactory, _resourceFactory, _resourceGraph, _dbContext.Model); - var expression = builder.ApplyQuery(layer); + var expression = builder.ApplyQuery(rewrittenLayer); return source.Provider.CreateQuery(expression); } @@ -130,8 +144,9 @@ public virtual Task GetForCreateAsync(TId id, CancellationToken cance public virtual async Task CreateAsync(TResource resourceFromRequest, TResource resourceForDatabase, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {resourceFromRequest, resourceForDatabase}); - if (resourceFromRequest == null) throw new ArgumentNullException(nameof(resourceFromRequest)); - if (resourceForDatabase == null) throw new ArgumentNullException(nameof(resourceForDatabase)); + + ArgumentGuard.NotNull(resourceFromRequest, nameof(resourceFromRequest)); + ArgumentGuard.NotNull(resourceForDatabase, nameof(resourceForDatabase)); using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext); @@ -147,7 +162,7 @@ public virtual async Task CreateAsync(TResource resourceFromRequest, TResource r } var dbSet = _dbContext.Set(); - dbSet.Add(resourceForDatabase); + await dbSet.AddAsync(resourceForDatabase, cancellationToken); await SaveChangesAsync(cancellationToken); } @@ -163,8 +178,9 @@ public virtual async Task GetForUpdateAsync(QueryLayer queryLayer, Ca public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource resourceFromDatabase, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {resourceFromRequest, resourceFromDatabase}); - if (resourceFromRequest == null) throw new ArgumentNullException(nameof(resourceFromRequest)); - if (resourceFromDatabase == null) throw new ArgumentNullException(nameof(resourceFromDatabase)); + + ArgumentGuard.NotNull(resourceFromRequest, nameof(resourceFromRequest)); + ArgumentGuard.NotNull(resourceFromDatabase, nameof(resourceFromDatabase)); using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext); @@ -309,7 +325,8 @@ public virtual async Task SetRelationshipAsync(TResource primaryResource, object public virtual async Task AddToToManyRelationshipAsync(TId primaryId, ISet secondaryResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {primaryId, secondaryResourceIds}); - if (secondaryResourceIds == null) throw new ArgumentNullException(nameof(secondaryResourceIds)); + + ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); var relationship = _targetedFields.Relationships.Single(); @@ -328,7 +345,8 @@ public virtual async Task AddToToManyRelationshipAsync(TId primaryId, ISet secondaryResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {primaryResource, secondaryResourceIds}); - if (secondaryResourceIds == null) throw new ArgumentNullException(nameof(secondaryResourceIds)); + + ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); var relationship = (HasManyAttribute)_targetedFields.Relationships.Single(); @@ -417,6 +435,7 @@ protected virtual async Task SaveChangesAsync(CancellationToken cancellationToke /// /// Implements the foundational Repository layer in the JsonApiDotNetCore architecture that uses Entity Framework Core. /// + [PublicAPI] public class EntityFrameworkCoreRepository : EntityFrameworkCoreRepository, IResourceRepository where TResource : class, IIdentifiable { diff --git a/src/JsonApiDotNetCore/Repositories/IRepositorySupportsTransaction.cs b/src/JsonApiDotNetCore/Repositories/IRepositorySupportsTransaction.cs index d134e39bd9..fb1f026d31 100644 --- a/src/JsonApiDotNetCore/Repositories/IRepositorySupportsTransaction.cs +++ b/src/JsonApiDotNetCore/Repositories/IRepositorySupportsTransaction.cs @@ -1,10 +1,12 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Repositories { /// - /// Used to indicate that a supports execution inside a transaction. + /// Used to indicate that an supports execution inside a transaction. /// + [PublicAPI] public interface IRepositorySupportsTransaction { /// diff --git a/src/JsonApiDotNetCore/Repositories/IResourceReadRepository.cs b/src/JsonApiDotNetCore/Repositories/IResourceReadRepository.cs index 907792c8b7..ec373094db 100644 --- a/src/JsonApiDotNetCore/Repositories/IResourceReadRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceReadRepository.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources; @@ -18,6 +19,7 @@ public interface IResourceReadRepository /// /// The resource type. /// The resource identifier type. + [PublicAPI] public interface IResourceReadRepository where TResource : class, IIdentifiable { diff --git a/src/JsonApiDotNetCore/Repositories/IResourceRepository.cs b/src/JsonApiDotNetCore/Repositories/IResourceRepository.cs index 9400612d82..266672cf77 100644 --- a/src/JsonApiDotNetCore/Repositories/IResourceRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceRepository.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCore.Repositories @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Repositories /// Represents the foundational Resource Repository layer in the JsonApiDotNetCore architecture that provides data access to an underlying store. /// /// The resource type. + [PublicAPI] public interface IResourceRepository : IResourceRepository, IResourceReadRepository, IResourceWriteRepository where TResource : class, IIdentifiable diff --git a/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs index 824bfe0423..93f9640038 100644 --- a/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs @@ -9,7 +9,7 @@ namespace JsonApiDotNetCore.Repositories { /// - /// Retrieves a instance from the D/I container and invokes a method on it. + /// Retrieves an instance from the D/I container and invokes a method on it. /// public interface IResourceRepositoryAccessor { diff --git a/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs b/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs index 27244c5f17..35f7ab1459 100644 --- a/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Resources; @@ -17,6 +18,7 @@ public interface IResourceWriteRepository /// /// The resource type. /// The resource identifier type. + [PublicAPI] public interface IResourceWriteRepository where TResource : class, IIdentifiable { diff --git a/src/JsonApiDotNetCore/Repositories/MemoryLeakDetectionBugRewriter.cs b/src/JsonApiDotNetCore/Repositories/MemoryLeakDetectionBugRewriter.cs index bf26979ad7..0b7509fc28 100644 --- a/src/JsonApiDotNetCore/Repositories/MemoryLeakDetectionBugRewriter.cs +++ b/src/JsonApiDotNetCore/Repositories/MemoryLeakDetectionBugRewriter.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Resources; @@ -15,11 +15,12 @@ namespace JsonApiDotNetCore.Repositories /// Note that by using this workaround, nested filtering, paging and sorting all remain broken in EF Core 3.1 when using injected parameters in resources. /// But at least it enables simple top-level queries to succeed without an exception. /// + [PublicAPI] public sealed class MemoryLeakDetectionBugRewriter { public QueryLayer Rewrite(QueryLayer queryLayer) { - if (queryLayer == null) throw new ArgumentNullException(nameof(queryLayer)); + ArgumentGuard.NotNull(queryLayer, nameof(queryLayer)); return RewriteLayer(queryLayer); } diff --git a/src/JsonApiDotNetCore/Repositories/PlaceholderEntityCollector.cs b/src/JsonApiDotNetCore/Repositories/PlaceholderResourceCollector.cs similarity index 90% rename from src/JsonApiDotNetCore/Repositories/PlaceholderEntityCollector.cs rename to src/JsonApiDotNetCore/Repositories/PlaceholderResourceCollector.cs index dfdcf02d91..3dfa3c9d99 100644 --- a/src/JsonApiDotNetCore/Repositories/PlaceholderEntityCollector.cs +++ b/src/JsonApiDotNetCore/Repositories/PlaceholderResourceCollector.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using Microsoft.EntityFrameworkCore; @@ -10,6 +11,7 @@ namespace JsonApiDotNetCore.Repositories /// Entity Framework Core change tracker so they can be used in relationship updates without fetching the resource. /// On disposal, the created placeholders are detached, leaving the change tracker in a clean state for reuse. /// + [PublicAPI] public sealed class PlaceholderResourceCollector : IDisposable { private readonly IResourceFactory _resourceFactory; @@ -18,8 +20,11 @@ public sealed class PlaceholderResourceCollector : IDisposable public PlaceholderResourceCollector(IResourceFactory resourceFactory, DbContext dbContext) { - _resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); - _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); + ArgumentGuard.NotNull(dbContext, nameof(dbContext)); + + _resourceFactory = resourceFactory; + _dbContext = dbContext; } /// diff --git a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs index cc7af76e18..8dfe3971e3 100644 --- a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs +++ b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; @@ -13,6 +14,7 @@ namespace JsonApiDotNetCore.Repositories { /// + [PublicAPI] public class ResourceRepositoryAccessor : IResourceRepositoryAccessor { private readonly IServiceProvider _serviceProvider; @@ -21,9 +23,13 @@ public class ResourceRepositoryAccessor : IResourceRepositoryAccessor public ResourceRepositoryAccessor(IServiceProvider serviceProvider, IResourceContextProvider resourceContextProvider, IJsonApiRequest request) { - _serviceProvider = serviceProvider ?? throw new ArgumentException(nameof(serviceProvider)); - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentException(nameof(serviceProvider)); - _request = request ?? throw new ArgumentNullException(nameof(request)); + ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(request, nameof(request)); + + _serviceProvider = serviceProvider; + _resourceContextProvider = resourceContextProvider; + _request = request; } /// @@ -37,7 +43,7 @@ public async Task> GetAsync(QueryLayer /// public async Task> GetAsync(Type resourceType, QueryLayer layer, CancellationToken cancellationToken) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); dynamic repository = ResolveReadRepository(resourceType); return (IReadOnlyCollection) await repository.GetAsync(layer, cancellationToken); diff --git a/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs index d31415df57..bd19f5e850 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs @@ -1,10 +1,12 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Resources.Annotations { /// /// Used to expose a property on a resource class as a JSON:API attribute (https://jsonapi.org/format/#document-resource-object-attributes). /// + [PublicAPI] [AttributeUsage(AttributeTargets.Property)] public sealed class AttrAttribute : ResourceFieldAttribute { @@ -33,15 +35,11 @@ public AttrCapabilities Capabilities /// /// Get the value of the attribute for the given object. - /// Returns null if the attribute does not belong to the - /// provided object. + /// Throws if the attribute does not belong to the provided object. /// public object GetValue(object resource) { - if (resource == null) - { - throw new ArgumentNullException(nameof(resource)); - } + ArgumentGuard.NotNull(resource, nameof(resource)); if (Property.GetMethod == null) { @@ -56,10 +54,7 @@ public object GetValue(object resource) /// public void SetValue(object resource, object newValue) { - if (resource == null) - { - throw new ArgumentNullException(nameof(resource)); - } + ArgumentGuard.NotNull(resource, nameof(resource)); if (Property.SetMethod == null) { diff --git a/src/JsonApiDotNetCore/Resources/Annotations/EagerLoadAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/EagerLoadAttribute.cs index bb31d018ef..17685ccd47 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/EagerLoadAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/EagerLoadAttribute.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Resources.Annotations { @@ -33,6 +34,7 @@ namespace JsonApiDotNetCore.Resources.Annotations /// } /// ]]> /// + [PublicAPI] [AttributeUsage(AttributeTargets.Property)] public sealed class EagerLoadAttribute : Attribute { diff --git a/src/JsonApiDotNetCore/Resources/Annotations/HasManyThroughAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/HasManyThroughAttribute.cs index 818017cae8..49124a6822 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/HasManyThroughAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/HasManyThroughAttribute.cs @@ -3,6 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using JetBrains.Annotations; + +// ReSharper disable NonReadonlyMemberInGetHashCode namespace JsonApiDotNetCore.Resources.Annotations { @@ -40,6 +43,7 @@ namespace JsonApiDotNetCore.Resources.Annotations /// } /// ]]> /// + [PublicAPI] [AttributeUsage(AttributeTargets.Property)] public sealed class HasManyThroughAttribute : HasManyAttribute { @@ -123,7 +127,9 @@ public sealed class HasManyThroughAttribute : HasManyAttribute /// The name of the navigation property that will be used to access the join relationship. public HasManyThroughAttribute(string throughPropertyName) { - ThroughPropertyName = throughPropertyName ?? throw new ArgumentNullException(nameof(throughPropertyName)); + ArgumentGuard.NotNull(throughPropertyName, nameof(throughPropertyName)); + + ThroughPropertyName = throughPropertyName; } /// @@ -132,7 +138,7 @@ public HasManyThroughAttribute(string throughPropertyName) /// public override object GetValue(object resource) { - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(resource, nameof(resource)); var throughEntity = ThroughProperty.GetValue(resource); if (throughEntity == null) @@ -153,7 +159,7 @@ public override object GetValue(object resource) /// public override void SetValue(object resource, object newValue) { - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(resource, nameof(resource)); base.SetValue(resource, newValue); diff --git a/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs index 4efb72832f..cd7188b3a5 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs @@ -1,12 +1,16 @@ using System; using System.Reflection; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; +// ReSharper disable NonReadonlyMemberInGetHashCode + namespace JsonApiDotNetCore.Resources.Annotations { /// /// Used to expose a property on a resource class as a JSON:API relationship (https://jsonapi.org/format/#document-resource-object-relationships). /// + [PublicAPI] public abstract class RelationshipAttribute : ResourceFieldAttribute { /// @@ -73,7 +77,7 @@ public abstract class RelationshipAttribute : ResourceFieldAttribute /// public virtual object GetValue(object resource) { - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(resource, nameof(resource)); return Property.GetValue(resource); } @@ -83,7 +87,7 @@ public virtual object GetValue(object resource) /// public virtual void SetValue(object resource, object newValue) { - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(resource, nameof(resource)); Property.SetValue(resource, newValue); } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs index f3d92fd2a1..df3227acf1 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs @@ -1,10 +1,12 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Resources.Annotations { /// /// When put on a resource class, overrides the convention-based resource name. /// + [PublicAPI] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] public sealed class ResourceAttribute : Attribute { @@ -16,7 +18,9 @@ public sealed class ResourceAttribute : Attribute public ResourceAttribute(string publicName) { - PublicName = publicName ?? throw new ArgumentNullException(nameof(publicName)); + ArgumentGuard.NotNull(publicName, nameof(publicName)); + + PublicName = publicName; } } } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs index 04c4f67ba8..c538c6a12c 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs @@ -1,5 +1,8 @@ using System; using System.Reflection; +using JetBrains.Annotations; + +// ReSharper disable NonReadonlyMemberInGetHashCode namespace JsonApiDotNetCore.Resources.Annotations { @@ -7,6 +10,7 @@ namespace JsonApiDotNetCore.Resources.Annotations /// Used to expose a property on a resource class as a JSON:API field (attribute or relationship). /// See https://jsonapi.org/format/#document-resource-object-fields. /// + [PublicAPI] public abstract class ResourceFieldAttribute : Attribute { private string _publicName; diff --git a/src/JsonApiDotNetCore/Resources/Annotations/ResourceLinksAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/ResourceLinksAttribute.cs index 4a4e73027e..ede3a402be 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/ResourceLinksAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/ResourceLinksAttribute.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; namespace JsonApiDotNetCore.Resources.Annotations @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Resources.Annotations /// /// When put on a resource class, overrides global configuration for which links to render. /// + [PublicAPI] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] public sealed class ResourceLinksAttribute : Attribute { diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs index 5273e7638e..0ca5e77950 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Queries.Expressions; namespace JsonApiDotNetCore.Resources @@ -9,6 +10,7 @@ namespace JsonApiDotNetCore.Resources /// The goal here is to reduce the need for overriding the service and repository layers. /// /// The resource type. + [PublicAPI] public interface IResourceDefinition : IResourceDefinition where TResource : class, IIdentifiable { @@ -20,6 +22,7 @@ public interface IResourceDefinition : IResourceDefinition /// The resource type. /// The resource identifier type. + [PublicAPI] public interface IResourceDefinition where TResource : class, IIdentifiable { diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs index 2f7a5837ee..b3b744ba1f 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.Resources { /// - /// Retrieves a instance from the D/I container and invokes a callback on it. + /// Retrieves an instance from the D/I container and invokes a callback on it. /// public interface IResourceDefinitionAccessor { diff --git a/src/JsonApiDotNetCore/Resources/IdentifiableComparer.cs b/src/JsonApiDotNetCore/Resources/IdentifiableComparer.cs index 99885c951f..21782057db 100644 --- a/src/JsonApiDotNetCore/Resources/IdentifiableComparer.cs +++ b/src/JsonApiDotNetCore/Resources/IdentifiableComparer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Resources { @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.Resources /// Compares `IIdentifiable` instances with each other based on their type and , /// falling back to when both StringIds are null. /// + [PublicAPI] public sealed class IdentifiableComparer : IEqualityComparer { public static readonly IdentifiableComparer Instance = new IdentifiableComparer(); diff --git a/src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs b/src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs index 59918a25d9..b89e8ec580 100644 --- a/src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs +++ b/src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs @@ -7,8 +7,8 @@ internal static class IdentifiableExtensions { public static object GetTypedId(this IIdentifiable identifiable) { - if (identifiable == null) throw new ArgumentNullException(nameof(identifiable)); - + ArgumentGuard.NotNull(identifiable, nameof(identifiable)); + PropertyInfo property = identifiable.GetType().GetProperty(nameof(Identifiable.Id)); if (property == null) diff --git a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs index 282ec058ef..1d3e945c00 100644 --- a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Linq; using System.Linq.Expressions; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; @@ -13,6 +14,7 @@ namespace JsonApiDotNetCore.Resources /// The goal here is to reduce the need for overriding the service and repository layers. /// /// The resource type. + [PublicAPI] public class JsonApiResourceDefinition : JsonApiResourceDefinition, IResourceDefinition where TResource : class, IIdentifiable { @@ -23,6 +25,7 @@ public JsonApiResourceDefinition(IResourceGraph resourceGraph) } /// + [PublicAPI] public class JsonApiResourceDefinition : IResourceDefinition where TResource : class, IIdentifiable { @@ -30,7 +33,9 @@ public class JsonApiResourceDefinition : IResourceDefinition @@ -65,10 +70,7 @@ public virtual SortExpression OnApplySort(SortExpression existingSort) /// protected SortExpression CreateSortExpressionFromLambda(PropertySortOrder keySelectors) { - if (keySelectors == null) - { - throw new ArgumentNullException(nameof(keySelectors)); - } + ArgumentGuard.NotNull(keySelectors, nameof(keySelectors)); List sortElements = new List(); diff --git a/src/JsonApiDotNetCore/Resources/OperationContainer.cs b/src/JsonApiDotNetCore/Resources/OperationContainer.cs index ff37241666..85000dff0e 100644 --- a/src/JsonApiDotNetCore/Resources/OperationContainer.cs +++ b/src/JsonApiDotNetCore/Resources/OperationContainer.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Resources { /// /// Represents a write operation on a JSON:API resource. /// + [PublicAPI] public sealed class OperationContainer { public OperationKind Kind { get; } @@ -17,10 +20,14 @@ public sealed class OperationContainer public OperationContainer(OperationKind kind, IIdentifiable resource, ITargetedFields targetedFields, IJsonApiRequest request) { + ArgumentGuard.NotNull(resource, nameof(resource)); + ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); + ArgumentGuard.NotNull(request, nameof(request)); + Kind = kind; - Resource = resource ?? throw new ArgumentNullException(nameof(resource)); - TargetedFields = targetedFields ?? throw new ArgumentNullException(nameof(targetedFields)); - Request = request ?? throw new ArgumentNullException(nameof(request)); + Resource = resource; + TargetedFields = targetedFields; + Request = request; } public void SetTransactionId(Guid transactionId) @@ -30,7 +37,7 @@ public void SetTransactionId(Guid transactionId) public OperationContainer WithResource(IIdentifiable resource) { - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(resource, nameof(resource)); return new OperationContainer(Kind, resource, TargetedFields, Request); } @@ -41,14 +48,20 @@ public ISet GetSecondaryResources() foreach (var relationship in TargetedFields.Relationships) { - var rightValue = relationship.GetValue(Resource); - foreach (var rightResource in TypeHelper.ExtractResources(rightValue)) - { - secondaryResources.Add(rightResource); - } + AddSecondaryResources(relationship, secondaryResources); } return secondaryResources; } + + private void AddSecondaryResources(RelationshipAttribute relationship, HashSet secondaryResources) + { + var rightValue = relationship.GetValue(Resource); + + foreach (var rightResource in TypeHelper.ExtractResources(rightValue)) + { + secondaryResources.Add(rightResource); + } + } } } diff --git a/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs index 701b677c48..f4e88c8c27 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; using Newtonsoft.Json; @@ -7,6 +7,7 @@ namespace JsonApiDotNetCore.Resources { /// + [PublicAPI] public sealed class ResourceChangeTracker : IResourceChangeTracker where TResource : class, IIdentifiable { private readonly IJsonApiOptions _options; @@ -20,15 +21,19 @@ public sealed class ResourceChangeTracker : IResourceChangeTracker public void SetInitiallyStoredAttributeValues(TResource resource) { - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(resource, nameof(resource)); var resourceContext = _resourceContextProvider.GetResourceContext(); _initiallyStoredAttributeValues = CreateAttributeDictionary(resource, resourceContext.Attributes); @@ -37,7 +42,7 @@ public void SetInitiallyStoredAttributeValues(TResource resource) /// public void SetRequestedAttributeValues(TResource resource) { - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(resource, nameof(resource)); _requestedAttributeValues = CreateAttributeDictionary(resource, _targetedFields.Attributes); } @@ -45,7 +50,7 @@ public void SetRequestedAttributeValues(TResource resource) /// public void SetFinallyStoredAttributeValues(TResource resource) { - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(resource, nameof(resource)); var resourceContext = _resourceContextProvider.GetResourceContext(); _finallyStoredAttributeValues = CreateAttributeDictionary(resource, resourceContext.Attributes); diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs index 44ed85ffe5..fe5dc40088 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using Microsoft.Extensions.DependencyInjection; @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.Resources { /// + [PublicAPI] public class ResourceDefinitionAccessor : IResourceDefinitionAccessor { private readonly IResourceContextProvider _resourceContextProvider; @@ -14,14 +16,17 @@ public class ResourceDefinitionAccessor : IResourceDefinitionAccessor public ResourceDefinitionAccessor(IResourceContextProvider resourceContextProvider, IServiceProvider serviceProvider) { - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); + + _resourceContextProvider = resourceContextProvider; + _serviceProvider = serviceProvider; } /// public IReadOnlyCollection OnApplyIncludes(Type resourceType, IReadOnlyCollection existingIncludes) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); dynamic resourceDefinition = ResolveResourceDefinition(resourceType); return resourceDefinition.OnApplyIncludes(existingIncludes); @@ -30,7 +35,7 @@ public IReadOnlyCollection OnApplyIncludes(Type resour /// public FilterExpression OnApplyFilter(Type resourceType, FilterExpression existingFilter) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); dynamic resourceDefinition = ResolveResourceDefinition(resourceType); return resourceDefinition.OnApplyFilter(existingFilter); @@ -39,7 +44,7 @@ public FilterExpression OnApplyFilter(Type resourceType, FilterExpression existi /// public SortExpression OnApplySort(Type resourceType, SortExpression existingSort) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); dynamic resourceDefinition = ResolveResourceDefinition(resourceType); return resourceDefinition.OnApplySort(existingSort); @@ -48,7 +53,7 @@ public SortExpression OnApplySort(Type resourceType, SortExpression existingSort /// public PaginationExpression OnApplyPagination(Type resourceType, PaginationExpression existingPagination) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); dynamic resourceDefinition = ResolveResourceDefinition(resourceType); return resourceDefinition.OnApplyPagination(existingPagination); @@ -57,7 +62,7 @@ public PaginationExpression OnApplyPagination(Type resourceType, PaginationExpre /// public SparseFieldSetExpression OnApplySparseFieldSet(Type resourceType, SparseFieldSetExpression existingSparseFieldSet) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); dynamic resourceDefinition = ResolveResourceDefinition(resourceType); return resourceDefinition.OnApplySparseFieldSet(existingSparseFieldSet); @@ -66,8 +71,8 @@ public SparseFieldSetExpression OnApplySparseFieldSet(Type resourceType, SparseF /// public object GetQueryableHandlerForQueryStringParameter(Type resourceType, string parameterName) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); - if (parameterName == null) throw new ArgumentNullException(nameof(parameterName)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + ArgumentGuard.NotNull(parameterName, nameof(parameterName)); dynamic resourceDefinition = ResolveResourceDefinition(resourceType); var handlers = resourceDefinition.OnRegisterQueryableHandlersForQueryStringParameters(); @@ -78,7 +83,7 @@ public object GetQueryableHandlerForQueryStringParameter(Type resourceType, stri /// public IDictionary GetMeta(Type resourceType, IIdentifiable resourceInstance) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); dynamic resourceDefinition = ResolveResourceDefinition(resourceType); return resourceDefinition.GetMeta((dynamic) resourceInstance); diff --git a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs index e0a7a6646e..76e96cb6f5 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs @@ -14,16 +14,15 @@ internal sealed class ResourceFactory : IResourceFactory public ResourceFactory(IServiceProvider serviceProvider) { - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); + + _serviceProvider = serviceProvider; } /// public IIdentifiable CreateInstance(Type resourceType) { - if (resourceType == null) - { - throw new ArgumentNullException(nameof(resourceType)); - } + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); return InnerCreateInstance(resourceType, _serviceProvider); } @@ -57,7 +56,7 @@ private static IIdentifiable InnerCreateInstance(Type type, IServiceProvider ser /// public NewExpression CreateNewExpression(Type resourceType) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); if (HasSingleConstructorWithoutParameters(resourceType)) { diff --git a/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs b/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs index 4687c5b531..6b20919de2 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Hooks.Internal; using JsonApiDotNetCore.Hooks.Internal.Execution; @@ -12,13 +12,16 @@ namespace JsonApiDotNetCore.Resources /// The goal of this class is to reduce the frequency with which developers have to override the service and repository layers. /// /// The resource type. + [PublicAPI] public class ResourceHooksDefinition : IResourceHookContainer where TResource : class, IIdentifiable { protected IResourceGraph ResourceGraph { get; } public ResourceHooksDefinition(IResourceGraph resourceGraph) { - ResourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + + ResourceGraph = resourceGraph; } /// diff --git a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs index ba69bc0be6..b9692bfac2 100644 --- a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; @@ -12,6 +13,7 @@ namespace JsonApiDotNetCore.Serialization /// /// Server serializer implementation of for atomic:operations responses. /// + [PublicAPI] public sealed class AtomicOperationsResponseSerializer : BaseSerializer, IJsonApiSerializer { private readonly IMetaBuilder _metaBuilder; @@ -28,11 +30,17 @@ public AtomicOperationsResponseSerializer(IResourceObjectBuilder resourceObjectB IJsonApiRequest request, IJsonApiOptions options) : base(resourceObjectBuilder) { - _metaBuilder = metaBuilder ?? throw new ArgumentNullException(nameof(metaBuilder)); - _linkBuilder = linkBuilder ?? throw new ArgumentNullException(nameof(linkBuilder)); - _fieldsToSerialize = fieldsToSerialize ?? throw new ArgumentNullException(nameof(fieldsToSerialize)); - _request = request ?? throw new ArgumentNullException(nameof(request)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder)); + ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); + ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize)); + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(options, nameof(options)); + + _metaBuilder = metaBuilder; + _linkBuilder = linkBuilder; + _fieldsToSerialize = fieldsToSerialize; + _request = request; + _options = options; } /// diff --git a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs index 0b9626ffc1..070f501d01 100644 --- a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -16,6 +17,7 @@ namespace JsonApiDotNetCore.Serialization /// Abstract base class for deserialization. Deserializes JSON content into s /// and constructs instances of the resource(s) in the document body. /// + [PublicAPI] public abstract class BaseDeserializer { protected IResourceContextProvider ResourceContextProvider { get; } @@ -26,8 +28,11 @@ public abstract class BaseDeserializer protected BaseDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory) { - ResourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); - ResourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); + + ResourceContextProvider = resourceContextProvider; + ResourceFactory = resourceFactory; } /// @@ -46,7 +51,7 @@ protected BaseDeserializer(IResourceContextProvider resourceContextProvider, IRe protected object DeserializeBody(string body) { - if (body == null) throw new ArgumentNullException(nameof(body)); + ArgumentGuard.NotNull(body, nameof(body)); var bodyJToken = LoadJToken(body); Document = bodyJToken.ToObject(); @@ -74,11 +79,13 @@ protected object DeserializeBody(string body) /// Exposed attributes for . protected IIdentifiable SetAttributes(IIdentifiable resource, IDictionary attributeValues, IReadOnlyCollection attributes) { - if (resource == null) throw new ArgumentNullException(nameof(resource)); - if (attributes == null) throw new ArgumentNullException(nameof(attributes)); + ArgumentGuard.NotNull(resource, nameof(resource)); + ArgumentGuard.NotNull(attributes, nameof(attributes)); if (attributeValues == null || attributeValues.Count == 0) + { return resource; + } foreach (var attr in attributes) { @@ -107,8 +114,8 @@ protected IIdentifiable SetAttributes(IIdentifiable resource, IDictionaryExposed relationships for . protected virtual IIdentifiable SetRelationships(IIdentifiable resource, IDictionary relationshipValues, IReadOnlyCollection relationshipAttributes) { - if (resource == null) throw new ArgumentNullException(nameof(resource)); - if (relationshipAttributes == null) throw new ArgumentNullException(nameof(relationshipAttributes)); + ArgumentGuard.NotNull(resource, nameof(resource)); + ArgumentGuard.NotNull(relationshipAttributes, nameof(relationshipAttributes)); if (relationshipValues == null || relationshipValues.Count == 0) { @@ -138,14 +145,13 @@ protected virtual IIdentifiable SetRelationships(IIdentifiable resource, IDictio protected JToken LoadJToken(string body) { - JToken jToken; - using (JsonReader jsonReader = new JsonTextReader(new StringReader(body))) + using JsonReader jsonReader = new JsonTextReader(new StringReader(body)) { // https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/509 - jsonReader.DateParseHandling = DateParseHandling.None; - jToken = JToken.Load(jsonReader); - } - return jToken; + DateParseHandling = DateParseHandling.None + }; + + return JToken.Load(jsonReader); } /// @@ -256,6 +262,7 @@ private IIdentifiable CreateRightResource(RelationshipAttribute relationship, return null; } + [AssertionMethod] private void AssertHasType(ResourceIdentifierObject resourceIdentifierObject, RelationshipAttribute relationship) { if (resourceIdentifierObject.Type == null) @@ -296,6 +303,7 @@ private void AssertHasIdOrLid(ResourceIdentifierObject resourceIdentifierObject, } } + [AssertionMethod] private void AssertHasNoLid(ResourceIdentifierObject resourceIdentifierObject) { if (resourceIdentifierObject.Lid != null) @@ -318,8 +326,10 @@ private void AssertRightTypeIsCompatible(ResourceContext rightResourceContext, R private object ConvertAttrValue(object newValue, Type targetType) { if (newValue is JContainer jObject) + { // the attribute value is a complex type that needs additional deserialization return DeserializeComplexType(jObject, targetType); + } // the attribute value is a native C# type. var convertedValue = TypeHelper.ConvertType(newValue, targetType); diff --git a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs index da61a4b188..d9248c56d7 100644 --- a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs @@ -19,7 +19,9 @@ public abstract class BaseSerializer protected BaseSerializer(IResourceObjectBuilder resourceObjectBuilder) { - ResourceObjectBuilder = resourceObjectBuilder ?? throw new ArgumentNullException(nameof(resourceObjectBuilder)); + ArgumentGuard.NotNull(resourceObjectBuilder, nameof(resourceObjectBuilder)); + + ResourceObjectBuilder = resourceObjectBuilder; } /// @@ -33,7 +35,9 @@ protected BaseSerializer(IResourceObjectBuilder resourceObjectBuilder) protected Document Build(IIdentifiable resource, IReadOnlyCollection attributes, IReadOnlyCollection relationships) { if (resource == null) + { return new Document(); + } return new Document { Data = ResourceObjectBuilder.Build(resource, attributes, relationships) }; } @@ -48,28 +52,28 @@ protected Document Build(IIdentifiable resource, IReadOnlyCollectionThe resource object that was built. protected Document Build(IReadOnlyCollection resources, IReadOnlyCollection attributes, IReadOnlyCollection relationships) { - if (resources == null) throw new ArgumentNullException(nameof(resources)); + ArgumentGuard.NotNull(resources, nameof(resources)); var data = new List(); foreach (IIdentifiable resource in resources) + { data.Add(ResourceObjectBuilder.Build(resource, attributes, relationships)); + } return new Document { Data = data }; } protected string SerializeObject(object value, JsonSerializerSettings defaultSettings, Action changeSerializer = null) { - if (defaultSettings == null) throw new ArgumentNullException(nameof(defaultSettings)); + ArgumentGuard.NotNull(defaultSettings, nameof(defaultSettings)); JsonSerializer serializer = JsonSerializer.CreateDefault(defaultSettings); changeSerializer?.Invoke(serializer); using var stringWriter = new StringWriter(); - using (var jsonWriter = new JsonTextWriter(stringWriter)) - { - serializer.Serialize(jsonWriter, value); - } + using var jsonWriter = new JsonTextWriter(stringWriter); + serializer.Serialize(jsonWriter, value); return stringWriter.ToString(); } } diff --git a/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs index c56607db75..0a83126407 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Building { /// /// Builds the top-level meta object. /// + [PublicAPI] public interface IMetaBuilder { /// diff --git a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs index 2697eb63a2..99ee0c2e7d 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs @@ -1,7 +1,7 @@ -using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Internal; @@ -11,6 +11,7 @@ namespace JsonApiDotNetCore.Serialization.Building { + [PublicAPI] public class IncludedResourceObjectBuilder : ResourceObjectBuilder, IIncludedResourceObjectBuilder { private readonly HashSet _included; @@ -27,10 +28,15 @@ public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, IResourceObjectBuilderSettingsProvider settingsProvider) : base(resourceContextProvider, settingsProvider.Get()) { + ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize)); + ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); + ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); + ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); + _included = new HashSet(ResourceIdentifierObjectComparer.Instance); - _fieldsToSerialize = fieldsToSerialize ?? throw new ArgumentNullException(nameof(fieldsToSerialize)); - _linkBuilder = linkBuilder ?? throw new ArgumentNullException(nameof(linkBuilder)); - _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); + _fieldsToSerialize = fieldsToSerialize; + _linkBuilder = linkBuilder; + _resourceDefinitionAccessor = resourceDefinitionAccessor; _sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); } @@ -44,22 +50,9 @@ public IList Build() { if (resourceObject.Relationships != null) { - foreach (var relationshipName in resourceObject.Relationships.Keys.ToArray()) - { - var resourceContext = ResourceContextProvider.GetResourceContext(resourceObject.Type); - var relationship = resourceContext.Relationships.Single(rel => rel.PublicName == relationshipName); - - if (!IsRelationshipInSparseFieldSet(relationship)) - { - resourceObject.Relationships.Remove(relationshipName); - } - } - - // removes relationship entries (s) if they're completely empty. - var pruned = resourceObject.Relationships.Where(p => p.Value.IsPopulated || p.Value.Links != null).ToDictionary(p => p.Key, p => p.Value); - if (!pruned.Any()) pruned = null; - resourceObject.Relationships = pruned; + UpdateRelationships(resourceObject); } + resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); } return _included.ToArray(); @@ -67,6 +60,31 @@ public IList Build() return null; } + private void UpdateRelationships(ResourceObject resourceObject) + { + foreach (var relationshipName in resourceObject.Relationships.Keys.ToArray()) + { + var resourceContext = ResourceContextProvider.GetResourceContext(resourceObject.Type); + var relationship = resourceContext.Relationships.Single(rel => rel.PublicName == relationshipName); + + if (!IsRelationshipInSparseFieldSet(relationship)) + { + resourceObject.Relationships.Remove(relationshipName); + } + } + + resourceObject.Relationships = PruneRelationshipEntries(resourceObject); + } + + private static IDictionary PruneRelationshipEntries(ResourceObject resourceObject) + { + var pruned = resourceObject.Relationships + .Where(pair => pair.Value.IsPopulated || pair.Value.Links != null) + .ToDictionary(pair => pair.Key, pair => pair.Value); + + return !pruned.Any() ? null : pruned; + } + private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship) { var resourceContext = ResourceContextProvider.GetResourceContext(relationship.LeftType); @@ -89,8 +107,8 @@ public override ResourceObject Build(IIdentifiable resource, IReadOnlyCollection /// public void IncludeRelationshipChain(IReadOnlyCollection inclusionChain, IIdentifiable rootResource) { - if (inclusionChain == null) throw new ArgumentNullException(nameof(inclusionChain)); - if (rootResource == null) throw new ArgumentNullException(nameof(rootResource)); + ArgumentGuard.NotNull(inclusionChain, nameof(inclusionChain)); + ArgumentGuard.NotNull(rootResource, nameof(rootResource)); // We don't have to build a resource object for the root resource because // this one is already encoded in the documents primary data, so we process the chain @@ -101,21 +119,30 @@ public void IncludeRelationshipChain(IReadOnlyCollection ProcessChain(related, chainRemainder); } - private void ProcessChain(object related, List inclusionChain) + private void ProcessChain(object related, IList inclusionChain) { if (related is IEnumerable children) + { foreach (IIdentifiable child in children) + { ProcessRelationship(child, inclusionChain); + } + } else + { ProcessRelationship((IIdentifiable)related, inclusionChain); + } } - private void ProcessRelationship(IIdentifiable parent, List inclusionChain) + private void ProcessRelationship(IIdentifiable parent, IList inclusionChain) { // get the resource object for parent. var resourceObject = GetOrBuildResourceObject(parent); if (!inclusionChain.Any()) + { return; + } + var nextRelationship = inclusionChain.First(); var chainRemainder = inclusionChain.ToList(); chainRemainder.RemoveAt(0); @@ -124,7 +151,10 @@ private void ProcessRelationship(IIdentifiable parent, List ShiftChain(IReadOnlyCollection chain) + private IList ShiftChain(IReadOnlyCollection chain) { var chainRemainder = chain.ToList(); chainRemainder.RemoveAt(0); @@ -149,8 +179,8 @@ private List ShiftChain(IReadOnlyCollection protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { - if (relationship == null) throw new ArgumentNullException(nameof(relationship)); - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(relationship, nameof(relationship)); + ArgumentGuard.NotNull(resource, nameof(resource)); return new RelationshipEntry { Links = _linkBuilder.GetRelationshipLinks(relationship, resource) }; } diff --git a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs index fd7b0d7c50..db15f31300 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; @@ -15,10 +16,11 @@ namespace JsonApiDotNetCore.Serialization.Building { + [PublicAPI] public class LinkBuilder : ILinkBuilder { - private const string _pageSizeParameterName = "page[size]"; - private const string _pageNumberParameterName = "page[number]"; + private const string PageSizeParameterName = "page[size]"; + private const string PageNumberParameterName = "page[number]"; private readonly IResourceContextProvider _provider; private readonly IRequestQueryStringAccessor _queryStringAccessor; @@ -32,11 +34,17 @@ public LinkBuilder(IJsonApiOptions options, IResourceContextProvider provider, IRequestQueryStringAccessor queryStringAccessor) { - _options = options ?? throw new ArgumentNullException(nameof(options)); - _request = request ?? throw new ArgumentNullException(nameof(request)); - _paginationContext = paginationContext ?? throw new ArgumentNullException(nameof(paginationContext)); - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - _queryStringAccessor = queryStringAccessor ?? throw new ArgumentNullException(nameof(queryStringAccessor)); + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(paginationContext, nameof(paginationContext)); + ArgumentGuard.NotNull(provider, nameof(provider)); + ArgumentGuard.NotNull(queryStringAccessor, nameof(queryStringAccessor)); + + _options = options; + _request = request; + _paginationContext = paginationContext; + _provider = provider; + _queryStringAccessor = queryStringAccessor; } /// @@ -106,12 +114,12 @@ private string GetSelfTopLevelLink(ResourceContext resourceContext, Action { - var existingPageSizeParameterValue = parameters.ContainsKey(_pageSizeParameterName) - ? parameters[_pageSizeParameterName] + var existingPageSizeParameterValue = parameters.ContainsKey(PageSizeParameterName) + ? parameters[PageSizeParameterName] : null; PageSize newTopPageSize = Equals(pageSize, _options.DefaultPageSize) ? null : pageSize; @@ -159,20 +167,20 @@ private string GetPageLink(ResourceContext resourceContext, int pageOffset, Page string newPageSizeParameterValue = ChangeTopPageSize(existingPageSizeParameterValue, newTopPageSize); if (newPageSizeParameterValue == null) { - parameters.Remove(_pageSizeParameterName); + parameters.Remove(PageSizeParameterName); } else { - parameters[_pageSizeParameterName] = newPageSizeParameterValue; + parameters[PageSizeParameterName] = newPageSizeParameterValue; } if (pageOffset == 1) { - parameters.Remove(_pageNumberParameterName); + parameters.Remove(PageNumberParameterName); } else { - parameters[_pageNumberParameterName] = pageOffset.ToString(); + parameters[PageNumberParameterName] = pageOffset.ToString(); } }); } @@ -221,14 +229,14 @@ private List ParsePageSizeExpressio var parser = new PaginationParser(_provider); var paginationExpression = parser.Parse(pageSizeParameterValue, requestResource); - return new List(paginationExpression.Elements); + return paginationExpression.Elements.ToList(); } /// public ResourceLinks GetResourceLinks(string resourceName, string id) { - if (resourceName == null) throw new ArgumentNullException(nameof(resourceName)); - if (id == null) throw new ArgumentNullException(nameof(id)); + ArgumentGuard.NotNull(resourceName, nameof(resourceName)); + ArgumentGuard.NotNull(id, nameof(id)); var resourceContext = _provider.GetResourceContext(resourceName); if (ShouldAddResourceLink(resourceContext, LinkTypes.Self)) @@ -242,8 +250,8 @@ public ResourceLinks GetResourceLinks(string resourceName, string id) /// public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable parent) { - if (relationship == null) throw new ArgumentNullException(nameof(relationship)); - if (parent == null) throw new ArgumentNullException(nameof(parent)); + ArgumentGuard.NotNull(relationship, nameof(relationship)); + ArgumentGuard.NotNull(parent, nameof(parent)); var parentResourceContext = _provider.GetResourceContext(parent.GetType()); var childNavigation = relationship.PublicName; diff --git a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs index dc455fe50c..d641910969 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs @@ -1,12 +1,13 @@ -using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; namespace JsonApiDotNetCore.Serialization.Building { /// + [PublicAPI] public class MetaBuilder : IMetaBuilder { private readonly IPaginationContext _paginationContext; @@ -17,15 +18,19 @@ public class MetaBuilder : IMetaBuilder public MetaBuilder(IPaginationContext paginationContext, IJsonApiOptions options, IResponseMeta responseMeta) { - _paginationContext = paginationContext ?? throw new ArgumentNullException(nameof(paginationContext)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - _responseMeta = responseMeta ?? throw new ArgumentNullException(nameof(responseMeta)); + ArgumentGuard.NotNull(paginationContext, nameof(paginationContext)); + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(responseMeta, nameof(responseMeta)); + + _paginationContext = paginationContext; + _options = options; + _responseMeta = responseMeta; } /// public void Add(IReadOnlyDictionary values) { - if (values == null) throw new ArgumentNullException(nameof(values)); + ArgumentGuard.NotNull(values, nameof(values)); _meta = values.Keys.Union(_meta.Keys) .ToDictionary(key => key, @@ -37,8 +42,7 @@ public IDictionary Build() { if (_paginationContext.TotalResourceCount != null) { - var namingStrategy = _options.SerializerContractResolver.NamingStrategy; - string key = namingStrategy.GetPropertyName("TotalResources", false); + string key = _options.SerializerNamingStrategy.GetPropertyName("TotalResources", false); _meta.Add(key, _paginationContext.TotalResourceCount); } diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs index 28808bb6ba..b67e04e410 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs @@ -1,6 +1,6 @@ -using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -10,6 +10,7 @@ namespace JsonApiDotNetCore.Serialization.Building { /// + [PublicAPI] public class ResourceObjectBuilder : IResourceObjectBuilder { protected IResourceContextProvider ResourceContextProvider { get; } @@ -17,14 +18,17 @@ public class ResourceObjectBuilder : IResourceObjectBuilder public ResourceObjectBuilder(IResourceContextProvider resourceContextProvider, ResourceObjectBuilderSettings settings) { - ResourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); - _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(settings, nameof(settings)); + + ResourceContextProvider = resourceContextProvider; + _settings = settings; } /// public virtual ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) { - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(resource, nameof(resource)); var resourceContext = ResourceContextProvider.GetResourceContext(resource.GetType()); @@ -32,12 +36,20 @@ public virtual ResourceObject Build(IIdentifiable resource, IReadOnlyCollection< var resourceObject = new ResourceObject { Type = resourceContext.PublicName, Id = resource.StringId }; // populating the top-level "attribute" member of a resource object. never include "id" as an attribute - if (attributes != null && (attributes = attributes.Where(attr => attr.Property.Name != nameof(Identifiable.Id)).ToArray()).Any()) - ProcessAttributes(resource, attributes, resourceObject); + if (attributes != null) + { + var attributesWithoutId = attributes.Where(attr => attr.Property.Name != nameof(Identifiable.Id)).ToArray(); + if (attributesWithoutId.Any()) + { + ProcessAttributes(resource, attributesWithoutId, resourceObject); + } + } // populating the top-level "relationship" member of a resource object. if (relationships != null) + { ProcessRelationships(resource, relationships, resourceObject); + } return resourceObject; } @@ -51,8 +63,8 @@ public virtual ResourceObject Build(IIdentifiable resource, IReadOnlyCollection< /// protected virtual RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { - if (relationship == null) throw new ArgumentNullException(nameof(relationship)); - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(relationship, nameof(relationship)); + ArgumentGuard.NotNull(resource, nameof(resource)); return new RelationshipEntry { Data = GetRelatedResourceLinkage(relationship, resource) }; } @@ -62,8 +74,8 @@ protected virtual RelationshipEntry GetRelationshipData(RelationshipAttribute re /// protected object GetRelatedResourceLinkage(RelationshipAttribute relationship, IIdentifiable resource) { - if (relationship == null) throw new ArgumentNullException(nameof(relationship)); - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(relationship, nameof(relationship)); + ArgumentGuard.NotNull(resource, nameof(resource)); return relationship is HasOneAttribute hasOne ? (object) GetRelatedResourceLinkageForHasOne(hasOne, resource) @@ -88,7 +100,7 @@ private ResourceIdentifierObject GetRelatedResourceLinkageForHasOne(HasOneAttrib /// /// Builds the s for a HasMany relationship. /// - private List GetRelatedResourceLinkageForHasMany(HasManyAttribute relationship, IIdentifiable resource) + private IList GetRelatedResourceLinkageForHasMany(HasManyAttribute relationship, IIdentifiable resource) { var value = relationship.GetValue(resource); var relatedResources = TypeHelper.ExtractResources(value); @@ -127,7 +139,9 @@ private void ProcessRelationships(IIdentifiable resource, IEnumerable()).Add(rel.PublicName, relData); + } } } diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettings.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettings.cs index c6ecf2234b..b14ea44ea3 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettings.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettings.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.Serialization.Building /// Options used to configure how fields of a model get serialized into /// a JSON:API . /// + [PublicAPI] public sealed class ResourceObjectBuilderSettings { public NullValueHandling SerializerNullValueHandling { get; } diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs index 8b317c3e3e..83422527ed 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs @@ -1,4 +1,3 @@ -using System; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.QueryStrings; @@ -15,8 +14,11 @@ public sealed class ResourceObjectBuilderSettingsProvider : IResourceObjectBuild public ResourceObjectBuilderSettingsProvider(IDefaultsQueryStringParameterReader defaultsReader, INullsQueryStringParameterReader nullsReader) { - _defaultsReader = defaultsReader ?? throw new ArgumentNullException(nameof(defaultsReader)); - _nullsReader = nullsReader ?? throw new ArgumentNullException(nameof(nullsReader)); + ArgumentGuard.NotNull(defaultsReader, nameof(defaultsReader)); + ArgumentGuard.NotNull(nullsReader, nameof(nullsReader)); + + _defaultsReader = defaultsReader; + _nullsReader = nullsReader; } /// diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs index 2da9e21875..eb3e445c94 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs @@ -1,6 +1,6 @@ -using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; @@ -12,6 +12,7 @@ namespace JsonApiDotNetCore.Serialization.Building { + [PublicAPI] public class ResponseResourceObjectBuilder : ResourceObjectBuilder { private readonly IIncludedResourceObjectBuilder _includedBuilder; @@ -29,18 +30,24 @@ public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, IResourceObjectBuilderSettingsProvider settingsProvider) : base(resourceContextProvider, settingsProvider.Get()) { - _linkBuilder = linkBuilder ?? throw new ArgumentNullException(nameof(linkBuilder)); - _includedBuilder = includedBuilder ?? throw new ArgumentNullException(nameof(includedBuilder)); - _constraintProviders = constraintProviders ?? throw new ArgumentNullException(nameof(constraintProviders)); - _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); - _sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); + ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); + ArgumentGuard.NotNull(includedBuilder, nameof(includedBuilder)); + ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); + ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); + + _linkBuilder = linkBuilder; + _includedBuilder = includedBuilder; + _constraintProviders = constraintProviders; + _resourceDefinitionAccessor = resourceDefinitionAccessor; + _sparseFieldSetCache = new SparseFieldSetCache(_constraintProviders, resourceDefinitionAccessor); } public RelationshipEntry Build(IIdentifiable resource, RelationshipAttribute requestRelationship) { - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(resource, nameof(resource)); + ArgumentGuard.NotNull(requestRelationship, nameof(requestRelationship)); - _requestRelationship = requestRelationship ?? throw new ArgumentNullException(nameof(requestRelationship)); + _requestRelationship = requestRelationship; return GetRelationshipData(requestRelationship, resource); } @@ -64,8 +71,8 @@ public override ResourceObject Build(IIdentifiable resource, IReadOnlyCollection /// protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { - if (relationship == null) throw new ArgumentNullException(nameof(relationship)); - if (resource == null) throw new ArgumentNullException(nameof(resource)); + ArgumentGuard.NotNull(relationship, nameof(relationship)); + ArgumentGuard.NotNull(resource, nameof(resource)); RelationshipEntry relationshipEntry = null; List> relationshipChains = null; @@ -73,9 +80,13 @@ protected override RelationshipEntry GetRelationshipData(RelationshipAttribute r { relationshipEntry = base.GetRelationshipData(relationship, resource); if (relationshipChains != null && relationshipEntry.HasResource) + { foreach (var chain in relationshipChains) + { // traverses (recursively) and extracts all (nested) related resources for the current inclusion chain. _includedBuilder.IncludeRelationshipChain(chain, resource); + } + } } if (!IsRelationshipInSparseFieldSet(relationship)) @@ -110,13 +121,19 @@ private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship) /// private bool ShouldInclude(RelationshipAttribute relationship, out List> inclusionChain) { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var chains = _constraintProviders - .SelectMany(p => p.GetConstraints()) + .SelectMany(provider => provider.GetConstraints()) .Select(expressionInScope => expressionInScope.Expression) .OfType() .SelectMany(IncludeChainConverter.GetRelationshipChains) .ToArray(); + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + inclusionChain = new List>(); foreach (var chain in chains) diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs index c2831e327e..14587a1cb2 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Serialization.Client.Internal @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal /// TODO: Currently and /// information is ignored by the serializer. This is out of scope for now because /// it is not considered mission critical for v4. + [PublicAPI] public abstract class DeserializedResponseBase { public TopLevelLinks Links { get; set; } diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs index c088bdeeee..86e2b8bcbf 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -9,6 +10,7 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal /// Interface for client serializer that can be used to register with the DI container, for usage in /// custom services or repositories. /// + [PublicAPI] public interface IRequestSerializer { /// @@ -28,13 +30,13 @@ public interface IRequestSerializer /// You can use /// to conveniently access the desired instances. /// - public IReadOnlyCollection AttributesToSerialize { set; } + public IReadOnlyCollection AttributesToSerialize { get; set; } /// /// Sets the relationships that will be included in the serialized request body. /// You can use /// to conveniently access the desired instances. /// - public IReadOnlyCollection RelationshipsToSerialize { set; } + public IReadOnlyCollection RelationshipsToSerialize { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs index c4ede45737..6d29d2779d 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCore.Serialization.Client.Internal @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal /// except for in the tests. Exposed publicly to make testing easier or to implement /// server-to-server communication. /// + [PublicAPI] public interface IResponseDeserializer { /// diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs index 954b65e167..c48a3f54b9 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCore.Serialization.Client.Internal @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal /// Represents a deserialized document with "many data". /// /// Type of the resource(s) in the primary data. + [PublicAPI] public sealed class ManyResponse : DeserializedResponseBase where TResource : class, IIdentifiable { public IReadOnlyCollection Data { get; set; } diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs index b01ab6f11a..61d3c5a2fe 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -13,6 +14,7 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal /// /// Client serializer implementation of . /// + [PublicAPI] public class RequestSerializer : BaseSerializer, IRequestSerializer { private Type _currentTargetedResource; @@ -23,7 +25,9 @@ public RequestSerializer(IResourceGraph resourceGraph, IResourceObjectBuilder resourceObjectBuilder) : base(resourceObjectBuilder) { - _resourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + + _resourceGraph = resourceGraph; } /// @@ -45,7 +49,7 @@ public string Serialize(IIdentifiable resource) /// public string Serialize(IReadOnlyCollection resources) { - if (resources == null) throw new ArgumentNullException(nameof(resources)); + ArgumentGuard.NotNull(resources, nameof(resources)); IIdentifiable firstResource = resources.FirstOrDefault(); @@ -67,10 +71,10 @@ public string Serialize(IReadOnlyCollection resources) } /// - public IReadOnlyCollection AttributesToSerialize { private get; set; } + public IReadOnlyCollection AttributesToSerialize { get; set; } /// - public IReadOnlyCollection RelationshipsToSerialize { private get; set; } + public IReadOnlyCollection RelationshipsToSerialize { get; set; } /// /// By default, the client serializer includes all attributes in the result, @@ -81,12 +85,16 @@ private IReadOnlyCollection GetAttributesToSerialize(IIdentifiabl { var currentResourceType = resource.GetType(); if (_currentTargetedResource != currentResourceType) + { // We're dealing with a relationship that is being serialized, for which // we never want to include any attributes in the request body. return new List(); + } if (AttributesToSerialize == null) + { return _resourceGraph.GetAttributes(currentResourceType); + } return AttributesToSerialize; } diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs index 91b6a0392c..a9e4232076 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -11,6 +12,7 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal /// /// Client deserializer implementation of the . /// + [PublicAPI] public class ResponseDeserializer : BaseDeserializer, IResponseDeserializer { public ResponseDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory) : base(resourceContextProvider, resourceFactory) { } @@ -18,7 +20,7 @@ public ResponseDeserializer(IResourceContextProvider resourceContextProvider, IR /// public SingleResponse DeserializeSingle(string body) where TResource : class, IIdentifiable { - if (body == null) throw new ArgumentNullException(nameof(body)); + ArgumentGuard.NotNull(body, nameof(body)); var resource = DeserializeBody(body); return new SingleResponse @@ -34,7 +36,7 @@ public SingleResponse DeserializeSingle(string body) where /// public ManyResponse DeserializeMany(string body) where TResource : class, IIdentifiable { - if (body == null) throw new ArgumentNullException(nameof(body)); + ArgumentGuard.NotNull(body, nameof(body)); var resources = DeserializeBody(body); return new ManyResponse @@ -57,28 +59,36 @@ public ManyResponse DeserializeMany(string body) where TRe /// Relationship data for . Is null when is not a . protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null) { - if (resource == null) throw new ArgumentNullException(nameof(resource)); - if (field == null) throw new ArgumentNullException(nameof(field)); + ArgumentGuard.NotNull(resource, nameof(resource)); + ArgumentGuard.NotNull(field, nameof(field)); // Client deserializers do not need additional processing for attributes. if (field is AttrAttribute) + { return; + } // if the included property is empty or absent, there is no additional data to be parsed. if (Document.Included == null || Document.Included.Count == 0) + { return; + } - if (field is HasOneAttribute hasOneAttr) + if (data != null) { - // add attributes and relationships of a parsed HasOne relationship - var rio = data.SingleData; - hasOneAttr.SetValue(resource, rio == null ? null : ParseIncludedRelationship(rio)); - } - else if (field is HasManyAttribute hasManyAttr) - { // add attributes and relationships of a parsed HasMany relationship - var items = data.ManyData.Select(rio => ParseIncludedRelationship(rio)); - var values = TypeHelper.CopyToTypedCollection(items, hasManyAttr.Property.PropertyType); - hasManyAttr.SetValue(resource, values); + if (field is HasOneAttribute hasOneAttr) + { + // add attributes and relationships of a parsed HasOne relationship + var rio = data.SingleData; + hasOneAttr.SetValue(resource, rio == null ? null : ParseIncludedRelationship(rio)); + } + else if (field is HasManyAttribute hasManyAttr) + { + // add attributes and relationships of a parsed HasMany relationship + var items = data.ManyData.Select(ParseIncludedRelationship); + var values = TypeHelper.CopyToTypedCollection(items, hasManyAttr.Property.PropertyType); + hasManyAttr.SetValue(resource, values); + } } } diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs index 562317f2bf..d40421567b 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCore.Serialization.Client.Internal @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal /// Represents a deserialized document with "single data". /// /// Type of the resource in the primary data. + [PublicAPI] public sealed class SingleResponse : DeserializedResponseBase where TResource : class, IIdentifiable { public TResource Data { get; set; } diff --git a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs index 59abbab7fb..aea6a2d1e7 100644 --- a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs +++ b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; @@ -11,6 +12,7 @@ namespace JsonApiDotNetCore.Serialization { /// + [PublicAPI] public class FieldsToSerialize : IFieldsToSerialize { private readonly IResourceContextProvider _resourceContextProvider; @@ -23,15 +25,18 @@ public FieldsToSerialize( IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiRequest request) { - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); - _request = request ?? throw new ArgumentNullException(nameof(request)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(request, nameof(request)); + + _resourceContextProvider = resourceContextProvider; + _request = request; _sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); } /// public IReadOnlyCollection GetAttributes(Type resourceType) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); if (_request.Kind == EndpointKind.Relationship) { @@ -53,7 +58,7 @@ public IReadOnlyCollection GetAttributes(Type resourceType) /// public IReadOnlyCollection GetRelationships(Type resourceType) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); if (_request.Kind == EndpointKind.Relationship) { diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs index 37fc344c0e..9313d5b8c6 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.Formatters; namespace JsonApiDotNetCore.Serialization @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.Serialization /// The deserializer of the body, used in ASP.NET Core internally /// to process `FromBody`. /// + [PublicAPI] public interface IJsonApiReader { Task ReadAsync(InputFormatterContext context); diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiSerializerFactory.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializerFactory.cs index 75d7f928af..38796a596e 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiSerializerFactory.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializerFactory.cs @@ -1,5 +1,8 @@ -namespace JsonApiDotNetCore.Serialization +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.Serialization { + [PublicAPI] public interface IJsonApiSerializerFactory { /// @@ -7,4 +10,4 @@ public interface IJsonApiSerializerFactory /// IJsonApiSerializer GetSerializer(); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs index 0f8287801a..ac29395115 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs @@ -1,10 +1,12 @@ using System.Threading.Tasks; +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.Formatters; namespace JsonApiDotNetCore.Serialization { + [PublicAPI] public interface IJsonApiWriter { Task WriteAsync(OutputFormatterWriteContext context); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index 83208e4941..91d4fc80b9 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -2,10 +2,11 @@ using System.Collections; using System.Collections.Generic; using System.IO; -using System.Net.Http; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; @@ -20,6 +21,7 @@ namespace JsonApiDotNetCore.Serialization { /// + [PublicAPI] public class JsonApiReader : IJsonApiReader { private readonly IJsonApiDeserializer _deserializer; @@ -32,18 +34,20 @@ public JsonApiReader(IJsonApiDeserializer deserializer, IResourceContextProvider resourceContextProvider, ILoggerFactory loggerFactory) { - if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); - - _deserializer = deserializer ?? throw new ArgumentNullException(nameof(deserializer)); - _request = request ?? throw new ArgumentNullException(nameof(request)); - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + ArgumentGuard.NotNull(deserializer, nameof(deserializer)); + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); + + _deserializer = deserializer; + _request = request; + _resourceContextProvider = resourceContextProvider; _traceWriter = new TraceLogWriter(loggerFactory); } public async Task ReadAsync(InputFormatterContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); + ArgumentGuard.NotNull(context, nameof(context)); string body = await GetRequestBodyAsync(context.HttpContext.Request.Body); @@ -136,6 +140,7 @@ private void ValidateRequestBody(object model, string body, HttpRequest httpRequ } } + [AssertionMethod] private static void AssertHasRequestBody(object model, string body) { if (model == null && string.IsNullOrWhiteSpace(body)) @@ -183,7 +188,7 @@ private IEnumerable GetResourceTypesFromRequestBody(object model) return resourceCollection.Select(r => r.GetType()).Distinct(); } - return model == null ? Array.Empty() : new[] { model.GetType() }; + return model == null ? Enumerable.Empty() : model.GetType().AsEnumerable(); } private void ValidateRequestIncludesId(object model, string body) @@ -242,6 +247,7 @@ private static bool TryGetId(object model, out string id) return false; } + [AssertionMethod] private void ValidateForRelationshipType(string requestMethod, object model, string body) { if (_request.Relationship is HasOneAttribute) diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializationException.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializationException.cs index 92848c3eca..a575ce59d8 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiSerializationException.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiSerializationException.cs @@ -1,10 +1,12 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization { /// /// The error that is thrown when (de)serialization of a JSON:API body fails. /// + [PublicAPI] public class JsonApiSerializationException : Exception { public string GenericMessage { get; } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs index 72ecb569ee..c925825a80 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Serialization.Objects; @@ -18,6 +19,7 @@ namespace JsonApiDotNetCore.Serialization /// Formats the response data used (see https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-3.0). /// It was intended to have as little dependencies as possible in formatting layer for greater extensibility. /// + [PublicAPI] public class JsonApiWriter : IJsonApiWriter { private readonly IJsonApiSerializer _serializer; @@ -26,16 +28,18 @@ public class JsonApiWriter : IJsonApiWriter public JsonApiWriter(IJsonApiSerializer serializer, IExceptionHandler exceptionHandler, ILoggerFactory loggerFactory) { - if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + ArgumentGuard.NotNull(serializer, nameof(serializer)); + ArgumentGuard.NotNull(exceptionHandler, nameof(exceptionHandler)); + ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); - _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); - _exceptionHandler = exceptionHandler ?? throw new ArgumentNullException(nameof(exceptionHandler)); + _serializer = serializer; + _exceptionHandler = exceptionHandler; _traceWriter = new TraceLogWriter(loggerFactory); } public async Task WriteAsync(OutputFormatterWriteContext context) { - if (context == null) throw new ArgumentNullException(nameof(context)); + ArgumentGuard.NotNull(context, nameof(context)); var response = context.HttpContext.Response; response.ContentType = _serializer.ContentType; @@ -83,21 +87,21 @@ private string SerializeResponse(object contextObject, HttpStatusCode statusCode } } - contextObject = WrapErrors(contextObject); + var contextObjectWrapped = WrapErrors(contextObject); - return _serializer.Serialize(contextObject); + return _serializer.Serialize(contextObjectWrapped); } private static object WrapErrors(object contextObject) { if (contextObject is IEnumerable errors) { - contextObject = new ErrorDocument(errors); + return new ErrorDocument(errors); } if (contextObject is Error error) { - contextObject = new ErrorDocument(error); + return new ErrorDocument(error); } return contextObject; diff --git a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationObject.cs index baa787aed0..b6b4a134b8 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationObject.cs @@ -12,7 +12,8 @@ public sealed class AtomicOperationObject : ExposableData [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] public Dictionary Meta { get; set; } - [JsonProperty("op"), JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("op")] + [JsonConverter(typeof(StringEnumConverter))] public AtomicOperationCode Code { get; set; } [JsonProperty("ref", NullValueHandling = NullValueHandling.Ignore)] diff --git a/src/JsonApiDotNetCore/Serialization/Objects/Error.cs b/src/JsonApiDotNetCore/Serialization/Objects/Error.cs index ffc7c38ef4..545c71da54 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/Error.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/Error.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Net; +using JetBrains.Annotations; using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization.Objects @@ -9,6 +10,7 @@ namespace JsonApiDotNetCore.Serialization.Objects /// Provides additional information about a problem encountered while performing an operation. /// Error objects MUST be returned as an array keyed by errors in the top level of a JSON:API document. /// + [PublicAPI] public sealed class Error { public Error(HttpStatusCode statusCode) diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs index 34b89596e4..67d0e82a27 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { + [PublicAPI] public sealed class ErrorDocument { public IReadOnlyList Errors { get; } @@ -15,13 +17,13 @@ public ErrorDocument() } public ErrorDocument(Error error) - : this(new[] {error}) + : this(error.AsEnumerable()) { } public ErrorDocument(IEnumerable errors) { - if (errors == null) throw new ArgumentNullException(nameof(errors)); + ArgumentGuard.NotNull(errors, nameof(errors)); Errors = errors.ToList(); } @@ -34,7 +36,9 @@ public HttpStatusCode GetErrorStatusCode() .ToArray(); if (statusCodes.Length == 1) + { return (HttpStatusCode)statusCodes[0]; + } var statusCode = int.Parse(statusCodes.Max().ToString()[0] + "00"); return (HttpStatusCode)statusCode; diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs index 43b327c359..9c4477dd02 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization.Objects @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.Serialization.Objects /// /// A meta object containing non-standard meta-information about the error. /// + [PublicAPI] public sealed class ErrorMeta { [JsonExtensionData] diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs b/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs index aa2b603406..40292cfb36 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace JsonApiDotNetCore.Serialization.Objects { + [PublicAPI] public abstract class ExposableData where TResource : class { /// @@ -28,7 +30,10 @@ public object Data public bool ShouldSerializeData() { if (GetType() == typeof(RelationshipEntry)) + { return IsPopulated; + } + return true; } @@ -70,7 +75,10 @@ public bool ShouldSerializeData() protected object GetPrimaryData() { if (IsManyData) + { return ManyData; + } + return SingleData; } @@ -81,16 +89,24 @@ protected void SetPrimaryData(object value) { IsPopulated = true; if (value is JObject jObject) + { SingleData = jObject.ToObject(); + } else if (value is TResource ro) + { SingleData = ro; + } else if (value != null) { IsManyData = true; if (value is JArray jArray) + { ManyData = jArray.ToObject>(); + } else + { ManyData = (List)value; + } } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs index b216011bf7..672255d96e 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs @@ -43,7 +43,7 @@ protected static void WriteMember(StringBuilder builder, string memberName, stri builder.Append(memberName); builder.Append("=\""); builder.Append(memberValue); - builder.Append("\""); + builder.Append('"'); } } } diff --git a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs index e942081d2c..746e12654b 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net.Http; using Humanizer; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; @@ -16,6 +17,7 @@ namespace JsonApiDotNetCore.Serialization /// /// Server deserializer implementation of the . /// + [PublicAPI] public class RequestDeserializer : BaseDeserializer, IJsonApiDeserializer { private readonly ITargetedFields _targetedFields; @@ -32,16 +34,21 @@ public RequestDeserializer( IJsonApiOptions options) : base(resourceContextProvider, resourceFactory) { - _targetedFields = targetedFields ?? throw new ArgumentNullException(nameof(targetedFields)); - _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); - _request = request ?? throw new ArgumentNullException(nameof(request)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); + ArgumentGuard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(options, nameof(options)); + + _targetedFields = targetedFields; + _httpContextAccessor = httpContextAccessor; + _request = request; + _options = options; } /// public object Deserialize(string body) { - if (body == null) throw new ArgumentNullException(nameof(body)); + ArgumentGuard.NotNull(body, nameof(body)); if (_request.Kind == EndpointKind.Relationship) { @@ -117,6 +124,7 @@ private OperationContainer DeserializeOperation(AtomicOperationObject operation) return ParseForRelationshipOperation(operation, kind, requireToManyRelationship); } + [AssertionMethod] private void AssertHasNoHref(AtomicOperationObject operation) { if (operation.Href != null) @@ -238,6 +246,7 @@ private ResourceObject GetRequiredSingleDataForResourceOperation(AtomicOperation return operation.SingleData; } + [AssertionMethod] private void AssertElementHasType(ResourceIdentifierObject resourceIdentifierObject, string elementPath) { if (resourceIdentifierObject.Type == null) diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs index cef0f45efe..70f5fca56b 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; @@ -22,15 +22,16 @@ namespace JsonApiDotNetCore.Serialization /// /// Type of the resource associated with the scope of the request /// for which this serializer is used. + [PublicAPI] public class ResponseSerializer : BaseSerializer, IJsonApiSerializer where TResource : class, IIdentifiable { - private readonly IFieldsToSerialize _fieldsToSerialize; - private readonly IJsonApiOptions _options; private readonly IMetaBuilder _metaBuilder; - private readonly Type _primaryResourceType; private readonly ILinkBuilder _linkBuilder; private readonly IIncludedResourceObjectBuilder _includedBuilder; + private readonly IFieldsToSerialize _fieldsToSerialize; + private readonly IJsonApiOptions _options; + private readonly Type _primaryResourceType; /// public string ContentType { get; } = HeaderConstants.MediaType; @@ -43,28 +44,34 @@ public ResponseSerializer(IMetaBuilder metaBuilder, IJsonApiOptions options) : base(resourceObjectBuilder) { - _fieldsToSerialize = fieldsToSerialize ?? throw new ArgumentNullException(nameof(fieldsToSerialize)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - _linkBuilder = linkBuilder ?? throw new ArgumentNullException(nameof(linkBuilder)); - _metaBuilder = metaBuilder ?? throw new ArgumentNullException(nameof(metaBuilder)); - _includedBuilder = includedBuilder ?? throw new ArgumentNullException(nameof(includedBuilder)); + ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder)); + ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); + ArgumentGuard.NotNull(includedBuilder, nameof(includedBuilder)); + ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize)); + ArgumentGuard.NotNull(options, nameof(options)); + + _metaBuilder = metaBuilder; + _linkBuilder = linkBuilder; + _includedBuilder = includedBuilder; + _fieldsToSerialize = fieldsToSerialize; + _options = options; _primaryResourceType = typeof(TResource); } /// - public string Serialize(object data) + public string Serialize(object content) { - if (data == null || data is IIdentifiable) + if (content == null || content is IIdentifiable) { - return SerializeSingle((IIdentifiable)data); + return SerializeSingle((IIdentifiable)content); } - if (data is IEnumerable collectionOfIdentifiable) + if (content is IEnumerable collectionOfIdentifiable) { return SerializeMany(collectionOfIdentifiable.ToArray()); } - if (data is ErrorDocument errorDocument) + if (content is ErrorDocument errorDocument) { return SerializeErrorDocument(errorDocument); } @@ -85,7 +92,9 @@ private string SerializeErrorDocument(ErrorDocument errorDocument) /// internal string SerializeSingle(IIdentifiable resource) { - var (attributes, relationships) = GetFieldsToSerialize(); + var attributes = _fieldsToSerialize.GetAttributes(_primaryResourceType); + var relationships = _fieldsToSerialize.GetRelationships(_primaryResourceType); + var document = Build(resource, attributes, relationships); var resourceObject = document.SingleData; if (resourceObject != null) @@ -98,11 +107,6 @@ internal string SerializeSingle(IIdentifiable resource) return SerializeObject(document, _options.SerializerSettings, serializer => { serializer.NullValueHandling = NullValueHandling.Include; }); } - private (IReadOnlyCollection, IReadOnlyCollection) GetFieldsToSerialize() - { - return (_fieldsToSerialize.GetAttributes(_primaryResourceType), _fieldsToSerialize.GetRelationships(_primaryResourceType)); - } - /// /// Converts a collection of resources into a serialized . /// @@ -111,7 +115,9 @@ internal string SerializeSingle(IIdentifiable resource) /// internal string SerializeMany(IReadOnlyCollection resources) { - var (attributes, relationships) = GetFieldsToSerialize(); + var attributes = _fieldsToSerialize.GetAttributes(_primaryResourceType); + var relationships = _fieldsToSerialize.GetRelationships(_primaryResourceType); + var document = Build(resources, attributes, relationships); foreach (ResourceObject resourceObject in document.ManyData) { diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs index 386fd2bf8b..4b6b1c12b5 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using Microsoft.Extensions.DependencyInjection; @@ -9,6 +10,7 @@ namespace JsonApiDotNetCore.Serialization /// A factory class to abstract away the initialization of the serializer from the /// ASP.NET Core formatter pipeline. /// + [PublicAPI] public class ResponseSerializerFactory : IJsonApiSerializerFactory { private readonly IServiceProvider _provider; @@ -16,8 +18,11 @@ public class ResponseSerializerFactory : IJsonApiSerializerFactory public ResponseSerializerFactory(IJsonApiRequest request, IRequestScopedServiceProvider provider) { - _request = request ?? throw new ArgumentNullException(nameof(request)); - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(provider, nameof(provider)); + + _request = request; + _provider = provider; } /// diff --git a/src/JsonApiDotNetCore/Services/AsyncCollectionExtensions.cs b/src/JsonApiDotNetCore/Services/AsyncCollectionExtensions.cs index 70268bb416..be11ea908e 100644 --- a/src/JsonApiDotNetCore/Services/AsyncCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Services/AsyncCollectionExtensions.cs @@ -1,13 +1,18 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Services { + [PublicAPI] public static class AsyncCollectionExtensions { public static async Task AddRangeAsync(this ICollection source, IAsyncEnumerable elementsToAdd, CancellationToken cancellationToken = default) { + ArgumentGuard.NotNull(source, nameof(source)); + ArgumentGuard.NotNull(elementsToAdd, nameof(elementsToAdd)); + await foreach (var missingResource in elementsToAdd.WithCancellation(cancellationToken)) { source.Add(missingResource); @@ -16,6 +21,8 @@ public static async Task AddRangeAsync(this ICollection source, IAsyncEnum public static async Task> ToListAsync(this IAsyncEnumerable source, CancellationToken cancellationToken = default) { + ArgumentGuard.NotNull(source, nameof(source)); + var list = new List(); await foreach (var element in source.WithCancellation(cancellationToken)) diff --git a/src/JsonApiDotNetCore/Services/IAddToRelationshipService.cs b/src/JsonApiDotNetCore/Services/IAddToRelationshipService.cs index 49baddbcf3..ca1e968767 100644 --- a/src/JsonApiDotNetCore/Services/IAddToRelationshipService.cs +++ b/src/JsonApiDotNetCore/Services/IAddToRelationshipService.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; +// ReSharper disable UnusedTypeParameter + namespace JsonApiDotNetCore.Services { /// @@ -11,6 +14,7 @@ public interface IAddToRelationshipService : IAddToRelationshipServic { } /// + [PublicAPI] public interface IAddToRelationshipService where TResource : class, IIdentifiable { diff --git a/src/JsonApiDotNetCore/Services/IDeleteService.cs b/src/JsonApiDotNetCore/Services/IDeleteService.cs index 548ddad6f3..5dcaebdf19 100644 --- a/src/JsonApiDotNetCore/Services/IDeleteService.cs +++ b/src/JsonApiDotNetCore/Services/IDeleteService.cs @@ -2,6 +2,8 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Resources; +// ReSharper disable UnusedTypeParameter + namespace JsonApiDotNetCore.Services { /// diff --git a/src/JsonApiDotNetCore/Services/IGetRelationshipService.cs b/src/JsonApiDotNetCore/Services/IGetRelationshipService.cs index d986728aea..575f8ca6bf 100644 --- a/src/JsonApiDotNetCore/Services/IGetRelationshipService.cs +++ b/src/JsonApiDotNetCore/Services/IGetRelationshipService.cs @@ -2,6 +2,8 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Resources; +// ReSharper disable UnusedTypeParameter + namespace JsonApiDotNetCore.Services { /// diff --git a/src/JsonApiDotNetCore/Services/IGetSecondaryService.cs b/src/JsonApiDotNetCore/Services/IGetSecondaryService.cs index bb376c7307..b035aa6327 100644 --- a/src/JsonApiDotNetCore/Services/IGetSecondaryService.cs +++ b/src/JsonApiDotNetCore/Services/IGetSecondaryService.cs @@ -2,6 +2,8 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Resources; +// ReSharper disable UnusedTypeParameter + namespace JsonApiDotNetCore.Services { /// diff --git a/src/JsonApiDotNetCore/Services/IRemoveFromRelationshipService.cs b/src/JsonApiDotNetCore/Services/IRemoveFromRelationshipService.cs index baf4bf2313..56b3d5137b 100644 --- a/src/JsonApiDotNetCore/Services/IRemoveFromRelationshipService.cs +++ b/src/JsonApiDotNetCore/Services/IRemoveFromRelationshipService.cs @@ -3,6 +3,8 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Resources; +// ReSharper disable UnusedTypeParameter + namespace JsonApiDotNetCore.Services { /// diff --git a/src/JsonApiDotNetCore/Services/ISetRelationshipService.cs b/src/JsonApiDotNetCore/Services/ISetRelationshipService.cs index aa2075a2f1..46ec2153f1 100644 --- a/src/JsonApiDotNetCore/Services/ISetRelationshipService.cs +++ b/src/JsonApiDotNetCore/Services/ISetRelationshipService.cs @@ -2,6 +2,8 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Resources; +// ReSharper disable UnusedTypeParameter + namespace JsonApiDotNetCore.Services { /// diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs index 0eb6ae09a8..6b08585445 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Hooks; @@ -19,6 +20,7 @@ namespace JsonApiDotNetCore.Services { /// + [PublicAPI] public class JsonApiResourceService : IResourceService where TResource : class, IIdentifiable @@ -42,16 +44,23 @@ public JsonApiResourceService( IResourceChangeTracker resourceChangeTracker, IResourceHookExecutorFacade hookExecutor) { - if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); - - _repositoryAccessor = repositoryAccessor ?? throw new ArgumentNullException(nameof(repositoryAccessor)); - _queryLayerComposer = queryLayerComposer ?? throw new ArgumentNullException(nameof(queryLayerComposer)); - _paginationContext = paginationContext ?? throw new ArgumentNullException(nameof(paginationContext)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + ArgumentGuard.NotNull(repositoryAccessor, nameof(repositoryAccessor)); + ArgumentGuard.NotNull(queryLayerComposer, nameof(queryLayerComposer)); + ArgumentGuard.NotNull(paginationContext, nameof(paginationContext)); + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(resourceChangeTracker, nameof(resourceChangeTracker)); + ArgumentGuard.NotNull(hookExecutor, nameof(hookExecutor)); + + _repositoryAccessor = repositoryAccessor; + _queryLayerComposer = queryLayerComposer; + _paginationContext = paginationContext; + _options = options; + _request = request; + _resourceChangeTracker = resourceChangeTracker; + _hookExecutor = hookExecutor; _traceWriter = new TraceLogWriter>(loggerFactory); - _request = request ?? throw new ArgumentNullException(nameof(request)); - _resourceChangeTracker = resourceChangeTracker ?? throw new ArgumentNullException(nameof(resourceChangeTracker)); - _hookExecutor = hookExecutor ?? throw new ArgumentNullException(nameof(hookExecutor)); } /// @@ -141,7 +150,8 @@ public virtual async Task GetSecondaryAsync(TId id, string relationshipN public virtual async Task GetRelationshipAsync(TId id, string relationshipName, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {id, relationshipName}); - if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); + + ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); AssertHasRelationship(_request.Relationship, relationshipName); @@ -166,7 +176,8 @@ public virtual async Task GetRelationshipAsync(TId id, string relationsh public virtual async Task CreateAsync(TResource resource, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {resource}); - if (resource == null) throw new ArgumentNullException(nameof(resource)); + + ArgumentGuard.NotNull(resource, nameof(resource)); var resourceFromRequest = resource; _resourceChangeTracker.SetRequestedAttributeValues(resourceFromRequest); @@ -255,8 +266,9 @@ private async IAsyncEnumerable GetMissingRightRes public async Task AddToToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {primaryId, secondaryResourceIds}); - if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); - if (secondaryResourceIds == null) throw new ArgumentNullException(nameof(secondaryResourceIds)); + + ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); + ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); AssertHasRelationship(_request.Relationship, relationshipName); @@ -266,7 +278,7 @@ public async Task AddToToManyRelationshipAsync(TId primaryId, string relationshi { // In the case of a many-to-many relationship, creating a duplicate entry in the join table results in a // unique constraint violation. We avoid that by excluding already-existing entries from the set in advance. - await RemoveExistingIdsFromSecondarySet(primaryId, secondaryResourceIds, hasManyThrough, cancellationToken); + await RemoveExistingIdsFromSecondarySetAsync(primaryId, secondaryResourceIds, hasManyThrough, cancellationToken); } try @@ -284,7 +296,7 @@ public async Task AddToToManyRelationshipAsync(TId primaryId, string relationshi } } - private async Task RemoveExistingIdsFromSecondarySet(TId primaryId, ISet secondaryResourceIds, + private async Task RemoveExistingIdsFromSecondarySetAsync(TId primaryId, ISet secondaryResourceIds, HasManyThroughAttribute hasManyThrough, CancellationToken cancellationToken) { var queryLayer = _queryLayerComposer.ComposeForHasMany(hasManyThrough, primaryId, secondaryResourceIds); @@ -314,7 +326,8 @@ private async Task AssertResourcesExistAsync(ICollection secondar public virtual async Task UpdateAsync(TId id, TResource resource, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {id, resource}); - if (resource == null) throw new ArgumentNullException(nameof(resource)); + + ArgumentGuard.NotNull(resource, nameof(resource)); var resourceFromRequest = resource; _resourceChangeTracker.SetRequestedAttributeValues(resourceFromRequest); @@ -356,7 +369,8 @@ public virtual async Task UpdateAsync(TId id, TResource resource, Can public virtual async Task SetRelationshipAsync(TId primaryId, string relationshipName, object secondaryResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {primaryId, relationshipName, secondaryResourceIds}); - if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); + + ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); AssertHasRelationship(_request.Relationship, relationshipName); @@ -402,8 +416,9 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke public async Task RemoveFromToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new {primaryId, relationshipName, secondaryResourceIds}); - if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); - if (secondaryResourceIds == null) throw new ArgumentNullException(nameof(secondaryResourceIds)); + + ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); + ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); AssertHasRelationship(_request.Relationship, relationshipName); @@ -433,6 +448,7 @@ private async Task GetPrimaryResourceForUpdateAsync(TId id, Cancellat return resource; } + [AssertionMethod] private void AssertPrimaryResourceExists(TResource resource) { if (resource == null) @@ -441,6 +457,7 @@ private void AssertPrimaryResourceExists(TResource resource) } } + [AssertionMethod] private void AssertHasRelationship(RelationshipAttribute relationship, string name) { if (relationship == null) @@ -454,6 +471,7 @@ private void AssertHasRelationship(RelationshipAttribute relationship, string na /// Represents the foundational Resource Service layer in the JsonApiDotNetCore architecture that uses a Resource Repository for data access. /// /// The resource type. + [PublicAPI] public class JsonApiResourceService : JsonApiResourceService, IResourceService where TResource : class, IIdentifiable diff --git a/src/JsonApiDotNetCore/TypeHelper.cs b/src/JsonApiDotNetCore/TypeHelper.cs index 50a0387b47..c3b004979a 100644 --- a/src/JsonApiDotNetCore/TypeHelper.cs +++ b/src/JsonApiDotNetCore/TypeHelper.cs @@ -11,17 +11,14 @@ namespace JsonApiDotNetCore { internal static class TypeHelper { - private static readonly Type[] _hashSetCompatibleCollectionTypes = + private static readonly Type[] HashSetCompatibleCollectionTypes = { typeof(HashSet<>), typeof(ICollection<>), typeof(ISet<>), typeof(IEnumerable<>), typeof(IReadOnlyCollection<>) }; public static object ConvertType(object value, Type type) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } + ArgumentGuard.NotNull(type, nameof(type)); if (value == null) { @@ -119,6 +116,8 @@ public static Type TryGetCollectionElementType(Type type) /// public static PropertyInfo ParseNavigationExpression(Expression> navigationExpression) { + ArgumentGuard.NotNull(navigationExpression, nameof(navigationExpression)); + MemberExpression exp; //this line is necessary, because sometimes the expression comes in as Convert(originalExpression) @@ -130,7 +129,7 @@ public static PropertyInfo ParseNavigationExpression(Expression(Expression> ConvertRelat /// public static Dictionary> ConvertAttributeDictionary(IEnumerable attributes, HashSet resources) { - return attributes?.ToDictionary(attr => attr.Property, attr => resources); + return attributes.ToDictionary(attr => attr.Property, _ => resources); } /// @@ -183,19 +182,23 @@ public static Dictionary> ConvertAttributeDicti /// Open generic type public static object CreateInstanceOfOpenType(Type openType, Type parameter, params object[] constructorArguments) { - return CreateInstanceOfOpenType(openType, new[] {parameter}, constructorArguments); + return CreateInstanceOfOpenType(openType, parameter.AsArray(), constructorArguments); } /// - /// Use this overload if you need to instantiate a type that has a internal constructor + /// Use this overload if you need to instantiate a type that has an internal constructor /// public static object CreateInstanceOfOpenType(Type openType, Type parameter, bool hasInternalConstructor, params object[] constructorArguments) { Type[] parameters = {parameter}; - if (!hasInternalConstructor) return CreateInstanceOfOpenType(openType, parameters, constructorArguments); + if (!hasInternalConstructor) + { + return CreateInstanceOfOpenType(openType, parameters, constructorArguments); + } + var parameterizedType = openType.MakeGenericType(parameters); // note that if for whatever reason the constructor of AffectedResource is set from - // internal to public, this will throw an error, as it is looking for a no + // internal to public, this will throw an error, as it is looking for a non-public one. return Activator.CreateInstance(parameterizedType, BindingFlags.NonPublic | BindingFlags.Instance, null, constructorArguments, null); } @@ -203,18 +206,18 @@ public static object CreateInstanceOfOpenType(Type openType, Type parameter, boo /// Reflectively instantiates a list of a certain type. /// /// The list of the target type - /// The target type - public static IList CreateListFor(Type type) + /// The target type + public static IList CreateListFor(Type elementType) { - return (IList)CreateInstanceOfOpenType(typeof(List<>), type); + return (IList)CreateInstanceOfOpenType(typeof(List<>), elementType); } /// /// Reflectively instantiates a hashset of a certain type. /// - public static IEnumerable CreateHashSetFor(Type type, object elements = null) + public static IEnumerable CreateHashSetFor(Type type, object elements) { - return (IEnumerable)CreateInstanceOfOpenType(typeof(HashSet<>), type, elements ?? new object()); + return (IEnumerable)CreateInstanceOfOpenType(typeof(HashSet<>), type, elements); } /// @@ -250,19 +253,23 @@ public static bool TypeCanContainHashSet(Type collectionType) if (collectionType.IsGenericType) { var openCollectionType = collectionType.GetGenericTypeDefinition(); - return _hashSetCompatibleCollectionTypes.Contains(openCollectionType); + return HashSetCompatibleCollectionTypes.Contains(openCollectionType); } return false; } /// - /// Gets the type (Guid or int) of the Id of a type that implements IIdentifiable + /// Gets the type (such as Guid or int) of the Id property on a type that implements . /// public static Type GetIdType(Type resourceType) { var property = resourceType.GetProperty(nameof(Identifiable.Id)); - if (property == null) throw new ArgumentException("Type does not have 'Id' property."); + if (property == null) + { + throw new ArgumentException($"Type '{resourceType.Name}' does not have 'Id' property."); + } + return property.PropertyType; } @@ -280,7 +287,7 @@ public static ICollection ExtractResources(object value) if (value is IIdentifiable resource) { - return new[] {resource}; + return resource.AsArray(); } return Array.Empty(); @@ -288,10 +295,7 @@ public static ICollection ExtractResources(object value) public static object CreateInstance(Type type) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } + ArgumentGuard.NotNull(type, nameof(type)); try { @@ -331,8 +335,8 @@ public static IList CopyToList(IEnumerable copyFrom, Type elementType, Converter /// Target collection type, for example: typeof(List{Article}) or typeof(ISet{Person}). public static IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType) { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (collectionType == null) throw new ArgumentNullException(nameof(collectionType)); + ArgumentGuard.NotNull(source, nameof(source)); + ArgumentGuard.NotNull(collectionType, nameof(collectionType)); var concreteCollectionType = ToConcreteCollectionType(collectionType); dynamic concreteCollectionInstance = CreateInstance(concreteCollectionType); @@ -350,10 +354,7 @@ public static IEnumerable CopyToTypedCollection(IEnumerable source, Type collect /// public static bool IsOrImplementsInterface(Type source, Type interfaceType) { - if (interfaceType == null) - { - throw new ArgumentNullException(nameof(interfaceType)); - } + ArgumentGuard.NotNull(interfaceType, nameof(interfaceType)); if (source == null) { diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 6845430880..4f568d9555 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using FluentAssertions; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Hooks; using JsonApiDotNetCore.Middleware; @@ -19,7 +20,7 @@ namespace DiscoveryTests { public sealed class ServiceDiscoveryFacadeTests { - private static readonly NullLoggerFactory _loggerFactory = NullLoggerFactory.Instance; + private static readonly NullLoggerFactory LoggerFactory = NullLoggerFactory.Instance; private readonly IServiceCollection _services = new ServiceCollection(); private readonly JsonApiOptions _options = new JsonApiOptions(); private readonly ResourceGraphBuilder _resourceGraphBuilder; @@ -31,7 +32,7 @@ public ServiceDiscoveryFacadeTests() _services.AddScoped(_ => dbResolverMock.Object); _services.AddSingleton(_options); - _services.AddSingleton(_loggerFactory); + _services.AddSingleton(LoggerFactory); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); @@ -44,14 +45,14 @@ public ServiceDiscoveryFacadeTests() _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); - _resourceGraphBuilder = new ResourceGraphBuilder(_options, _loggerFactory); + _resourceGraphBuilder = new ResourceGraphBuilder(_options, LoggerFactory); } [Fact] public void Can_add_resources_from_assembly_to_graph() { // Arrange - var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, _loggerFactory); + var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, LoggerFactory); facade.AddAssembly(typeof(Person).Assembly); // Act @@ -71,7 +72,7 @@ public void Can_add_resources_from_assembly_to_graph() public void Can_add_resource_from_current_assembly_to_graph() { // Arrange - var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, _loggerFactory); + var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, LoggerFactory); facade.AddCurrentAssembly(); // Act @@ -88,7 +89,7 @@ public void Can_add_resource_from_current_assembly_to_graph() public void Can_add_resource_service_from_current_assembly_to_container() { // Arrange - var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, _loggerFactory); + var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, LoggerFactory); facade.AddCurrentAssembly(); // Act @@ -105,7 +106,7 @@ public void Can_add_resource_service_from_current_assembly_to_container() public void Can_add_resource_repository_from_current_assembly_to_container() { // Arrange - var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, _loggerFactory); + var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, LoggerFactory); facade.AddCurrentAssembly(); // Act @@ -122,7 +123,7 @@ public void Can_add_resource_repository_from_current_assembly_to_container() public void Can_add_resource_definition_from_current_assembly_to_container() { // Arrange - var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, _loggerFactory); + var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, LoggerFactory); facade.AddCurrentAssembly(); // Act @@ -139,7 +140,7 @@ public void Can_add_resource_definition_from_current_assembly_to_container() public void Can_add_resource_hooks_definition_from_current_assembly_to_container() { // Arrange - var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, _loggerFactory); + var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, LoggerFactory); facade.AddCurrentAssembly(); _options.EnableResourceHooks = true; @@ -154,9 +155,11 @@ public void Can_add_resource_hooks_definition_from_current_assembly_to_container resourceHooksDefinition.Should().BeOfType(); } + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class TestResource : Identifiable { } - public class TestResourceService : JsonApiResourceService + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class TestResourceService : JsonApiResourceService { public TestResourceService( IResourceRepositoryAccessor repositoryAccessor, @@ -173,7 +176,8 @@ public TestResourceService( } } - public class TestResourceRepository : EntityFrameworkCoreRepository + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class TestResourceRepository : EntityFrameworkCoreRepository { public TestResourceRepository( ITargetedFields targetedFields, @@ -185,13 +189,15 @@ public TestResourceRepository( : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) { } } - - public class TestResourceHooksDefinition : ResourceHooksDefinition + + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class TestResourceHooksDefinition : ResourceHooksDefinition { public TestResourceHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } } - public class TestResourceDefinition : JsonApiResourceDefinition + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class TestResourceDefinition : JsonApiResourceDefinition { public TestResourceDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } } diff --git a/test/JsonApiDotNetCoreExampleTests/ExampleFakers.cs b/test/JsonApiDotNetCoreExampleTests/ExampleFakers.cs index 1b9718ca7e..a67c525537 100644 --- a/test/JsonApiDotNetCoreExampleTests/ExampleFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/ExampleFakers.cs @@ -4,6 +4,9 @@ using TestBuildingBlocks; using Person = JsonApiDotNetCoreExample.Models.Person; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests { internal sealed class ExampleFakers : FakerContainer diff --git a/test/JsonApiDotNetCoreExampleTests/ExampleIntegrationTestContext.cs b/test/JsonApiDotNetCoreExampleTests/ExampleIntegrationTestContext.cs index 249b61cad4..311d18b4d1 100644 --- a/test/JsonApiDotNetCoreExampleTests/ExampleIntegrationTestContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/ExampleIntegrationTestContext.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Startups; using Microsoft.EntityFrameworkCore; using TestBuildingBlocks; @@ -9,7 +9,7 @@ namespace JsonApiDotNetCoreExampleTests /// /// The server Startup class, which can be defined in the test project. /// The EF Core database context, which can be defined in the test project. - public class ExampleIntegrationTestContext : BaseIntegrationTestContext + public sealed class ExampleIntegrationTestContext : BaseIntegrationTestContext where TStartup : class where TDbContext : DbContext { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs index 90884f55a3..bcd75da764 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs @@ -59,7 +59,7 @@ public async Task Can_create_resources_for_matching_resource_type() } }; - var route = "/operations/musicTracks/create"; + const string route = "/operations/musicTracks/create"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -92,7 +92,7 @@ public async Task Cannot_create_resource_for_mismatching_resource_type() } }; - var route = "/operations/musicTracks/create"; + const string route = "/operations/musicTracks/create"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -101,10 +101,12 @@ public async Task Cannot_create_resource_for_mismatching_resource_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); - responseDocument.Errors[0].Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); + error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -138,7 +140,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations/musicTracks/create"; + const string route = "/operations/musicTracks/create"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -147,10 +149,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); - responseDocument.Errors[0].Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); + error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -191,7 +195,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations/musicTracks/create"; + const string route = "/operations/musicTracks/create"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -200,10 +204,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); - responseDocument.Errors[0].Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); + error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs index 1a3db3a952..d4a69eb31a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs @@ -15,7 +15,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Controllers { - [DisableRoutingConvention, Route("/operations/musicTracks/create")] + [DisableRoutingConvention] + [Route("/operations/musicTracks/create")] public sealed class CreateMusicTrackOperationsController : JsonApiOperationsController { public CreateMusicTrackOperationsController(IJsonApiOptions options, ILoggerFactory loggerFactory, diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index c073964d81..7c008892e3 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -53,7 +53,7 @@ public async Task Can_create_resource() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -72,8 +72,7 @@ public async Task Can_create_resource() await _testContext.RunOnDatabaseAsync(async dbContext => { - var performerInDatabase = await dbContext.Performers - .FirstAsync(performer => performer.Id == newPerformerId); + var performerInDatabase = await dbContext.Performers.FirstWithIdAsync(newPerformerId); performerInDatabase.ArtistName.Should().Be(newArtistName); performerInDatabase.BornAt.Should().BeCloseTo(newBornAt); @@ -113,7 +112,7 @@ public async Task Can_create_resources() atomic__operations = operationElements }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -125,13 +124,15 @@ public async Task Can_create_resources() for (int index = 0; index < elementCount; index++) { - responseDocument.Results[index].SingleData.Should().NotBeNull(); - responseDocument.Results[index].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[index].SingleData.Attributes["title"].Should().Be(newTracks[index].Title); - responseDocument.Results[index].SingleData.Attributes["lengthInSeconds"].As().Should().BeApproximately(newTracks[index].LengthInSeconds, 0.00000000001M); - responseDocument.Results[index].SingleData.Attributes["genre"].Should().Be(newTracks[index].Genre); - responseDocument.Results[index].SingleData.Attributes["releasedAt"].Should().BeCloseTo(newTracks[index].ReleasedAt); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + var singleData = responseDocument.Results[index].SingleData; + + singleData.Should().NotBeNull(); + singleData.Type.Should().Be("musicTracks"); + singleData.Attributes["title"].Should().Be(newTracks[index].Title); + singleData.Attributes["lengthInSeconds"].As().Should().BeApproximately(newTracks[index].LengthInSeconds); + singleData.Attributes["genre"].Should().Be(newTracks[index].Genre); + singleData.Attributes["releasedAt"].Should().BeCloseTo(newTracks[index].ReleasedAt); + singleData.Relationships.Should().NotBeEmpty(); } var newTrackIds = responseDocument.Results.Select(result => Guid.Parse(result.SingleData.Id)); @@ -149,7 +150,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var trackInDatabase = tracksInDatabase.Single(musicTrack => musicTrack.Id == Guid.Parse(responseDocument.Results[index].SingleData.Id)); trackInDatabase.Title.Should().Be(newTracks[index].Title); - trackInDatabase.LengthInSeconds.Should().BeApproximately(newTracks[index].LengthInSeconds, 0.00000000001M); + trackInDatabase.LengthInSeconds.Should().BeApproximately(newTracks[index].LengthInSeconds); trackInDatabase.Genre.Should().Be(newTracks[index].Genre); trackInDatabase.ReleasedAt.Should().BeCloseTo(newTracks[index].ReleasedAt); } @@ -181,7 +182,7 @@ public async Task Can_create_resource_without_attributes_or_relationships() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -200,8 +201,7 @@ public async Task Can_create_resource_without_attributes_or_relationships() await _testContext.RunOnDatabaseAsync(async dbContext => { - var performerInDatabase = await dbContext.Performers - .FirstAsync(performer => performer.Id == newPerformerId); + var performerInDatabase = await dbContext.Performers.FirstWithIdAsync(newPerformerId); performerInDatabase.ArtistName.Should().BeNull(); performerInDatabase.BornAt.Should().Be(default); @@ -234,7 +234,7 @@ public async Task Can_create_resource_with_unknown_attribute() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -252,8 +252,7 @@ public async Task Can_create_resource_with_unknown_attribute() await _testContext.RunOnDatabaseAsync(async dbContext => { - var performerInDatabase = await dbContext.Playlists - .FirstAsync(playlist => playlist.Id == newPlaylistId); + var performerInDatabase = await dbContext.Playlists.FirstWithIdAsync(newPlaylistId); performerInDatabase.Name.Should().Be(newName); }); @@ -289,7 +288,7 @@ public async Task Can_create_resource_with_unknown_relationship() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -307,8 +306,7 @@ public async Task Can_create_resource_with_unknown_relationship() await _testContext.RunOnDatabaseAsync(async dbContext => { - var lyricInDatabase = await dbContext.Lyrics - .FirstAsync(lyric => lyric.Id == newLyricId); + var lyricInDatabase = await dbContext.Lyrics.FirstWithIdAsync(newLyricId); lyricInDatabase.Should().NotBeNull(); }); @@ -340,7 +338,7 @@ public async Task Cannot_create_resource_with_client_generated_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -349,10 +347,12 @@ public async Task Cannot_create_resource_with_client_generated_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("Specifying the resource ID in operations that create a resource is not allowed."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("Specifying the resource ID in operations that create a resource is not allowed."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); } [Fact] @@ -371,7 +371,7 @@ public async Task Cannot_create_resource_for_href_element() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -380,10 +380,12 @@ public async Task Cannot_create_resource_for_href_element() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -405,7 +407,7 @@ public async Task Cannot_create_resource_for_ref_element() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -414,10 +416,12 @@ public async Task Cannot_create_resource_for_ref_element() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.relationship' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.relationship' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -435,7 +439,7 @@ public async Task Cannot_create_resource_for_missing_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -444,10 +448,12 @@ public async Task Cannot_create_resource_for_missing_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -471,7 +477,7 @@ public async Task Cannot_create_resource_for_missing_type() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -480,10 +486,12 @@ public async Task Cannot_create_resource_for_missing_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -505,7 +513,7 @@ public async Task Cannot_create_resource_for_unknown_type() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -514,10 +522,12 @@ public async Task Cannot_create_resource_for_unknown_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -548,7 +558,7 @@ public async Task Cannot_create_resource_for_array() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -557,10 +567,12 @@ public async Task Cannot_create_resource_for_array() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected single data element for create/update resource operation."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected single data element for create/update resource operation."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -586,7 +598,7 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -595,10 +607,12 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Setting the initial value of the requested attribute is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Setting the initial value of 'createdAt' is not allowed."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Setting the initial value of the requested attribute is not allowed."); + error.Detail.Should().Be("Setting the initial value of 'createdAt' is not allowed."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -627,7 +641,7 @@ public async Task Cannot_create_resource_with_readonly_attribute() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -636,10 +650,12 @@ public async Task Cannot_create_resource_with_readonly_attribute() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); - responseDocument.Errors[0].Detail.Should().Be("Attribute 'isArchived' is read-only."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); + error.Detail.Should().Be("Attribute 'isArchived' is read-only."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -665,7 +681,7 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -674,10 +690,12 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'DateTimeOffset'. - Request body:"); - responseDocument.Errors[0].Source.Pointer.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'DateTimeOffset'. - Request body:"); + error.Source.Pointer.Should().BeNull(); } [Fact] @@ -745,7 +763,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -763,11 +781,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Lyric) .Include(musicTrack => musicTrack.OwnedBy) .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + .FirstWithIdAsync(newTrackId); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore trackInDatabase.Title.Should().Be(newTitle); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs index 4befbd34db..8e11640cfe 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs @@ -5,7 +5,6 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExampleTests.Startups; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -55,7 +54,7 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -72,8 +71,7 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ await _testContext.RunOnDatabaseAsync(async dbContext => { - var languageInDatabase = await dbContext.TextLanguages - .FirstAsync(language => language.Id == newLanguage.Id); + var languageInDatabase = await dbContext.TextLanguages.FirstWithIdAsync(newLanguage.Id); languageInDatabase.IsoCode.Should().Be(newLanguage.IsoCode); }); @@ -107,7 +105,7 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_no_ } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -119,11 +117,10 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_no_ await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .FirstAsync(musicTrack => musicTrack.Id == newTrack.Id); + var trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(newTrack.Id); trackInDatabase.Title.Should().Be(newTrack.Title); - trackInDatabase.LengthInSeconds.Should().BeApproximately(newTrack.LengthInSeconds, 0.00000000001M); + trackInDatabase.LengthInSeconds.Should().BeApproximately(newTrack.LengthInSeconds); }); } @@ -140,7 +137,6 @@ public async Task Cannot_create_resource_for_existing_client_generated_ID() await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.TextLanguages.Add(languageToCreate); - await dbContext.SaveChangesAsync(); }); @@ -164,7 +160,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -173,10 +169,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); - responseDocument.Errors[0].Title.Should().Be("Another resource with the specified ID already exists."); - responseDocument.Errors[0].Detail.Should().Be($"Another resource of type 'textLanguages' with ID '{languageToCreate.StringId}' already exists."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Another resource with the specified ID already exists."); + error.Detail.Should().Be($"Another resource of type 'textLanguages' with ID '{languageToCreate.StringId}' already exists."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -204,7 +202,7 @@ public async Task Cannot_create_resource_for_incompatible_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -213,10 +211,12 @@ public async Task Cannot_create_resource_for_incompatible_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -240,7 +240,7 @@ public async Task Cannot_create_resource_for_ID_and_local_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -249,10 +249,12 @@ public async Task Cannot_create_resource_for_ID_and_local_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index 4c87dfa89f..bbaedf4faa 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -74,7 +74,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -94,7 +94,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + .FirstWithIdAsync(newTrackId); trackInDatabase.Performers.Should().HaveCount(2); trackInDatabase.Performers.Should().ContainSingle(performer => performer.Id == existingPerformers[0].Id); @@ -158,7 +158,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -176,10 +176,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == newPlaylistId); + .FirstWithIdAsync(newPlaylistId); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.PlaylistMusicTracks.Should().HaveCount(3); playlistInDatabase.PlaylistMusicTracks.Should().ContainSingle(playlistMusicTrack => playlistMusicTrack.MusicTrack.Id == existingTracks[0].Id); @@ -220,7 +226,7 @@ public async Task Cannot_create_for_missing_relationship_type() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -229,10 +235,12 @@ public async Task Cannot_create_for_missing_relationship_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected 'type' element in 'performers' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'performers' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -268,7 +276,7 @@ public async Task Cannot_create_for_unknown_relationship_type() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -277,10 +285,12 @@ public async Task Cannot_create_for_unknown_relationship_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -315,7 +325,7 @@ public async Task Cannot_create_for_missing_relationship_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -324,10 +334,12 @@ public async Task Cannot_create_for_missing_relationship_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); + error.Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -374,7 +386,7 @@ public async Task Cannot_create_for_unknown_relationship_IDs() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -384,15 +396,17 @@ public async Task Cannot_create_for_unknown_relationship_IDs() responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'performers' with ID '12345678' in relationship 'performers' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); - - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[1].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[1].Detail.Should().Be("Related resource of type 'performers' with ID '87654321' in relationship 'performers' does not exist."); - responseDocument.Errors[1].Source.Pointer.Should().Be("/atomic:operations[0]"); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.NotFound); + error1.Title.Should().Be("A related resource does not exist."); + error1.Detail.Should().Be("Related resource of type 'performers' with ID '12345678' in relationship 'performers' does not exist."); + error1.Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.NotFound); + error2.Title.Should().Be("A related resource does not exist."); + error2.Detail.Should().Be("Related resource of type 'performers' with ID '87654321' in relationship 'performers' does not exist."); + error2.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -428,7 +442,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -437,10 +451,12 @@ public async Task Cannot_create_on_relationship_type_mismatch() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); + error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -494,7 +510,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -514,7 +530,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + .FirstWithIdAsync(newTrackId); trackInDatabase.Performers.Should().HaveCount(1); trackInDatabase.Performers[0].Id.Should().Be(existingPerformer.Id); @@ -547,7 +563,7 @@ public async Task Cannot_create_with_null_data_in_HasMany_relationship() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -556,10 +572,12 @@ public async Task Cannot_create_with_null_data_in_HasMany_relationship() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().Be("Expected data[] element for 'performers' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -588,7 +606,7 @@ public async Task Cannot_create_with_null_data_in_HasManyThrough_relationship() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -597,10 +615,12 @@ public async Task Cannot_create_with_null_data_in_HasManyThrough_relationship() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().Be("Expected data[] element for 'tracks' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().Be("Expected data[] element for 'tracks' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index d5603ec35a..999e214e5c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -64,7 +64,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -84,7 +84,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var lyricInDatabase = await dbContext.Lyrics .Include(lyric => lyric.Track) - .FirstAsync(lyric => lyric.Id == newLyricId); + .FirstWithIdAsync(newLyricId); lyricInDatabase.Track.Should().NotBeNull(); lyricInDatabase.Track.Id.Should().Be(existingTrack.Id); @@ -134,7 +134,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -154,7 +154,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Lyric) - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + .FirstWithIdAsync(newTrackId); trackInDatabase.Lyric.Should().NotBeNull(); trackInDatabase.Lyric.Id.Should().Be(existingLyric.Id); @@ -209,7 +209,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => atomic__operations = operationElements }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -230,11 +230,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var tracksInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) .Where(musicTrack => newTrackIds.Contains(musicTrack.Id)) .ToListAsync(); + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + tracksInDatabase.Should().HaveCount(elementCount); for (int index = 0; index < elementCount; index++) @@ -279,7 +285,7 @@ public async Task Cannot_create_for_missing_relationship_type() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -288,10 +294,12 @@ public async Task Cannot_create_for_missing_relationship_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected 'type' element in 'lyric' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'lyric' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -324,7 +332,7 @@ public async Task Cannot_create_for_unknown_relationship_type() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -333,10 +341,12 @@ public async Task Cannot_create_for_unknown_relationship_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -368,7 +378,7 @@ public async Task Cannot_create_for_missing_relationship_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -377,10 +387,12 @@ public async Task Cannot_create_for_missing_relationship_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); + error.Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -413,7 +425,7 @@ public async Task Cannot_create_with_unknown_relationship_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -422,10 +434,12 @@ public async Task Cannot_create_with_unknown_relationship_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'lyrics' with ID '12345678' in relationship 'lyric' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("A related resource does not exist."); + error.Detail.Should().Be("Related resource of type 'lyrics' with ID '12345678' in relationship 'lyric' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -458,7 +472,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -467,10 +481,12 @@ public async Task Cannot_create_on_relationship_type_mismatch() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); + error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -526,7 +542,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBodyText = JsonConvert.SerializeObject(requestBody).Replace("ownedBy_duplicate", "ownedBy"); - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBodyText); @@ -546,7 +562,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + .FirstWithIdAsync(newTrackId); trackInDatabase.OwnedBy.Should().NotBeNull(); trackInDatabase.OwnedBy.Id.Should().Be(existingCompany.Id); @@ -586,7 +602,7 @@ public async Task Cannot_create_with_data_array_in_relationship() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -595,10 +611,12 @@ public async Task Cannot_create_with_data_array_in_relationship() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); - responseDocument.Errors[0].Detail.Should().Be("Expected single data element for 'lyric' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); + error.Detail.Should().Be("Expected single data element for 'lyric' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index 67716414a5..14464a777b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -53,7 +53,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -65,8 +65,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var performerInDatabase = await dbContext.Performers - .FirstOrDefaultAsync(performer => performer.Id == existingPerformer.Id); + var performerInDatabase = await dbContext.Performers.FirstWithIdOrDefaultAsync(existingPerformer.Id); performerInDatabase.Should().BeNull(); }); @@ -106,7 +105,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => atomic__operations = operationElements }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -154,7 +153,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -166,13 +165,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var lyricsInDatabase = await dbContext.Lyrics - .FirstOrDefaultAsync(lyric => lyric.Id == existingLyric.Id); + var lyricsInDatabase = await dbContext.Lyrics.FirstWithIdOrDefaultAsync(existingLyric.Id); lyricsInDatabase.Should().BeNull(); - var trackInDatabase = await dbContext.MusicTracks - .FirstAsync(musicTrack => musicTrack.Id == existingLyric.Track.Id); + var trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(existingLyric.Track.Id); trackInDatabase.Lyric.Should().BeNull(); }); @@ -207,7 +204,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -219,13 +216,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var tracksInDatabase = await dbContext.MusicTracks - .FirstOrDefaultAsync(musicTrack => musicTrack.Id == existingTrack.Id); + var tracksInDatabase = await dbContext.MusicTracks.FirstWithIdOrDefaultAsync(existingTrack.Id); tracksInDatabase.Should().BeNull(); - var lyricInDatabase = await dbContext.Lyrics - .FirstAsync(lyric => lyric.Id == existingTrack.Lyric.Id); + var lyricInDatabase = await dbContext.Lyrics.FirstWithIdAsync(existingTrack.Lyric.Id); lyricInDatabase.Track.Should().BeNull(); }); @@ -260,7 +255,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -272,8 +267,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .FirstOrDefaultAsync(musicTrack => musicTrack.Id == existingTrack.Id); + var trackInDatabase = await dbContext.MusicTracks.FirstWithIdOrDefaultAsync(existingTrack.Id); trackInDatabase.Should().BeNull(); @@ -316,7 +310,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -328,8 +322,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var playlistInDatabase = await dbContext.Playlists - .FirstOrDefaultAsync(playlist => playlist.Id == existingPlaylistMusicTrack.Playlist.Id); + var playlistInDatabase = await dbContext.Playlists.FirstWithIdOrDefaultAsync(existingPlaylistMusicTrack.Playlist.Id); playlistInDatabase.Should().BeNull(); @@ -356,7 +349,7 @@ public async Task Cannot_delete_resource_for_href_element() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -365,10 +358,12 @@ public async Task Cannot_delete_resource_for_href_element() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -386,7 +381,7 @@ public async Task Cannot_delete_resource_for_missing_ref_element() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -395,10 +390,12 @@ public async Task Cannot_delete_resource_for_missing_ref_element() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -420,7 +417,7 @@ public async Task Cannot_delete_resource_for_missing_type() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -429,10 +426,12 @@ public async Task Cannot_delete_resource_for_missing_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -455,7 +454,7 @@ public async Task Cannot_delete_resource_for_unknown_type() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -464,10 +463,12 @@ public async Task Cannot_delete_resource_for_unknown_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -489,7 +490,7 @@ public async Task Cannot_delete_resource_for_missing_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -498,10 +499,12 @@ public async Task Cannot_delete_resource_for_missing_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -524,7 +527,7 @@ public async Task Cannot_delete_resource_for_unknown_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -533,10 +536,12 @@ public async Task Cannot_delete_resource_for_unknown_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'performers' with ID '99999999' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'performers' with ID '99999999' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -561,7 +566,7 @@ public async Task Cannot_delete_resource_for_incompatible_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -570,10 +575,12 @@ public async Task Cannot_delete_resource_for_incompatible_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int64'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int64'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -597,7 +604,7 @@ public async Task Cannot_delete_resource_for_ID_and_local_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -606,10 +613,12 @@ public async Task Cannot_delete_resource_for_ID_and_local_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs index 21f91eca53..f2f9ccdc25 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs @@ -13,6 +13,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Links public sealed class AtomicAbsoluteLinksTests : IClassFixture, OperationsDbContext>> { + private const string HostPrefix = "http://localhost"; + private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new OperationsFakers(); @@ -72,7 +74,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -82,21 +84,27 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Links.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Links.Self.Should().Be("http://localhost/textLanguages/" + existingLanguage.StringId); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Self.Should().Be($"http://localhost/textLanguages/{existingLanguage.StringId}/relationships/lyrics"); - responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Related.Should().Be($"http://localhost/textLanguages/{existingLanguage.StringId}/lyrics"); - - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Links.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Links.Self.Should().Be("http://localhost/recordCompanies/" + existingCompany.StringId); - responseDocument.Results[1].SingleData.Relationships.Should().NotBeEmpty(); - responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Self.Should().Be($"http://localhost/recordCompanies/{existingCompany.StringId}/relationships/tracks"); - responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Related.Should().Be($"http://localhost/recordCompanies/{existingCompany.StringId}/tracks"); + string languageLink = HostPrefix + "/textLanguages/" + existingLanguage.StringId; + + var singleData1 = responseDocument.Results[0].SingleData; + singleData1.Should().NotBeNull(); + singleData1.Links.Should().NotBeNull(); + singleData1.Links.Self.Should().Be(languageLink); + singleData1.Relationships.Should().NotBeEmpty(); + singleData1.Relationships["lyrics"].Links.Should().NotBeNull(); + singleData1.Relationships["lyrics"].Links.Self.Should().Be(languageLink + "/relationships/lyrics"); + singleData1.Relationships["lyrics"].Links.Related.Should().Be(languageLink + "/lyrics"); + + string companyLink = HostPrefix + "/recordCompanies/" + existingCompany.StringId; + + var singleData2 = responseDocument.Results[1].SingleData; + singleData2.Should().NotBeNull(); + singleData2.Links.Should().NotBeNull(); + singleData2.Links.Self.Should().Be(companyLink); + singleData2.Relationships.Should().NotBeEmpty(); + singleData2.Relationships["tracks"].Links.Should().NotBeNull(); + singleData2.Relationships["tracks"].Links.Self.Should().Be(companyLink + "/relationships/tracks"); + singleData2.Relationships["tracks"].Links.Related.Should().Be(companyLink + "/tracks"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs index cb3585d95f..1931b4725d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs @@ -61,7 +61,7 @@ public async Task Create_resource_with_side_effects_returns_relative_links() } }; - var route = "/api/operations"; + const string route = "/api/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -72,26 +72,26 @@ public async Task Create_resource_with_side_effects_returns_relative_links() responseDocument.Results.Should().HaveCount(2); responseDocument.Results[0].SingleData.Should().NotBeNull(); - - var newLanguageId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + + string languageLink = "/api/textLanguages/" + Guid.Parse(responseDocument.Results[0].SingleData.Id); responseDocument.Results[0].SingleData.Links.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Links.Self.Should().Be("/api/textLanguages/" + newLanguageId); + responseDocument.Results[0].SingleData.Links.Self.Should().Be(languageLink); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Self.Should().Be($"/api/textLanguages/{newLanguageId}/relationships/lyrics"); - responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Related.Should().Be($"/api/textLanguages/{newLanguageId}/lyrics"); + responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Self.Should().Be(languageLink + "/relationships/lyrics"); + responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Related.Should().Be(languageLink + "/lyrics"); responseDocument.Results[1].SingleData.Should().NotBeNull(); - var newCompanyId = short.Parse(responseDocument.Results[1].SingleData.Id); + string companyLink = "/api/recordCompanies/" + short.Parse(responseDocument.Results[1].SingleData.Id); responseDocument.Results[1].SingleData.Links.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Links.Self.Should().Be("/api/recordCompanies/" + newCompanyId); + responseDocument.Results[1].SingleData.Links.Self.Should().Be(companyLink); responseDocument.Results[1].SingleData.Relationships.Should().NotBeEmpty(); responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Self.Should().Be($"/api/recordCompanies/{newCompanyId}/relationships/tracks"); - responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Related.Should().Be($"/api/recordCompanies/{newCompanyId}/tracks"); + responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Self.Should().Be(companyLink + "/relationships/tracks"); + responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Related.Should().Be(companyLink + "/tracks"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs index 63cc817103..197c8f51ed 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs @@ -76,7 +76,7 @@ public async Task Can_create_resource_with_ToOne_relationship_using_local_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -104,7 +104,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + .FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -171,7 +171,7 @@ public async Task Can_create_resource_with_OneToMany_relationship_using_local_ID } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -199,7 +199,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + .FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -265,7 +265,7 @@ public async Task Can_create_resource_with_ManyToMany_relationship_using_local_I } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -290,10 +290,16 @@ public async Task Can_create_resource_with_ManyToMany_relationship_using_local_I await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == newPlaylistId); + .FirstWithIdAsync(newPlaylistId); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.Name.Should().Be(newPlaylistName); @@ -345,7 +351,7 @@ public async Task Cannot_consume_local_ID_that_is_assigned_in_same_operation() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -354,10 +360,12 @@ public async Task Cannot_consume_local_ID_that_is_assigned_in_same_operation() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Local ID cannot be both defined and used within the same operation."); - responseDocument.Errors[0].Detail.Should().Be("Local ID 'company-1' cannot be both defined and used within the same operation."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[1]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Local ID cannot be both defined and used within the same operation."); + error.Detail.Should().Be("Local ID 'company-1' cannot be both defined and used within the same operation."); + error.Source.Pointer.Should().Be("/atomic:operations[1]"); } [Fact] @@ -409,7 +417,7 @@ public async Task Cannot_reassign_local_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -418,10 +426,12 @@ public async Task Cannot_reassign_local_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Another local ID with the same name is already defined at this point."); - responseDocument.Errors[0].Detail.Should().Be("Another local ID with name 'playlist-1' is already defined at this point."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[2]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Another local ID with the same name is already defined at this point."); + error.Detail.Should().Be("Another local ID with name 'playlist-1' is already defined at this point."); + error.Source.Pointer.Should().Be("/atomic:operations[2]"); } [Fact] @@ -466,7 +476,7 @@ public async Task Can_update_resource_using_local_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -488,8 +498,7 @@ public async Task Can_update_resource_using_local_ID() await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + var trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); trackInDatabase.Genre.Should().Be(newTrackGenre); @@ -585,7 +594,7 @@ public async Task Can_update_resource_with_relationships_using_local_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -618,10 +627,16 @@ public async Task Can_update_resource_with_relationships_using_local_ID() await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + .FirstWithIdAsync(newTrackId); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore trackInDatabase.Title.Should().Be(newTrackTitle); @@ -692,7 +707,7 @@ public async Task Can_create_ToOne_relationship_using_local_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -721,7 +736,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + .FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -792,7 +807,7 @@ public async Task Can_create_OneToMany_relationship_using_local_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -821,7 +836,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + .FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -892,7 +907,7 @@ public async Task Can_create_ManyToMany_relationship_using_local_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -919,10 +934,16 @@ public async Task Can_create_ManyToMany_relationship_using_local_ID() await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == newPlaylistId); + .FirstWithIdAsync(newPlaylistId); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.Name.Should().Be(newPlaylistName); @@ -1015,7 +1036,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1044,7 +1065,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + .FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -1137,7 +1158,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1164,10 +1185,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == newPlaylistId); + .FirstWithIdAsync(newPlaylistId); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.Name.Should().Be(newPlaylistName); @@ -1260,7 +1287,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1289,7 +1316,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + .FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -1404,7 +1431,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1433,10 +1460,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == newPlaylistId); + .FirstWithIdAsync(newPlaylistId); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.Name.Should().Be(newPlaylistName); @@ -1560,7 +1593,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1593,7 +1626,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == newTrackId); + .FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -1704,7 +1737,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1727,10 +1760,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == existingPlaylist.Id); + .FirstWithIdAsync(existingPlaylist.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.PlaylistMusicTracks.Should().HaveCount(1); playlistInDatabase.PlaylistMusicTracks[0].MusicTrack.Id.Should().Be(existingPlaylist.PlaylistMusicTracks[0].MusicTrack.Id); @@ -1774,7 +1813,7 @@ public async Task Can_delete_resource_using_local_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1795,8 +1834,7 @@ public async Task Can_delete_resource_using_local_ID() await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .FirstOrDefaultAsync(musicTrack => musicTrack.Id == newTrackId); + var trackInDatabase = await dbContext.MusicTracks.FirstWithIdOrDefaultAsync(newTrackId); trackInDatabase.Should().BeNull(); }); @@ -1831,7 +1869,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1840,10 +1878,12 @@ public async Task Cannot_consume_unassigned_local_ID_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Server-generated value for local ID is not available at this point."); - responseDocument.Errors[0].Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[1]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Server-generated value for local ID is not available at this point."); + error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Source.Pointer.Should().Be("/atomic:operations[1]"); } [Fact] @@ -1878,7 +1918,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_data_element() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1887,10 +1927,12 @@ public async Task Cannot_consume_unassigned_local_ID_in_data_element() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Server-generated value for local ID is not available at this point."); - responseDocument.Errors[0].Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[1]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Server-generated value for local ID is not available at this point."); + error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Source.Pointer.Should().Be("/atomic:operations[1]"); } [Fact] @@ -1939,7 +1981,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1948,10 +1990,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Server-generated value for local ID is not available at this point."); - responseDocument.Errors[0].Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[1]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Server-generated value for local ID is not available at this point."); + error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Source.Pointer.Should().Be("/atomic:operations[1]"); } [Fact] @@ -1993,7 +2037,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_elemen } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -2002,10 +2046,12 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_elemen httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Server-generated value for local ID is not available at this point."); - responseDocument.Errors[0].Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[1]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Server-generated value for local ID is not available at this point."); + error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Source.Pointer.Should().Be("/atomic:operations[1]"); } [Fact] @@ -2050,7 +2096,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_array( } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -2059,10 +2105,12 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_array( httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Server-generated value for local ID is not available at this point."); - responseDocument.Errors[0].Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[1]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Server-generated value for local ID is not available at this point."); + error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Source.Pointer.Should().Be("/atomic:operations[1]"); } [Fact] @@ -2107,7 +2155,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_same_operation() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -2116,10 +2164,12 @@ public async Task Cannot_consume_local_ID_of_different_type_in_same_operation() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Type mismatch in local ID usage."); - responseDocument.Errors[0].Detail.Should().Be("Local ID 'track-1' belongs to resource type 'musicTracks' instead of 'recordCompanies'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[1]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Type mismatch in local ID usage."); + error.Detail.Should().Be("Local ID 'track-1' belongs to resource type 'musicTracks' instead of 'recordCompanies'."); + error.Source.Pointer.Should().Be("/atomic:operations[1]"); } [Fact] @@ -2162,7 +2212,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -2171,10 +2221,12 @@ public async Task Cannot_consume_local_ID_of_different_type_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Type mismatch in local ID usage."); - responseDocument.Errors[0].Detail.Should().Be("Local ID 'company-1' belongs to resource type 'recordCompanies' instead of 'musicTracks'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[2]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Type mismatch in local ID usage."); + error.Detail.Should().Be("Local ID 'company-1' belongs to resource type 'recordCompanies' instead of 'musicTracks'."); + error.Source.Pointer.Should().Be("/atomic:operations[2]"); } [Fact] @@ -2220,7 +2272,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_data_element() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -2229,10 +2281,12 @@ public async Task Cannot_consume_local_ID_of_different_type_in_data_element() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Type mismatch in local ID usage."); - responseDocument.Errors[0].Detail.Should().Be("Local ID 'performer-1' belongs to resource type 'performers' instead of 'playlists'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[2]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Type mismatch in local ID usage."); + error.Detail.Should().Be("Local ID 'performer-1' belongs to resource type 'performers' instead of 'playlists'."); + error.Source.Pointer.Should().Be("/atomic:operations[2]"); } [Fact] @@ -2292,7 +2346,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -2301,10 +2355,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Type mismatch in local ID usage."); - responseDocument.Errors[0].Detail.Should().Be("Local ID 'company-1' belongs to resource type 'recordCompanies' instead of 'performers'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[2]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Type mismatch in local ID usage."); + error.Detail.Should().Be("Local ID 'company-1' belongs to resource type 'recordCompanies' instead of 'performers'."); + error.Source.Pointer.Should().Be("/atomic:operations[2]"); } [Fact] @@ -2363,7 +2419,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -2372,10 +2428,12 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Type mismatch in local ID usage."); - responseDocument.Errors[0].Detail.Should().Be("Local ID 'playlist-1' belongs to resource type 'playlists' instead of 'recordCompanies'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[2]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Type mismatch in local ID usage."); + error.Detail.Should().Be("Local ID 'playlist-1' belongs to resource type 'playlists' instead of 'recordCompanies'."); + error.Source.Pointer.Should().Be("/atomic:operations[2]"); } [Fact] @@ -2431,7 +2489,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -2440,10 +2498,12 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Type mismatch in local ID usage."); - responseDocument.Errors[0].Detail.Should().Be("Local ID 'performer-1' belongs to resource type 'performers' instead of 'musicTracks'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[2]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Type mismatch in local ID usage."); + error.Detail.Should().Be("Local ID 'performer-1' belongs to resource type 'performers' instead of 'musicTracks'."); + error.Source.Pointer.Should().Be("/atomic:operations[2]"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Lyric.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Lyric.cs index a02949e2e9..31e9e5106a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Lyric.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Lyric.cs @@ -1,9 +1,11 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Lyric : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs index b366560d13..ecbfb911e2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs @@ -71,7 +71,7 @@ public async Task Returns_resource_meta_in_create_resource_with_side_effects() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -119,7 +119,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -129,7 +129,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results.Should().HaveCount(1); responseDocument.Results[0].SingleData.Meta.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Meta["Notice"].Should().Be("See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes for ISO 639-1 language codes."); + responseDocument.Results[0].SingleData.Meta["Notice"].Should().Be(TextLanguageMetaDefinition.NoticeText); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs new file mode 100644 index 0000000000..65f95ff844 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Serialization; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Meta +{ + public sealed class AtomicResponseMeta : IResponseMeta + { + public IReadOnlyDictionary GetMeta() + { + return new Dictionary + { + ["license"] = "MIT", + ["projectUrl"] = "https://github.com/json-api-dotnet/JsonApiDotNetCore/", + ["versions"] = new[] + { + "v4.0.0", + "v3.1.0", + "v2.5.2", + "v1.3.1" + } + }; + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs index 4fa740e292..bb6d147bcc 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs @@ -53,7 +53,7 @@ public async Task Returns_top_level_meta_in_create_resource_with_side_effects() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -105,7 +105,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -126,23 +126,4 @@ await _testContext.RunOnDatabaseAsync(async dbContext => versionArray.Should().Contain("v1.3.1"); } } - - public sealed class AtomicResponseMeta : IResponseMeta - { - public IReadOnlyDictionary GetMeta() - { - return new Dictionary - { - ["license"] = "MIT", - ["projectUrl"] = "https://github.com/json-api-dotnet/JsonApiDotNetCore/", - ["versions"] = new[] - { - "v4.0.0", - "v3.1.0", - "v2.5.2", - "v1.3.1" - } - }; - } - } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs index 2ff54786a0..27e13c0b7e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Meta { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class MusicTrackMetaDefinition : JsonApiResourceDefinition { public MusicTrackMetaDefinition(IResourceGraph resourceGraph) : base(resourceGraph) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs index e0fd3f9f78..ace9c8828f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs @@ -1,12 +1,16 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Meta { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class TextLanguageMetaDefinition : JsonApiResourceDefinition { + internal const string NoticeText = "See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes for ISO 639-1 language codes."; + public TextLanguageMetaDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } @@ -15,7 +19,7 @@ public override IDictionary GetMeta(TextLanguage resource) { return new Dictionary { - ["Notice"] = "See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes for ISO 639-1 language codes." + ["Notice"] = NoticeText }; } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs index 05d4196a47..7468352626 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs @@ -25,7 +25,7 @@ public AtomicRequestBodyTests(ExampleIntegrationTestContext(route, null); @@ -34,10 +34,12 @@ public async Task Cannot_process_for_missing_request_body() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Missing request body."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Missing request body."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -53,9 +55,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_process_for_broken_JSON_request_body() { // Arrange - var requestBody = "{\"atomic__operations\":[{\"op\":"; + const string requestBody = "{\"atomic__operations\":[{\"op\":"; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -64,10 +66,12 @@ public async Task Cannot_process_for_broken_JSON_request_body() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().StartWith("Unexpected end of content while loading JObject."); - responseDocument.Errors[0].Source.Pointer.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().StartWith("Unexpected end of content while loading JObject."); + error.Source.Pointer.Should().BeNull(); } [Fact] @@ -77,11 +81,9 @@ public async Task Cannot_process_empty_operations_array() var requestBody = new { atomic__operations = new object[0] - { - } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -90,10 +92,12 @@ public async Task Cannot_process_empty_operations_array() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: No operations found."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: No operations found."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -127,7 +131,7 @@ public async Task Cannot_process_for_unknown_operation_code() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -136,10 +140,12 @@ public async Task Cannot_process_for_unknown_operation_code() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().StartWith("Error converting value \"merge\" to type"); - responseDocument.Errors[0].Source.Pointer.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().StartWith("Error converting value \"merge\" to type"); + error.Source.Pointer.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs index 931af9e668..2d497077c4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs @@ -58,7 +58,7 @@ public async Task Cannot_process_more_operations_than_maximum() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -67,10 +67,12 @@ public async Task Cannot_process_more_operations_than_maximum() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request exceeds the maximum number of operations."); - responseDocument.Errors[0].Detail.Should().Be("The number of operations in this request (3) is higher than 2."); - responseDocument.Errors[0].Source.Pointer.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request exceeds the maximum number of operations."); + error.Detail.Should().Be("The number of operations in this request (3) is higher than 2."); + error.Source.Pointer.Should().BeNull(); } [Fact] @@ -109,7 +111,7 @@ public async Task Can_process_operations_same_as_maximum() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -148,7 +150,7 @@ public async Task Can_process_high_number_of_operations_when_unconstrained() atomic__operations = operationElements }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs index ef7c2d5a14..5f6e19fc92 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs @@ -45,7 +45,7 @@ public async Task Cannot_create_resource_with_multiple_violations() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -55,15 +55,17 @@ public async Task Cannot_create_resource_with_multiple_violations() responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Input validation failed."); - responseDocument.Errors[0].Detail.Should().Be("The Title field is required."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/title"); - - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[1].Title.Should().Be("Input validation failed."); - responseDocument.Errors[1].Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); - responseDocument.Errors[1].Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/lengthInSeconds"); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error1.Title.Should().Be("Input validation failed."); + error1.Detail.Should().Be("The Title field is required."); + error1.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/title"); + + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error2.Title.Should().Be("Input validation failed."); + error2.Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); + error2.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/lengthInSeconds"); } [Fact] @@ -112,7 +114,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -126,10 +128,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == newPlaylistId); + .FirstWithIdAsync(newPlaylistId); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.PlaylistMusicTracks.Should().HaveCount(1); playlistInDatabase.PlaylistMusicTracks[0].MusicTrack.Id.Should().Be(existingTrack.Id); @@ -169,7 +177,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -179,15 +187,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Input validation failed."); - responseDocument.Errors[0].Detail.Should().Be("The Title field is required."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/title"); - - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[1].Title.Should().Be("Input validation failed."); - responseDocument.Errors[1].Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); - responseDocument.Errors[1].Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/lengthInSeconds"); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error1.Title.Should().Be("Input validation failed."); + error1.Detail.Should().Be("The Title field is required."); + error1.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/title"); + + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error2.Title.Should().Be("Input validation failed."); + error2.Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); + error2.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/lengthInSeconds"); } [Fact] @@ -223,7 +233,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -235,8 +245,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + var trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(existingTrack.Id); trackInDatabase.Title.Should().Be(existingTrack.Title); trackInDatabase.Genre.Should().Be(newTrackGenre); @@ -286,7 +295,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -298,10 +307,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == existingPlaylist.Id); + .FirstWithIdAsync(existingPlaylist.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.PlaylistMusicTracks.Should().HaveCount(1); playlistInDatabase.PlaylistMusicTracks[0].MusicTrack.Id.Should().Be(existingTrack.Id); @@ -343,7 +358,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -357,7 +372,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Should().NotBeNull(); trackInDatabase.OwnedBy.Id.Should().Be(existingCompany.Id); @@ -402,7 +417,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -414,10 +429,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == existingPlaylist.Id); + .FirstWithIdAsync(existingPlaylist.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.PlaylistMusicTracks.Should().HaveCount(1); playlistInDatabase.PlaylistMusicTracks[0].MusicTrack.Id.Should().Be(existingTrack.Id); @@ -460,7 +481,7 @@ public async Task Validates_all_operations_before_execution_starts() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -470,20 +491,23 @@ public async Task Validates_all_operations_before_execution_starts() responseDocument.Errors.Should().HaveCount(3); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Input validation failed."); - responseDocument.Errors[0].Detail.Should().Be("The Name field is required."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/name"); - - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[1].Title.Should().Be("Input validation failed."); - responseDocument.Errors[1].Detail.Should().Be("The Title field is required."); - responseDocument.Errors[1].Source.Pointer.Should().Be("/atomic:operations[1]/data/attributes/title"); - - responseDocument.Errors[2].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[2].Title.Should().Be("Input validation failed."); - responseDocument.Errors[2].Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); - responseDocument.Errors[2].Source.Pointer.Should().Be("/atomic:operations[1]/data/attributes/lengthInSeconds"); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error1.Title.Should().Be("Input validation failed."); + error1.Detail.Should().Be("The Name field is required."); + error1.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/name"); + + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error2.Title.Should().Be("Input validation failed."); + error2.Detail.Should().Be("The Title field is required."); + error2.Source.Pointer.Should().Be("/atomic:operations[1]/data/attributes/title"); + + var error3 = responseDocument.Errors[2]; + error3.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error3.Title.Should().Be("Input validation failed."); + error3.Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); + error3.Source.Pointer.Should().Be("/atomic:operations[1]/data/attributes/lengthInSeconds"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/MusicTrack.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/MusicTrack.cs index 8639702c78..991d999f1d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/MusicTrack.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/MusicTrack.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class MusicTrack : Identifiable { [RegularExpression(@"(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$")] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/OperationsDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/OperationsDbContext.cs index 0e8ab53cdf..839c6cd7b8 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/OperationsDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/OperationsDbContext.cs @@ -1,7 +1,11 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +// @formatter:wrap_chained_method_calls chop_always + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class OperationsDbContext : DbContext { public DbSet Playlists { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/OperationsFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/OperationsFakers.cs index 63734c5523..d652a8bed5 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/OperationsFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/OperationsFakers.cs @@ -5,11 +5,14 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations { internal sealed class OperationsFakers : FakerContainer { - private static readonly Lazy> _lazyLanguageIsoCodes = + private static readonly Lazy> LazyLanguageIsoCodes = new Lazy>(() => CultureInfo .GetCultures(CultureTypes.NeutralCultures) .Where(culture => !string.IsNullOrEmpty(culture.Name)) @@ -38,7 +41,7 @@ internal sealed class OperationsFakers : FakerContainer private readonly Lazy> _lazyTextLanguageFaker = new Lazy>(() => new Faker() .UseSeed(GetFakerSeed()) - .RuleFor(textLanguage => textLanguage.IsoCode, f => f.PickRandom(_lazyLanguageIsoCodes.Value))); + .RuleFor(textLanguage => textLanguage.IsoCode, f => f.PickRandom(LazyLanguageIsoCodes.Value))); private readonly Lazy> _lazyPerformerFaker = new Lazy>(() => new Faker() diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Performer.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Performer.cs index 9b6d85c610..37a0f0c16b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Performer.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Performer.cs @@ -1,9 +1,11 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Performer : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Playlist.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Playlist.cs index 11fd778cc2..5b0713c45b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Playlist.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Playlist.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Playlist : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/PlaylistMusicTrack.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/PlaylistMusicTrack.cs index 9c5389867a..47540cafdf 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/PlaylistMusicTrack.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/PlaylistMusicTrack.cs @@ -1,7 +1,9 @@ using System; +using JetBrains.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class PlaylistMusicTrack { public long PlaylistId { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs index f20ec15e37..4b71f7215c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs @@ -17,7 +17,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.QueryS public sealed class AtomicQueryStringTests : IClassFixture, OperationsDbContext>> { - private static readonly DateTime _frozenTime = 30.July(2018).At(13, 46, 12); + private static readonly DateTime FrozenTime = 30.July(2018).At(13, 46, 12); private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new OperationsFakers(); @@ -30,7 +30,7 @@ public AtomicQueryStringTests(ExampleIntegrationTestContext(new FrozenSystemClock {UtcNow = _frozenTime}); + services.AddSingleton(new FrozenSystemClock {UtcNow = FrozenTime}); services.AddScoped, MusicTrackReleaseDefinition>(); }); @@ -61,7 +61,7 @@ public async Task Cannot_include_on_operations_endpoint() } }; - var route = "/operations?include=recordCompanies"; + const string route = "/operations?include=recordCompanies"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -70,10 +70,12 @@ public async Task Cannot_include_on_operations_endpoint() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - responseDocument.Errors[0].Detail.Should().Be("The parameter 'include' cannot be used at this endpoint."); - responseDocument.Errors[0].Source.Parameter.Should().Be("include"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); + error.Detail.Should().Be("The parameter 'include' cannot be used at this endpoint."); + error.Source.Parameter.Should().Be("include"); } [Fact] @@ -98,7 +100,7 @@ public async Task Cannot_filter_on_operations_endpoint() } }; - var route = "/operations?filter=equals(id,'1')"; + const string route = "/operations?filter=equals(id,'1')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -107,10 +109,12 @@ public async Task Cannot_filter_on_operations_endpoint() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - responseDocument.Errors[0].Detail.Should().Be("The parameter 'filter' cannot be used at this endpoint."); - responseDocument.Errors[0].Source.Parameter.Should().Be("filter"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); + error.Detail.Should().Be("The parameter 'filter' cannot be used at this endpoint."); + error.Source.Parameter.Should().Be("filter"); } [Fact] @@ -135,7 +139,7 @@ public async Task Cannot_sort_on_operations_endpoint() } }; - var route = "/operations?sort=-id"; + const string route = "/operations?sort=-id"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -144,10 +148,12 @@ public async Task Cannot_sort_on_operations_endpoint() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - responseDocument.Errors[0].Detail.Should().Be("The parameter 'sort' cannot be used at this endpoint."); - responseDocument.Errors[0].Source.Parameter.Should().Be("sort"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); + error.Detail.Should().Be("The parameter 'sort' cannot be used at this endpoint."); + error.Source.Parameter.Should().Be("sort"); } [Fact] @@ -172,7 +178,7 @@ public async Task Cannot_use_pagination_number_on_operations_endpoint() } }; - var route = "/operations?page[number]=1"; + const string route = "/operations?page[number]=1"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -181,10 +187,12 @@ public async Task Cannot_use_pagination_number_on_operations_endpoint() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - responseDocument.Errors[0].Detail.Should().Be("The parameter 'page[number]' cannot be used at this endpoint."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); + error.Detail.Should().Be("The parameter 'page[number]' cannot be used at this endpoint."); + error.Source.Parameter.Should().Be("page[number]"); } [Fact] @@ -209,7 +217,7 @@ public async Task Cannot_use_pagination_size_on_operations_endpoint() } }; - var route = "/operations?page[size]=1"; + const string route = "/operations?page[size]=1"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -218,10 +226,12 @@ public async Task Cannot_use_pagination_size_on_operations_endpoint() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - responseDocument.Errors[0].Detail.Should().Be("The parameter 'page[size]' cannot be used at this endpoint."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[size]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); + error.Detail.Should().Be("The parameter 'page[size]' cannot be used at this endpoint."); + error.Source.Parameter.Should().Be("page[size]"); } [Fact] @@ -246,7 +256,7 @@ public async Task Cannot_use_sparse_fieldset_on_operations_endpoint() } }; - var route = "/operations?fields[recordCompanies]=id"; + const string route = "/operations?fields[recordCompanies]=id"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -255,10 +265,12 @@ public async Task Cannot_use_sparse_fieldset_on_operations_endpoint() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - responseDocument.Errors[0].Detail.Should().Be("The parameter 'fields[recordCompanies]' cannot be used at this endpoint."); - responseDocument.Errors[0].Source.Parameter.Should().Be("fields[recordCompanies]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); + error.Detail.Should().Be("The parameter 'fields[recordCompanies]' cannot be used at this endpoint."); + error.Source.Parameter.Should().Be("fields[recordCompanies]"); } [Fact] @@ -266,19 +278,18 @@ public async Task Can_use_Queryable_handler_on_resource_endpoint() { // Arrange var musicTracks = _fakers.MusicTrack.Generate(3); - musicTracks[0].ReleasedAt = _frozenTime.AddMonths(5); - musicTracks[1].ReleasedAt = _frozenTime.AddMonths(-5); - musicTracks[2].ReleasedAt = _frozenTime.AddMonths(-1); + musicTracks[0].ReleasedAt = FrozenTime.AddMonths(5); + musicTracks[1].ReleasedAt = FrozenTime.AddMonths(-5); + musicTracks[2].ReleasedAt = FrozenTime.AddMonths(-1); await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.MusicTracks.AddRange(musicTracks); - await dbContext.SaveChangesAsync(); }); - var route = "/musicTracks?isRecentlyReleased=true"; + const string route = "/musicTracks?isRecentlyReleased=true"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -315,7 +326,7 @@ public async Task Cannot_use_Queryable_handler_on_operations_endpoint() } }; - var route = "/operations?isRecentlyReleased=true"; + const string route = "/operations?isRecentlyReleased=true"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -324,10 +335,13 @@ public async Task Cannot_use_Queryable_handler_on_operations_endpoint() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Unknown query string parameter."); - responseDocument.Errors[0].Detail.Should().Be("Query string parameter 'isRecentlyReleased' is unknown. Set 'AllowUnknownQueryStringParameters' to 'true' in options to ignore unknown parameters."); - responseDocument.Errors[0].Source.Parameter.Should().Be("isRecentlyReleased"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Unknown query string parameter."); + error.Detail.Should().Be("Query string parameter 'isRecentlyReleased' is unknown. " + + "Set 'AllowUnknownQueryStringParameters' to 'true' in options to ignore unknown parameters."); + error.Source.Parameter.Should().Be("isRecentlyReleased"); } [Fact] @@ -357,7 +371,7 @@ public async Task Can_use_defaults_on_operations_endpoint() } }; - var route = "/operations?defaults=false"; + const string route = "/operations?defaults=false"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -370,7 +384,7 @@ public async Task Can_use_defaults_on_operations_endpoint() responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); responseDocument.Results[0].SingleData.Attributes.Should().HaveCount(2); responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[0].SingleData.Attributes["lengthInSeconds"].As().Should().BeApproximately(newTrackLength, 0.00000000001M); + responseDocument.Results[0].SingleData.Attributes["lengthInSeconds"].As().Should().BeApproximately(newTrackLength); } [Fact] @@ -400,7 +414,7 @@ public async Task Can_use_nulls_on_operations_endpoint() } }; - var route = "/operations?nulls=false"; + const string route = "/operations?nulls=false"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -413,7 +427,7 @@ public async Task Can_use_nulls_on_operations_endpoint() responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); responseDocument.Results[0].SingleData.Attributes.Should().HaveCount(2); responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[0].SingleData.Attributes["lengthInSeconds"].As().Should().BeApproximately(newTrackLength, 0.00000000001M); + responseDocument.Results[0].SingleData.Attributes["lengthInSeconds"].As().Should().BeApproximately(newTrackLength); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs index 439cd821b5..d3869ba33d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using JetBrains.Annotations; +using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using Microsoft.AspNetCore.Authentication; @@ -7,6 +9,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.QueryStrings { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class MusicTrackReleaseDefinition : JsonApiResourceDefinition { private readonly ISystemClock _systemClock; @@ -14,7 +17,9 @@ public sealed class MusicTrackReleaseDefinition : JsonApiResourceDefinition OnRegisterQueryableHandlersForQueryStringParameters() @@ -27,14 +32,16 @@ public override QueryStringParameterHandlers OnRegisterQueryableHand private IQueryable FilterOnRecentlyReleased(IQueryable source, StringValues parameterValue) { + var tracks = source; + if (bool.Parse(parameterValue)) { - source = source.Where(musicTrack => + tracks = tracks.Where(musicTrack => musicTrack.ReleasedAt < _systemClock.UtcNow && musicTrack.ReleasedAt > _systemClock.UtcNow.AddMonths(-3)); } - return source; + return tracks; } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/RecordCompany.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/RecordCompany.cs index 5dc89a7a87..d735fe4d7b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/RecordCompany.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/RecordCompany.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class RecordCompany : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/AtomicSparseFieldSetResourceDefinitionTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/AtomicSparseFieldSetResourceDefinitionTests.cs index 653ff88a31..01ce190904 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/AtomicSparseFieldSetResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/AtomicSparseFieldSetResourceDefinitionTests.cs @@ -73,7 +73,7 @@ public async Task Hides_text_in_create_resource_with_side_effects() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -139,7 +139,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/LyricPermissionProvider.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/LyricPermissionProvider.cs index 501c820391..07140f6fa1 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/LyricPermissionProvider.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/LyricPermissionProvider.cs @@ -2,7 +2,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Resour { public sealed class LyricPermissionProvider { - public bool CanViewText { get; set; } - public int HitCount { get; set; } + internal bool CanViewText { get; set; } + internal int HitCount { get; set; } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/LyricTextDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/LyricTextDefinition.cs index be315c3f0a..bf89c79b70 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/LyricTextDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/LyricTextDefinition.cs @@ -1,9 +1,11 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.ResourceDefinitions { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class LyricTextDefinition : JsonApiResourceDefinition { private readonly LyricPermissionProvider _lyricPermissionProvider; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/TextLanguage.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/TextLanguage.cs index 57452eb2d2..be25c89b7d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/TextLanguage.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/TextLanguage.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class TextLanguage : Identifiable { [Attr] @@ -16,7 +18,7 @@ public sealed class TextLanguage : Identifiable public Guid ConcurrencyToken { get => Guid.NewGuid(); - set { } + set => _ = value; } [HasMany] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs index 60f6818305..ad624f6189 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs @@ -81,7 +81,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -90,10 +90,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'performers' with ID '99999999' in relationship 'performers' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[1]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("A related resource does not exist."); + error.Detail.Should().Be("Related resource of type 'performers' with ID '99999999' in relationship 'performers' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[1]"); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs index 6097ca1ccd..4b18f53178 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs @@ -58,7 +58,7 @@ public async Task Cannot_use_non_transactional_repository() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -67,10 +67,12 @@ public async Task Cannot_use_non_transactional_repository() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Unsupported resource type in atomic:operations request."); - responseDocument.Errors[0].Detail.Should().Be("Operations on resources of type 'performers' cannot be used because transaction support is unavailable."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Unsupported resource type in atomic:operations request."); + error.Detail.Should().Be("Operations on resources of type 'performers' cannot be used because transaction support is unavailable."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -95,7 +97,7 @@ public async Task Cannot_use_transactional_repository_without_active_transaction } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -104,10 +106,12 @@ public async Task Cannot_use_transactional_repository_without_active_transaction httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Unsupported combination of resource types in atomic:operations request."); - responseDocument.Errors[0].Detail.Should().Be("All operations need to participate in a single shared transaction, which is not the case for this request."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Unsupported combination of resource types in atomic:operations request."); + error.Detail.Should().Be("All operations need to participate in a single shared transaction, which is not the case for this request."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -132,7 +136,7 @@ public async Task Cannot_use_distributed_transaction() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -141,10 +145,12 @@ public async Task Cannot_use_distributed_transaction() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Unsupported combination of resource types in atomic:operations request."); - responseDocument.Errors[0].Detail.Should().Be("All operations need to participate in a single shared transaction, which is not the case for this request."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Unsupported combination of resource types in atomic:operations request."); + error.Detail.Should().Be("All operations need to participate in a single shared transaction, which is not the case for this request."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/ExtraDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/ExtraDbContext.cs index f9776989a0..fee681e5e3 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/ExtraDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/ExtraDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Transactions { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ExtraDbContext : DbContext { public ExtraDbContext(DbContextOptions options) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs index 581ff9f64a..4417ee9d3b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Repositories; @@ -8,6 +9,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Transactions { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class LyricRepository : EntityFrameworkCoreRepository { private readonly ExtraDbContext _extraDbContext; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs index 740195172a..554f6b6d3e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Repositories; @@ -8,6 +9,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Transactions { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class MusicTrackRepository : EntityFrameworkCoreRepository { public override Guid? TransactionId => null; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs index 354e4a01a1..f22a3d8846 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Repositories; @@ -9,6 +10,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Transactions { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class PerformerRepository : IResourceRepository { public Task> GetAsync(QueryLayer layer, CancellationToken cancellationToken) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index 303632937e..3a0f791045 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; +using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExampleTests.Startups; using Microsoft.EntityFrameworkCore; @@ -59,7 +60,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -68,9 +69,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted in 'add' operations."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted in 'add' operations."); + error.Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); } [Fact] @@ -132,7 +135,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -146,7 +149,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().HaveCount(3); trackInDatabase.Performers.Should().ContainSingle(performer => performer.Id == existingTrack.Performers[0].Id); @@ -220,7 +223,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -232,13 +235,21 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == existingPlaylist.Id); + .FirstWithIdAsync(existingPlaylist.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + + Guid initialTrackId = existingPlaylist.PlaylistMusicTracks[0].MusicTrack.Id; playlistInDatabase.PlaylistMusicTracks.Should().HaveCount(3); - playlistInDatabase.PlaylistMusicTracks.Should().ContainSingle(playlistMusicTrack => playlistMusicTrack.MusicTrack.Id == existingPlaylist.PlaylistMusicTracks[0].MusicTrack.Id); + playlistInDatabase.PlaylistMusicTracks.Should().ContainSingle(playlistMusicTrack => playlistMusicTrack.MusicTrack.Id == initialTrackId); playlistInDatabase.PlaylistMusicTracks.Should().ContainSingle(playlistMusicTrack => playlistMusicTrack.MusicTrack.Id == existingTracks[0].Id); playlistInDatabase.PlaylistMusicTracks.Should().ContainSingle(playlistMusicTrack => playlistMusicTrack.MusicTrack.Id == existingTracks[1].Id); }); @@ -260,7 +271,7 @@ public async Task Cannot_add_for_href_element() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -269,10 +280,12 @@ public async Task Cannot_add_for_href_element() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -295,7 +308,7 @@ public async Task Cannot_add_for_missing_type_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -304,10 +317,12 @@ public async Task Cannot_add_for_missing_type_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -331,7 +346,7 @@ public async Task Cannot_add_for_unknown_type_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -340,10 +355,12 @@ public async Task Cannot_add_for_unknown_type_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -366,7 +383,7 @@ public async Task Cannot_add_for_missing_ID_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -375,10 +392,12 @@ public async Task Cannot_add_for_missing_ID_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -418,7 +437,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -427,10 +446,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'recordCompanies' with ID '9999' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'recordCompanies' with ID '9999' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -455,7 +476,7 @@ public async Task Cannot_add_for_ID_and_local_ID_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -464,10 +485,12 @@ public async Task Cannot_add_for_ID_and_local_ID_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -490,7 +513,7 @@ public async Task Cannot_add_for_missing_relationship_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -499,10 +522,12 @@ public async Task Cannot_add_for_missing_relationship_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.relationship' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.relationship' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -526,7 +551,7 @@ public async Task Cannot_add_for_unknown_relationship_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -535,10 +560,12 @@ public async Task Cannot_add_for_unknown_relationship_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); + error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -571,7 +598,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -580,10 +607,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().Be("Expected data[] element for 'performers' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -614,7 +643,7 @@ public async Task Cannot_add_for_missing_type_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -623,10 +652,12 @@ public async Task Cannot_add_for_missing_type_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -658,7 +689,7 @@ public async Task Cannot_add_for_unknown_type_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -667,10 +698,12 @@ public async Task Cannot_add_for_unknown_type_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -701,7 +734,7 @@ public async Task Cannot_add_for_missing_ID_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -710,10 +743,12 @@ public async Task Cannot_add_for_missing_ID_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -746,7 +781,7 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -755,10 +790,12 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -766,7 +803,7 @@ public async Task Cannot_add_for_unknown_IDs_in_data() { // Arrange var existingCompany = _fakers.RecordCompany.Generate(); - var trackIds = new[] {Guid.NewGuid(), Guid.NewGuid()}; + var trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -804,7 +841,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -814,15 +851,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); - - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[1].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[1].Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); - responseDocument.Errors[1].Source.Pointer.Should().Be("/atomic:operations[0]"); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.NotFound); + error1.Title.Should().Be("A related resource does not exist."); + error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); + error1.Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.NotFound); + error2.Title.Should().Be("A related resource does not exist."); + error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); + error2.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -862,7 +901,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -871,10 +910,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); + error.Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -908,7 +949,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -922,7 +963,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().HaveCount(1); trackInDatabase.Performers[0].Id.Should().Be(existingTrack.Performers[0].Id); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index c7e77fa4e3..50de26773d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; +using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExampleTests.Startups; using Microsoft.EntityFrameworkCore; @@ -59,7 +60,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -68,9 +69,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted in 'remove' operations."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted in 'remove' operations."); + error.Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); } [Fact] @@ -130,7 +133,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -144,7 +147,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().HaveCount(1); trackInDatabase.Performers[0].Id.Should().Be(existingTrack.Performers[1].Id); @@ -225,7 +228,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -237,10 +240,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == existingPlaylist.Id); + .FirstWithIdAsync(existingPlaylist.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.PlaylistMusicTracks.Should().HaveCount(1); playlistInDatabase.PlaylistMusicTracks[0].MusicTrack.Id.Should().Be(existingPlaylist.PlaylistMusicTracks[1].MusicTrack.Id); @@ -266,7 +275,7 @@ public async Task Cannot_remove_for_href_element() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -275,10 +284,12 @@ public async Task Cannot_remove_for_href_element() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -301,7 +312,7 @@ public async Task Cannot_remove_for_missing_type_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -310,10 +321,12 @@ public async Task Cannot_remove_for_missing_type_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -337,7 +350,7 @@ public async Task Cannot_remove_for_unknown_type_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -346,10 +359,12 @@ public async Task Cannot_remove_for_unknown_type_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -372,7 +387,7 @@ public async Task Cannot_remove_for_missing_ID_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -381,10 +396,12 @@ public async Task Cannot_remove_for_missing_ID_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -424,7 +441,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -433,10 +450,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'recordCompanies' with ID '9999' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'recordCompanies' with ID '9999' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -461,7 +480,7 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -470,10 +489,12 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -497,7 +518,7 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -506,10 +527,12 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); + error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -542,7 +565,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -551,10 +574,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().Be("Expected data[] element for 'performers' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -585,7 +610,7 @@ public async Task Cannot_remove_for_missing_type_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -594,10 +619,12 @@ public async Task Cannot_remove_for_missing_type_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -629,7 +656,7 @@ public async Task Cannot_remove_for_unknown_type_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -638,10 +665,12 @@ public async Task Cannot_remove_for_unknown_type_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -672,7 +701,7 @@ public async Task Cannot_remove_for_missing_ID_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -681,10 +710,12 @@ public async Task Cannot_remove_for_missing_ID_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -717,7 +748,7 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -726,10 +757,12 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -737,7 +770,7 @@ public async Task Cannot_remove_for_unknown_IDs_in_data() { // Arrange var existingCompany = _fakers.RecordCompany.Generate(); - var trackIds = new[] {Guid.NewGuid(), Guid.NewGuid()}; + var trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -775,7 +808,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -785,15 +818,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); - - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[1].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[1].Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); - responseDocument.Errors[1].Source.Pointer.Should().Be("/atomic:operations[0]"); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.NotFound); + error1.Title.Should().Be("A related resource does not exist."); + error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); + error1.Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.NotFound); + error2.Title.Should().Be("A related resource does not exist."); + error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); + error2.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -833,7 +868,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -842,10 +877,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); + error.Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -880,7 +917,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -894,7 +931,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().HaveCount(1); trackInDatabase.Performers[0].Id.Should().Be(existingTrack.Performers[0].Id); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index bb87d19f2c..108a070664 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; +using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExampleTests.Startups; using Microsoft.EntityFrameworkCore; @@ -56,7 +57,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -70,7 +71,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().BeEmpty(); @@ -121,7 +122,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -133,10 +134,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == existingPlaylist.Id); + .FirstWithIdAsync(existingPlaylist.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.PlaylistMusicTracks.Should().BeEmpty(); @@ -192,7 +199,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -206,7 +213,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().HaveCount(2); trackInDatabase.Performers.Should().ContainSingle(performer => performer.Id == existingPerformers[0].Id); @@ -270,7 +277,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -282,10 +289,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == existingPlaylist.Id); + .FirstWithIdAsync(existingPlaylist.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.PlaylistMusicTracks.Should().HaveCount(2); playlistInDatabase.PlaylistMusicTracks.Should().ContainSingle(playlistMusicTrack => playlistMusicTrack.MusicTrack.Id == existingTracks[0].Id); @@ -312,7 +325,7 @@ public async Task Cannot_replace_for_href_element() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -321,10 +334,12 @@ public async Task Cannot_replace_for_href_element() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -347,7 +362,7 @@ public async Task Cannot_replace_for_missing_type_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -356,10 +371,12 @@ public async Task Cannot_replace_for_missing_type_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -383,7 +400,7 @@ public async Task Cannot_replace_for_unknown_type_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -392,10 +409,12 @@ public async Task Cannot_replace_for_unknown_type_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -418,7 +437,7 @@ public async Task Cannot_replace_for_missing_ID_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -427,10 +446,12 @@ public async Task Cannot_replace_for_missing_ID_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -470,7 +491,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -479,10 +500,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'recordCompanies' with ID '9999' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'recordCompanies' with ID '9999' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -524,7 +547,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -533,10 +556,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int16'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int16'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -561,7 +586,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -570,10 +595,12 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -597,7 +624,7 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -606,10 +633,12 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); + error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -642,7 +671,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -651,10 +680,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().Be("Expected data[] element for 'performers' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -685,7 +716,7 @@ public async Task Cannot_replace_for_missing_type_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -694,10 +725,12 @@ public async Task Cannot_replace_for_missing_type_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -729,7 +762,7 @@ public async Task Cannot_replace_for_unknown_type_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -738,10 +771,12 @@ public async Task Cannot_replace_for_unknown_type_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -772,7 +807,7 @@ public async Task Cannot_replace_for_missing_ID_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -781,10 +816,12 @@ public async Task Cannot_replace_for_missing_ID_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -817,7 +854,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -826,10 +863,12 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -837,7 +876,7 @@ public async Task Cannot_replace_for_unknown_IDs_in_data() { // Arrange var existingCompany = _fakers.RecordCompany.Generate(); - var trackIds = new[] {Guid.NewGuid(), Guid.NewGuid()}; + var trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -875,7 +914,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -885,15 +924,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); - - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[1].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[1].Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); - responseDocument.Errors[1].Source.Pointer.Should().Be("/atomic:operations[0]"); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.NotFound); + error1.Title.Should().Be("A related resource does not exist."); + error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); + error1.Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.NotFound); + error2.Title.Should().Be("A related resource does not exist."); + error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); + error2.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -933,7 +974,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -942,10 +983,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -985,7 +1028,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -994,10 +1037,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); + error.Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index 2cae0afafa..216878d00e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -55,7 +55,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -69,7 +69,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var lyricInDatabase = await dbContext.Lyrics .Include(lyric => lyric.Track) - .FirstAsync(lyric => lyric.Id == existingLyric.Id); + .FirstWithIdAsync(existingLyric.Id); lyricInDatabase.Track.Should().BeNull(); @@ -110,7 +110,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -124,7 +124,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Lyric) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Lyric.Should().BeNull(); @@ -165,7 +165,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -179,7 +179,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Should().BeNull(); @@ -223,7 +223,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -237,7 +237,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var lyricInDatabase = await dbContext.Lyrics .Include(lyric => lyric.Track) - .FirstAsync(lyric => lyric.Id == existingLyric.Id); + .FirstWithIdAsync(existingLyric.Id); lyricInDatabase.Track.Id.Should().Be(existingTrack.Id); }); @@ -278,7 +278,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -292,7 +292,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Lyric) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Lyric.Id.Should().Be(existingLyric.Id); }); @@ -333,7 +333,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -347,7 +347,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Id.Should().Be(existingCompany.Id); }); @@ -391,7 +391,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -405,7 +405,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var lyricInDatabase = await dbContext.Lyrics .Include(lyric => lyric.Track) - .FirstAsync(lyric => lyric.Id == existingLyric.Id); + .FirstWithIdAsync(existingLyric.Id); lyricInDatabase.Track.Id.Should().Be(existingTrack.Id); @@ -452,7 +452,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -466,7 +466,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Lyric) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Lyric.Id.Should().Be(existingLyric.Id); @@ -513,7 +513,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -527,7 +527,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Id.Should().Be(existingCompany.Id); @@ -552,7 +552,7 @@ public async Task Cannot_create_for_href_element() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -561,10 +561,12 @@ public async Task Cannot_create_for_href_element() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -587,7 +589,7 @@ public async Task Cannot_create_for_missing_type_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -596,10 +598,12 @@ public async Task Cannot_create_for_missing_type_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -623,7 +627,7 @@ public async Task Cannot_create_for_unknown_type_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -632,10 +636,12 @@ public async Task Cannot_create_for_unknown_type_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -658,7 +664,7 @@ public async Task Cannot_create_for_missing_ID_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -667,10 +673,12 @@ public async Task Cannot_create_for_missing_ID_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -709,7 +717,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -718,10 +726,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be($"Resource of type 'musicTracks' with ID '{missingTrackId}' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be($"Resource of type 'musicTracks' with ID '{missingTrackId}' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -758,7 +768,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -767,10 +777,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -795,7 +807,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -804,10 +816,12 @@ public async Task Cannot_create_for_ID_and_local_ID_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -831,7 +845,7 @@ public async Task Cannot_create_for_unknown_relationship_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -840,10 +854,12 @@ public async Task Cannot_create_for_unknown_relationship_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); + error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -883,7 +899,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -892,10 +908,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); - responseDocument.Errors[0].Detail.Should().Be("Expected single data element for 'lyric' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); + error.Detail.Should().Be("Expected single data element for 'lyric' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -923,7 +941,7 @@ public async Task Cannot_create_for_missing_type_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -932,10 +950,12 @@ public async Task Cannot_create_for_missing_type_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -964,7 +984,7 @@ public async Task Cannot_create_for_unknown_type_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -973,10 +993,12 @@ public async Task Cannot_create_for_unknown_type_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1004,7 +1026,7 @@ public async Task Cannot_create_for_missing_ID_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1013,10 +1035,12 @@ public async Task Cannot_create_for_missing_ID_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1046,7 +1070,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1055,10 +1079,12 @@ public async Task Cannot_create_for_ID_and_local_ID_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1095,7 +1121,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1104,10 +1130,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'lyrics' with ID '99999999' in relationship 'lyric' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("A related resource does not exist."); + error.Detail.Should().Be("Related resource of type 'lyrics' with ID '99999999' in relationship 'lyric' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1144,7 +1172,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1153,10 +1181,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1193,7 +1223,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1202,10 +1232,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data.type' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected resource of type 'lyrics' in 'data.type', instead of 'playlists'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data.type' element."); + error.Detail.Should().Be("Expected resource of type 'lyrics' in 'data.type', instead of 'playlists'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index e8bdb8db3a..1b4c51d93b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; +using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExampleTests.Startups; using Microsoft.EntityFrameworkCore; @@ -61,7 +62,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -75,7 +76,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().BeEmpty(); @@ -131,7 +132,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -143,10 +144,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == existingPlaylist.Id); + .FirstWithIdAsync(existingPlaylist.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.PlaylistMusicTracks.Should().BeEmpty(); @@ -207,7 +214,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -221,7 +228,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().HaveCount(2); trackInDatabase.Performers.Should().ContainSingle(performer => performer.Id == existingPerformers[0].Id); @@ -290,7 +297,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -302,10 +309,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) - .FirstAsync(playlist => playlist.Id == existingPlaylist.Id); + .FirstWithIdAsync(existingPlaylist.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore playlistInDatabase.PlaylistMusicTracks.Should().HaveCount(2); playlistInDatabase.PlaylistMusicTracks.Should().ContainSingle(playlistMusicTrack => playlistMusicTrack.MusicTrack.Id == existingTracks[0].Id); @@ -351,7 +364,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -360,10 +373,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().Be("Expected data[] element for 'performers' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -399,7 +414,7 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -408,10 +423,12 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected 'type' element in 'tracks' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'tracks' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -448,7 +465,7 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -457,10 +474,12 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -496,7 +515,7 @@ public async Task Cannot_replace_for_missing_ID_in_relationship_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -505,10 +524,12 @@ public async Task Cannot_replace_for_missing_ID_in_relationship_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); + error.Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -546,7 +567,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -555,10 +576,12 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); + error.Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -566,7 +589,7 @@ public async Task Cannot_replace_for_unknown_IDs_in_relationship_data() { // Arrange var existingCompany = _fakers.RecordCompany.Generate(); - var trackIds = new[] {Guid.NewGuid(), Guid.NewGuid()}; + var trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -609,7 +632,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -619,15 +642,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); - - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[1].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[1].Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); - responseDocument.Errors[1].Source.Pointer.Should().Be("/atomic:operations[0]"); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.NotFound); + error1.Title.Should().Be("A related resource does not exist."); + error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); + error1.Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.NotFound); + error2.Title.Should().Be("A related resource does not exist."); + error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); + error2.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -672,7 +697,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -681,10 +706,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); + error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index 3529dd3e66..626fa31e02 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -65,7 +65,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => atomic__operations = operationElements }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -127,7 +127,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -141,7 +141,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Title.Should().Be(existingTrack.Title); trackInDatabase.Genre.Should().Be(existingTrack.Genre); @@ -185,7 +185,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -197,8 +197,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + var trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(existingTrack.Id); trackInDatabase.Title.Should().Be(newTitle); }); @@ -243,7 +242,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -289,7 +288,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -303,7 +302,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Title.Should().Be(existingTrack.Title); trackInDatabase.LengthInSeconds.Should().Be(existingTrack.LengthInSeconds); @@ -356,7 +355,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -370,7 +369,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Title.Should().Be(newTitle); trackInDatabase.LengthInSeconds.Should().Be(newLengthInSeconds); @@ -415,7 +414,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -432,8 +431,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var languageInDatabase = await dbContext.TextLanguages - .FirstAsync(language => language.Id == existingLanguage.Id); + var languageInDatabase = await dbContext.TextLanguages.FirstWithIdAsync(existingLanguage.Id); languageInDatabase.IsoCode.Should().Be(newIsoCode); }); @@ -468,7 +466,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -498,7 +496,7 @@ public async Task Cannot_update_resource_for_href_element() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -507,10 +505,12 @@ public async Task Cannot_update_resource_for_href_element() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -551,7 +551,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -563,8 +563,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var performerInDatabase = await dbContext.Performers - .FirstAsync(performer => performer.Id == existingPerformer.Id); + var performerInDatabase = await dbContext.Performers.FirstWithIdAsync(existingPerformer.Id); performerInDatabase.ArtistName.Should().Be(newArtistName); performerInDatabase.BornAt.Should().BeCloseTo(existingPerformer.BornAt); @@ -601,7 +600,7 @@ public async Task Cannot_update_resource_for_missing_type_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -610,10 +609,12 @@ public async Task Cannot_update_resource_for_missing_type_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -646,7 +647,7 @@ public async Task Cannot_update_resource_for_missing_ID_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -655,10 +656,12 @@ public async Task Cannot_update_resource_for_missing_ID_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -693,7 +696,7 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_ref() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -702,10 +705,12 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_ref() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -723,7 +728,7 @@ public async Task Cannot_update_resource_for_missing_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -732,10 +737,12 @@ public async Task Cannot_update_resource_for_missing_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -763,7 +770,7 @@ public async Task Cannot_update_resource_for_missing_type_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -772,10 +779,12 @@ public async Task Cannot_update_resource_for_missing_type_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -803,7 +812,7 @@ public async Task Cannot_update_resource_for_missing_ID_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -812,10 +821,12 @@ public async Task Cannot_update_resource_for_missing_ID_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -845,7 +856,7 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -854,10 +865,12 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -895,7 +908,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -904,10 +917,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected single data element for create/update resource operation."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected single data element for create/update resource operation."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -941,7 +956,7 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -950,10 +965,12 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.type' and 'data.type' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected resource of type 'performers' in 'data.type', instead of 'playlists'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.type' and 'data.type' element."); + error.Detail.Should().Be("Expected resource of type 'performers' in 'data.type', instead of 'playlists'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -987,7 +1004,7 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -996,10 +1013,12 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Resource ID mismatch between 'ref.id' and 'data.id' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected resource with ID '12345678' in 'data.id', instead of '87654321'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Resource ID mismatch between 'ref.id' and 'data.id' element."); + error.Detail.Should().Be("Expected resource with ID '12345678' in 'data.id', instead of '87654321'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1033,7 +1052,7 @@ public async Task Cannot_update_on_resource_local_ID_mismatch_between_ref_and_da } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1042,10 +1061,12 @@ public async Task Cannot_update_on_resource_local_ID_mismatch_between_ref_and_da httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Resource local ID mismatch between 'ref.lid' and 'data.lid' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected resource with local ID 'local-1' in 'data.lid', instead of 'local-2'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Resource local ID mismatch between 'ref.lid' and 'data.lid' element."); + error.Detail.Should().Be("Expected resource with local ID 'local-1' in 'data.lid', instead of 'local-2'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1079,7 +1100,7 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1088,10 +1109,12 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Resource identity mismatch between 'ref.id' and 'data.lid' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected resource with ID '12345678' in 'data.id', instead of 'local-1' in 'data.lid'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Resource identity mismatch between 'ref.id' and 'data.lid' element."); + error.Detail.Should().Be("Expected resource with ID '12345678' in 'data.id', instead of 'local-1' in 'data.lid'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1125,7 +1148,7 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1134,10 +1157,12 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Resource identity mismatch between 'ref.lid' and 'data.id' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected resource with local ID 'local-1' in 'data.lid', instead of '12345678' in 'data.id'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Resource identity mismatch between 'ref.lid' and 'data.id' element."); + error.Detail.Should().Be("Expected resource with local ID 'local-1' in 'data.lid', instead of '12345678' in 'data.id'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1166,7 +1191,7 @@ public async Task Cannot_update_resource_for_unknown_type() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1175,10 +1200,12 @@ public async Task Cannot_update_resource_for_unknown_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1207,7 +1234,7 @@ public async Task Cannot_update_resource_for_unknown_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1216,10 +1243,12 @@ public async Task Cannot_update_resource_for_unknown_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'performers' with ID '99999999' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'performers' with ID '99999999' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1252,7 +1281,7 @@ public async Task Cannot_update_resource_for_incompatible_ID() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1261,10 +1290,12 @@ public async Task Cannot_update_resource_for_incompatible_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1299,7 +1330,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1308,10 +1339,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Changing the value of the requested attribute is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Changing the value of 'createdAt' is not allowed."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Changing the value of the requested attribute is not allowed."); + error.Detail.Should().Be("Changing the value of 'createdAt' is not allowed."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1346,7 +1379,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1355,10 +1388,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); - responseDocument.Errors[0].Detail.Should().Be("Attribute 'isArchived' is read-only."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); + error.Detail.Should().Be("Attribute 'isArchived' is read-only."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1393,7 +1428,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1402,10 +1437,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -1440,7 +1477,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1449,10 +1486,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'DateTimeOffset'. - Request body:"); - responseDocument.Errors[0].Source.Pointer.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'DateTimeOffset'. - Request body:"); + error.Source.Pointer.Should().BeNull(); } [Fact] @@ -1526,7 +1565,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -1538,11 +1577,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Lyric) .Include(musicTrack => musicTrack.OwnedBy) .Include(musicTrack => musicTrack.Performers) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore trackInDatabase.Title.Should().Be(existingTrack.Title); trackInDatabase.Genre.Should().Be(newGenre); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index 708ad40fbf..f3069f8f07 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -60,7 +60,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -74,7 +74,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var lyricInDatabase = await dbContext.Lyrics .Include(lyric => lyric.Track) - .FirstAsync(lyric => lyric.Id == existingLyric.Id); + .FirstWithIdAsync(existingLyric.Id); lyricInDatabase.Track.Should().BeNull(); @@ -120,7 +120,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -134,7 +134,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Lyric) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Lyric.Should().BeNull(); @@ -180,7 +180,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -194,7 +194,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Should().BeNull(); @@ -243,7 +243,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -257,7 +257,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var lyricInDatabase = await dbContext.Lyrics .Include(lyric => lyric.Track) - .FirstAsync(lyric => lyric.Id == existingLyric.Id); + .FirstWithIdAsync(existingLyric.Id); lyricInDatabase.Track.Id.Should().Be(existingTrack.Id); }); @@ -303,7 +303,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -317,7 +317,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Lyric) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Lyric.Id.Should().Be(existingLyric.Id); }); @@ -363,7 +363,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -377,7 +377,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Id.Should().Be(existingCompany.Id); }); @@ -426,7 +426,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -440,7 +440,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var lyricInDatabase = await dbContext.Lyrics .Include(lyric => lyric.Track) - .FirstAsync(lyric => lyric.Id == existingLyric.Id); + .FirstWithIdAsync(existingLyric.Id); lyricInDatabase.Track.Id.Should().Be(existingTrack.Id); @@ -492,7 +492,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -506,7 +506,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Lyric) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.Lyric.Id.Should().Be(existingLyric.Id); @@ -558,7 +558,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -572,7 +572,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) - .FirstAsync(musicTrack => musicTrack.Id == existingTrack.Id); + .FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Id.Should().Be(existingCompany.Id); @@ -623,7 +623,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -632,10 +632,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); - responseDocument.Errors[0].Detail.Should().Be("Expected single data element for 'lyric' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); + error.Detail.Should().Be("Expected single data element for 'lyric' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -668,7 +670,7 @@ public async Task Cannot_create_for_missing_type_in_relationship_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -677,10 +679,12 @@ public async Task Cannot_create_for_missing_type_in_relationship_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected 'type' element in 'track' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'track' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -714,7 +718,7 @@ public async Task Cannot_create_for_unknown_type_in_relationship_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -723,10 +727,12 @@ public async Task Cannot_create_for_unknown_type_in_relationship_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -759,7 +765,7 @@ public async Task Cannot_create_for_missing_ID_in_relationship_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -768,10 +774,12 @@ public async Task Cannot_create_for_missing_ID_in_relationship_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); + error.Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -806,7 +814,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -815,10 +823,12 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); - responseDocument.Errors[0].Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); + error.Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -860,7 +870,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -869,10 +879,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'lyrics' with ID '99999999' in relationship 'lyric' does not exist."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("A related resource does not exist."); + error.Detail.Should().Be("Related resource of type 'lyrics' with ID '99999999' in relationship 'lyric' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -914,7 +926,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -923,10 +935,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[0]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); + error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Car.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Car.cs index 70591f317f..eccd7acda0 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Car.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Car.cs @@ -1,10 +1,12 @@ using System; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Car : Identifiable { [NotMapped] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs index 4251351818..2da565d34e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources; @@ -16,7 +17,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys /// /// This enables queries to use , which is not mapped in the database. /// - public sealed class CarExpressionRewriter : QueryExpressionRewriter + internal sealed class CarExpressionRewriter : QueryExpressionRewriter { private readonly AttrAttribute _regionIdAttribute; private readonly AttrAttribute _licensePlateAttribute; @@ -47,7 +48,7 @@ public override QueryExpression VisitComparison(ComparisonExpression expression, throw new NotSupportedException("Only equality comparisons are possible on Car IDs."); } - return RewriteFilterOnCarStringIds(leftChain, new[] {rightConstant.Value}); + return RewriteFilterOnCarStringIds(leftChain, rightConstant.Value.AsEnumerable()); } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarRepository.cs index d266ae4ae1..a167ae4fff 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarRepository.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; @@ -9,6 +10,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class CarRepository : EntityFrameworkCoreRepository { private readonly IResourceGraph _resourceGraph; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeDbContext.cs index 0f64043bd8..602dd90e15 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeDbContext.cs @@ -1,7 +1,11 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +// @formatter:wrap_chained_method_calls chop_always + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class CompositeDbContext : DbContext { public DbSet Cars { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs index ac382e46ae..b8a74e6232 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs @@ -48,7 +48,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/cars?filter=any(id,'123:AA-BB-11','999:XX-YY-22')"; + const string route = "/cars?filter=any(id,'123:AA-BB-11','999:XX-YY-22')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -106,7 +106,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/cars?sort=id"; + const string route = "/cars?sort=id"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -135,7 +135,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/cars?fields[cars]=id"; + const string route = "/cars?fields[cars]=id"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -169,7 +169,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/cars"; + const string route = "/cars"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -245,7 +245,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var engineInDatabase = await dbContext.Engines .Include(engine => engine.Car) - .FirstAsync(engine => engine.Id == existingEngine.Id); + .FirstWithIdAsync(existingEngine.Id); engineInDatabase.Car.Should().NotBeNull(); engineInDatabase.Car.Id.Should().Be(existingCar.StringId); @@ -303,7 +303,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var engineInDatabase = await dbContext.Engines .Include(engine => engine.Car) - .FirstAsync(engine => engine.Id == existingEngine.Id); + .FirstWithIdAsync(existingEngine.Id); engineInDatabase.Car.Should().BeNull(); }); @@ -364,7 +364,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var dealershipInDatabase = await dbContext.Dealerships .Include(dealership => dealership.Inventory) - .FirstOrDefaultAsync(dealership => dealership.Id == existingDealership.Id); + .FirstWithIdOrDefaultAsync(existingDealership.Id); dealershipInDatabase.Inventory.Should().HaveCount(1); dealershipInDatabase.Inventory.Should().ContainSingle(car => car.Id == existingDealership.Inventory.ElementAt(1).Id); @@ -418,7 +418,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var dealershipInDatabase = await dbContext.Dealerships .Include(dealership => dealership.Inventory) - .FirstOrDefaultAsync(dealership => dealership.Id == existingDealership.Id); + .FirstWithIdOrDefaultAsync(existingDealership.Id); dealershipInDatabase.Inventory.Should().HaveCount(1); dealershipInDatabase.Inventory.Should().ContainSingle(car => car.Id == existingCar.Id); @@ -491,7 +491,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var dealershipInDatabase = await dbContext.Dealerships .Include(dealership => dealership.Inventory) - .FirstOrDefaultAsync(dealership => dealership.Id == existingDealership.Id); + .FirstWithIdOrDefaultAsync(existingDealership.Id); dealershipInDatabase.Inventory.Should().HaveCount(2); dealershipInDatabase.Inventory.Should().ContainSingle(car => car.Id == existingCar.Id); @@ -536,9 +536,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'cars' with ID '999:XX-YY-22' in relationship 'inventory' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("A related resource does not exist."); + error.Detail.Should().Be("Related resource of type 'cars' with ID '999:XX-YY-22' in relationship 'inventory' does not exist."); } [Fact] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Dealership.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Dealership.cs index b8c845dc7c..8ac8d4e50d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Dealership.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Dealership.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Dealership : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Engine.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Engine.cs index 33ecaf4b6c..b58c3b53ec 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Engine.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Engine.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Engine : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs index f1f9cbbb89..a892129ddc 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs @@ -26,7 +26,7 @@ public AcceptHeaderTests(ExampleIntegrationTestContext(route); @@ -58,7 +58,7 @@ public async Task Returns_JsonApi_ContentType_header_with_AtomicOperations_exten } }; - var route = "/operations"; + const string route = "/operations"; // Act var (httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); @@ -84,8 +84,8 @@ public async Task Denies_unknown_ContentType_header() } }; - var route = "/policies"; - var contentType = "text/html"; + const string route = "/policies"; + const string contentType = "text/html"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); @@ -94,9 +94,11 @@ public async Task Denies_unknown_ContentType_header() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); - responseDocument.Errors[0].Title.Should().Be("The specified Content-Type header value is not supported."); - responseDocument.Errors[0].Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'text/html' for the Content-Type header value."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); + error.Title.Should().Be("The specified Content-Type header value is not supported."); + error.Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'text/html' for the Content-Type header value."); } [Fact] @@ -115,10 +117,11 @@ public async Task Permits_JsonApi_ContentType_header() } }; - var route = "/policies"; - var contentType = HeaderConstants.MediaType; + const string route = "/policies"; + const string contentType = HeaderConstants.MediaType; // Act + // ReSharper disable once RedundantArgumentDefaultValue var (httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert @@ -148,8 +151,8 @@ public async Task Permits_JsonApi_ContentType_header_with_AtomicOperations_exten } }; - var route = "/operations"; - var contentType = HeaderConstants.AtomicOperationsMediaType; + const string route = "/operations"; + const string contentType = HeaderConstants.AtomicOperationsMediaType; // Act var (httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); @@ -174,8 +177,8 @@ public async Task Denies_JsonApi_ContentType_header_with_profile() } }; - var route = "/policies"; - var contentType = HeaderConstants.MediaType + "; profile=something"; + const string route = "/policies"; + const string contentType = HeaderConstants.MediaType + "; profile=something"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); @@ -184,9 +187,11 @@ public async Task Denies_JsonApi_ContentType_header_with_profile() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); - responseDocument.Errors[0].Title.Should().Be("The specified Content-Type header value is not supported."); - responseDocument.Errors[0].Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; profile=something' for the Content-Type header value."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); + error.Title.Should().Be("The specified Content-Type header value is not supported."); + error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); } [Fact] @@ -205,8 +210,8 @@ public async Task Denies_JsonApi_ContentType_header_with_extension() } }; - var route = "/policies"; - var contentType = HeaderConstants.MediaType + "; ext=something"; + const string route = "/policies"; + const string contentType = HeaderConstants.MediaType + "; ext=something"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); @@ -215,9 +220,11 @@ public async Task Denies_JsonApi_ContentType_header_with_extension() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); - responseDocument.Errors[0].Title.Should().Be("The specified Content-Type header value is not supported."); - responseDocument.Errors[0].Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; ext=something' for the Content-Type header value."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); + error.Title.Should().Be("The specified Content-Type header value is not supported."); + error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); } [Fact] @@ -236,8 +243,8 @@ public async Task Denies_JsonApi_ContentType_header_with_AtomicOperations_extens } }; - var route = "/policies"; - var contentType = HeaderConstants.AtomicOperationsMediaType; + const string route = "/policies"; + const string contentType = HeaderConstants.AtomicOperationsMediaType; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); @@ -246,9 +253,11 @@ public async Task Denies_JsonApi_ContentType_header_with_AtomicOperations_extens httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); - responseDocument.Errors[0].Title.Should().Be("The specified Content-Type header value is not supported."); - responseDocument.Errors[0].Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; ext=\"https://jsonapi.org/ext/atomic\"' for the Content-Type header value."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); + error.Title.Should().Be("The specified Content-Type header value is not supported."); + error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); } [Fact] @@ -267,8 +276,8 @@ public async Task Denies_JsonApi_ContentType_header_with_CharSet() } }; - var route = "/policies"; - var contentType = HeaderConstants.MediaType + "; charset=ISO-8859-4"; + const string route = "/policies"; + const string contentType = HeaderConstants.MediaType + "; charset=ISO-8859-4"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); @@ -277,9 +286,11 @@ public async Task Denies_JsonApi_ContentType_header_with_CharSet() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); - responseDocument.Errors[0].Title.Should().Be("The specified Content-Type header value is not supported."); - responseDocument.Errors[0].Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; charset=ISO-8859-4' for the Content-Type header value."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); + error.Title.Should().Be("The specified Content-Type header value is not supported."); + error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); } [Fact] @@ -298,8 +309,8 @@ public async Task Denies_JsonApi_ContentType_header_with_unknown_parameter() } }; - var route = "/policies"; - var contentType = HeaderConstants.MediaType + "; unknown=unexpected"; + const string route = "/policies"; + const string contentType = HeaderConstants.MediaType + "; unknown=unexpected"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); @@ -308,9 +319,11 @@ public async Task Denies_JsonApi_ContentType_header_with_unknown_parameter() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); - responseDocument.Errors[0].Title.Should().Be("The specified Content-Type header value is not supported."); - responseDocument.Errors[0].Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; unknown=unexpected' for the Content-Type header value."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); + error.Title.Should().Be("The specified Content-Type header value is not supported."); + error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); } [Fact] @@ -336,19 +349,24 @@ public async Task Denies_JsonApi_ContentType_header_at_operations_endpoint() } }; - var route = "/operations"; - var contentType = HeaderConstants.MediaType; + const string route = "/operations"; + const string contentType = HeaderConstants.MediaType; // Act + // ReSharper disable once RedundantArgumentDefaultValue var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); - responseDocument.Errors[0].Title.Should().Be("The specified Content-Type header value is not supported."); - responseDocument.Errors[0].Detail.Should().Be("Please specify 'application/vnd.api+json; ext=\"https://jsonapi.org/ext/atomic\"' instead of 'application/vnd.api+json' for the Content-Type header value."); + + string detail = $"Please specify '{HeaderConstants.AtomicOperationsMediaType}' instead of '{contentType}' for the Content-Type header value."; + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); + error.Title.Should().Be("The specified Content-Type header value is not supported."); + error.Detail.Should().Be(detail); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/Policy.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/Policy.cs index 3a09cce5e6..155bcea4a4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/Policy.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/Policy.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ContentNegotiation { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Policy : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PolicyDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PolicyDbContext.cs index 4402f859c8..f5481e1865 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PolicyDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PolicyDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ContentNegotiation { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class PolicyDbContext : DbContext { public DbSet Policies { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ActionResultDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ActionResultDbContext.cs index 4db7d81157..9407c8f255 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ActionResultDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ActionResultDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ControllerActionResults { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ActionResultDbContext : DbContext { public DbSet Toothbrushes { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs index 87c091a597..daffab38a9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs @@ -46,7 +46,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Converts_empty_ActionResult_to_error_collection() { // Arrange - var route = "/toothbrushes/" + BaseToothbrushesController._emptyActionResultId; + var route = "/toothbrushes/" + BaseToothbrushesController.EmptyActionResultId; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -55,16 +55,18 @@ public async Task Converts_empty_ActionResult_to_error_collection() httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("NotFound"); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("NotFound"); + error.Detail.Should().BeNull(); } [Fact] public async Task Converts_ActionResult_with_error_object_to_error_collection() { // Arrange - var route = "/toothbrushes/" + BaseToothbrushesController._actionResultWithErrorObjectId; + var route = "/toothbrushes/" + BaseToothbrushesController.ActionResultWithErrorObjectId; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -73,16 +75,18 @@ public async Task Converts_ActionResult_with_error_object_to_error_collection() httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("No toothbrush with that ID exists."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("No toothbrush with that ID exists."); + error.Detail.Should().BeNull(); } [Fact] public async Task Cannot_convert_ActionResult_with_string_parameter_to_error_collection() { // Arrange - var route = "/toothbrushes/" + BaseToothbrushesController._actionResultWithStringParameter; + var route = "/toothbrushes/" + BaseToothbrushesController.ActionResultWithStringParameter; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -91,16 +95,18 @@ public async Task Cannot_convert_ActionResult_with_string_parameter_to_error_col httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.InternalServerError); - responseDocument.Errors[0].Title.Should().Be("An unhandled error occurred while processing this request."); - responseDocument.Errors[0].Detail.Should().Be("Data being returned must be errors or resources."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + error.Title.Should().Be("An unhandled error occurred while processing this request."); + error.Detail.Should().Be("Data being returned must be errors or resources."); } [Fact] public async Task Converts_ObjectResult_with_error_object_to_error_collection() { // Arrange - var route = "/toothbrushes/" + BaseToothbrushesController._objectResultWithErrorObjectId; + var route = "/toothbrushes/" + BaseToothbrushesController.ObjectResultWithErrorObjectId; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -109,16 +115,18 @@ public async Task Converts_ObjectResult_with_error_object_to_error_collection() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadGateway); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadGateway); - responseDocument.Errors[0].Title.Should().BeNull(); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadGateway); + error.Title.Should().BeNull(); + error.Detail.Should().BeNull(); } [Fact] public async Task Converts_ObjectResult_with_error_objects_to_error_collection() { // Arrange - var route = "/toothbrushes/" + BaseToothbrushesController._objectResultWithErrorCollectionId; + var route = "/toothbrushes/" + BaseToothbrushesController.ObjectResultWithErrorCollectionId; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -128,17 +136,20 @@ public async Task Converts_ObjectResult_with_error_objects_to_error_collection() responseDocument.Errors.Should().HaveCount(3); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.PreconditionFailed); - responseDocument.Errors[0].Title.Should().BeNull(); - responseDocument.Errors[0].Detail.Should().BeNull(); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.PreconditionFailed); + error1.Title.Should().BeNull(); + error1.Detail.Should().BeNull(); - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.Unauthorized); - responseDocument.Errors[1].Title.Should().BeNull(); - responseDocument.Errors[1].Detail.Should().BeNull(); + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + error2.Title.Should().BeNull(); + error2.Detail.Should().BeNull(); - responseDocument.Errors[2].StatusCode.Should().Be(HttpStatusCode.ExpectationFailed); - responseDocument.Errors[2].Title.Should().Be("This is not a very great request."); - responseDocument.Errors[2].Detail.Should().BeNull(); + var error3 = responseDocument.Errors[2]; + error3.StatusCode.Should().Be(HttpStatusCode.ExpectationFailed); + error3.Title.Should().Be("This is not a very great request."); + error3.Detail.Should().BeNull(); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs index 0b03712d62..e8d6f252f4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs @@ -12,11 +12,11 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ControllerActionResults { public abstract class BaseToothbrushesController : BaseJsonApiController { - public const int _emptyActionResultId = 11111111; - public const int _actionResultWithErrorObjectId = 22222222; - public const int _actionResultWithStringParameter = 33333333; - public const int _objectResultWithErrorObjectId = 44444444; - public const int _objectResultWithErrorCollectionId = 55555555; + internal const int EmptyActionResultId = 11111111; + internal const int ActionResultWithErrorObjectId = 22222222; + internal const int ActionResultWithStringParameter = 33333333; + internal const int ObjectResultWithErrorObjectId = 44444444; + internal const int ObjectResultWithErrorCollectionId = 55555555; protected BaseToothbrushesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) @@ -26,12 +26,12 @@ protected BaseToothbrushesController(IJsonApiOptions options, ILoggerFactory log public override async Task GetAsync(int id, CancellationToken cancellationToken) { - if (id == _emptyActionResultId) + if (id == EmptyActionResultId) { return NotFound(); } - if (id == _actionResultWithErrorObjectId) + if (id == ActionResultWithErrorObjectId) { return NotFound(new Error(HttpStatusCode.NotFound) { @@ -39,17 +39,17 @@ public override async Task GetAsync(int id, CancellationToken can }); } - if (id == _actionResultWithStringParameter) + if (id == ActionResultWithStringParameter) { return Conflict("Something went wrong."); } - if (id == _objectResultWithErrorObjectId) + if (id == ObjectResultWithErrorObjectId) { return Error(new Error(HttpStatusCode.BadGateway)); } - if (id == _objectResultWithErrorCollectionId) + if (id == ObjectResultWithErrorCollectionId) { var errors = new[] { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/Toothbrush.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/Toothbrush.cs index be88ad4f4e..3a9f69ec1f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/Toothbrush.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/Toothbrush.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ControllerActionResults { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Toothbrush : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs index f124fe6ae6..494a9b66e0 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs @@ -22,7 +22,7 @@ public ApiControllerAttributeTests(ExampleIntegrationTestContext(route); @@ -31,7 +31,9 @@ public async Task ApiController_attribute_transforms_NotFound_action_result_with httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].Links.About.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.4"); + + var error = responseDocument.Errors[0]; + error.Links.About.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.4"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/Civilian.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/Civilian.cs index c5f75a2c38..65b3acedc7 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/Civilian.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/Civilian.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CustomRoutes { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Civilian : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CiviliansController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CiviliansController.cs index dd2df9b6d6..e8a730e15c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CiviliansController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CiviliansController.cs @@ -9,7 +9,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CustomRoutes { [ApiController] - [DisableRoutingConvention, Route("world-civilians")] + [DisableRoutingConvention] + [Route("world-civilians")] public sealed class CiviliansController : JsonApiController { public CiviliansController(IJsonApiOptions options, ILoggerFactory loggerFactory, diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteDbContext.cs index 4b76077d27..d368ca14b4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CustomRoutes { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class CustomRouteDbContext : DbContext { public DbSet Towns { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteFakers.cs index 0fa5c5d008..2c15eb3e30 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteFakers.cs @@ -2,6 +2,9 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CustomRoutes { internal sealed class CustomRouteFakers : FakerContainer diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs index 5978ae3b4c..aaebbce763 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs @@ -12,6 +12,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CustomRoutes public sealed class CustomRouteTests : IClassFixture, CustomRouteDbContext>> { + private const string HostPrefix = "http://localhost"; + private readonly ExampleIntegrationTestContext, CustomRouteDbContext> _testContext; private readonly CustomRouteFakers _fakers = new CustomRouteFakers(); @@ -46,10 +48,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.SingleData.Attributes["name"].Should().Be(town.Name); responseDocument.SingleData.Attributes["latitude"].Should().Be(town.Latitude); responseDocument.SingleData.Attributes["longitude"].Should().Be(town.Longitude); - responseDocument.SingleData.Relationships["civilians"].Links.Self.Should().Be($"http://localhost/world-api/civilization/popular/towns/{town.Id}/relationships/civilians"); - responseDocument.SingleData.Relationships["civilians"].Links.Related.Should().Be($"http://localhost/world-api/civilization/popular/towns/{town.Id}/civilians"); - responseDocument.SingleData.Links.Self.Should().Be($"http://localhost/world-api/civilization/popular/towns/{town.Id}"); - responseDocument.Links.Self.Should().Be($"http://localhost/world-api/civilization/popular/towns/{town.Id}"); + responseDocument.SingleData.Relationships["civilians"].Links.Self.Should().Be(HostPrefix + route + "/relationships/civilians"); + responseDocument.SingleData.Relationships["civilians"].Links.Related.Should().Be(HostPrefix + route + "/civilians"); + responseDocument.SingleData.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be(HostPrefix + route); } [Fact] @@ -65,7 +67,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/world-api/civilization/popular/towns/largest-5"; + const string route = "/world-api/civilization/popular/towns/largest-5"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/Town.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/Town.cs index 0f2dc93926..ba0ba27fe2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/Town.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/Town.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CustomRoutes { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Town : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/TownsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/TownsController.cs index af390bd683..a39b9c96c9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/TownsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/TownsController.cs @@ -11,7 +11,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CustomRoutes { - [DisableRoutingConvention, Route("world-api/civilization/popular/towns")] + [DisableRoutingConvention] + [Route("world-api/civilization/popular/towns")] public sealed class TownsController : JsonApiController { private readonly CustomRouteDbContext _dbContext; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Building.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Building.cs index ff68b9a0c5..79af5eac30 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Building.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Building.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Building : Identifiable { private string _tempPrimaryDoorColor; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingRepository.cs index e8fd8644d5..78501d46b2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingRepository.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Repositories; @@ -9,6 +10,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class BuildingRepository : EntityFrameworkCoreRepository { public BuildingRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/City.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/City.cs index 2fbff2cf67..ec8c85b56a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/City.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/City.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class City : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Door.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Door.cs index 919d0a1907..068c06414c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Door.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Door.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Door { public int Id { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingDbContext.cs index fb60ddb51e..efce1b3014 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingDbContext.cs @@ -1,7 +1,11 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +// @formatter:wrap_chained_method_calls chop_always + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class EagerLoadingDbContext : DbContext { public DbSet States { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingFakers.cs index 80ae5c9d14..5910102644 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingFakers.cs @@ -2,6 +2,9 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading { internal sealed class EagerLoadingFakers : FakerContainer diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs index 76d7d9a972..2acabc76c4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs @@ -224,7 +224,7 @@ public async Task Can_create_resource() } }; - var route = "/buildings"; + const string route = "/buildings"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -238,15 +238,21 @@ public async Task Can_create_resource() responseDocument.SingleData.Attributes["primaryDoorColor"].Should().BeNull(); responseDocument.SingleData.Attributes["secondaryDoorColor"].Should().BeNull(); - var newId = int.Parse(responseDocument.SingleData.Id); + var newBuildingId = int.Parse(responseDocument.SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var buildingInDatabase = await dbContext.Buildings .Include(building => building.PrimaryDoor) .Include(building => building.SecondaryDoor) .Include(building => building.Windows) - .FirstOrDefaultAsync(building => building.Id == newId); + .FirstWithIdOrDefaultAsync(newBuildingId); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore buildingInDatabase.Should().NotBeNull(); buildingInDatabase.Number.Should().Be(newBuilding.Number); @@ -300,11 +306,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var buildingInDatabase = await dbContext.Buildings .Include(building => building.PrimaryDoor) .Include(building => building.SecondaryDoor) .Include(building => building.Windows) - .FirstOrDefaultAsync(building => building.Id == existingBuilding.Id); + .FirstWithIdOrDefaultAsync(existingBuilding.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore buildingInDatabase.Should().NotBeNull(); buildingInDatabase.Number.Should().Be(newBuildingNumber); @@ -340,8 +352,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var buildingInDatabase = await dbContext.Buildings - .FirstOrDefaultAsync(building => building.Id == existingBuilding.Id); + var buildingInDatabase = await dbContext.Buildings.FirstWithIdOrDefaultAsync(existingBuilding.Id); buildingInDatabase.Should().BeNull(); }); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/State.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/State.cs index 28fb189d05..99e46ff0a3 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/State.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/State.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class State : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Street.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Street.cs index 7e275d03a2..6ea7264c67 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Street.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Street.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Street : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Window.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Window.cs index 85a10775e2..88dd4a6896 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Window.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Window.cs @@ -1,5 +1,8 @@ -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading +using JetBrains.Annotations; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Window { public int Id { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticle.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticle.cs index 320355edfa..a7c2b686e8 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticle.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticle.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ExceptionHandling { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ConsumerArticle : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleIsNoLongerAvailableException.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleIsNoLongerAvailableException.cs index f4420dea90..bae3760b76 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleIsNoLongerAvailableException.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleIsNoLongerAvailableException.cs @@ -4,9 +4,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ExceptionHandling { - public sealed class ConsumerArticleIsNoLongerAvailableException : JsonApiException + internal sealed class ConsumerArticleIsNoLongerAvailableException : JsonApiException { - public string ArticleCode { get; } public string SupportEmailAddress { get; } public ConsumerArticleIsNoLongerAvailableException(string articleCode, string supportEmailAddress) @@ -16,7 +15,6 @@ public ConsumerArticleIsNoLongerAvailableException(string articleCode, string su Detail = $"Article with code '{articleCode}' is no longer available." }) { - ArticleCode = articleCode; SupportEmailAddress = supportEmailAddress; } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs index 2d867ea0f7..7013299c6a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs @@ -1,5 +1,7 @@ +using System; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Hooks; using JsonApiDotNetCore.Middleware; @@ -11,11 +13,12 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ExceptionHandling { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class ConsumerArticleService : JsonApiResourceService { - public const string UnavailableArticlePrefix = "X"; + internal const string UnavailableArticlePrefix = "X"; - private const string _supportEmailAddress = "company@email.com"; + private const string SupportEmailAddress = "company@email.com"; public ConsumerArticleService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, @@ -30,9 +33,9 @@ public override async Task GetAsync(int id, CancellationToken c { var consumerArticle = await base.GetAsync(id, cancellationToken); - if (consumerArticle.Code.StartsWith(UnavailableArticlePrefix)) + if (consumerArticle.Code.StartsWith(UnavailableArticlePrefix, StringComparison.Ordinal)) { - throw new ConsumerArticleIsNoLongerAvailableException(consumerArticle.Code, _supportEmailAddress); + throw new ConsumerArticleIsNoLongerAvailableException(consumerArticle.Code, SupportEmailAddress); } return consumerArticle; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ErrorDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ErrorDbContext.cs index 9e4d0feab8..ff9abd6509 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ErrorDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ErrorDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ExceptionHandling { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ErrorDbContext : DbContext { public DbSet ConsumerArticles { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs index 6b1dd941ed..b86bd84787 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs @@ -76,10 +76,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Gone); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Gone); - responseDocument.Errors[0].Title.Should().Be("The requested article is no longer available."); - responseDocument.Errors[0].Detail.Should().Be("Article with code 'X123' is no longer available."); - responseDocument.Errors[0].Meta.Data["support"].Should().Be("Please contact us for info about similar articles at company@email.com."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Gone); + error.Title.Should().Be("The requested article is no longer available."); + error.Detail.Should().Be("Article with code 'X123' is no longer available."); + error.Meta.Data["support"].Should().Be("Please contact us for info about similar articles at company@email.com."); loggerFactory.Logger.Messages.Should().HaveCount(1); loggerFactory.Logger.Messages.Single().LogLevel.Should().Be(LogLevel.Warning); @@ -110,13 +112,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.InternalServerError); - responseDocument.Errors[0].Title.Should().Be("An unhandled error occurred while processing this request."); - responseDocument.Errors[0].Detail.Should().Be("Exception has been thrown by the target of an invocation."); - var stackTraceLines = - ((JArray) responseDocument.Errors[0].Meta.Data["stackTrace"]).Select(token => token.Value()); + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + error.Title.Should().Be("An unhandled error occurred while processing this request."); + error.Detail.Should().Be("Exception has been thrown by the target of an invocation."); + var stackTraceLines = ((JArray) error.Meta.Data["stackTrace"]).Select(token => token.Value()); stackTraceLines.Should().ContainMatch("* System.InvalidOperationException: Article status could not be determined.*"); loggerFactory.Logger.Messages.Should().HaveCount(1); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ThrowingArticle.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ThrowingArticle.cs index af4f7890ac..fe2d61c6d6 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ThrowingArticle.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ThrowingArticle.cs @@ -1,10 +1,12 @@ using System; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ExceptionHandling { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ThrowingArticle : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGallery.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGallery.cs index 470b7c4e63..b3fd2d7317 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGallery.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGallery.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ArtGallery : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingDbContext.cs index 03795fe1cf..82d77b2b16 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class HostingDbContext : DbContext { public DbSet ArtGalleries { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingStartup.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingStartup.cs index 8d3da5f0e3..2b369b3c6e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingStartup.cs @@ -1,19 +1,16 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCoreExampleTests.Startups; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class HostingStartup : TestableStartup where TDbContext : DbContext { - public HostingStartup(IConfiguration configuration) : base(configuration) - { - } - protected override void SetJsonApiOptions(JsonApiOptions options) { base.SetJsonApiOptions(options); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/Painting.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/Painting.cs index 75979bf30c..2b4c3d5d4f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/Painting.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/Painting.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Painting : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/BankAccount.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/BankAccount.cs index d2f33c7dc7..da98000a04 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/BankAccount.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/BankAccount.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class BankAccount : ObfuscatedIdentifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/DebitCard.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/DebitCard.cs index 9bd4bcc789..2c0a12fd30 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/DebitCard.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/DebitCard.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class DebitCard : ObfuscatedIdentifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs index 9a73263aa2..cc563b5f4b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Globalization; using System.Net; @@ -16,7 +17,7 @@ public static int Decode(string value) return 0; } - if (!value.StartsWith("x")) + if (!value.StartsWith("x", StringComparison.Ordinal)) { throw new JsonApiException(new Error(HttpStatusCode.BadRequest) { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs index 9b5bde276a..71e130bb4b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs @@ -24,17 +24,16 @@ public IdObfuscationTests(ExampleIntegrationTestContext { await dbContext.ClearTableAsync(); - dbContext.BankAccounts.AddRange(bankAccounts); - + dbContext.BankAccounts.AddRange(accounts); await dbContext.SaveChangesAsync(); }); - var route = $"/bankAccounts?filter=equals(id,'{bankAccounts[1].StringId}')"; + var route = $"/bankAccounts?filter=equals(id,'{accounts[1].StringId}')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -43,24 +42,23 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(bankAccounts[1].StringId); + responseDocument.ManyData[0].Id.Should().Be(accounts[1].StringId); } [Fact] public async Task Can_filter_any_in_primary_resources() { // Arrange - var bankAccounts = _fakers.BankAccount.Generate(2); + var accounts = _fakers.BankAccount.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); - dbContext.BankAccounts.AddRange(bankAccounts); - + dbContext.BankAccounts.AddRange(accounts); await dbContext.SaveChangesAsync(); }); - var route = $"/bankAccounts?filter=any(id,'{bankAccounts[1].StringId}','{HexadecimalCodec.Encode(99999999)}')"; + var route = $"/bankAccounts?filter=any(id,'{accounts[1].StringId}','{HexadecimalCodec.Encode(99999999)}')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -69,14 +67,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(bankAccounts[1].StringId); + responseDocument.ManyData[0].Id.Should().Be(accounts[1].StringId); } [Fact] public async Task Cannot_get_primary_resource_for_invalid_ID() { // Arrange - var route = "/bankAccounts/not-a-hex-value"; + const string route = "/bankAccounts/not-a-hex-value"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -85,24 +83,26 @@ public async Task Cannot_get_primary_resource_for_invalid_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Invalid ID value."); - responseDocument.Errors[0].Detail.Should().Be("The value 'not-a-hex-value' is not a valid hexadecimal value."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Invalid ID value."); + error.Detail.Should().Be("The value 'not-a-hex-value' is not a valid hexadecimal value."); } [Fact] public async Task Can_get_primary_resource_by_ID() { // Arrange - var debitCard = _fakers.DebitCard.Generate(); + var card = _fakers.DebitCard.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { - dbContext.DebitCards.Add(debitCard); + dbContext.DebitCards.Add(card); await dbContext.SaveChangesAsync(); }); - var route = "/debitCards/" + debitCard.StringId; + var route = "/debitCards/" + card.StringId; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -111,23 +111,23 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(debitCard.StringId); + responseDocument.SingleData.Id.Should().Be(card.StringId); } [Fact] public async Task Can_get_secondary_resources() { // Arrange - var bankAccount = _fakers.BankAccount.Generate(); - bankAccount.Cards = _fakers.DebitCard.Generate(2); + var account = _fakers.BankAccount.Generate(); + account.Cards = _fakers.DebitCard.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { - dbContext.BankAccounts.Add(bankAccount); + dbContext.BankAccounts.Add(account); await dbContext.SaveChangesAsync(); }); - var route = $"/bankAccounts/{bankAccount.StringId}/cards"; + var route = $"/bankAccounts/{account.StringId}/cards"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -136,25 +136,24 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(bankAccount.Cards[0].StringId); - responseDocument.ManyData[1].Id.Should().Be(bankAccount.Cards[1].StringId); + responseDocument.ManyData[0].Id.Should().Be(account.Cards[0].StringId); + responseDocument.ManyData[1].Id.Should().Be(account.Cards[1].StringId); } [Fact] public async Task Can_include_resource_with_sparse_fieldset() { // Arrange - var bankAccount = _fakers.BankAccount.Generate(); - bankAccount.Cards = _fakers.DebitCard.Generate(1); + var account = _fakers.BankAccount.Generate(); + account.Cards = _fakers.DebitCard.Generate(1); await _testContext.RunOnDatabaseAsync(async dbContext => { - dbContext.BankAccounts.Add(bankAccount); - + dbContext.BankAccounts.Add(account); await dbContext.SaveChangesAsync(); }); - var route = $"/bankAccounts/{bankAccount.StringId}?include=cards&fields[debitCards]=ownerName"; + var route = $"/bankAccounts/{account.StringId}?include=cards&fields[debitCards]=ownerName"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -163,10 +162,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(bankAccount.StringId); + responseDocument.SingleData.Id.Should().Be(account.StringId); responseDocument.Included.Should().HaveCount(1); - responseDocument.Included[0].Id.Should().Be(bankAccount.Cards[0].StringId); + responseDocument.Included[0].Id.Should().Be(account.Cards[0].StringId); responseDocument.Included[0].Attributes.Should().HaveCount(1); responseDocument.Included[0].Relationships.Should().BeNull(); } @@ -175,16 +174,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_get_relationship() { // Arrange - var bankAccount = _fakers.BankAccount.Generate(); - bankAccount.Cards = _fakers.DebitCard.Generate(1); + var account = _fakers.BankAccount.Generate(); + account.Cards = _fakers.DebitCard.Generate(1); await _testContext.RunOnDatabaseAsync(async dbContext => { - dbContext.BankAccounts.Add(bankAccount); + dbContext.BankAccounts.Add(account); await dbContext.SaveChangesAsync(); }); - var route = $"/bankAccounts/{bankAccount.StringId}/relationships/cards"; + var route = $"/bankAccounts/{account.StringId}/relationships/cards"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -193,19 +192,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(bankAccount.Cards[0].StringId); + responseDocument.ManyData[0].Id.Should().Be(account.Cards[0].StringId); } [Fact] public async Task Can_create_resource_with_relationship() { // Arrange - var existingBankAccount = _fakers.BankAccount.Generate(); - var newDebitCard = _fakers.DebitCard.Generate(); + var existingAccount = _fakers.BankAccount.Generate(); + var newCard = _fakers.DebitCard.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { - dbContext.BankAccounts.Add(existingBankAccount); + dbContext.BankAccounts.Add(existingAccount); await dbContext.SaveChangesAsync(); }); @@ -216,8 +215,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => type = "debitCards", attributes = new { - ownerName = newDebitCard.OwnerName, - pinCode = newDebitCard.PinCode + ownerName = newCard.OwnerName, + pinCode = newCard.PinCode }, relationships = new { @@ -226,14 +225,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "bankAccounts", - id = existingBankAccount.StringId + id = existingAccount.StringId } } } } }; - - var route = "/debitCards"; + + const string route = "/debitCards"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -241,23 +240,23 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Attributes["ownerName"].Should().Be(newDebitCard.OwnerName); - responseDocument.SingleData.Attributes["pinCode"].Should().Be(newDebitCard.PinCode); + responseDocument.SingleData.Attributes["ownerName"].Should().Be(newCard.OwnerName); + responseDocument.SingleData.Attributes["pinCode"].Should().Be(newCard.PinCode); - var newDebitCardId = HexadecimalCodec.Decode(responseDocument.SingleData.Id); + var newCardId = HexadecimalCodec.Decode(responseDocument.SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var debitCardInDatabase = await dbContext.DebitCards - .Include(debitCard => debitCard.Account) - .FirstAsync(debitCard => debitCard.Id == newDebitCardId); + var cardInDatabase = await dbContext.DebitCards + .Include(card => card.Account) + .FirstWithIdAsync(newCardId); - debitCardInDatabase.OwnerName.Should().Be(newDebitCard.OwnerName); - debitCardInDatabase.PinCode.Should().Be(newDebitCard.PinCode); + cardInDatabase.OwnerName.Should().Be(newCard.OwnerName); + cardInDatabase.PinCode.Should().Be(newCard.PinCode); - debitCardInDatabase.Account.Should().NotBeNull(); - debitCardInDatabase.Account.Id.Should().Be(existingBankAccount.Id); - debitCardInDatabase.Account.StringId.Should().Be(existingBankAccount.StringId); + cardInDatabase.Account.Should().NotBeNull(); + cardInDatabase.Account.Id.Should().Be(existingAccount.Id); + cardInDatabase.Account.StringId.Should().Be(existingAccount.StringId); }); } @@ -265,16 +264,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_with_relationship() { // Arrange - var existingBankAccount = _fakers.BankAccount.Generate(); - existingBankAccount.Cards = _fakers.DebitCard.Generate(1); + var existingAccount = _fakers.BankAccount.Generate(); + existingAccount.Cards = _fakers.DebitCard.Generate(1); - var existingDebitCard = _fakers.DebitCard.Generate(); + var existingCard = _fakers.DebitCard.Generate(); var newIban = _fakers.BankAccount.Generate().Iban; await _testContext.RunOnDatabaseAsync(async dbContext => { - dbContext.AddRange(existingBankAccount, existingDebitCard); + dbContext.AddRange(existingAccount, existingCard); await dbContext.SaveChangesAsync(); }); @@ -283,7 +282,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "bankAccounts", - id = existingBankAccount.StringId, + id = existingAccount.StringId, attributes = new { iban = newIban @@ -297,7 +296,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "debitCards", - id = existingDebitCard.StringId + id = existingCard.StringId } } } @@ -305,7 +304,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/bankAccounts/" + existingBankAccount.StringId; + var route = "/bankAccounts/" + existingAccount.StringId; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -317,15 +316,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var bankAccountInDatabase = await dbContext.BankAccounts - .Include(bankAccount => bankAccount.Cards) - .FirstAsync(bankAccount => bankAccount.Id == existingBankAccount.Id); + var accountInDatabase = await dbContext.BankAccounts + .Include(account => account.Cards) + .FirstWithIdAsync(existingAccount.Id); - bankAccountInDatabase.Iban.Should().Be(newIban); + accountInDatabase.Iban.Should().Be(newIban); - bankAccountInDatabase.Cards.Should().HaveCount(1); - bankAccountInDatabase.Cards[0].Id.Should().Be(existingDebitCard.Id); - bankAccountInDatabase.Cards[0].StringId.Should().Be(existingDebitCard.StringId); + accountInDatabase.Cards.Should().HaveCount(1); + accountInDatabase.Cards[0].Id.Should().Be(existingCard.Id); + accountInDatabase.Cards[0].StringId.Should().Be(existingCard.StringId); }); } @@ -334,14 +333,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_add_to_ToMany_relationship() { // Arrange - var existingBankAccount = _fakers.BankAccount.Generate(); - existingBankAccount.Cards = _fakers.DebitCard.Generate(1); + var existingAccount = _fakers.BankAccount.Generate(); + existingAccount.Cards = _fakers.DebitCard.Generate(1); var existingDebitCard = _fakers.DebitCard.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { - dbContext.AddRange(existingBankAccount, existingDebitCard); + dbContext.AddRange(existingAccount, existingDebitCard); await dbContext.SaveChangesAsync(); }); @@ -357,7 +356,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = $"/bankAccounts/{existingBankAccount.StringId}/relationships/cards"; + var route = $"/bankAccounts/{existingAccount.StringId}/relationships/cards"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -369,11 +368,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var bankAccountInDatabase = await dbContext.BankAccounts - .Include(bankAccount => bankAccount.Cards) - .FirstAsync(bankAccount => bankAccount.Id == existingBankAccount.Id); + var accountInDatabase = await dbContext.BankAccounts + .Include(account => account.Cards) + .FirstWithIdAsync(existingAccount.Id); - bankAccountInDatabase.Cards.Should().HaveCount(2); + accountInDatabase.Cards.Should().HaveCount(2); }); } @@ -381,12 +380,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_remove_from_ToMany_relationship() { // Arrange - var existingBankAccount = _fakers.BankAccount.Generate(); - existingBankAccount.Cards = _fakers.DebitCard.Generate(2); + var existingAccount = _fakers.BankAccount.Generate(); + existingAccount.Cards = _fakers.DebitCard.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { - dbContext.BankAccounts.Add(existingBankAccount); + dbContext.BankAccounts.Add(existingAccount); await dbContext.SaveChangesAsync(); }); @@ -397,12 +396,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "debitCards", - id = existingBankAccount.Cards[0].StringId + id = existingAccount.Cards[0].StringId } } }; - var route = $"/bankAccounts/{existingBankAccount.StringId}/relationships/cards"; + var route = $"/bankAccounts/{existingAccount.StringId}/relationships/cards"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); @@ -414,11 +413,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var bankAccountInDatabase = await dbContext.BankAccounts - .Include(bankAccount => bankAccount.Cards) - .FirstAsync(bankAccount => bankAccount.Id == existingBankAccount.Id); + var accountInDatabase = await dbContext.BankAccounts + .Include(account => account.Cards) + .FirstWithIdAsync(existingAccount.Id); - bankAccountInDatabase.Cards.Should().HaveCount(1); + accountInDatabase.Cards.Should().HaveCount(1); }); } @@ -426,16 +425,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_delete_resource() { // Arrange - var existingBankAccount = _fakers.BankAccount.Generate(); - existingBankAccount.Cards = _fakers.DebitCard.Generate(1); + var existingAccount = _fakers.BankAccount.Generate(); + existingAccount.Cards = _fakers.DebitCard.Generate(1); await _testContext.RunOnDatabaseAsync(async dbContext => { - dbContext.BankAccounts.Add(existingBankAccount); + dbContext.BankAccounts.Add(existingAccount); await dbContext.SaveChangesAsync(); }); - var route = "/bankAccounts/" + existingBankAccount.StringId; + var route = "/bankAccounts/" + existingAccount.StringId; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -447,11 +446,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var bankAccountInDatabase = await dbContext.BankAccounts - .Include(bankAccount => bankAccount.Cards) - .FirstOrDefaultAsync(bankAccount => bankAccount.Id == existingBankAccount.Id); + var accountInDatabase = await dbContext.BankAccounts + .Include(account => account.Cards) + .FirstWithIdOrDefaultAsync(existingAccount.Id); - bankAccountInDatabase.Should().BeNull(); + accountInDatabase.Should().BeNull(); }); } @@ -470,10 +469,12 @@ public async Task Cannot_delete_missing_resource() httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be($"Resource of type 'bankAccounts' with ID '{stringId}' does not exist."); - responseDocument.Errors[0].Source.Parameter.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be($"Resource of type 'bankAccounts' with ID '{stringId}' does not exist."); + error.Source.Parameter.Should().BeNull(); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscationDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscationDbContext.cs index 489ba4970c..939bae05bf 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscationDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscationDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ObfuscationDbContext : DbContext { public DbSet BankAccounts { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscationFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscationFakers.cs index 06fd5ac9ab..d6a08114e4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscationFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscationFakers.cs @@ -2,6 +2,9 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation { internal sealed class ObfuscationFakers : FakerContainer diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs index a5fa5e1f26..36ca78da28 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs @@ -15,6 +15,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Links public sealed class AbsoluteLinksWithNamespaceTests : IClassFixture, LinksDbContext>> { + private const string HostPrefix = "http://localhost"; + private readonly ExampleIntegrationTestContext, LinksDbContext> _testContext; private readonly LinksFakers _fakers = new LinksFakers(); @@ -51,7 +53,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"http://localhost/api/photoAlbums/{album.StringId}"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); @@ -59,9 +61,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Next.Should().BeNull(); responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"http://localhost/api/photoAlbums/{album.StringId}"); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be($"http://localhost/api/photoAlbums/{album.StringId}/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be($"http://localhost/api/photoAlbums/{album.StringId}/photos"); + responseDocument.SingleData.Links.Self.Should().Be(HostPrefix + route); + responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(HostPrefix + route + "/relationships/photos"); + responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(HostPrefix + route + "/photos"); } [Fact] @@ -78,7 +80,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/api/photoAlbums?include=photos"; + const string route = "/api/photoAlbums?include=photos"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -86,22 +88,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be("http://localhost/api/photoAlbums?include=photos"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be("http://localhost/api/photoAlbums?include=photos"); - responseDocument.Links.Last.Should().Be("http://localhost/api/photoAlbums?include=photos"); + responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.Last.Should().Be(HostPrefix + route); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string albumLink = HostPrefix + $"/api/photoAlbums/{album.StringId}"; + responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be($"http://localhost/api/photoAlbums/{album.StringId}"); - responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be($"http://localhost/api/photoAlbums/{album.StringId}/relationships/photos"); - responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be($"http://localhost/api/photoAlbums/{album.StringId}/photos"); + responseDocument.ManyData[0].Links.Self.Should().Be(albumLink); + responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + + string photoLink = HostPrefix + $"/api/photos/{album.Photos.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); - responseDocument.Included[0].Links.Self.Should().Be($"http://localhost/api/photos/{album.Photos.ElementAt(0).StringId}"); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"http://localhost/api/photos/{album.Photos.ElementAt(0).StringId}/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"http://localhost/api/photos/{album.Photos.ElementAt(0).StringId}/album"); + responseDocument.Included[0].Links.Self.Should().Be(photoLink); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); } [Fact] @@ -125,17 +131,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"http://localhost/api/photos/{photo.StringId}/album"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string albumLink = HostPrefix + $"/api/photoAlbums/{photo.Album.StringId}"; + responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"http://localhost/api/photoAlbums/{photo.Album.StringId}"); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be($"http://localhost/api/photoAlbums/{photo.Album.StringId}/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be($"http://localhost/api/photoAlbums/{photo.Album.StringId}/photos"); + responseDocument.SingleData.Links.Self.Should().Be(albumLink); + responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); } [Fact] @@ -159,17 +167,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"http://localhost/api/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be($"http://localhost/api/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.First.Should().Be(HostPrefix + route); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string photoLink = HostPrefix + $"/api/photos/{album.Photos.ElementAt(0).StringId}"; + responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be($"http://localhost/api/photos/{album.Photos.ElementAt(0).StringId}"); - responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be($"http://localhost/api/photos/{album.Photos.ElementAt(0).StringId}/relationships/album"); - responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be($"http://localhost/api/photos/{album.Photos.ElementAt(0).StringId}/album"); + responseDocument.ManyData[0].Links.Self.Should().Be(photoLink); + responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); } [Fact] @@ -193,8 +203,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"http://localhost/api/photos/{photo.StringId}/relationships/album"); - responseDocument.Links.Related.Should().Be($"http://localhost/api/photos/{photo.StringId}/album"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Related.Should().Be(HostPrefix + $"/api/photos/{photo.StringId}/album"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); @@ -226,9 +236,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"http://localhost/api/photoAlbums/{album.StringId}/relationships/photos"); - responseDocument.Links.Related.Should().Be($"http://localhost/api/photoAlbums/{album.StringId}/photos"); - responseDocument.Links.First.Should().Be($"http://localhost/api/photoAlbums/{album.StringId}/relationships/photos"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Related.Should().Be(HostPrefix + $"/api/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.First.Should().Be(HostPrefix + route); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); @@ -272,7 +282,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/api/photoAlbums?include=photos"; + const string route = "/api/photoAlbums?include=photos"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -280,24 +290,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.Links.Self.Should().Be("http://localhost/api/photoAlbums?include=photos"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - var newAlbumId = responseDocument.SingleData.Id; + string albumLink = HostPrefix + $"/api/photoAlbums/{responseDocument.SingleData.Id}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"http://localhost/api/photoAlbums/{newAlbumId}"); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be($"http://localhost/api/photoAlbums/{newAlbumId}/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be($"http://localhost/api/photoAlbums/{newAlbumId}/photos"); + responseDocument.SingleData.Links.Self.Should().Be(albumLink); + responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + + string photoLink = HostPrefix + $"/api/photos/{existingPhoto.StringId}"; responseDocument.Included.Should().HaveCount(1); - responseDocument.Included[0].Links.Self.Should().Be($"http://localhost/api/photos/{existingPhoto.StringId}"); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"http://localhost/api/photos/{existingPhoto.StringId}/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"http://localhost/api/photos/{existingPhoto.StringId}/album"); + responseDocument.Included[0].Links.Self.Should().Be(photoLink); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); } [Fact] @@ -341,22 +352,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"http://localhost/api/photos/{existingPhoto.StringId}?include=album"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string photoLink = HostPrefix + $"/api/photos/{existingPhoto.StringId}"; + responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"http://localhost/api/photos/{existingPhoto.StringId}"); - responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be($"http://localhost/api/photos/{existingPhoto.StringId}/relationships/album"); - responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be($"http://localhost/api/photos/{existingPhoto.StringId}/album"); + responseDocument.SingleData.Links.Self.Should().Be(photoLink); + responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + + string albumLink = HostPrefix + $"/api/photoAlbums/{existingAlbum.StringId}"; responseDocument.Included.Should().HaveCount(1); - responseDocument.Included[0].Links.Self.Should().Be($"http://localhost/api/photoAlbums/{existingAlbum.StringId}"); - responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be($"http://localhost/api/photoAlbums/{existingAlbum.StringId}/relationships/photos"); - responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be($"http://localhost/api/photoAlbums/{existingAlbum.StringId}/photos"); + responseDocument.Included[0].Links.Self.Should().Be(albumLink); + responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs index 7de9adbaee..37faa2c92a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs @@ -15,6 +15,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Links public sealed class AbsoluteLinksWithoutNamespaceTests : IClassFixture, LinksDbContext>> { + private const string HostPrefix = "http://localhost"; + private readonly ExampleIntegrationTestContext, LinksDbContext> _testContext; private readonly LinksFakers _fakers = new LinksFakers(); @@ -51,7 +53,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"http://localhost/photoAlbums/{album.StringId}"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); @@ -59,9 +61,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Next.Should().BeNull(); responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"http://localhost/photoAlbums/{album.StringId}"); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be($"http://localhost/photoAlbums/{album.StringId}/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be($"http://localhost/photoAlbums/{album.StringId}/photos"); + responseDocument.SingleData.Links.Self.Should().Be(HostPrefix + route); + responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(HostPrefix + route + "/relationships/photos"); + responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(HostPrefix + route + "/photos"); } [Fact] @@ -78,7 +80,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/photoAlbums?include=photos"; + const string route = "/photoAlbums?include=photos"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -86,22 +88,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be("http://localhost/photoAlbums?include=photos"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be("http://localhost/photoAlbums?include=photos"); - responseDocument.Links.Last.Should().Be("http://localhost/photoAlbums?include=photos"); + responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.Last.Should().Be(HostPrefix + route); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string albumLink = HostPrefix + $"/photoAlbums/{album.StringId}"; + responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be($"http://localhost/photoAlbums/{album.StringId}"); - responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be($"http://localhost/photoAlbums/{album.StringId}/relationships/photos"); - responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be($"http://localhost/photoAlbums/{album.StringId}/photos"); + responseDocument.ManyData[0].Links.Self.Should().Be(albumLink); + responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + + string photoLink = HostPrefix + $"/photos/{album.Photos.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); - responseDocument.Included[0].Links.Self.Should().Be($"http://localhost/photos/{album.Photos.ElementAt(0).StringId}"); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"http://localhost/photos/{album.Photos.ElementAt(0).StringId}/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"http://localhost/photos/{album.Photos.ElementAt(0).StringId}/album"); + responseDocument.Included[0].Links.Self.Should().Be(photoLink); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); } [Fact] @@ -125,17 +131,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"http://localhost/photos/{photo.StringId}/album"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string albumLink = HostPrefix + $"/photoAlbums/{photo.Album.StringId}"; + responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"http://localhost/photoAlbums/{photo.Album.StringId}"); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be($"http://localhost/photoAlbums/{photo.Album.StringId}/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be($"http://localhost/photoAlbums/{photo.Album.StringId}/photos"); + responseDocument.SingleData.Links.Self.Should().Be(albumLink); + responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); } [Fact] @@ -159,17 +167,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"http://localhost/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be($"http://localhost/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.First.Should().Be(HostPrefix + route); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string photoLink = HostPrefix + $"/photos/{album.Photos.ElementAt(0).StringId}"; + responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be($"http://localhost/photos/{album.Photos.ElementAt(0).StringId}"); - responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be($"http://localhost/photos/{album.Photos.ElementAt(0).StringId}/relationships/album"); - responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be($"http://localhost/photos/{album.Photos.ElementAt(0).StringId}/album"); + responseDocument.ManyData[0].Links.Self.Should().Be(photoLink); + responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); } [Fact] @@ -193,8 +203,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"http://localhost/photos/{photo.StringId}/relationships/album"); - responseDocument.Links.Related.Should().Be($"http://localhost/photos/{photo.StringId}/album"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Related.Should().Be(HostPrefix + $"/photos/{photo.StringId}/album"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); @@ -226,9 +236,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"http://localhost/photoAlbums/{album.StringId}/relationships/photos"); - responseDocument.Links.Related.Should().Be($"http://localhost/photoAlbums/{album.StringId}/photos"); - responseDocument.Links.First.Should().Be($"http://localhost/photoAlbums/{album.StringId}/relationships/photos"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Related.Should().Be(HostPrefix + $"/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.First.Should().Be(HostPrefix + route); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); @@ -272,7 +282,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/photoAlbums?include=photos"; + const string route = "/photoAlbums?include=photos"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -280,24 +290,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.Links.Self.Should().Be("http://localhost/photoAlbums?include=photos"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - var newAlbumId = responseDocument.SingleData.Id; + string albumLink = HostPrefix + $"/photoAlbums/{responseDocument.SingleData.Id}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"http://localhost/photoAlbums/{newAlbumId}"); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be($"http://localhost/photoAlbums/{newAlbumId}/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be($"http://localhost/photoAlbums/{newAlbumId}/photos"); + responseDocument.SingleData.Links.Self.Should().Be(albumLink); + responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + + string photoLink = HostPrefix + $"/photos/{existingPhoto.StringId}"; responseDocument.Included.Should().HaveCount(1); - responseDocument.Included[0].Links.Self.Should().Be($"http://localhost/photos/{existingPhoto.StringId}"); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"http://localhost/photos/{existingPhoto.StringId}/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"http://localhost/photos/{existingPhoto.StringId}/album"); + responseDocument.Included[0].Links.Self.Should().Be(photoLink); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); } [Fact] @@ -341,22 +352,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"http://localhost/photos/{existingPhoto.StringId}?include=album"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string photoLink = HostPrefix + $"/photos/{existingPhoto.StringId}"; + responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"http://localhost/photos/{existingPhoto.StringId}"); - responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be($"http://localhost/photos/{existingPhoto.StringId}/relationships/album"); - responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be($"http://localhost/photos/{existingPhoto.StringId}/album"); + responseDocument.SingleData.Links.Self.Should().Be(photoLink); + responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + + string albumLink = HostPrefix + $"/photoAlbums/{existingAlbum.StringId}"; responseDocument.Included.Should().HaveCount(1); - responseDocument.Included[0].Links.Self.Should().Be($"http://localhost/photoAlbums/{existingAlbum.StringId}"); - responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be($"http://localhost/photoAlbums/{existingAlbum.StringId}/relationships/photos"); - responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be($"http://localhost/photoAlbums/{existingAlbum.StringId}/photos"); + responseDocument.Included[0].Links.Self.Should().Be(albumLink); + responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/LinksDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/LinksDbContext.cs index 8f1e638891..1e0a63bfad 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/LinksDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/LinksDbContext.cs @@ -1,7 +1,11 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +// @formatter:wrap_chained_method_calls chop_always + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Links { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class LinksDbContext : DbContext { public DbSet PhotoAlbums { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/LinksFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/LinksFakers.cs index 2bd9552fa4..b0ade264dc 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/LinksFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/LinksFakers.cs @@ -2,6 +2,9 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Links { internal sealed class LinksFakers : FakerContainer diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/Photo.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/Photo.cs index 592d5ccb12..e13d93fd92 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/Photo.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/Photo.cs @@ -1,9 +1,11 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Links { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Photo : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoAlbum.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoAlbum.cs index d5a6b448e3..19ff317933 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoAlbum.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoAlbum.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Links { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class PhotoAlbum : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoLocation.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoLocation.cs index 362c6edb36..feb6582694 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoLocation.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoLocation.cs @@ -1,9 +1,11 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Links { [ResourceLinks(TopLevelLinks = LinkTypes.None, ResourceLinks = LinkTypes.None, RelationshipLinks = LinkTypes.Related)] + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class PhotoLocation : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs index e06f78f6a7..7dbd40ae0f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs @@ -51,7 +51,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"/api/photoAlbums/{album.StringId}"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); @@ -59,9 +59,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Next.Should().BeNull(); responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"/api/photoAlbums/{album.StringId}"); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be($"/api/photoAlbums/{album.StringId}/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be($"/api/photoAlbums/{album.StringId}/photos"); + responseDocument.SingleData.Links.Self.Should().Be(route); + responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(route + "/relationships/photos"); + responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(route + "/photos"); } [Fact] @@ -78,7 +78,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/api/photoAlbums?include=photos"; + const string route = "/api/photoAlbums?include=photos"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -86,22 +86,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be("/api/photoAlbums?include=photos"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be("/api/photoAlbums?include=photos"); - responseDocument.Links.Last.Should().Be("/api/photoAlbums?include=photos"); + responseDocument.Links.First.Should().Be(route); + responseDocument.Links.Last.Should().Be(route); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string albumLink = $"/api/photoAlbums/{album.StringId}"; + responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be($"/api/photoAlbums/{album.StringId}"); - responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be($"/api/photoAlbums/{album.StringId}/relationships/photos"); - responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be($"/api/photoAlbums/{album.StringId}/photos"); + responseDocument.ManyData[0].Links.Self.Should().Be(albumLink); + responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + + string photoLink = $"/api/photos/{album.Photos.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); - responseDocument.Included[0].Links.Self.Should().Be($"/api/photos/{album.Photos.ElementAt(0).StringId}"); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"/api/photos/{album.Photos.ElementAt(0).StringId}/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"/api/photos/{album.Photos.ElementAt(0).StringId}/album"); + responseDocument.Included[0].Links.Self.Should().Be(photoLink); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); } [Fact] @@ -125,17 +129,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"/api/photos/{photo.StringId}/album"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string albumLink = $"/api/photoAlbums/{photo.Album.StringId}"; + responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"/api/photoAlbums/{photo.Album.StringId}"); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be($"/api/photoAlbums/{photo.Album.StringId}/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be($"/api/photoAlbums/{photo.Album.StringId}/photos"); + responseDocument.SingleData.Links.Self.Should().Be(albumLink); + responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); } [Fact] @@ -159,17 +165,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"/api/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be($"/api/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.First.Should().Be(route); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string photoLink = $"/api/photos/{album.Photos.ElementAt(0).StringId}"; + responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be($"/api/photos/{album.Photos.ElementAt(0).StringId}"); - responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be($"/api/photos/{album.Photos.ElementAt(0).StringId}/relationships/album"); - responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be($"/api/photos/{album.Photos.ElementAt(0).StringId}/album"); + responseDocument.ManyData[0].Links.Self.Should().Be(photoLink); + responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); } [Fact] @@ -193,7 +201,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"/api/photos/{photo.StringId}/relationships/album"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().Be($"/api/photos/{photo.StringId}/album"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); @@ -226,9 +234,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"/api/photoAlbums/{album.StringId}/relationships/photos"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().Be($"/api/photoAlbums/{album.StringId}/photos"); - responseDocument.Links.First.Should().Be($"/api/photoAlbums/{album.StringId}/relationships/photos"); + responseDocument.Links.First.Should().Be(route); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); @@ -272,7 +280,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/api/photoAlbums?include=photos"; + const string route = "/api/photoAlbums?include=photos"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -280,24 +288,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.Links.Self.Should().Be("/api/photoAlbums?include=photos"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - var newAlbumId = responseDocument.SingleData.Id; + string albumLink = $"/api/photoAlbums/{responseDocument.SingleData.Id}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"/api/photoAlbums/{newAlbumId}"); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be($"/api/photoAlbums/{newAlbumId}/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be($"/api/photoAlbums/{newAlbumId}/photos"); + responseDocument.SingleData.Links.Self.Should().Be(albumLink); + responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + + string photoLink = $"/api/photos/{existingPhoto.StringId}"; responseDocument.Included.Should().HaveCount(1); - responseDocument.Included[0].Links.Self.Should().Be($"/api/photos/{existingPhoto.StringId}"); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"/api/photos/{existingPhoto.StringId}/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"/api/photos/{existingPhoto.StringId}/album"); + responseDocument.Included[0].Links.Self.Should().Be(photoLink); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); } [Fact] @@ -341,22 +350,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"/api/photos/{existingPhoto.StringId}?include=album"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string photoLink = $"/api/photos/{existingPhoto.StringId}"; + responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"/api/photos/{existingPhoto.StringId}"); - responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be($"/api/photos/{existingPhoto.StringId}/relationships/album"); - responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be($"/api/photos/{existingPhoto.StringId}/album"); + responseDocument.SingleData.Links.Self.Should().Be(photoLink); + responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + + string albumLink = $"/api/photoAlbums/{existingAlbum.StringId}"; responseDocument.Included.Should().HaveCount(1); - responseDocument.Included[0].Links.Self.Should().Be($"/api/photoAlbums/{existingAlbum.StringId}"); - responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be($"/api/photoAlbums/{existingAlbum.StringId}/relationships/photos"); - responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be($"/api/photoAlbums/{existingAlbum.StringId}/photos"); + responseDocument.Included[0].Links.Self.Should().Be(albumLink); + responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs index dfbffa6123..b42f2b8c1b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs @@ -51,7 +51,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"/photoAlbums/{album.StringId}"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); @@ -59,9 +59,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Next.Should().BeNull(); responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"/photoAlbums/{album.StringId}"); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be($"/photoAlbums/{album.StringId}/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be($"/photoAlbums/{album.StringId}/photos"); + responseDocument.SingleData.Links.Self.Should().Be(route); + responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(route + "/relationships/photos"); + responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(route + "/photos"); } [Fact] @@ -78,7 +78,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/photoAlbums?include=photos"; + const string route = "/photoAlbums?include=photos"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -86,22 +86,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be("/photoAlbums?include=photos"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be("/photoAlbums?include=photos"); - responseDocument.Links.Last.Should().Be("/photoAlbums?include=photos"); + responseDocument.Links.First.Should().Be(route); + responseDocument.Links.Last.Should().Be(route); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string albumLink = $"/photoAlbums/{album.StringId}"; + responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be($"/photoAlbums/{album.StringId}"); - responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be($"/photoAlbums/{album.StringId}/relationships/photos"); - responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be($"/photoAlbums/{album.StringId}/photos"); + responseDocument.ManyData[0].Links.Self.Should().Be(albumLink); + responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + + string photoLink = $"/photos/{album.Photos.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); - responseDocument.Included[0].Links.Self.Should().Be($"/photos/{album.Photos.ElementAt(0).StringId}"); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"/photos/{album.Photos.ElementAt(0).StringId}/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"/photos/{album.Photos.ElementAt(0).StringId}/album"); + responseDocument.Included[0].Links.Self.Should().Be(photoLink); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); } [Fact] @@ -125,17 +129,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"/photos/{photo.StringId}/album"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string albumLink = $"/photoAlbums/{photo.Album.StringId}"; + responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"/photoAlbums/{photo.Album.StringId}"); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be($"/photoAlbums/{photo.Album.StringId}/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be($"/photoAlbums/{photo.Album.StringId}/photos"); + responseDocument.SingleData.Links.Self.Should().Be(albumLink); + responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); } [Fact] @@ -159,17 +165,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be($"/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.First.Should().Be(route); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string photoLink = $"/photos/{album.Photos.ElementAt(0).StringId}"; + responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be($"/photos/{album.Photos.ElementAt(0).StringId}"); - responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be($"/photos/{album.Photos.ElementAt(0).StringId}/relationships/album"); - responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be($"/photos/{album.Photos.ElementAt(0).StringId}/album"); + responseDocument.ManyData[0].Links.Self.Should().Be(photoLink); + responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); } [Fact] @@ -193,7 +201,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"/photos/{photo.StringId}/relationships/album"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().Be($"/photos/{photo.StringId}/album"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); @@ -226,9 +234,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"/photoAlbums/{album.StringId}/relationships/photos"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().Be($"/photoAlbums/{album.StringId}/photos"); - responseDocument.Links.First.Should().Be($"/photoAlbums/{album.StringId}/relationships/photos"); + responseDocument.Links.First.Should().Be(route); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); @@ -272,7 +280,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/photoAlbums?include=photos"; + const string route = "/photoAlbums?include=photos"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -280,24 +288,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.Links.Self.Should().Be("/photoAlbums?include=photos"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - var newAlbumId = responseDocument.SingleData.Id; + string albumLink = $"/photoAlbums/{responseDocument.SingleData.Id}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"/photoAlbums/{newAlbumId}"); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be($"/photoAlbums/{newAlbumId}/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be($"/photoAlbums/{newAlbumId}/photos"); + responseDocument.SingleData.Links.Self.Should().Be(albumLink); + responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + + string photoLink = $"/photos/{existingPhoto.StringId}"; responseDocument.Included.Should().HaveCount(1); - responseDocument.Included[0].Links.Self.Should().Be($"/photos/{existingPhoto.StringId}"); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"/photos/{existingPhoto.StringId}/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"/photos/{existingPhoto.StringId}/album"); + responseDocument.Included[0].Links.Self.Should().Be(photoLink); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); } [Fact] @@ -341,22 +350,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be($"/photos/{existingPhoto.StringId}?include=album"); + responseDocument.Links.Self.Should().Be(route); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); + string photoLink = $"/photos/{existingPhoto.StringId}"; + responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be($"/photos/{existingPhoto.StringId}"); - responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be($"/photos/{existingPhoto.StringId}/relationships/album"); - responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be($"/photos/{existingPhoto.StringId}/album"); + responseDocument.SingleData.Links.Self.Should().Be(photoLink); + responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); + responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + + string albumLink = $"/photoAlbums/{existingAlbum.StringId}"; responseDocument.Included.Should().HaveCount(1); - responseDocument.Included[0].Links.Self.Should().Be($"/photoAlbums/{existingAlbum.StringId}"); - responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be($"/photoAlbums/{existingAlbum.StringId}/relationships/photos"); - responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be($"/photoAlbums/{existingAlbum.StringId}/photos"); + responseDocument.Included[0].Links.Self.Should().Be(albumLink); + responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditDbContext.cs index f640452877..416b381728 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Logging { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class AuditDbContext : DbContext { public DbSet AuditEntries { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditEntry.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditEntry.cs index e9fcfcfe5e..d7001f400f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditEntry.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditEntry.cs @@ -1,9 +1,11 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Logging { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class AuditEntry : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditFakers.cs index 214fcd9bda..ea3e4c79a4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditFakers.cs @@ -2,6 +2,9 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Logging { internal sealed class AuditFakers : FakerContainer diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs index 754aedc690..44ba7be6c4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs @@ -1,3 +1,4 @@ +using System; using System.Net; using System.Threading.Tasks; using FluentAssertions; @@ -28,7 +29,7 @@ public LoggingTests(ExampleIntegrationTestContext true); + options.AddFilter((_, __) => true); }); testContext.ConfigureServicesBeforeStartup(services => @@ -63,7 +64,7 @@ public async Task Logs_request_body_at_Trace_level() }; // Arrange - var route = "/auditEntries"; + const string route = "/auditEntries"; // Act var (httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody); @@ -74,7 +75,7 @@ public async Task Logs_request_body_at_Trace_level() loggerFactory.Logger.Messages.Should().NotBeEmpty(); loggerFactory.Logger.Messages.Should().ContainSingle(message => message.LogLevel == LogLevel.Trace && - message.Text.StartsWith("Received request at 'http://localhost/auditEntries' with body: <<")); + message.Text.StartsWith("Received request at 'http://localhost/auditEntries' with body: <<", StringComparison.Ordinal)); } [Fact] @@ -85,7 +86,7 @@ public async Task Logs_response_body_at_Trace_level() loggerFactory.Logger.Clear(); // Arrange - var route = "/auditEntries"; + const string route = "/auditEntries"; // Act var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); @@ -96,7 +97,7 @@ public async Task Logs_response_body_at_Trace_level() loggerFactory.Logger.Messages.Should().NotBeEmpty(); loggerFactory.Logger.Messages.Should().ContainSingle(message => message.LogLevel == LogLevel.Trace && - message.Text.StartsWith("Sending 200 response for request at 'http://localhost/auditEntries' with body: <<")); + message.Text.StartsWith("Sending 200 response for request at 'http://localhost/auditEntries' with body: <<", StringComparison.Ordinal)); } [Fact] @@ -107,9 +108,9 @@ public async Task Logs_invalid_request_body_error_at_Information_level() loggerFactory.Logger.Clear(); // Arrange - var requestBody = "{ \"data\" {"; + const string requestBody = "{ \"data\" {"; - var route = "/auditEntries"; + const string route = "/auditEntries"; // Act var (httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ProductFamily.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ProductFamily.cs index 75a8e34441..ea40d2c09f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ProductFamily.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ProductFamily.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ProductFamily : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs index 6a943c0615..951f752d20 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs @@ -41,7 +41,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/supportTickets"; + const string route = "/supportTickets"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResponseMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResponseMetaTests.cs index 71a85c5122..d98fd84e8d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResponseMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResponseMetaTests.cs @@ -37,7 +37,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.ClearTableAsync(); }); - var route = "/supportTickets"; + const string route = "/supportTickets"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportDbContext.cs index 6d8850c63a..0e0376264c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class SupportDbContext : DbContext { public DbSet ProductFamilies { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportFakers.cs index 9fd4704cdd..59de00b55c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportFakers.cs @@ -2,6 +2,9 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta { internal sealed class SupportFakers : FakerContainer diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicket.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicket.cs index b006ba51e2..b93fcb6b8c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicket.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicket.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class SupportTicket : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketDefinition.cs index 3b7b945dae..1f3d1be0a2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketDefinition.cs @@ -1,9 +1,12 @@ +using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class SupportTicketDefinition : JsonApiResourceDefinition { public SupportTicketDefinition(IResourceGraph resourceGraph) : base(resourceGraph) @@ -12,7 +15,7 @@ public SupportTicketDefinition(IResourceGraph resourceGraph) : base(resourceGrap public override IDictionary GetMeta(SupportTicket resource) { - if (resource.Description != null && resource.Description.StartsWith("Critical:")) + if (resource.Description != null && resource.Description.StartsWith("Critical:", StringComparison.Ordinal)) { return new Dictionary { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs index efbab3be27..e2aa06abab 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs @@ -43,7 +43,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/supportTickets"; + const string route = "/supportTickets"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -64,7 +64,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.ClearTableAsync(); }); - var route = "/supportTickets"; + const string route = "/supportTickets"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -94,7 +94,7 @@ public async Task Hides_resource_count_in_create_resource_response() } }; - var route = "/supportTickets"; + const string route = "/supportTickets"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateDbContext.cs index 6add39c7b0..6de2695ae9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateDbContext.cs @@ -1,7 +1,11 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +// @formatter:wrap_chained_method_calls chop_always + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ModelStateValidation { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ModelStateDbContext : DbContext { public DbSet Directories { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateValidationTests.cs index 50a489f9d9..337caf9c9a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateValidationTests.cs @@ -34,7 +34,7 @@ public async Task Cannot_create_resource_with_omitted_required_attribute() } }; - string route = "/systemDirectories"; + const string route = "/systemDirectories"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -43,10 +43,12 @@ public async Task Cannot_create_resource_with_omitted_required_attribute() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Input validation failed."); - responseDocument.Errors[0].Detail.Should().Be("The Name field is required."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/data/attributes/name"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Input validation failed."); + error.Detail.Should().Be("The Name field is required."); + error.Source.Pointer.Should().Be("/data/attributes/name"); } [Fact] @@ -66,7 +68,7 @@ public async Task Cannot_create_resource_with_null_for_required_attribute_value( } }; - string route = "/systemDirectories"; + const string route = "/systemDirectories"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -75,10 +77,12 @@ public async Task Cannot_create_resource_with_null_for_required_attribute_value( httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Input validation failed."); - responseDocument.Errors[0].Detail.Should().Be("The Name field is required."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/data/attributes/name"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Input validation failed."); + error.Detail.Should().Be("The Name field is required."); + error.Source.Pointer.Should().Be("/data/attributes/name"); } [Fact] @@ -98,7 +102,7 @@ public async Task Cannot_create_resource_with_invalid_attribute_value() } }; - string route = "/systemDirectories"; + const string route = "/systemDirectories"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -107,10 +111,12 @@ public async Task Cannot_create_resource_with_invalid_attribute_value() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Input validation failed."); - responseDocument.Errors[0].Detail.Should().Be("The field Name must match the regular expression '^[\\w\\s]+$'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/data/attributes/name"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Input validation failed."); + error.Detail.Should().Be("The field Name must match the regular expression '^[\\w\\s]+$'."); + error.Source.Pointer.Should().Be("/data/attributes/name"); } [Fact] @@ -130,7 +136,7 @@ public async Task Can_create_resource_with_valid_attribute_value() } }; - string route = "/systemDirectories"; + const string route = "/systemDirectories"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -159,7 +165,7 @@ public async Task Cannot_create_resource_with_multiple_violations() } }; - string route = "/systemDirectories"; + const string route = "/systemDirectories"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -169,20 +175,23 @@ public async Task Cannot_create_resource_with_multiple_violations() responseDocument.Errors.Should().HaveCount(3); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Input validation failed."); - responseDocument.Errors[0].Detail.Should().Be("The Name field is required."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/data/attributes/name"); - - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[1].Title.Should().Be("Input validation failed."); - responseDocument.Errors[1].Detail.Should().Be("The field SizeInBytes must be between 0 and 9223372036854775807."); - responseDocument.Errors[1].Source.Pointer.Should().Be("/data/attributes/sizeInBytes"); - - responseDocument.Errors[2].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[2].Title.Should().Be("Input validation failed."); - responseDocument.Errors[2].Detail.Should().Be("The IsCaseSensitive field is required."); - responseDocument.Errors[2].Source.Pointer.Should().Be("/data/attributes/isCaseSensitive"); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error1.Title.Should().Be("Input validation failed."); + error1.Detail.Should().Be("The Name field is required."); + error1.Source.Pointer.Should().Be("/data/attributes/name"); + + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error2.Title.Should().Be("Input validation failed."); + error2.Detail.Should().Be("The field SizeInBytes must be between 0 and 9223372036854775807."); + error2.Source.Pointer.Should().Be("/data/attributes/sizeInBytes"); + + var error3 = responseDocument.Errors[2]; + error3.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error3.Title.Should().Be("Input validation failed."); + error3.Detail.Should().Be("The IsCaseSensitive field is required."); + error3.Source.Pointer.Should().Be("/data/attributes/isCaseSensitive"); } [Fact] @@ -211,7 +220,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.Directories.AddRange(parentDirectory, subdirectory); dbContext.Files.Add(file); - await dbContext.SaveChangesAsync(); }); @@ -261,7 +269,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories"; + const string route = "/systemDirectories"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -397,10 +405,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Input validation failed."); - responseDocument.Errors[0].Detail.Should().Be("The Name field is required."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/data/attributes/name"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Input validation failed."); + error.Detail.Should().Be("The Name field is required."); + error.Source.Pointer.Should().Be("/data/attributes/name"); } [Fact] @@ -441,10 +451,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Input validation failed."); - responseDocument.Errors[0].Detail.Should().Be("The field Name must match the regular expression '^[\\w\\s]+$'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/data/attributes/name"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Input validation failed."); + error.Detail.Should().Be("The field Name must match the regular expression '^[\\w\\s]+$'."); + error.Source.Pointer.Should().Be("/data/attributes/name"); } [Fact] @@ -490,7 +502,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/-1"; + const string route = "/systemDirectories/-1"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -499,16 +511,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(2); - - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Input validation failed."); - responseDocument.Errors[0].Detail.Should().Be("The field Id must match the regular expression '^[0-9]+$'."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/data/attributes/id"); - - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[1].Title.Should().Be("Input validation failed."); - responseDocument.Errors[1].Detail.Should().Be("The field Id must match the regular expression '^[0-9]+$'."); - responseDocument.Errors[1].Source.Pointer.Should().Be("/data/attributes/Subdirectories[0].Id"); + + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error1.Title.Should().Be("Input validation failed."); + error1.Detail.Should().Be("The field Id must match the regular expression '^[0-9]+$'."); + error1.Source.Pointer.Should().Be("/data/attributes/id"); + + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error2.Title.Should().Be("Input validation failed."); + error2.Detail.Should().Be("The field Id must match the regular expression '^[0-9]+$'."); + error2.Source.Pointer.Should().Be("/data/attributes/Subdirectories[0].Id"); } [Fact] @@ -602,7 +616,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.Directories.AddRange(directory, otherParent, otherSubdirectory); dbContext.Files.Add(otherFile); - await dbContext.SaveChangesAsync(); }); @@ -853,7 +866,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.Directories.Add(directory); dbContext.Files.Add(otherFile); - await dbContext.SaveChangesAsync(); }); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/NoModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/NoModelStateValidationTests.cs index f24da7a5d1..1780d32aba 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/NoModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/NoModelStateValidationTests.cs @@ -34,7 +34,7 @@ public async Task Can_create_resource_with_invalid_attribute_value() } }; - string route = "/systemDirectories"; + const string route = "/systemDirectories"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectory.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectory.cs index 9c851fe832..f78848863a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectory.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectory.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ModelStateValidation { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class SystemDirectory : Identifiable { [Required] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemFile.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemFile.cs index a7890730f2..8598695d35 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemFile.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemFile.cs @@ -1,9 +1,11 @@ using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ModelStateValidation { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class SystemFile : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/DivingBoard.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/DivingBoard.cs index c46c4f410f..fc23994b25 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/DivingBoard.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/DivingBoard.cs @@ -1,9 +1,11 @@ using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.NamingConventions { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class DivingBoard : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs index b7980fcb10..b6e762d46b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs @@ -1,18 +1,15 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCoreExampleTests.Startups; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.NamingConventions { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class KebabCasingConventionStartup : TestableStartup where TDbContext : DbContext { - public KebabCasingConventionStartup(IConfiguration configuration) : base(configuration) - { - } - protected override void SetJsonApiOptions(JsonApiOptions options) { base.SetJsonApiOptions(options); @@ -22,8 +19,10 @@ protected override void SetJsonApiOptions(JsonApiOptions options) options.IncludeTotalResourceCount = true; options.ValidateModelState = true; - var resolver = (DefaultContractResolver) options.SerializerSettings.ContractResolver; - resolver!.NamingStrategy = new KebabCaseNamingStrategy(); + options.SerializerSettings.ContractResolver = new DefaultContractResolver + { + NamingStrategy = new KebabCaseNamingStrategy() + }; } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingTests.cs index 08d4d28f3f..91efaed905 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingTests.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.EntityFrameworkCore; using TestBuildingBlocks; using Xunit; @@ -34,7 +33,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/public-api/swimming-pools?include=diving-boards"; + const string route = "/public-api/swimming-pools?include=diving-boards"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -51,7 +50,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("diving-boards"); responseDocument.Included[0].Id.Should().Be(pools[1].DivingBoards[0].StringId); - responseDocument.Included[0].Attributes["height-in-meters"].As().Should().BeApproximately(pools[1].DivingBoards[0].HeightInMeters, 0.00000000001M); + responseDocument.Included[0].Attributes["height-in-meters"].As().Should().BeApproximately(pools[1].DivingBoards[0].HeightInMeters); responseDocument.Included[0].Relationships.Should().BeNull(); responseDocument.Included[0].Links.Self.Should().Be($"/public-api/diving-boards/{pools[1].DivingBoards[0].StringId}"); @@ -73,7 +72,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/public-api/swimming-pools/{pool.StringId}/water-slides?filter=greaterThan(length-in-meters,'1')&fields[water-slides]=length-in-meters"; + var route = $"/public-api/swimming-pools/{pool.StringId}/water-slides" + + "?filter=greaterThan(length-in-meters,'1')&fields[water-slides]=length-in-meters"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -105,7 +105,7 @@ public async Task Can_create_resource() } }; - var route = "/public-api/swimming-pools"; + const string route = "/public-api/swimming-pools"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -118,17 +118,17 @@ public async Task Can_create_resource() responseDocument.SingleData.Attributes["is-indoor"].Should().Be(newPool.IsIndoor); var newPoolId = int.Parse(responseDocument.SingleData.Id); + string poolLink = route + $"/{newPoolId}"; responseDocument.SingleData.Relationships.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships["water-slides"].Links.Self.Should().Be($"/public-api/swimming-pools/{newPoolId}/relationships/water-slides"); - responseDocument.SingleData.Relationships["water-slides"].Links.Related.Should().Be($"/public-api/swimming-pools/{newPoolId}/water-slides"); - responseDocument.SingleData.Relationships["diving-boards"].Links.Self.Should().Be($"/public-api/swimming-pools/{newPoolId}/relationships/diving-boards"); - responseDocument.SingleData.Relationships["diving-boards"].Links.Related.Should().Be($"/public-api/swimming-pools/{newPoolId}/diving-boards"); + responseDocument.SingleData.Relationships["water-slides"].Links.Self.Should().Be(poolLink + "/relationships/water-slides"); + responseDocument.SingleData.Relationships["water-slides"].Links.Related.Should().Be(poolLink + "/water-slides"); + responseDocument.SingleData.Relationships["diving-boards"].Links.Self.Should().Be(poolLink + "/relationships/diving-boards"); + responseDocument.SingleData.Relationships["diving-boards"].Links.Related.Should().Be(poolLink + "/diving-boards"); await _testContext.RunOnDatabaseAsync(async dbContext => { - var poolInDatabase = await dbContext.SwimmingPools - .FirstAsync(pool => pool.Id == newPoolId); + var poolInDatabase = await dbContext.SwimmingPools.FirstWithIdAsync(newPoolId); poolInDatabase.IsIndoor.Should().Be(newPool.IsIndoor); }); @@ -138,9 +138,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Applies_casing_convention_on_error_stack_trace() { // Arrange - var requestBody = "{ \"data\": {"; + const string requestBody = "{ \"data\": {"; - var route = "/public-api/swimming-pools"; + const string route = "/public-api/swimming-pools"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -149,9 +149,11 @@ public async Task Applies_casing_convention_on_error_stack_trace() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Meta.Data.Should().ContainKey("stack-trace"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Meta.Data.Should().ContainKey("stack-trace"); } [Fact] @@ -188,10 +190,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Input validation failed."); - responseDocument.Errors[0].Detail.Should().Be("The field HeightInMeters must be between 1 and 20."); - responseDocument.Errors[0].Source.Pointer.Should().Be("/data/attributes/height-in-meters"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Input validation failed."); + error.Detail.Should().Be("The field HeightInMeters must be between 1 and 20."); + error.Source.Pointer.Should().Be("/data/attributes/height-in-meters"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingDbContext.cs index 9d8de4aa26..2722b91cc9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.NamingConventions { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class SwimmingDbContext : DbContext { public DbSet SwimmingPools { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingFakers.cs index 8636078884..86ecc8d1ad 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingFakers.cs @@ -2,6 +2,9 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.NamingConventions { internal sealed class SwimmingFakers : FakerContainer diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingPool.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingPool.cs index 6c1274816e..324b984522 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingPool.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingPool.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.NamingConventions { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class SwimmingPool : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/WaterSlide.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/WaterSlide.cs index b7dea1d5ba..b9007dfe98 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/WaterSlide.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/WaterSlide.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.NamingConventions { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class WaterSlide : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NonJsonApiControllers/NonJsonApiControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NonJsonApiControllers/NonJsonApiControllerTests.cs index 768757e43f..db76e954ab 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NonJsonApiControllers/NonJsonApiControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NonJsonApiControllers/NonJsonApiControllerTests.cs @@ -3,7 +3,7 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Startups; using Microsoft.AspNetCore.Mvc.Testing; using TestBuildingBlocks; using Xunit; @@ -23,7 +23,7 @@ public NonJsonApiControllerTests(WebApplicationFactory factory) public async Task Get_skips_middleware_and_formatters() { // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "/NonJsonApi"); + using var request = new HttpRequestMessage(HttpMethod.Get, "/NonJsonApi"); var client = _factory.CreateClient(); @@ -42,7 +42,7 @@ public async Task Get_skips_middleware_and_formatters() public async Task Post_skips_middleware_and_formatters() { // Arrange - var request = new HttpRequestMessage(HttpMethod.Post, "/NonJsonApi") + using var request = new HttpRequestMessage(HttpMethod.Post, "/NonJsonApi") { Content = new StringContent("Jack") { @@ -70,7 +70,7 @@ public async Task Post_skips_middleware_and_formatters() public async Task Post_skips_error_handler() { // Arrange - var request = new HttpRequestMessage(HttpMethod.Post, "/NonJsonApi"); + using var request = new HttpRequestMessage(HttpMethod.Post, "/NonJsonApi"); var client = _factory.CreateClient(); @@ -89,7 +89,7 @@ public async Task Post_skips_error_handler() public async Task Put_skips_middleware_and_formatters() { // Arrange - var request = new HttpRequestMessage(HttpMethod.Put, "/NonJsonApi") + using var request = new HttpRequestMessage(HttpMethod.Put, "/NonJsonApi") { Content = new StringContent("\"Jane\"") { @@ -117,7 +117,7 @@ public async Task Put_skips_middleware_and_formatters() public async Task Patch_skips_middleware_and_formatters() { // Arrange - var request = new HttpRequestMessage(HttpMethod.Patch, "/NonJsonApi?name=Janice"); + using var request = new HttpRequestMessage(HttpMethod.Patch, "/NonJsonApi?name=Janice"); var client = _factory.CreateClient(); @@ -136,7 +136,7 @@ public async Task Patch_skips_middleware_and_formatters() public async Task Delete_skips_middleware_and_formatters() { // Arrange - var request = new HttpRequestMessage(HttpMethod.Delete, "/NonJsonApi"); + using var request = new HttpRequestMessage(HttpMethod.Delete, "/NonJsonApi"); var client = _factory.CreateClient(); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/AccountPreferences.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/AccountPreferences.cs index ecc7308906..f623cf82d9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/AccountPreferences.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/AccountPreferences.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class AccountPreferences : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Appointment.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Appointment.cs index 62838a6df8..d7a6f84e9a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Appointment.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Appointment.cs @@ -1,9 +1,11 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Appointment : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Blog.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Blog.cs index cc1e1765e9..2edbb10c99 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Blog.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Blog.cs @@ -1,9 +1,12 @@ +using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Blog : Identifiable { [Attr] @@ -13,7 +16,7 @@ public sealed class Blog : Identifiable public string PlatformName { get; set; } [Attr(Capabilities = AttrCapabilities.All & ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))] - public bool ShowAdvertisements => PlatformName.EndsWith("(using free account)"); + public bool ShowAdvertisements => PlatformName.EndsWith("(using free account)", StringComparison.Ordinal); [HasMany] public IList Posts { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/BlogPost.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/BlogPost.cs index 10205816ab..dea9789436 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/BlogPost.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/BlogPost.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class BlogPost : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/BlogPostLabel.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/BlogPostLabel.cs index fd14bbf831..d394a0b1bd 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/BlogPostLabel.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/BlogPostLabel.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class BlogPostLabel { public int BlogPostId { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Calendar.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Calendar.cs index 5447aba40b..494184ee68 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Calendar.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Calendar.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Calendar : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Comment.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Comment.cs index 848915f7ed..199f8c1287 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Comment.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Comment.cs @@ -1,9 +1,11 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Comment : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs index 528a8339b4..38ff0d200d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs @@ -207,7 +207,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/filterableResources?filter=equals(someInt32,'ABC')"; + const string route = "/filterableResources?filter=equals(someInt32,'ABC')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -216,10 +216,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Query creation failed due to incompatible types."); - responseDocument.Errors[0].Detail.Should().Be("Failed to convert 'ABC' of type 'String' to type 'Int32'."); - responseDocument.Errors[0].Source.Parameter.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Query creation failed due to incompatible types."); + error.Detail.Should().Be("Failed to convert 'ABC' of type 'String' to type 'Int32'."); + error.Source.Parameter.Should().BeNull(); } [Theory] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDbContext.cs index ec0e971b39..d8f0fb49bb 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings.Filtering { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class FilterDbContext : DbContext { public DbSet FilterableResources { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs index 1d79f68e10..e3904fa095 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs @@ -45,7 +45,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?filter=equals(caption,'Two')"; + const string route = "/blogPosts?filter=equals(caption,'Two')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -78,10 +78,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid."); - responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); - responseDocument.Errors[0].Source.Parameter.Should().Be("filter"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified filter is invalid."); + error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); + error.Source.Parameter.Should().Be("filter"); } [Fact] @@ -132,10 +134,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid."); - responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); - responseDocument.Errors[0].Source.Parameter.Should().Be("filter"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified filter is invalid."); + error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); + error.Source.Parameter.Should().Be("filter"); } [Fact] @@ -155,7 +159,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?include=author&filter=equals(author.userName,'Smith')"; + const string route = "/blogPosts?include=author&filter=equals(author.userName,'Smith')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -183,7 +187,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogs?filter=greaterThan(count(posts),'0')"; + const string route = "/blogs?filter=greaterThan(count(posts),'0')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -215,7 +219,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?filter=has(labels)"; + const string route = "/blogPosts?filter=has(labels)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -243,7 +247,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogs?include=posts&filter[posts]=equals(caption,'Two')"; + const string route = "/blogs?include=posts&filter[posts]=equals(caption,'Two')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -322,7 +326,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => options.DisableTopPagination = false; options.DisableChildrenPagination = true; - var route = "/blogPosts?include=labels&filter[labels]=equals(name,'Hot')"; + const string route = "/blogPosts?include=labels&filter[labels]=equals(name,'Hot')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -353,7 +357,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogs?include=owner.posts&filter[owner.posts]=equals(caption,'Two')"; + const string route = "/blogs?include=owner.posts&filter[owner.posts]=equals(caption,'Two')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -384,7 +388,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?filter=equals(caption,'One')&filter=equals(caption,'Three')"; + const string route = "/blogPosts?filter=equals(caption,'One')&filter=equals(caption,'Three')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -422,11 +426,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Posts.AddRange(posts); - await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?filter[author.userName]=John&filter[author.displayName]=Smith"; + const string route = "/blogPosts?filter[author.userName]=John&filter[author.displayName]=Smith"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -461,10 +464,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogs?include=owner.posts.comments&" + - "filter=and(equals(title,'Technology'),has(owner.posts),equals(owner.userName,'Smith'))&" + - "filter[owner.posts]=equals(caption,'Two')&" + - "filter[owner.posts.comments]=greaterThan(createdAt,'2005-05-05')"; + // @formatter:keep_existing_linebreaks true + + const string route = "/blogs?include=owner.posts.comments&" + + "filter=and(equals(title,'Technology'),has(owner.posts),equals(owner.userName,'Smith'))&" + + "filter[owner.posts]=equals(caption,'Two')&" + + "filter[owner.posts.comments]=greaterThan(createdAt,'2005-05-05')"; + + // @formatter:keep_existing_linebreaks restore // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs index a8f0485a4b..4604a8d658 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs @@ -79,7 +79,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/filterableResources?filter=equals(someInt32,otherInt32)"; + const string route = "/filterableResources?filter=equals(someInt32,otherInt32)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -115,7 +115,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/filterableResources?filter=equals(someNullableInt32,otherNullableInt32)"; + const string route = "/filterableResources?filter=equals(someNullableInt32,otherNullableInt32)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -151,7 +151,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/filterableResources?filter=equals(someNullableInt32,someInt32)"; + const string route = "/filterableResources?filter=equals(someNullableInt32,someInt32)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -187,7 +187,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/filterableResources?filter=equals(someInt32,someNullableInt32)"; + const string route = "/filterableResources?filter=equals(someInt32,someNullableInt32)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -223,7 +223,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/filterableResources?filter=equals(someInt32,someUnsignedInt64)"; + const string route = "/filterableResources?filter=equals(someInt32,someUnsignedInt64)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -240,7 +240,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_filter_equality_on_two_attributes_of_incompatible_types() { // Arrange - var route = "/filterableResources?filter=equals(someDouble,someTimeSpan)"; + const string route = "/filterableResources?filter=equals(someDouble,someTimeSpan)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -249,10 +249,12 @@ public async Task Cannot_filter_equality_on_two_attributes_of_incompatible_types httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Query creation failed due to incompatible types."); - responseDocument.Errors[0].Detail.Should().Be("No coercion operator is defined between types 'System.TimeSpan' and 'System.Double'."); - responseDocument.Errors[0].Source.Parameter.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Query creation failed due to incompatible types."); + error.Detail.Should().Be("No coercion operator is defined between types 'System.TimeSpan' and 'System.Double'."); + error.Source.Parameter.Should().BeNull(); } [Theory] @@ -366,7 +368,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someDateTime,'{DateTime.ParseExact(filterDateTime, "yyyy-MM-dd", null)}')"; + var route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someDateTime," + + $"'{DateTime.ParseExact(filterDateTime, "yyyy-MM-dd", null)}')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -470,7 +473,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/filterableResources?filter=has(children)"; + const string route = "/filterableResources?filter=has(children)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -502,7 +505,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/filterableResources?filter=equals(count(children),'2')"; + const string route = "/filterableResources?filter=equals(count(children),'2')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs index 4b4d0cd836..db289c5ffd 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs @@ -28,7 +28,7 @@ public FilterTests(ExampleIntegrationTestContext(route); @@ -37,17 +37,19 @@ public async Task Cannot_filter_in_unknown_scope() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); - responseDocument.Errors[0].Source.Parameter.Should().Be("filter[doesNotExist]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified filter is invalid."); + error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); + error.Source.Parameter.Should().Be("filter[doesNotExist]"); } [Fact] public async Task Cannot_filter_in_unknown_nested_scope() { // Arrange - var route = "/webAccounts?filter[posts.doesNotExist]=equals(title,null)"; + const string route = "/webAccounts?filter[posts.doesNotExist]=equals(title,null)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -56,17 +58,19 @@ public async Task Cannot_filter_in_unknown_nested_scope() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); - responseDocument.Errors[0].Source.Parameter.Should().Be("filter[posts.doesNotExist]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified filter is invalid."); + error.Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); + error.Source.Parameter.Should().Be("filter[posts.doesNotExist]"); } [Fact] public async Task Cannot_filter_on_attribute_with_blocked_capability() { // Arrange - var route = "/webAccounts?filter=equals(dateOfBirth,null)"; + const string route = "/webAccounts?filter=equals(dateOfBirth,null)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -75,10 +79,12 @@ public async Task Cannot_filter_on_attribute_with_blocked_capability() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Filtering on the requested attribute is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Filtering on attribute 'dateOfBirth' is not allowed."); - responseDocument.Errors[0].Source.Parameter.Should().Be("filter"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Filtering on the requested attribute is not allowed."); + error.Detail.Should().Be("Filtering on attribute 'dateOfBirth' is not allowed."); + error.Source.Parameter.Should().Be("filter"); } [Fact] @@ -91,7 +97,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Accounts.AddRange(accounts); - await dbContext.SaveChangesAsync(); }); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterableResource.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterableResource.cs index 4be6b98e02..60cd72d510 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterableResource.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterableResource.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings.Filtering { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class FilterableResource : Identifiable { [Attr] public string SomeString { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs index 7d798f5c5d..132270d951 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs @@ -37,11 +37,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Posts.Add(post); - await dbContext.SaveChangesAsync(); }); - var route = "blogPosts?include=author"; + const string route = "blogPosts?include=author"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -101,7 +100,6 @@ public async Task Can_include_in_secondary_resource() await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.Blogs.Add(blog); - await dbContext.SaveChangesAsync(); }); @@ -537,11 +535,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Posts.AddRange(posts); - await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?include=author"; + const string route = "/blogPosts?include=author"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -561,7 +558,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_include_unknown_relationship() { // Arrange - var route = "/webAccounts?include=doesNotExist"; + const string route = "/webAccounts?include=doesNotExist"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -570,17 +567,19 @@ public async Task Cannot_include_unknown_relationship() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified include is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); - responseDocument.Errors[0].Source.Parameter.Should().Be("include"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified include is invalid."); + error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); + error.Source.Parameter.Should().Be("include"); } [Fact] public async Task Cannot_include_unknown_nested_relationship() { // Arrange - var route = "/blogs?include=posts.doesNotExist"; + const string route = "/blogs?include=posts.doesNotExist"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -589,17 +588,19 @@ public async Task Cannot_include_unknown_nested_relationship() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified include is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); - responseDocument.Errors[0].Source.Parameter.Should().Be("include"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified include is invalid."); + error.Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); + error.Source.Parameter.Should().Be("include"); } [Fact] public async Task Cannot_include_relationship_with_blocked_capability() { // Arrange - var route = "/blogPosts?include=parent"; + const string route = "/blogPosts?include=parent"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -608,10 +609,12 @@ public async Task Cannot_include_relationship_with_blocked_capability() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Including the requested relationship is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Including the relationship 'parent' on 'blogPosts' is not allowed."); - responseDocument.Errors[0].Source.Parameter.Should().Be("include"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Including the requested relationship is not allowed."); + error.Detail.Should().Be("Including the relationship 'parent' on 'blogPosts' is not allowed."); + error.Source.Parameter.Should().Be("include"); } [Fact] @@ -628,7 +631,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?include=reviewer.preferences"; + const string route = "/blogPosts?include=reviewer.preferences"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -683,7 +686,7 @@ public async Task Cannot_exceed_configured_maximum_inclusion_depth() var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.MaximumIncludeDepth = 1; - var route = "/blogs/123/owner?include=posts.comments"; + const string route = "/blogs/123/owner?include=posts.comments"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -692,10 +695,12 @@ public async Task Cannot_exceed_configured_maximum_inclusion_depth() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified include is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Including 'posts.comments' exceeds the maximum inclusion depth of 1."); - responseDocument.Errors[0].Source.Parameter.Should().Be("include"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified include is invalid."); + error.Detail.Should().Be("Including 'posts.comments' exceeds the maximum inclusion depth of 1."); + error.Source.Parameter.Should().Be("include"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Label.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Label.cs index 1a45f848e8..adfb7a1226 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Label.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Label.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Label : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/LabelColor.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/LabelColor.cs index 0f9f971eca..62a35c09d2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/LabelColor.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/LabelColor.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public enum LabelColor { Red, diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs index 0e5a7c98ac..d8a07d9473 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs @@ -15,7 +15,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings.Pagination public sealed class PaginationWithTotalCountTests : IClassFixture, QueryStringDbContext>> { - private const int _defaultPageSize = 5; + private const string HostPrefix = "http://localhost"; + private const int DefaultPageSize = 5; private readonly ExampleIntegrationTestContext, QueryStringDbContext> _testContext; private readonly QueryStringFakers _fakers = new QueryStringFakers(); @@ -26,7 +27,7 @@ public PaginationWithTotalCountTests(ExampleIntegrationTestContext(); options.IncludeTotalResourceCount = true; - options.DefaultPageSize = new PageSize(_defaultPageSize); + options.DefaultPageSize = new PageSize(DefaultPageSize); options.MaximumPageSize = null; options.MaximumPageNumber = null; options.AllowUnknownQueryStringParameters = true; @@ -45,11 +46,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Posts.AddRange(posts); - await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?page[number]=2&page[size]=1"; + const string route = "/blogPosts?page[number]=2&page[size]=1"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -61,8 +61,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.ManyData[0].Id.Should().Be(posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost" + route); - responseDocument.Links.First.Should().Be("http://localhost/blogPosts?page[size]=1"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?page[size]=1"); responseDocument.Links.Last.Should().Be(responseDocument.Links.Self); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); responseDocument.Links.Next.Should().BeNull(); @@ -89,10 +89,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified paging is invalid."); + error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); + error.Source.Parameter.Should().Be("page[number]"); } [Fact] @@ -120,11 +122,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.ManyData[0].Id.Should().Be(blog.Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost" + route); - responseDocument.Links.First.Should().Be($"http://localhost/blogs/{blog.StringId}/posts?page[size]=1"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be(HostPrefix + $"/blogs/{blog.StringId}/posts?page[size]=1"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); - responseDocument.Links.Next.Should().Be($"http://localhost/blogs/{blog.StringId}/posts?page[number]=3&page[size]=1"); + responseDocument.Links.Next.Should().Be(HostPrefix + $"/blogs/{blog.StringId}/posts?page[number]=3&page[size]=1"); } [Fact] @@ -148,10 +150,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[size]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified paging is invalid."); + error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); + error.Source.Parameter.Should().Be("page[size]"); } [Fact] @@ -169,7 +173,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogs?include=posts&page[number]=posts:2&page[size]=2,posts:1"; + const string route = "/blogs?include=posts&page[number]=posts:2&page[size]=2,posts:1"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -184,9 +188,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[1].Id.Should().Be(blogs[1].Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost" + route); - responseDocument.Links.First.Should().Be("http://localhost/blogs?include=posts&page[size]=2,posts:1"); - responseDocument.Links.Last.Should().Be("http://localhost/blogs?include=posts&page[number]=2&page[size]=2,posts:1"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be(HostPrefix + "/blogs?include=posts&page[size]=2,posts:1"); + responseDocument.Links.Last.Should().Be(HostPrefix + "/blogs?include=posts&page[number]=2&page[size]=2,posts:1"); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().Be(responseDocument.Links.Last); } @@ -218,7 +222,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[0].Id.Should().Be(blog.Owner.Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost" + route); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); @@ -250,8 +254,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.ManyData[0].Id.Should().Be(blog.Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost" + route); - responseDocument.Links.First.Should().Be($"http://localhost/blogs/{blog.StringId}/relationships/posts?page[size]=1"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be(HostPrefix + $"/blogs/{blog.StringId}/relationships/posts?page[size]=1"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); responseDocument.Links.Next.Should().BeNull(); @@ -297,7 +301,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => options.DisableTopPagination = true; options.DisableChildrenPagination = false; - var route = "/blogPosts?include=labels&page[number]=labels:2&page[size]=labels:1"; + const string route = "/blogPosts?include=labels&page[number]=labels:2&page[size]=labels:1"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -312,8 +316,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[1].Id.Should().Be(posts[1].BlogPostLabels.Skip(1).First().Label.StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost" + route); - responseDocument.Links.First.Should().Be("http://localhost/blogPosts?include=labels&page[size]=labels:1"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?include=labels&page[size]=labels:1"); responseDocument.Links.Last.Should().Be(responseDocument.Links.First); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); @@ -340,7 +344,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Posts.Add(post); - await dbContext.SaveChangesAsync(); }); @@ -356,8 +359,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.ManyData[0].Id.Should().Be(post.BlogPostLabels.ElementAt(1).Label.StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost" + route); - responseDocument.Links.First.Should().Be($"http://localhost/blogPosts/{post.StringId}/relationships/labels?page[size]=1"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be(HostPrefix + $"/blogPosts/{post.StringId}/relationships/labels?page[size]=1"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); responseDocument.Links.Next.Should().BeNull(); @@ -379,9 +382,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogs?include=owner.posts.comments&" + - "page[size]=1,owner.posts:1,owner.posts.comments:1&" + - "page[number]=2,owner.posts:2,owner.posts.comments:2"; + const string route = "/blogs?include=owner.posts.comments&page[size]=1,owner.posts:1,owner.posts.comments:1&" + + "page[number]=2,owner.posts:2,owner.posts.comments:2"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -397,10 +399,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[1].Id.Should().Be(blogs[1].Owner.Posts[1].StringId); responseDocument.Included[2].Id.Should().Be(blogs[1].Owner.Posts[1].Comments.Skip(1).First().StringId); + const string linkPrefix = HostPrefix + "/blogs?include=owner.posts.comments"; + responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost" + route); - responseDocument.Links.First.Should().Be("http://localhost/blogs?include=owner.posts.comments&page[size]=1,owner.posts:1,owner.posts.comments:1"); - responseDocument.Links.Last.Should().Be("http://localhost/blogs?include=owner.posts.comments&page[size]=1,owner.posts:1,owner.posts.comments:1&page[number]=2"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be(linkPrefix + "&page[size]=1,owner.posts:1,owner.posts.comments:1"); + responseDocument.Links.Last.Should().Be(linkPrefix + "&page[size]=1,owner.posts:1,owner.posts.comments:1&page[number]=2"); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); responseDocument.Links.Next.Should().BeNull(); } @@ -409,7 +413,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_paginate_in_unknown_scope() { // Arrange - var route = "/webAccounts?page[number]=doesNotExist:1"; + const string route = "/webAccounts?page[number]=doesNotExist:1"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -418,17 +422,19 @@ public async Task Cannot_paginate_in_unknown_scope() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified paging is invalid."); + error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); + error.Source.Parameter.Should().Be("page[number]"); } [Fact] public async Task Cannot_paginate_in_unknown_nested_scope() { // Arrange - var route = "/webAccounts?page[size]=posts.doesNotExist:1"; + const string route = "/webAccounts?page[size]=posts.doesNotExist:1"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -437,10 +443,12 @@ public async Task Cannot_paginate_in_unknown_nested_scope() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[size]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified paging is invalid."); + error.Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); + error.Source.Parameter.Should().Be("page[size]"); } [Fact] @@ -472,11 +480,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.ManyData[1].Id.Should().Be(blog.Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost" + route); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.First.Should().Be(responseDocument.Links.Self); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); - responseDocument.Links.Next.Should().Be($"http://localhost/blogs/{blog.StringId}/posts?page[number]=2"); + responseDocument.Links.Next.Should().Be(HostPrefix + $"/blogs/{blog.StringId}/posts?page[number]=2"); } [Fact] @@ -506,7 +514,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.ManyData.Should().HaveCount(25); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost" + route); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); @@ -524,7 +532,7 @@ public async Task Renders_correct_top_level_links_for_page_number(int pageNumber var account = _fakers.WebAccount.Generate(); account.UserName = "&" + account.UserName; - const int totalCount = 3 * _defaultPageSize + 3; + const int totalCount = 3 * DefaultPageSize + 3; var posts = _fakers.BlogPost.Generate(totalCount); foreach (var post in posts) @@ -536,7 +544,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Posts.AddRange(posts); - await dbContext.SaveChangesAsync(); }); @@ -550,11 +557,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be("http://localhost" + route); + responseDocument.Links.Self.Should().Be(HostPrefix + route); if (firstLink != null) { - var expected = "http://localhost" + SetPageNumberInUrl(routePrefix, firstLink.Value); + var expected = HostPrefix + SetPageNumberInUrl(routePrefix, firstLink.Value); responseDocument.Links.First.Should().Be(expected); } else @@ -564,7 +571,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => if (prevLink != null) { - var expected = "http://localhost" + SetPageNumberInUrl(routePrefix, prevLink.Value); + var expected = HostPrefix + SetPageNumberInUrl(routePrefix, prevLink.Value); responseDocument.Links.Prev.Should().Be(expected); } else @@ -574,7 +581,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => if (nextLink != null) { - var expected = "http://localhost" + SetPageNumberInUrl(routePrefix, nextLink.Value); + var expected = HostPrefix + SetPageNumberInUrl(routePrefix, nextLink.Value); responseDocument.Links.Next.Should().Be(expected); } else @@ -584,7 +591,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => if (lastLink != null) { - var expected = "http://localhost" + SetPageNumberInUrl(routePrefix, lastLink.Value); + var expected = HostPrefix + SetPageNumberInUrl(routePrefix, lastLink.Value); responseDocument.Links.Last.Should().Be(expected); } else diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/PaginationWithoutTotalCountTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/PaginationWithoutTotalCountTests.cs index 9e7c3f7a06..831859cace 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/PaginationWithoutTotalCountTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/PaginationWithoutTotalCountTests.cs @@ -13,7 +13,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings.Pagination public sealed class PaginationWithoutTotalCountTests : IClassFixture, QueryStringDbContext>> { - private const int _defaultPageSize = 5; + private const string HostPrefix = "http://localhost"; + private const int DefaultPageSize = 5; private readonly ExampleIntegrationTestContext, QueryStringDbContext> _testContext; private readonly QueryStringFakers _fakers = new QueryStringFakers(); @@ -25,7 +26,7 @@ public PaginationWithoutTotalCountTests(ExampleIntegrationTestContext(); options.IncludeTotalResourceCount = false; - options.DefaultPageSize = new PageSize(_defaultPageSize); + options.DefaultPageSize = new PageSize(DefaultPageSize); options.AllowUnknownQueryStringParameters = true; } @@ -36,7 +37,7 @@ public async Task Hides_pagination_links_when_unconstrained_page_size() var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.DefaultPageSize = null; - var route = "/blogPosts?foo=bar"; + const string route = "/blogPosts?foo=bar"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -45,7 +46,7 @@ public async Task Hides_pagination_links_when_unconstrained_page_size() httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost/blogPosts?foo=bar"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); @@ -65,7 +66,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?page[size]=8&foo=bar"; + const string route = "/blogPosts?page[size]=8&foo=bar"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -74,8 +75,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost/blogPosts?page[size]=8&foo=bar"); - responseDocument.Links.First.Should().Be("http://localhost/blogPosts?page[size]=8&foo=bar"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be(HostPrefix + route); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); @@ -91,7 +92,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?page[number]=2&foo=bar"; + const string route = "/blogPosts?page[number]=2&foo=bar"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -100,10 +101,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost/blogPosts?page[number]=2&foo=bar"); - responseDocument.Links.First.Should().Be("http://localhost/blogPosts?foo=bar"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?foo=bar"); responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be("http://localhost/blogPosts?foo=bar"); + responseDocument.Links.Prev.Should().Be(HostPrefix + "/blogPosts?foo=bar"); responseDocument.Links.Next.Should().BeNull(); } @@ -120,7 +121,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?foo=bar&page[number]=3"; + const string route = "/blogPosts?foo=bar&page[number]=3"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -128,13 +129,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Count.Should().BeLessThan(_defaultPageSize); + responseDocument.ManyData.Count.Should().BeLessThan(DefaultPageSize); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost/blogPosts?foo=bar&page[number]=3"); - responseDocument.Links.First.Should().Be("http://localhost/blogPosts?foo=bar"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?foo=bar"); responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be("http://localhost/blogPosts?foo=bar&page[number]=2"); + responseDocument.Links.Prev.Should().Be(HostPrefix + "/blogPosts?foo=bar&page[number]=2"); responseDocument.Links.Next.Should().BeNull(); } @@ -142,7 +143,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Renders_pagination_links_when_page_number_is_specified_in_query_string_with_full_page() { // Arrange - var posts = _fakers.BlogPost.Generate(_defaultPageSize * 3); + var posts = _fakers.BlogPost.Generate(DefaultPageSize * 3); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -151,7 +152,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?page[number]=3&foo=bar"; + const string route = "/blogPosts?page[number]=3&foo=bar"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -159,14 +160,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(_defaultPageSize); + responseDocument.ManyData.Should().HaveCount(DefaultPageSize); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost/blogPosts?page[number]=3&foo=bar"); - responseDocument.Links.First.Should().Be("http://localhost/blogPosts?foo=bar"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?foo=bar"); responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be("http://localhost/blogPosts?page[number]=2&foo=bar"); - responseDocument.Links.Next.Should().Be("http://localhost/blogPosts?page[number]=4&foo=bar"); + responseDocument.Links.Prev.Should().Be(HostPrefix + "/blogPosts?page[number]=2&foo=bar"); + responseDocument.Links.Next.Should().Be(HostPrefix + "/blogPosts?page[number]=4&foo=bar"); } [Fact] @@ -174,7 +175,7 @@ public async Task Renders_pagination_links_when_page_number_is_specified_in_quer { // Arrange var account = _fakers.WebAccount.Generate(); - account.Posts = _fakers.BlogPost.Generate(_defaultPageSize * 3); + account.Posts = _fakers.BlogPost.Generate(DefaultPageSize * 3); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -190,14 +191,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(_defaultPageSize); + responseDocument.ManyData.Should().HaveCount(DefaultPageSize); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be($"http://localhost/webAccounts/{account.StringId}/posts?page[number]=3&foo=bar"); - responseDocument.Links.First.Should().Be($"http://localhost/webAccounts/{account.StringId}/posts?foo=bar"); + responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be(HostPrefix + $"/webAccounts/{account.StringId}/posts?foo=bar"); responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be($"http://localhost/webAccounts/{account.StringId}/posts?page[number]=2&foo=bar"); - responseDocument.Links.Next.Should().Be($"http://localhost/webAccounts/{account.StringId}/posts?page[number]=4&foo=bar"); + responseDocument.Links.Prev.Should().Be(HostPrefix + $"/webAccounts/{account.StringId}/posts?page[number]=2&foo=bar"); + responseDocument.Links.Next.Should().Be(HostPrefix + $"/webAccounts/{account.StringId}/posts?page[number]=4&foo=bar"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs index a6cb5e6cc6..22eddc7d1c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs @@ -16,14 +16,14 @@ public sealed class RangeValidationTests private readonly ExampleIntegrationTestContext, QueryStringDbContext> _testContext; private readonly QueryStringFakers _fakers = new QueryStringFakers(); - private const int _defaultPageSize = 5; + private const int DefaultPageSize = 5; public RangeValidationTests(ExampleIntegrationTestContext, QueryStringDbContext> testContext) { _testContext = testContext; var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService(); - options.DefaultPageSize = new PageSize(_defaultPageSize); + options.DefaultPageSize = new PageSize(DefaultPageSize); options.MaximumPageSize = null; options.MaximumPageNumber = null; } @@ -32,7 +32,7 @@ public RangeValidationTests(ExampleIntegrationTestContext(route); @@ -41,17 +41,19 @@ public async Task Cannot_use_negative_page_number() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Page number cannot be negative or zero."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified paging is invalid."); + error.Detail.Should().Be("Page number cannot be negative or zero."); + error.Source.Parameter.Should().Be("page[number]"); } [Fact] public async Task Cannot_use_zero_page_number() { // Arrange - var route = "/blogs?page[number]=0"; + const string route = "/blogs?page[number]=0"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -60,17 +62,19 @@ public async Task Cannot_use_zero_page_number() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Page number cannot be negative or zero."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified paging is invalid."); + error.Detail.Should().Be("Page number cannot be negative or zero."); + error.Source.Parameter.Should().Be("page[number]"); } [Fact] public async Task Can_use_positive_page_number() { // Arrange - var route = "/blogs?page[number]=20"; + const string route = "/blogs?page[number]=20"; // Act var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); @@ -89,11 +93,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Blogs.AddRange(blogs); - await dbContext.SaveChangesAsync(); }); - var route = "/blogs?sort=id&page[size]=3&page[number]=2"; + const string route = "/blogs?sort=id&page[size]=3&page[number]=2"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -108,7 +111,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_use_negative_page_size() { // Arrange - var route = "/blogs?page[size]=-1"; + const string route = "/blogs?page[size]=-1"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -117,17 +120,19 @@ public async Task Cannot_use_negative_page_size() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Page size cannot be negative."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[size]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified paging is invalid."); + error.Detail.Should().Be("Page size cannot be negative."); + error.Source.Parameter.Should().Be("page[size]"); } [Fact] public async Task Can_use_zero_page_size() { // Arrange - var route = "/blogs?page[size]=0"; + const string route = "/blogs?page[size]=0"; // Act var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); @@ -140,7 +145,7 @@ public async Task Can_use_zero_page_size() public async Task Can_use_positive_page_size() { // Arrange - var route = "/blogs?page[size]=50"; + const string route = "/blogs?page[size]=50"; // Act var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/RangeValidationWithMaximumTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/RangeValidationWithMaximumTests.cs index b5731aeaf0..e5c03bd1c6 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/RangeValidationWithMaximumTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Pagination/RangeValidationWithMaximumTests.cs @@ -15,8 +15,8 @@ public sealed class RangeValidationWithMaximumTests { private readonly ExampleIntegrationTestContext, QueryStringDbContext> _testContext; - private const int _maximumPageSize = 15; - private const int _maximumPageNumber = 20; + private const int MaximumPageSize = 15; + private const int MaximumPageNumber = 20; public RangeValidationWithMaximumTests(ExampleIntegrationTestContext, QueryStringDbContext> testContext) { @@ -24,15 +24,15 @@ public RangeValidationWithMaximumTests(ExampleIntegrationTestContext(); options.DefaultPageSize = new PageSize(5); - options.MaximumPageSize = new PageSize(_maximumPageSize); - options.MaximumPageNumber = new PageNumber(_maximumPageNumber); + options.MaximumPageSize = new PageSize(MaximumPageSize); + options.MaximumPageNumber = new PageNumber(MaximumPageNumber); } [Fact] public async Task Can_use_page_number_below_maximum() { // Arrange - const int pageNumber = _maximumPageNumber - 1; + const int pageNumber = MaximumPageNumber - 1; var route = "/blogs?page[number]=" + pageNumber; // Act @@ -46,7 +46,7 @@ public async Task Can_use_page_number_below_maximum() public async Task Can_use_page_number_equal_to_maximum() { // Arrange - const int pageNumber = _maximumPageNumber; + const int pageNumber = MaximumPageNumber; var route = "/blogs?page[number]=" + pageNumber; // Act @@ -60,7 +60,7 @@ public async Task Can_use_page_number_equal_to_maximum() public async Task Cannot_use_page_number_over_maximum() { // Arrange - const int pageNumber = _maximumPageNumber + 1; + const int pageNumber = MaximumPageNumber + 1; var route = "/blogs?page[number]=" + pageNumber; // Act @@ -70,17 +70,19 @@ public async Task Cannot_use_page_number_over_maximum() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be($"Page number cannot be higher than {_maximumPageNumber}."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified paging is invalid."); + error.Detail.Should().Be($"Page number cannot be higher than {MaximumPageNumber}."); + error.Source.Parameter.Should().Be("page[number]"); } [Fact] public async Task Cannot_use_zero_page_size() { // Arrange - var route = "/blogs?page[size]=0"; + const string route = "/blogs?page[size]=0"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -89,17 +91,19 @@ public async Task Cannot_use_zero_page_size() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Page size cannot be unconstrained."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[size]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified paging is invalid."); + error.Detail.Should().Be("Page size cannot be unconstrained."); + error.Source.Parameter.Should().Be("page[size]"); } [Fact] public async Task Can_use_page_size_below_maximum() { // Arrange - const int pageSize = _maximumPageSize - 1; + const int pageSize = MaximumPageSize - 1; var route = "/blogs?page[size]=" + pageSize; // Act @@ -113,7 +117,7 @@ public async Task Can_use_page_size_below_maximum() public async Task Can_use_page_size_equal_to_maximum() { // Arrange - const int pageSize = _maximumPageSize; + const int pageSize = MaximumPageSize; var route = "/blogs?page[size]=" + pageSize; // Act @@ -127,7 +131,7 @@ public async Task Can_use_page_size_equal_to_maximum() public async Task Cannot_use_page_size_over_maximum() { // Arrange - const int pageSize = _maximumPageSize + 1; + const int pageSize = MaximumPageSize + 1; var route = "/blogs?page[size]=" + pageSize; // Act @@ -137,10 +141,12 @@ public async Task Cannot_use_page_size_over_maximum() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be($"Page size cannot be higher than {_maximumPageSize}."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[size]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified paging is invalid."); + error.Detail.Should().Be($"Page size cannot be higher than {MaximumPageSize}."); + error.Source.Parameter.Should().Be("page[size]"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringDbContext.cs index 2551765272..a26e1d47b8 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringDbContext.cs @@ -1,7 +1,11 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +// @formatter:wrap_chained_method_calls chop_always + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class QueryStringDbContext : DbContext { public DbSet Blogs { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringFakers.cs index 2d535d00d4..f4640f076c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringFakers.cs @@ -2,6 +2,9 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings { internal sealed class QueryStringFakers : FakerContainer @@ -55,7 +58,7 @@ internal sealed class QueryStringFakers : FakerContainer .UseSeed(GetFakerSeed()) .RuleFor(appointment => appointment.Title, f => f.Random.Word()) .RuleFor(appointment => appointment.StartTime, f => f.Date.FutureOffset()) - .RuleFor(appointment => appointment.EndTime, (f, appointment) => appointment.StartTime.AddHours(f.Random.Double(1, 4)))); + .RuleFor(appointment => appointment.EndTime,(f, appointment) => appointment.StartTime.AddHours(f.Random.Double(1, 4)))); public Faker Blog => _lazyBlogFaker.Value; public Faker BlogPost => _lazyBlogPostFaker.Value; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs index 1fc4c975d0..eda5933b70 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs @@ -27,7 +27,7 @@ public async Task Cannot_use_unknown_query_string_parameter() var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.AllowUnknownQueryStringParameters = false; - var route = "/calendars?foo=bar"; + const string route = "/calendars?foo=bar"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -36,10 +36,13 @@ public async Task Cannot_use_unknown_query_string_parameter() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Unknown query string parameter."); - responseDocument.Errors[0].Detail.Should().Be("Query string parameter 'foo' is unknown. Set 'AllowUnknownQueryStringParameters' to 'true' in options to ignore unknown parameters."); - responseDocument.Errors[0].Source.Parameter.Should().Be("foo"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Unknown query string parameter."); + error.Detail.Should().Be("Query string parameter 'foo' is unknown. " + + "Set 'AllowUnknownQueryStringParameters' to 'true' in options to ignore unknown parameters."); + error.Source.Parameter.Should().Be("foo"); } [Fact] @@ -49,7 +52,7 @@ public async Task Can_use_unknown_query_string_parameter() var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.AllowUnknownQueryStringParameters = true; - var route = "/calendars?foo=bar"; + const string route = "/calendars?foo=bar"; // Act var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); @@ -81,10 +84,12 @@ public async Task Cannot_use_empty_query_string_parameter_value(string parameter httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Missing query string parameter value."); - responseDocument.Errors[0].Detail.Should().Be($"Missing value for '{parameterName}' query string parameter."); - responseDocument.Errors[0].Source.Parameter.Should().Be(parameterName); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Missing query string parameter value."); + error.Detail.Should().Be($"Missing value for '{parameterName}' query string parameter."); + error.Source.Parameter.Should().Be(parameterName); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SerializerDefaultValueHandlingTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SerializerDefaultValueHandlingTests.cs index 898b6b7ad4..da5ecb4de2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SerializerDefaultValueHandlingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SerializerDefaultValueHandlingTests.cs @@ -30,7 +30,7 @@ public async Task Cannot_override_from_query_string() var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.AllowQueryStringOverrideForSerializerDefaultValueHandling = false; - var route = "/calendars?defaults=true"; + const string route = "/calendars?defaults=true"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -39,10 +39,12 @@ public async Task Cannot_override_from_query_string() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - responseDocument.Errors[0].Detail.Should().Be("The parameter 'defaults' cannot be used at this endpoint."); - responseDocument.Errors[0].Source.Parameter.Should().Be("defaults"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); + error.Detail.Should().Be("The parameter 'defaults' cannot be used at this endpoint."); + error.Source.Parameter.Should().Be("defaults"); } [Theory] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SerializerNullValueHandlingTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SerializerNullValueHandlingTests.cs index 5ee4f490c8..dc76bfd36b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SerializerNullValueHandlingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SerializerNullValueHandlingTests.cs @@ -30,7 +30,7 @@ public async Task Cannot_override_from_query_string() var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.AllowQueryStringOverrideForSerializerNullValueHandling = false; - var route = "/calendars?nulls=true"; + const string route = "/calendars?nulls=true"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -39,10 +39,12 @@ public async Task Cannot_override_from_query_string() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - responseDocument.Errors[0].Detail.Should().Be("The parameter 'nulls' cannot be used at this endpoint."); - responseDocument.Errors[0].Source.Parameter.Should().Be("nulls"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); + error.Detail.Should().Be("The parameter 'nulls' cannot be used at this endpoint."); + error.Source.Parameter.Should().Be("nulls"); } [Theory] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs index 25b26b798b..ded585645b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs @@ -35,11 +35,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Posts.AddRange(posts); - await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?sort=caption"; + const string route = "/blogPosts?sort=caption"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -74,10 +73,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified sort is invalid."); - responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); - responseDocument.Errors[0].Source.Parameter.Should().Be("sort"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified sort is invalid."); + error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); + error.Source.Parameter.Should().Be("sort"); } [Fact] @@ -131,10 +132,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified sort is invalid."); - responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); - responseDocument.Errors[0].Source.Parameter.Should().Be("sort"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified sort is invalid."); + error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); + error.Source.Parameter.Should().Be("sort"); } [Fact] @@ -152,7 +155,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogs?sort=count(posts)"; + const string route = "/blogs?sort=count(posts)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -193,11 +196,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Posts.AddRange(posts); - await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?sort=-count(labels)"; + const string route = "/blogPosts?sort=-count(labels)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -355,7 +357,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogs?include=posts.comments&sort=title&sort[posts]=caption,url&sort[posts.comments]=-createdAt"; + const string route = "/blogs?include=posts.comments&sort=title&sort[posts]=caption,url&sort[posts.comments]=-createdAt"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -409,7 +411,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?sort=-author.displayName"; + const string route = "/blogPosts?sort=-author.displayName"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -446,10 +448,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogs?include=owner.posts.comments&" + - "sort=-title&" + - "sort[owner.posts]=-caption&" + - "sort[owner.posts.comments]=-createdAt"; + const string route = "/blogs?include=owner.posts.comments&sort=-title&sort[owner.posts]=-caption&sort[owner.posts.comments]=-createdAt"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -473,7 +472,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_sort_in_unknown_scope() { // Arrange - var route = "/webAccounts?sort[doesNotExist]=id"; + const string route = "/webAccounts?sort[doesNotExist]=id"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -482,17 +481,19 @@ public async Task Cannot_sort_in_unknown_scope() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified sort is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); - responseDocument.Errors[0].Source.Parameter.Should().Be("sort[doesNotExist]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified sort is invalid."); + error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); + error.Source.Parameter.Should().Be("sort[doesNotExist]"); } [Fact] public async Task Cannot_sort_in_unknown_nested_scope() { // Arrange - var route = "/webAccounts?sort[posts.doesNotExist]=id"; + const string route = "/webAccounts?sort[posts.doesNotExist]=id"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -501,17 +502,19 @@ public async Task Cannot_sort_in_unknown_nested_scope() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified sort is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); - responseDocument.Errors[0].Source.Parameter.Should().Be("sort[posts.doesNotExist]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified sort is invalid."); + error.Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); + error.Source.Parameter.Should().Be("sort[posts.doesNotExist]"); } [Fact] public async Task Cannot_sort_on_attribute_with_blocked_capability() { // Arrange - var route = "/webAccounts?sort=dateOfBirth"; + const string route = "/webAccounts?sort=dateOfBirth"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -520,10 +523,12 @@ public async Task Cannot_sort_on_attribute_with_blocked_capability() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Sorting on the requested attribute is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Sorting on attribute 'dateOfBirth' is not allowed."); - responseDocument.Errors[0].Source.Parameter.Should().Be("sort"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Sorting on the requested attribute is not allowed."); + error.Detail.Should().Be("Sorting on attribute 'dateOfBirth' is not allowed."); + error.Source.Parameter.Should().Be("sort"); } [Fact] @@ -546,7 +551,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/webAccounts?sort=displayName,-id"; + const string route = "/webAccounts?sort=displayName,-id"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -574,11 +579,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Accounts.AddRange(accounts); - await dbContext.SaveChangesAsync(); }); - var route = "/webAccounts"; + const string route = "/webAccounts"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/ResourceCaptureStore.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/ResourceCaptureStore.cs index 2082031663..08e342ff21 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/ResourceCaptureStore.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/ResourceCaptureStore.cs @@ -5,14 +5,14 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings.SparseFiel { public sealed class ResourceCaptureStore { - public List Resources { get; } = new List(); + internal List Resources { get; } = new List(); - public void Add(IEnumerable resources) + internal void Add(IEnumerable resources) { Resources.AddRange(resources); } - public void Clear() + internal void Clear() { Resources.Clear(); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs index 1b0f8b7efc..8efb97d80b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Repositories; @@ -12,6 +13,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings.SparseFiel /// /// Enables sparse fieldset tests to verify which fields were (not) retrieved from the database. /// + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class ResultCapturingRepository : EntityFrameworkCoreRepository where TResource : class, IIdentifiable { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs index bb46bf65f7..49d7264e88 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs @@ -48,7 +48,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?fields[blogPosts]=caption,author"; + const string route = "/blogPosts?fields[blogPosts]=caption,author"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -86,7 +86,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?fields[blogPosts]=caption"; + const string route = "/blogPosts?fields[blogPosts]=caption"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -121,7 +121,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?fields[blogPosts]=author"; + const string route = "/blogPosts?fields[blogPosts]=author"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -554,7 +554,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/blogPosts?fields[blogPosts]=id,caption"; + const string route = "/blogPosts?fields[blogPosts]=id,caption"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -578,7 +578,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_select_on_unknown_resource_type() { // Arrange - var route = "/webAccounts?fields[doesNotExist]=id"; + const string route = "/webAccounts?fields[doesNotExist]=id"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -587,10 +587,12 @@ public async Task Cannot_select_on_unknown_resource_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified fieldset is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Parameter.Should().Be("fields[doesNotExist]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("The specified fieldset is invalid."); + error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Source.Parameter.Should().Be("fields[doesNotExist]"); } [Fact] @@ -608,10 +610,12 @@ public async Task Cannot_select_attribute_with_blocked_capability() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Retrieving the requested attribute is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Retrieving the attribute 'password' is not allowed."); - responseDocument.Errors[0].Source.Parameter.Should().Be("fields[webAccounts]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Retrieving the requested attribute is not allowed."); + error.Detail.Should().Be("Retrieving the attribute 'password' is not allowed."); + error.Source.Parameter.Should().Be("fields[webAccounts]"); } [Fact] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/WebAccount.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/WebAccount.cs index 576953088a..3a51366dc0 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/WebAccount.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/WebAccount.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class WebAccount : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 4e961c5e17..722f6a0302 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -47,7 +47,7 @@ public async Task Sets_location_header_for_created_resource() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -84,7 +84,7 @@ public async Task Can_create_resource_with_int_ID() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -102,8 +102,7 @@ public async Task Can_create_resource_with_int_ID() await _testContext.RunOnDatabaseAsync(async dbContext => { - var workItemInDatabase = await dbContext.WorkItems - .FirstAsync(workItem => workItem.Id == newWorkItemId); + var workItemInDatabase = await dbContext.WorkItems.FirstWithIdAsync(newWorkItemId); workItemInDatabase.Description.Should().Be(newWorkItem.Description); workItemInDatabase.DueAt.Should().Be(newWorkItem.DueAt); @@ -132,7 +131,7 @@ public async Task Can_create_resource_with_long_ID() } }; - var route = "/userAccounts"; + const string route = "/userAccounts"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -150,8 +149,7 @@ public async Task Can_create_resource_with_long_ID() await _testContext.RunOnDatabaseAsync(async dbContext => { - var userAccountInDatabase = await dbContext.UserAccounts - .FirstAsync(userAccount => userAccount.Id == newUserAccountId); + var userAccountInDatabase = await dbContext.UserAccounts.FirstWithIdAsync(newUserAccountId); userAccountInDatabase.FirstName.Should().Be(newUserAccount.FirstName); userAccountInDatabase.LastName.Should().Be(newUserAccount.LastName); @@ -179,7 +177,7 @@ public async Task Can_create_resource_with_guid_ID() } }; - var route = "/workItemGroups"; + const string route = "/workItemGroups"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -196,8 +194,7 @@ public async Task Can_create_resource_with_guid_ID() await _testContext.RunOnDatabaseAsync(async dbContext => { - var groupInDatabase = await dbContext.Groups - .FirstAsync(group => group.Id == newGroupId); + var groupInDatabase = await dbContext.Groups.FirstWithIdAsync(newGroupId); groupInDatabase.Name.Should().Be(newGroup.Name); }); @@ -224,7 +221,7 @@ public async Task Can_create_resource_without_attributes_or_relationships() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -242,8 +239,7 @@ public async Task Can_create_resource_without_attributes_or_relationships() await _testContext.RunOnDatabaseAsync(async dbContext => { - var workItemInDatabase = await dbContext.WorkItems - .FirstAsync(workItem => workItem.Id == newWorkItemId); + var workItemInDatabase = await dbContext.WorkItems.FirstWithIdAsync(newWorkItemId); workItemInDatabase.Description.Should().BeNull(); workItemInDatabase.DueAt.Should().BeNull(); @@ -269,7 +265,7 @@ public async Task Can_create_resource_with_unknown_attribute() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -286,8 +282,7 @@ public async Task Can_create_resource_with_unknown_attribute() await _testContext.RunOnDatabaseAsync(async dbContext => { - var workItemInDatabase = await dbContext.WorkItems - .FirstAsync(workItem => workItem.Id == newWorkItemId); + var workItemInDatabase = await dbContext.WorkItems.FirstWithIdAsync(newWorkItemId); workItemInDatabase.Description.Should().Be(newWorkItem.Description); }); @@ -316,7 +311,7 @@ public async Task Can_create_resource_with_unknown_relationship() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -333,8 +328,7 @@ public async Task Can_create_resource_with_unknown_relationship() await _testContext.RunOnDatabaseAsync(async dbContext => { - var workItemInDatabase = await dbContext.WorkItems - .FirstOrDefaultAsync(workItem => workItem.Id == newWorkItemId); + var workItemInDatabase = await dbContext.WorkItems.FirstWithIdOrDefaultAsync(newWorkItemId); workItemInDatabase.Should().NotBeNull(); }); @@ -357,7 +351,7 @@ public async Task Cannot_create_resource_with_client_generated_ID() } }; - var route = "/rgbColors"; + const string route = "/rgbColors"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -366,10 +360,12 @@ public async Task Cannot_create_resource_with_client_generated_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("Specifying the resource ID in POST requests is not allowed."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/data/id"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("Specifying the resource ID in POST requests is not allowed."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data/id"); } [Fact] @@ -378,7 +374,7 @@ public async Task Cannot_create_resource_for_missing_request_body() // Arrange var requestBody = string.Empty; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -387,9 +383,11 @@ public async Task Cannot_create_resource_for_missing_request_body() httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Missing request body."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Missing request body."); + error.Detail.Should().BeNull(); } [Fact] @@ -406,7 +404,7 @@ public async Task Cannot_create_resource_for_missing_type() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -415,9 +413,11 @@ public async Task Cannot_create_resource_for_missing_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); } [Fact] @@ -435,7 +435,7 @@ public async Task Cannot_create_resource_for_unknown_type() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -444,9 +444,11 @@ public async Task Cannot_create_resource_for_unknown_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); } [Fact] @@ -464,7 +466,7 @@ public async Task Cannot_create_resource_on_unknown_resource_type_in_url() } }; - var route = "/doesNotExist"; + const string route = "/doesNotExist"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -487,7 +489,8 @@ public async Task Cannot_create_on_resource_type_mismatch_between_url_and_body() id = "0A0B0C" } }; - var route = "/workItems"; + + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -496,9 +499,11 @@ public async Task Cannot_create_on_resource_type_mismatch_between_url_and_body() httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); - responseDocument.Errors[0].Title.Should().Be("Resource type mismatch between request body and endpoint URL."); - responseDocument.Errors[0].Detail.Should().Be("Expected resource of type 'workItems' in POST request body at endpoint '/workItems', instead of 'rgbColors'."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); + error.Detail.Should().Be("Expected resource of type 'workItems' in POST request body at endpoint '/workItems', instead of 'rgbColors'."); } [Fact] @@ -517,7 +522,7 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -526,9 +531,11 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Setting the initial value of the requested attribute is not allowed."); - responseDocument.Errors[0].Detail.Should().StartWith("Setting the initial value of 'concurrencyToken' is not allowed. - Request body:"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Setting the initial value of the requested attribute is not allowed."); + error.Detail.Should().StartWith("Setting the initial value of 'concurrencyToken' is not allowed. - Request body:"); } [Fact] @@ -547,7 +554,7 @@ public async Task Cannot_create_resource_with_readonly_attribute() } }; - var route = "/workItemGroups"; + const string route = "/workItemGroups"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -556,18 +563,20 @@ public async Task Cannot_create_resource_with_readonly_attribute() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); - responseDocument.Errors[0].Detail.Should().StartWith("Attribute 'concurrencyToken' is read-only. - Request body:"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); + error.Detail.Should().StartWith("Attribute 'concurrencyToken' is read-only. - Request body:"); } [Fact] public async Task Cannot_create_resource_for_broken_JSON_request_body() { // Arrange - var requestBody = "{ \"data\" {"; + const string requestBody = "{ \"data\" {"; - var route = "/workItemGroups"; + const string route = "/workItemGroups"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -576,9 +585,11 @@ public async Task Cannot_create_resource_for_broken_JSON_request_body() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().StartWith("Invalid character after parsing"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().StartWith("Invalid character after parsing"); } [Fact] @@ -597,7 +608,7 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -606,9 +617,11 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'Nullable`1'. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'Nullable`1'. - Request body: <<"); } [Fact] @@ -672,7 +685,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -688,12 +701,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Assignee) .Include(workItem => workItem.Subscribers) .Include(workItem => workItem.WorkItemTags) .ThenInclude(workItemTag => workItemTag.Tag) - .FirstAsync(workItem => workItem.Id == newWorkItemId); + .FirstWithIdAsync(newWorkItemId); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.Description.Should().Be(newDescription); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs index a82338667a..f19078a987 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs @@ -6,7 +6,6 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExampleTests.Startups; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -47,7 +46,7 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ } }; - var route = "/workItemGroups"; + const string route = "/workItemGroups"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -63,8 +62,7 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ await _testContext.RunOnDatabaseAsync(async dbContext => { - var groupInDatabase = await dbContext.Groups - .FirstAsync(group => group.Id == newGroup.Id); + var groupInDatabase = await dbContext.Groups.FirstWithIdAsync(newGroup.Id); groupInDatabase.Name.Should().Be(newGroup.Name); }); @@ -93,7 +91,7 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ } }; - var route = "/workItemGroups?fields[workItemGroups]=name"; + const string route = "/workItemGroups?fields[workItemGroups]=name"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -110,8 +108,7 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ await _testContext.RunOnDatabaseAsync(async dbContext => { - var groupInDatabase = await dbContext.Groups - .FirstAsync(group => group.Id == newGroup.Id); + var groupInDatabase = await dbContext.Groups.FirstWithIdAsync(newGroup.Id); groupInDatabase.Name.Should().Be(newGroup.Name); }); @@ -139,7 +136,7 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_no_ } }; - var route = "/rgbColors"; + const string route = "/rgbColors"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -151,8 +148,7 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_no_ await _testContext.RunOnDatabaseAsync(async dbContext => { - var colorInDatabase = await dbContext.RgbColors - .FirstAsync(color => color.Id == newColor.Id); + var colorInDatabase = await dbContext.RgbColors.FirstWithIdAsync(newColor.Id); colorInDatabase.DisplayName.Should().Be(newColor.DisplayName); }); @@ -180,7 +176,7 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_no_ } }; - var route = "/rgbColors?fields[rgbColors]=id"; + const string route = "/rgbColors?fields[rgbColors]=id"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -192,8 +188,7 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_no_ await _testContext.RunOnDatabaseAsync(async dbContext => { - var colorInDatabase = await dbContext.RgbColors - .FirstAsync(color => color.Id == newColor.Id); + var colorInDatabase = await dbContext.RgbColors.FirstWithIdAsync(newColor.Id); colorInDatabase.DisplayName.Should().Be(newColor.DisplayName); }); @@ -214,7 +209,6 @@ public async Task Cannot_create_resource_for_existing_client_generated_ID() await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.RgbColors.Add(existingColor); - await dbContext.SaveChangesAsync(); }); @@ -231,7 +225,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/rgbColors"; + const string route = "/rgbColors"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -240,9 +234,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); - responseDocument.Errors[0].Title.Should().Be("Another resource with the specified ID already exists."); - responseDocument.Errors[0].Detail.Should().Be($"Another resource of type 'rgbColors' with ID '{existingColor.StringId}' already exists."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Another resource with the specified ID already exists."); + error.Detail.Should().Be($"Another resource of type 'rgbColors' with ID '{existingColor.StringId}' already exists."); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs index 2d9cb8f5fa..5596546a99 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -60,7 +60,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -79,7 +79,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == newWorkItemId); + .FirstWithIdAsync(newWorkItemId); workItemInDatabase.Subscribers.Should().HaveCount(2); workItemInDatabase.Subscribers.Should().ContainSingle(subscriber => subscriber.Id == existingUserAccounts[0].Id); @@ -126,7 +126,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/workItems?include=subscribers"; + const string route = "/workItems?include=subscribers"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -152,7 +152,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == newWorkItemId); + .FirstWithIdAsync(newWorkItemId); workItemInDatabase.Subscribers.Should().HaveCount(2); workItemInDatabase.Subscribers.Should().ContainSingle(userAccount => userAccount.Id == existingUserAccounts[0].Id); @@ -199,7 +199,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/workItems?include=subscribers&fields[userAccounts]=firstName"; + const string route = "/workItems?include=subscribers&fields[userAccounts]=firstName"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -225,7 +225,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == newWorkItemId); + .FirstWithIdAsync(newWorkItemId); workItemInDatabase.Subscribers.Should().HaveCount(2); workItemInDatabase.Subscribers.Should().ContainSingle(userAccount => userAccount.Id == existingUserAccounts[0].Id); @@ -283,7 +283,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/workItems?fields[workItems]=priority,tags&include=tags&fields[workTags]=text"; + const string route = "/workItems?fields[workItems]=priority,tags&include=tags&fields[workTags]=text"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -313,10 +313,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.WorkItemTags) .ThenInclude(workItemTag => workItemTag.Tag) - .FirstAsync(workItem => workItem.Id == newWorkItemId); + .FirstWithIdAsync(newWorkItemId); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.WorkItemTags.Should().HaveCount(3); workItemInDatabase.WorkItemTags.Should().ContainSingle(workItemTag => workItemTag.Tag.Id == existingTags[0].Id); @@ -350,7 +356,7 @@ public async Task Cannot_create_for_missing_relationship_type() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -359,9 +365,11 @@ public async Task Cannot_create_for_missing_relationship_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'subscribers' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().StartWith("Expected 'type' element in 'subscribers' relationship. - Request body: <<"); } [Fact] @@ -390,7 +398,7 @@ public async Task Cannot_create_for_unknown_relationship_type() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -399,9 +407,11 @@ public async Task Cannot_create_for_unknown_relationship_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); } [Fact] @@ -429,7 +439,7 @@ public async Task Cannot_create_for_missing_relationship_ID() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -438,9 +448,11 @@ public async Task Cannot_create_for_missing_relationship_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'id' element in 'subscribers' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); + error.Detail.Should().StartWith("Expected 'id' element in 'subscribers' relationship. - Request body: <<"); } [Fact] @@ -474,7 +486,7 @@ public async Task Cannot_create_for_unknown_relationship_IDs() } }; - var route = "/userAccounts"; + const string route = "/userAccounts"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -484,13 +496,15 @@ public async Task Cannot_create_for_unknown_relationship_IDs() responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'workItems' with ID '12345678' in relationship 'assignedItems' does not exist."); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.NotFound); + error1.Title.Should().Be("A related resource does not exist."); + error1.Detail.Should().Be("Related resource of type 'workItems' with ID '12345678' in relationship 'assignedItems' does not exist."); - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[1].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[1].Detail.Should().Be("Related resource of type 'workItems' with ID '87654321' in relationship 'assignedItems' does not exist."); + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.NotFound); + error2.Title.Should().Be("A related resource does not exist."); + error2.Detail.Should().Be("Related resource of type 'workItems' with ID '87654321' in relationship 'assignedItems' does not exist."); } [Fact] @@ -519,7 +533,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -528,9 +542,11 @@ public async Task Cannot_create_on_relationship_type_mismatch() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Relationship 'subscribers' contains incompatible resource type 'rgbColors'. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); + error.Detail.Should().StartWith("Relationship 'subscribers' contains incompatible resource type 'rgbColors'. - Request body: <<"); } [Fact] @@ -572,7 +588,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/workItems?include=subscribers"; + const string route = "/workItems?include=subscribers"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -594,7 +610,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == newWorkItemId); + .FirstWithIdAsync(newWorkItemId); workItemInDatabase.Subscribers.Should().HaveCount(1); workItemInDatabase.Subscribers.Single().Id.Should().Be(existingUserAccount.Id); @@ -620,7 +636,7 @@ public async Task Cannot_create_with_null_data_in_HasMany_relationship() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -629,9 +645,11 @@ public async Task Cannot_create_with_null_data_in_HasMany_relationship() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); } [Fact] @@ -653,7 +671,7 @@ public async Task Cannot_create_with_null_data_in_HasManyThrough_relationship() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -662,9 +680,11 @@ public async Task Cannot_create_with_null_data_in_HasManyThrough_relationship() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); } [Fact] @@ -696,7 +716,7 @@ public async Task Cannot_create_resource_with_local_ID() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -705,9 +725,11 @@ public async Task Cannot_create_resource_with_local_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Local IDs cannot be used at this endpoint."); - responseDocument.Errors[0].Detail.Should().StartWith("Local IDs cannot be used at this endpoint. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Local IDs cannot be used at this endpoint."); + error.Detail.Should().StartWith("Local IDs cannot be used at this endpoint. - Request body: <<"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index 888cea9571..f743459eed 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -59,7 +59,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/workItemGroups"; + const string route = "/workItemGroups"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -102,7 +102,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string colorId = "0A0B0C"; + const string colorId = "0A0B0C"; var requestBody = new { @@ -124,7 +124,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/rgbColors"; + const string route = "/rgbColors"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -181,7 +181,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/workItems?include=assignee"; + const string route = "/workItems?include=assignee"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -206,7 +206,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Assignee) - .FirstAsync(workItem => workItem.Id == newWorkItemId); + .FirstWithIdAsync(newWorkItemId); workItemInDatabase.Assignee.Should().NotBeNull(); workItemInDatabase.Assignee.Id.Should().Be(existingUserAccount.Id); @@ -250,7 +250,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/workItems?fields[workItems]=description,assignee&include=assignee"; + const string route = "/workItems?fields[workItems]=description,assignee&include=assignee"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -277,7 +277,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Assignee) - .FirstAsync(workItem => workItem.Id == newWorkItemId); + .FirstWithIdAsync(newWorkItemId); workItemInDatabase.Description.Should().Be(newWorkItem.Description); workItemInDatabase.Priority.Should().Be(newWorkItem.Priority); @@ -308,7 +308,7 @@ public async Task Cannot_create_for_missing_relationship_type() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -317,9 +317,11 @@ public async Task Cannot_create_for_missing_relationship_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'assignee' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().StartWith("Expected 'type' element in 'assignee' relationship. - Request body: <<"); } [Fact] @@ -345,7 +347,7 @@ public async Task Cannot_create_for_unknown_relationship_type() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -354,9 +356,11 @@ public async Task Cannot_create_for_unknown_relationship_type() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); } [Fact] @@ -381,7 +385,7 @@ public async Task Cannot_create_for_missing_relationship_ID() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -390,9 +394,11 @@ public async Task Cannot_create_for_missing_relationship_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'id' element in 'assignee' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); + error.Detail.Should().StartWith("Expected 'id' element in 'assignee' relationship. - Request body: <<"); } [Fact] @@ -418,7 +424,7 @@ public async Task Cannot_create_with_unknown_relationship_ID() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -427,9 +433,11 @@ public async Task Cannot_create_with_unknown_relationship_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'userAccounts' with ID '12345678' in relationship 'assignee' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("A related resource does not exist."); + error.Detail.Should().Be("Related resource of type 'userAccounts' with ID '12345678' in relationship 'assignee' does not exist."); } [Fact] @@ -455,7 +463,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -464,9 +472,11 @@ public async Task Cannot_create_on_relationship_type_mismatch() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Relationship 'assignee' contains incompatible resource type 'rgbColors'. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); + error.Detail.Should().StartWith("Relationship 'assignee' contains incompatible resource type 'rgbColors'. - Request body: <<"); } [Fact] @@ -510,7 +520,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBodyText = JsonConvert.SerializeObject(requestBody).Replace("assignee_duplicate", "assignee"); - var route = "/workItems?include=assignee"; + const string route = "/workItems?include=assignee"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBodyText); @@ -535,7 +545,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Assignee) - .FirstAsync(workItem => workItem.Id == newWorkItemId); + .FirstWithIdAsync(newWorkItemId); workItemInDatabase.Assignee.Should().NotBeNull(); workItemInDatabase.Assignee.Id.Should().Be(existingUserAccounts[1].Id); @@ -576,7 +586,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -585,9 +595,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected single data element for 'assignee' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); + error.Detail.Should().StartWith("Expected single data element for 'assignee' relationship. - Request body: <<"); } [Fact] @@ -616,7 +628,7 @@ public async Task Cannot_create_resource_with_local_ID() } }; - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -625,9 +637,11 @@ public async Task Cannot_create_resource_with_local_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Local IDs cannot be used at this endpoint."); - responseDocument.Errors[0].Detail.Should().StartWith("Local IDs cannot be used at this endpoint. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Local IDs cannot be used at this endpoint."); + error.Detail.Should().StartWith("Local IDs cannot be used at this endpoint. - Request body: <<"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs index 3b49693fc3..cd3ed15de7 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs @@ -45,8 +45,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var workItemsInDatabase = await dbContext.WorkItems - .FirstOrDefaultAsync(workItem => workItem.Id == existingWorkItem.Id); + var workItemsInDatabase = await dbContext.WorkItems.FirstWithIdOrDefaultAsync(existingWorkItem.Id); workItemsInDatabase.Should().BeNull(); }); @@ -56,7 +55,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_delete_missing_resource() { // Arrange - var route = "/workItems/99999999"; + const string route = "/workItems/99999999"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -65,9 +64,11 @@ public async Task Cannot_delete_missing_resource() httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); } [Fact] @@ -95,13 +96,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var colorsInDatabase = await dbContext.RgbColors - .FirstOrDefaultAsync(color => color.Id == existingColor.Id); + var colorsInDatabase = await dbContext.RgbColors.FirstWithIdOrDefaultAsync(existingColor.Id); colorsInDatabase.Should().BeNull(); - var groupInDatabase = await dbContext.Groups - .FirstAsync(group => group.Id == existingColor.Group.Id); + var groupInDatabase = await dbContext.Groups.FirstWithIdAsync(existingColor.Group.Id); groupInDatabase.Color.Should().BeNull(); }); @@ -132,13 +131,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var groupsInDatabase = await dbContext.Groups - .FirstOrDefaultAsync(group => group.Id == existingGroup.Id); + var groupsInDatabase = await dbContext.Groups.FirstWithIdOrDefaultAsync(existingGroup.Id); groupsInDatabase.Should().BeNull(); - var colorInDatabase = await dbContext.RgbColors - .FirstOrDefaultAsync(color => color.Id == existingGroup.Color.Id); + var colorInDatabase = await dbContext.RgbColors.FirstWithIdOrDefaultAsync(existingGroup.Color.Id); colorInDatabase.Should().NotBeNull(); colorInDatabase.Group.Should().BeNull(); @@ -170,8 +167,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var workItemInDatabase = await dbContext.WorkItems - .FirstOrDefaultAsync(workItem => workItem.Id == existingWorkItem.Id); + var workItemInDatabase = await dbContext.WorkItems.FirstWithIdOrDefaultAsync(existingWorkItem.Id); workItemInDatabase.Should().BeNull(); @@ -210,8 +206,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var workItemsInDatabase = await dbContext.WorkItems - .FirstOrDefaultAsync(workItem => workItem.Id == existingWorkItemTag.Item.Id); + var workItemsInDatabase = await dbContext.WorkItems.FirstWithIdOrDefaultAsync(existingWorkItemTag.Item.Id); workItemsInDatabase.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs index 252539df21..d75f538a2f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs @@ -197,7 +197,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => [Fact] public async Task Cannot_get_relationship_for_unknown_primary_type() { - var route = "/doesNotExist/99999999/relationships/assignee"; + const string route = "/doesNotExist/99999999/relationships/assignee"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -211,7 +211,7 @@ public async Task Cannot_get_relationship_for_unknown_primary_type() [Fact] public async Task Cannot_get_relationship_for_unknown_primary_ID() { - var route = "/workItems/99999999/relationships/assignee"; + const string route = "/workItems/99999999/relationships/assignee"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -220,9 +220,11 @@ public async Task Cannot_get_relationship_for_unknown_primary_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); } [Fact] @@ -245,9 +247,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested relationship does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested relationship does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index 6042b9ded4..fee5589a9a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -34,7 +34,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/workItems"; + const string route = "/workItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -63,7 +63,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_get_primary_resources_for_unknown_type() { // Arrange - var route = "/doesNotExist"; + const string route = "/doesNotExist"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -107,7 +107,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_get_primary_resource_for_unknown_type() { // Arrange - var route = "/doesNotExist/99999999"; + const string route = "/doesNotExist/99999999"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -122,7 +122,7 @@ public async Task Cannot_get_primary_resource_for_unknown_type() public async Task Cannot_get_primary_resource_for_unknown_ID() { // Arrange - var route = "/workItems/99999999"; + const string route = "/workItems/99999999"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -131,9 +131,11 @@ public async Task Cannot_get_primary_resource_for_unknown_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); } [Fact] @@ -322,7 +324,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_get_secondary_resource_for_unknown_primary_type() { // Arrange - var route = "/doesNotExist/99999999/assignee"; + const string route = "/doesNotExist/99999999/assignee"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -337,7 +339,7 @@ public async Task Cannot_get_secondary_resource_for_unknown_primary_type() public async Task Cannot_get_secondary_resource_for_unknown_primary_ID() { // Arrange - var route = "/workItems/99999999/assignee"; + const string route = "/workItems/99999999/assignee"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -346,9 +348,11 @@ public async Task Cannot_get_secondary_resource_for_unknown_primary_ID() httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); } [Fact] @@ -373,9 +377,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested relationship does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested relationship does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/ReadWriteDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/ReadWriteDbContext.cs index ea18e26788..b8da68ee04 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/ReadWriteDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/ReadWriteDbContext.cs @@ -1,7 +1,11 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +// @formatter:wrap_chained_method_calls chop_always + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ReadWriteDbContext : DbContext { public DbSet WorkItems { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/ReadWriteFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/ReadWriteFakers.cs index cab209819c..17a8dfdb4f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/ReadWriteFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/ReadWriteFakers.cs @@ -2,6 +2,9 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite { internal sealed class ReadWriteFakers : FakerContainer diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/RgbColor.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/RgbColor.cs index 2e5f60d881..5e1094bd17 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/RgbColor.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/RgbColor.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class RgbColor : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index 1d43cb3a59..730ead680e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -53,9 +53,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("Only to-many relationships can be updated through this endpoint."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'assignee' must be a to-many relationship."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("Only to-many relationships can be updated through this endpoint."); + error.Detail.Should().Be("Relationship 'assignee' must be a to-many relationship."); } [Fact] @@ -104,7 +106,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Subscribers.Should().HaveCount(3); workItemInDatabase.Subscribers.Should().ContainSingle(subscriber => subscriber.Id == existingWorkItem.Subscribers.ElementAt(0).Id); @@ -168,21 +170,30 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemsInDatabase = await dbContext.WorkItems .Include(workItem => workItem.WorkItemTags) .ThenInclude(workItemTag => workItemTag.Tag) .ToListAsync(); + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + var workItemInDatabase1 = workItemsInDatabase.Single(workItem => workItem.Id == existingWorkItems[0].Id); + int tagId1 = existingWorkItems[0].WorkItemTags.ElementAt(0).Tag.Id; + int tagId2 = existingWorkItems[1].WorkItemTags.ElementAt(0).Tag.Id; + workItemInDatabase1.WorkItemTags.Should().HaveCount(2); - workItemInDatabase1.WorkItemTags.Should().ContainSingle(workItemTag => workItemTag.Tag.Id == existingWorkItems[0].WorkItemTags.ElementAt(0).Tag.Id); - workItemInDatabase1.WorkItemTags.Should().ContainSingle(workItemTag => workItemTag.Tag.Id == existingWorkItems[1].WorkItemTags.ElementAt(0).Tag.Id); + workItemInDatabase1.WorkItemTags.Should().ContainSingle(workItemTag => workItemTag.Tag.Id == tagId1); + workItemInDatabase1.WorkItemTags.Should().ContainSingle(workItemTag => workItemTag.Tag.Id == tagId2); var workItemInDatabase2 = workItemsInDatabase.Single(workItem => workItem.Id == existingWorkItems[1].Id); workItemInDatabase2.WorkItemTags.Should().HaveCount(1); - workItemInDatabase2.WorkItemTags.ElementAt(0).Tag.Id.Should().Be(existingWorkItems[1].WorkItemTags.ElementAt(0).Tag.Id); + workItemInDatabase2.WorkItemTags.ElementAt(0).Tag.Id.Should().Be(tagId2); }); } @@ -209,9 +220,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Missing request body."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Missing request body."); + error.Detail.Should().BeNull(); } [Fact] @@ -246,9 +259,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); } [Fact] @@ -284,9 +299,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); } [Fact] @@ -321,9 +338,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); + error.Detail.Should().StartWith("Request body: <<"); } [Fact] @@ -365,13 +384,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.NotFound); + error1.Title.Should().Be("A related resource does not exist."); + error1.Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[1].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[1].Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.NotFound); + error2.Title.Should().Be("A related resource does not exist."); + error2.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); } [Fact] @@ -413,13 +434,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.NotFound); + error1.Title.Should().Be("A related resource does not exist."); + error1.Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[1].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[1].Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.NotFound); + error2.Title.Should().Be("A related resource does not exist."); + error2.Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); } [Fact] @@ -482,7 +505,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/workItems/99999999/relationships/subscribers"; + const string route = "/workItems/99999999/relationships/subscribers"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -491,9 +514,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); } [Fact] @@ -529,9 +554,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested relationship does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested relationship does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); } [Fact] @@ -568,9 +595,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); - responseDocument.Errors[0].Title.Should().Be("Resource type mismatch between request body and endpoint URL."); - responseDocument.Errors[0].Detail.Should().Be($"Expected resource of type 'workTags' in POST request body at endpoint '/workItems/{existingWorkItem.StringId}/relationships/tags', instead of 'userAccounts'."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); + error.Detail.Should().Be("Expected resource of type 'workTags' in POST request body at endpoint " + + $"'/workItems/{existingWorkItem.StringId}/relationships/tags', instead of 'userAccounts'."); } [Fact] @@ -617,7 +647,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Subscribers.Should().HaveCount(1); workItemInDatabase.Subscribers.Single().Id.Should().Be(existingSubscriber.Id); @@ -655,7 +685,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Subscribers.Should().HaveCount(0); }); @@ -687,9 +717,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); } [Fact] @@ -718,9 +750,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); } [Fact] @@ -762,7 +796,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Children) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Children.Should().HaveCount(2); workItemInDatabase.Children.Should().ContainSingle(workItem => workItem.Id == existingWorkItem.Children[0].Id); @@ -813,15 +847,23 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.RelatedToItems) .ThenInclude(workItemToWorkItem => workItemToWorkItem.ToItem) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + + int toItemId = existingWorkItem.RelatedToItems[0].ToItem.Id; workItemInDatabase.RelatedToItems.Should().HaveCount(2); workItemInDatabase.RelatedToItems.Should().OnlyContain(workItemToWorkItem => workItemToWorkItem.FromItem.Id == existingWorkItem.Id); workItemInDatabase.RelatedToItems.Should().ContainSingle(workItemToWorkItem => workItemToWorkItem.ToItem.Id == existingWorkItem.Id); - workItemInDatabase.RelatedToItems.Should().ContainSingle(workItemToWorkItem => workItemToWorkItem.ToItem.Id == existingWorkItem.RelatedToItems[0].ToItem.Id); + workItemInDatabase.RelatedToItems.Should().ContainSingle(workItemToWorkItem => workItemToWorkItem.ToItem.Id == toItemId); }); } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index 550f020e73..35bc7a1d06 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -53,9 +53,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("Only to-many relationships can be updated through this endpoint."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'assignee' must be a to-many relationship."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("Only to-many relationships can be updated through this endpoint."); + error.Detail.Should().Be("Relationship 'assignee' must be a to-many relationship."); } [Fact] @@ -104,7 +106,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Subscribers.Should().HaveCount(1); workItemInDatabase.Subscribers.Single().Id.Should().Be(existingWorkItem.Subscribers.ElementAt(1).Id); @@ -168,10 +170,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.WorkItemTags) .ThenInclude(workItemTag => workItemTag.Tag) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.WorkItemTags.Should().HaveCount(1); workItemInDatabase.WorkItemTags.Single().Tag.Id.Should().Be(existingWorkItem.WorkItemTags.ElementAt(0).Tag.Id); @@ -204,9 +212,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Missing request body."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Missing request body."); + error.Detail.Should().BeNull(); } [Fact] @@ -242,9 +252,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); } [Fact] @@ -280,9 +292,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); } [Fact] @@ -317,9 +331,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); + error.Detail.Should().StartWith("Request body: <<"); } [Fact] @@ -361,13 +377,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.NotFound); + error1.Title.Should().Be("A related resource does not exist."); + error1.Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[1].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[1].Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.NotFound); + error2.Title.Should().Be("A related resource does not exist."); + error2.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); } [Fact] @@ -409,13 +427,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.NotFound); + error1.Title.Should().Be("A related resource does not exist."); + error1.Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[1].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[1].Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.NotFound); + error2.Title.Should().Be("A related resource does not exist."); + error2.Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); } [Fact] @@ -477,7 +497,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/workItems/99999999/relationships/subscribers"; + const string route = "/workItems/99999999/relationships/subscribers"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); @@ -486,9 +506,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); } [Fact] @@ -524,9 +546,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested relationship does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested relationship does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); } [Fact] @@ -563,9 +587,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); - responseDocument.Errors[0].Title.Should().Be("Resource type mismatch between request body and endpoint URL."); - responseDocument.Errors[0].Detail.Should().Be($"Expected resource of type 'workTags' in DELETE request body at endpoint '/workItems/{existingWorkItem.StringId}/relationships/tags', instead of 'userAccounts'."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); + error.Detail.Should().Be("Expected resource of type 'workTags' in DELETE request body at endpoint " + + $"'/workItems/{existingWorkItem.StringId}/relationships/tags', instead of 'userAccounts'."); } [Fact] @@ -612,7 +639,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Subscribers.Should().HaveCount(1); workItemInDatabase.Subscribers.Single().Id.Should().Be(existingWorkItem.Subscribers.ElementAt(1).Id); @@ -651,7 +678,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Subscribers.Should().HaveCount(1); workItemInDatabase.Subscribers.Single().Id.Should().Be(existingWorkItem.Subscribers.ElementAt(0).Id); @@ -684,9 +711,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); } [Fact] @@ -715,9 +744,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); } [Fact] @@ -762,7 +793,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Children) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Children.Should().HaveCount(1); workItemInDatabase.Children[0].Id.Should().Be(existingWorkItem.Children[0].Id); @@ -818,10 +849,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.RelatedFromItems) .ThenInclude(workItemToWorkItem => workItemToWorkItem.FromItem) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.RelatedFromItems.Should().HaveCount(1); workItemInDatabase.RelatedFromItems[0].FromItem.Id.Should().Be(existingWorkItem.RelatedFromItems[0].FromItem.Id); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index 5140bbfeba..bbbf94c7aa 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -54,7 +54,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Subscribers.Should().BeEmpty(); }); @@ -96,10 +96,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.WorkItemTags) .ThenInclude(workItemTag => workItemTag.Tag) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.WorkItemTags.Should().BeEmpty(); }); @@ -151,7 +157,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Subscribers.Should().HaveCount(2); workItemInDatabase.Subscribers.Should().ContainSingle(userAccount => userAccount.Id == existingWorkItem.Subscribers.ElementAt(1).Id); @@ -219,10 +225,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.WorkItemTags) .ThenInclude(workItemTag => workItemTag.Tag) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.WorkItemTags.Should().HaveCount(3); workItemInDatabase.WorkItemTags.Should().ContainSingle(workItemTag => workItemTag.Tag.Id == existingWorkItem.WorkItemTags.ElementAt(0).Tag.Id); @@ -254,9 +266,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Missing request body."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Missing request body."); + error.Detail.Should().BeNull(); } [Fact] @@ -291,9 +305,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); } [Fact] @@ -329,9 +345,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); } [Fact] @@ -366,9 +384,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); + error.Detail.Should().StartWith("Request body: <<"); } [Fact] @@ -410,13 +430,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.NotFound); + error1.Title.Should().Be("A related resource does not exist."); + error1.Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[1].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[1].Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.NotFound); + error2.Title.Should().Be("A related resource does not exist."); + error2.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); } [Fact] @@ -458,13 +480,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Errors.Should().HaveCount(2); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.NotFound); + error1.Title.Should().Be("A related resource does not exist."); + error1.Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[1].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[1].Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.NotFound); + error2.Title.Should().Be("A related resource does not exist."); + error2.Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); } [Fact] @@ -520,7 +544,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new object[0] }; - var route = "/workItems/99999999/relationships/subscribers"; + const string route = "/workItems/99999999/relationships/subscribers"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -529,9 +553,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); } [Fact] @@ -567,9 +593,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested relationship does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested relationship does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); } [Fact] @@ -606,9 +634,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); - responseDocument.Errors[0].Title.Should().Be("Resource type mismatch between request body and endpoint URL."); - responseDocument.Errors[0].Detail.Should().Be($"Expected resource of type 'workTags' in PATCH request body at endpoint '/workItems/{existingWorkItem.StringId}/relationships/tags', instead of 'userAccounts'."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); + error.Detail.Should().Be("Expected resource of type 'workTags' in PATCH request body at endpoint " + + $"'/workItems/{existingWorkItem.StringId}/relationships/tags', instead of 'userAccounts'."); } [Fact] @@ -657,7 +688,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Subscribers.Should().HaveCount(1); workItemInDatabase.Subscribers.Single().Id.Should().Be(existingSubscriber.Id); @@ -690,9 +721,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); } [Fact] @@ -721,9 +754,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); } [Fact] @@ -763,7 +798,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Children) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Children.Should().BeEmpty(); }); @@ -807,10 +842,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.RelatedFromItems) .ThenInclude(workItemToWorkItem => workItemToWorkItem.FromItem) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.RelatedFromItems.Should().BeEmpty(); }); @@ -854,7 +895,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Children) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Children.Should().HaveCount(1); workItemInDatabase.Children[0].Id.Should().Be(existingWorkItem.Id); @@ -897,10 +938,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.RelatedToItems) .ThenInclude(workItemToWorkItem => workItemToWorkItem.ToItem) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.RelatedToItems.Should().HaveCount(1); workItemInDatabase.RelatedToItems[0].FromItem.Id.Should().Be(existingWorkItem.Id); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index b86ec9b952..4a09dc6305 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -53,7 +53,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Assignee) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Assignee.Should().BeNull(); }); @@ -91,7 +91,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var groupInDatabase = await dbContext.Groups .Include(group => group.Color) - .FirstOrDefaultAsync(group => group.Id == existingGroup.Id); + .FirstWithIdOrDefaultAsync(existingGroup.Id); groupInDatabase.Color.Should().BeNull(); }); @@ -238,9 +238,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + int itemId = existingUserAccounts[0].AssignedItems.ElementAt(1).Id; + var workItemInDatabase2 = await dbContext.WorkItems .Include(workItem => workItem.Assignee) - .FirstAsync(workItem => workItem.Id == existingUserAccounts[0].AssignedItems.ElementAt(1).Id); + .FirstWithIdAsync(itemId); workItemInDatabase2.Assignee.Should().NotBeNull(); workItemInDatabase2.Assignee.Id.Should().Be(existingUserAccounts[1].Id); @@ -270,9 +272,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Missing request body."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Missing request body."); + error.Detail.Should().BeNull(); } [Fact] @@ -304,9 +308,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); } [Fact] @@ -339,9 +345,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); } [Fact] @@ -373,9 +381,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); + error.Detail.Should().StartWith("Request body: <<"); } [Fact] @@ -408,9 +418,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'assignee' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("A related resource does not exist."); + error.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'assignee' does not exist."); } [Fact] @@ -467,7 +479,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/workItems/99999999/relationships/assignee"; + const string route = "/workItems/99999999/relationships/assignee"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -476,9 +488,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); } [Fact] @@ -511,9 +525,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested relationship does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested relationship does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); } [Fact] @@ -547,9 +563,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); - responseDocument.Errors[0].Title.Should().Be("Resource type mismatch between request body and endpoint URL."); - responseDocument.Errors[0].Detail.Should().Be($"Expected resource of type 'userAccounts' in PATCH request body at endpoint '/workItems/{existingWorkItem.StringId}/relationships/assignee', instead of 'rgbColors'."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); + error.Detail.Should().Be("Expected resource of type 'userAccounts' in PATCH request body at endpoint " + + $"'/workItems/{existingWorkItem.StringId}/relationships/assignee', instead of 'rgbColors'."); } [Fact] @@ -586,9 +605,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected single data element for 'assignee' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); + error.Detail.Should().StartWith("Expected single data element for 'assignee' relationship. - Request body: <<"); } [Fact] @@ -625,7 +646,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Parent) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Parent.Should().BeNull(); }); @@ -666,7 +687,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Parent) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Parent.Should().NotBeNull(); workItemInDatabase.Parent.Id.Should().Be(existingWorkItem.Id); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index 310d833ed5..860691893f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; +using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExampleTests.Startups; using Microsoft.EntityFrameworkCore; @@ -65,7 +66,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Subscribers.Should().BeEmpty(); }); @@ -118,10 +119,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.WorkItemTags) .ThenInclude(workItemTag => workItemTag.Tag) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.WorkItemTags.Should().BeEmpty(); }); @@ -184,7 +191,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Subscribers.Should().HaveCount(2); workItemInDatabase.Subscribers.Should().ContainSingle(userAccount => userAccount.Id == existingWorkItem.Subscribers.ElementAt(1).Id); @@ -263,10 +270,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.WorkItemTags) .ThenInclude(workItemTag => workItemTag.Tag) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.WorkItemTags.Should().HaveCount(3); workItemInDatabase.WorkItemTags.Should().ContainSingle(workItemTag => workItemTag.Tag.Id == existingWorkItem.WorkItemTags.ElementAt(0).Tag.Id); @@ -336,7 +349,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Subscribers.Should().HaveCount(1); workItemInDatabase.Subscribers.Single().Id.Should().Be(existingUserAccount.Id); @@ -407,10 +420,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.WorkItemTags) .ThenInclude(workItemTag => workItemTag.Tag) - .FirstAsync(workItem => workItem.Id == newWorkItemId); + .FirstWithIdAsync(newWorkItemId); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.WorkItemTags.Should().HaveCount(1); workItemInDatabase.WorkItemTags.Single().Tag.Id.Should().Be(existingTag.Id); @@ -460,9 +479,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'subscribers' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().StartWith("Expected 'type' element in 'subscribers' relationship. - Request body: <<"); } [Fact] @@ -509,9 +530,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); } [Fact] @@ -557,9 +580,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'id' element in 'subscribers' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); + error.Detail.Should().StartWith("Expected 'id' element in 'subscribers' relationship. - Request body: <<"); } [Fact] @@ -628,21 +653,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Errors.Should().HaveCount(4); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); - - responseDocument.Errors[1].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[1].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[1].Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); - - responseDocument.Errors[2].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[2].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[2].Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); - - responseDocument.Errors[3].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[3].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[3].Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); + var error1 = responseDocument.Errors[0]; + error1.StatusCode.Should().Be(HttpStatusCode.NotFound); + error1.Title.Should().Be("A related resource does not exist."); + error1.Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); + + var error2 = responseDocument.Errors[1]; + error2.StatusCode.Should().Be(HttpStatusCode.NotFound); + error2.Title.Should().Be("A related resource does not exist."); + error2.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); + + var error3 = responseDocument.Errors[2]; + error3.StatusCode.Should().Be(HttpStatusCode.NotFound); + error3.Title.Should().Be("A related resource does not exist."); + error3.Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); + + var error4 = responseDocument.Errors[3]; + error4.StatusCode.Should().Be(HttpStatusCode.NotFound); + error4.Title.Should().Be("A related resource does not exist."); + error4.Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); } [Fact] @@ -689,9 +718,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Relationship 'subscribers' contains incompatible resource type 'rgbColors'. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); + error.Detail.Should().StartWith("Relationship 'subscribers' contains incompatible resource type 'rgbColors'. - Request body: <<"); } [Fact] @@ -751,7 +782,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Subscribers) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Subscribers.Should().HaveCount(1); workItemInDatabase.Subscribers.Single().Id.Should().Be(existingSubscriber.Id); @@ -795,9 +826,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); } [Fact] @@ -837,9 +870,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); + error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); } [Fact] @@ -853,10 +888,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => dbContext.WorkItems.Add(existingWorkItem); await dbContext.SaveChangesAsync(); - existingWorkItem.Children = new List - { - existingWorkItem - }; + existingWorkItem.Children = existingWorkItem.AsList(); await dbContext.SaveChangesAsync(); }); @@ -890,7 +922,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Children) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Children.Should().BeEmpty(); }); @@ -945,10 +977,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.RelatedFromItems) .ThenInclude(workItemToWorkItem => workItemToWorkItem.FromItem) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.RelatedFromItems.Should().BeEmpty(); }); @@ -1003,7 +1041,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Children) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Children.Should().HaveCount(1); workItemInDatabase.Children[0].Id.Should().Be(existingWorkItem.Id); @@ -1057,10 +1095,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.RelatedToItems) .ThenInclude(workItemToWorkItem => workItemToWorkItem.ToItem) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.RelatedToItems.Should().HaveCount(1); workItemInDatabase.RelatedToItems[0].FromItem.Id.Should().Be(existingWorkItem.Id); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index f35fc9477d..57f106ac8b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -63,8 +63,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var userAccountInDatabase = await dbContext.UserAccounts - .FirstAsync(userAccount => userAccount.Id == existingUserAccount.Id); + var userAccountInDatabase = await dbContext.UserAccounts.FirstWithIdAsync(existingUserAccount.Id); userAccountInDatabase.FirstName.Should().Be(existingUserAccount.FirstName); userAccountInDatabase.LastName.Should().Be(existingUserAccount.LastName); @@ -110,8 +109,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var userAccountInDatabase = await dbContext.UserAccounts - .FirstAsync(userAccount => userAccount.Id == existingUserAccount.Id); + var userAccountInDatabase = await dbContext.UserAccounts.FirstWithIdAsync(existingUserAccount.Id); userAccountInDatabase.FirstName.Should().Be(newFirstName); userAccountInDatabase.LastName.Should().Be(existingUserAccount.LastName); @@ -204,8 +202,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var groupInDatabase = await dbContext.Groups - .FirstAsync(group => group.Id == existingGroup.Id); + var groupInDatabase = await dbContext.Groups.FirstWithIdAsync(existingGroup.Id); groupInDatabase.Name.Should().Be(newName); groupInDatabase.IsPublic.Should().Be(existingGroup.IsPublic); @@ -253,8 +250,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var colorInDatabase = await dbContext.RgbColors - .FirstAsync(color => color.Id == existingColor.Id); + var colorInDatabase = await dbContext.RgbColors.FirstWithIdAsync(existingColor.Id); colorInDatabase.DisplayName.Should().Be(newDisplayName); }); @@ -302,8 +298,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var userAccountInDatabase = await dbContext.UserAccounts - .FirstAsync(userAccount => userAccount.Id == existingUserAccount.Id); + var userAccountInDatabase = await dbContext.UserAccounts.FirstWithIdAsync(existingUserAccount.Id); userAccountInDatabase.FirstName.Should().Be(newUserAccount.FirstName); userAccountInDatabase.LastName.Should().Be(newUserAccount.LastName); @@ -356,8 +351,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var workItemInDatabase = await dbContext.WorkItems - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + var workItemInDatabase = await dbContext.WorkItems.FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Description.Should().Be(newDescription); workItemInDatabase.DueAt.Should().BeNull(); @@ -410,8 +404,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var workItemInDatabase = await dbContext.WorkItems - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + var workItemInDatabase = await dbContext.WorkItems.FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Description.Should().Be(newDescription); workItemInDatabase.DueAt.Should().BeNull(); @@ -481,8 +474,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var workItemInDatabase = await dbContext.WorkItems - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + var workItemInDatabase = await dbContext.WorkItems.FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Description.Should().Be(newDescription); workItemInDatabase.DueAt.Should().BeNull(); @@ -550,9 +542,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Missing request body."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Missing request body."); + error.Detail.Should().BeNull(); } [Fact] @@ -584,9 +578,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); } [Fact] @@ -619,9 +615,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); } [Fact] @@ -653,9 +651,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); + error.Detail.Should().StartWith("Request body: <<"); } [Fact] @@ -703,7 +703,7 @@ public async Task Cannot_update_resource_on_unknown_resource_ID_in_url() } }; - var route = "/workItems/99999999"; + const string route = "/workItems/99999999"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -712,9 +712,11 @@ public async Task Cannot_update_resource_on_unknown_resource_ID_in_url() httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); } [Fact] @@ -747,9 +749,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); - responseDocument.Errors[0].Title.Should().Be("Resource type mismatch between request body and endpoint URL."); - responseDocument.Errors[0].Detail.Should().Be($"Expected resource of type 'workItems' in PATCH request body at endpoint '/workItems/{existingWorkItem.StringId}', instead of 'rgbColors'."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); + error.Detail.Should().Be("Expected resource of type 'workItems' in PATCH request body at endpoint " + + $"'/workItems/{existingWorkItem.StringId}', instead of 'rgbColors'."); } [Fact] @@ -782,9 +787,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); - responseDocument.Errors[0].Title.Should().Be("Resource ID mismatch between request body and endpoint URL."); - responseDocument.Errors[0].Detail.Should().Be($"Expected resource ID '{existingWorkItems[1].StringId}' in PATCH request body at endpoint '/workItems/{existingWorkItems[1].StringId}', instead of '{existingWorkItems[0].StringId}'."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Resource ID mismatch between request body and endpoint URL."); + error.Detail.Should().Be($"Expected resource ID '{existingWorkItems[1].StringId}' in PATCH request body at endpoint " + + $"'/workItems/{existingWorkItems[1].StringId}', instead of '{existingWorkItems[0].StringId}'."); } [Fact] @@ -821,9 +829,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Changing the value of the requested attribute is not allowed."); - responseDocument.Errors[0].Detail.Should().StartWith("Changing the value of 'concurrencyToken' is not allowed. - Request body:"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Changing the value of the requested attribute is not allowed."); + error.Detail.Should().StartWith("Changing the value of 'concurrencyToken' is not allowed. - Request body:"); } [Fact] @@ -860,9 +870,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); - responseDocument.Errors[0].Detail.Should().StartWith("Attribute 'concurrencyToken' is read-only. - Request body:"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); + error.Detail.Should().StartWith("Attribute 'concurrencyToken' is read-only. - Request body:"); } [Fact] @@ -877,7 +889,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var requestBody = "{ \"data\" {"; + const string requestBody = "{ \"data\" {"; var route = "/workItemGroups/" + existingWorkItem.StringId; @@ -888,9 +900,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().StartWith("Invalid character after parsing"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().StartWith("Invalid character after parsing"); } [Fact] @@ -927,9 +941,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource ID is read-only. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); + error.Detail.Should().StartWith("Resource ID is read-only. - Request body: <<"); } [Fact] @@ -966,9 +982,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'Nullable`1'. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'Nullable`1'. - Request body: <<"); } [Fact] @@ -1058,12 +1076,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Assignee) .Include(workItem => workItem.Subscribers) .Include(workItem => workItem.WorkItemTags) .ThenInclude(workItemTag => workItemTag.Tag) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.Description.Should().Be(newDescription); @@ -1153,12 +1177,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Parent) .Include(workItem => workItem.Children) .Include(workItem => workItem.RelatedToItems) .ThenInclude(workItemToWorkItem => workItemToWorkItem.ToItem) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore workItemInDatabase.Parent.Should().NotBeNull(); workItemInDatabase.Parent.Id.Should().Be(existingWorkItem.Id); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index 87c84d4327..8176376fee 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -64,7 +64,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Assignee) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Assignee.Should().BeNull(); }); @@ -196,7 +196,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var colorInDatabase2 = colorsInDatabase.SingleOrDefault(color => color.Id == existingGroups[1].Color.Id); colorInDatabase2.Should().NotBeNull(); - colorInDatabase2.Group.Should().BeNull(); + colorInDatabase2!.Group.Should().BeNull(); }); } @@ -243,7 +243,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var colorInDatabase = await dbContext.RgbColors .Include(color => color.Group) - .FirstOrDefaultAsync(color => color.Id == existingColor.Id); + .FirstWithIdOrDefaultAsync(existingColor.Id); colorInDatabase.Group.Should().BeNull(); }); @@ -293,9 +293,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + int itemId = existingUserAccounts[0].AssignedItems.ElementAt(1).Id; + var workItemInDatabase2 = await dbContext.WorkItems .Include(workItem => workItem.Assignee) - .FirstAsync(workItem => workItem.Id == existingUserAccounts[0].AssignedItems.ElementAt(1).Id); + .FirstWithIdAsync(itemId); workItemInDatabase2.Assignee.Should().NotBeNull(); workItemInDatabase2.Assignee.Id.Should().Be(existingUserAccounts[1].Id); @@ -360,7 +362,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Assignee) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Assignee.Should().NotBeNull(); workItemInDatabase.Assignee.Id.Should().Be(existingUserAccount.Id); @@ -429,7 +431,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Assignee) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Assignee.Should().NotBeNull(); workItemInDatabase.Assignee.Id.Should().Be(existingUserAccount.Id); @@ -476,9 +478,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'assignee' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().StartWith("Expected 'type' element in 'assignee' relationship. - Request body: <<"); } [Fact] @@ -522,9 +526,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); } [Fact] @@ -567,9 +573,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'id' element in 'assignee' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); + error.Detail.Should().StartWith("Expected 'id' element in 'assignee' relationship. - Request body: <<"); } [Fact] @@ -613,9 +621,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'assignee' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("A related resource does not exist."); + error.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'assignee' does not exist."); } [Fact] @@ -659,9 +669,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Relationship 'assignee' contains incompatible resource type 'rgbColors'. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); + error.Detail.Should().StartWith("Relationship 'assignee' contains incompatible resource type 'rgbColors'. - Request body: <<"); } [Fact] @@ -709,9 +721,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected single data element for 'assignee' relationship. - Request body: <<"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); + error.Detail.Should().StartWith("Expected single data element for 'assignee' relationship. - Request body: <<"); } [Fact] @@ -759,7 +773,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Parent) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Parent.Should().BeNull(); }); @@ -811,7 +825,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var workItemInDatabase = await dbContext.WorkItems .Include(workItem => workItem.Parent) - .FirstAsync(workItem => workItem.Id == existingWorkItem.Id); + .FirstWithIdAsync(existingWorkItem.Id); workItemInDatabase.Parent.Should().NotBeNull(); workItemInDatabase.Parent.Id.Should().Be(existingWorkItem.Id); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/UserAccount.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/UserAccount.cs index 1ed7034038..a13f98a5be 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/UserAccount.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/UserAccount.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class UserAccount : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItem.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItem.cs index b908090c48..1136e1c98b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItem.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItem.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class WorkItem : Identifiable { [Attr] @@ -22,7 +24,7 @@ public sealed class WorkItem : Identifiable public Guid ConcurrencyToken { get => Guid.NewGuid(); - set { } + set => _ = value; } [HasOne] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemGroup.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemGroup.cs index 07bc16ba31..740c96ad22 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemGroup.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemGroup.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class WorkItemGroup : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemPriority.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemPriority.cs index baa810f7c0..4ee4e283e5 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemPriority.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemPriority.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public enum WorkItemPriority { Low, diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemTag.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemTag.cs index d9c13d9e4c..bf17fd2a3e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemTag.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemTag.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class WorkItemTag { public WorkItem Item { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemToWorkItem.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemToWorkItem.cs index 5d2eb588e8..92fa943645 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemToWorkItem.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemToWorkItem.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class WorkItemToWorkItem { public WorkItem FromItem { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkTag.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkTag.cs index 04ef9d3d95..290a0285c5 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkTag.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkTag.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class WorkTag : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/Customer.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/Customer.cs index 1d2cf0c1c3..0dad711a61 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/Customer.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/Customer.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RequiredRelationships { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Customer : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorDbContext.cs index 406be13587..d8a617037b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorDbContext.cs @@ -1,7 +1,11 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +// @formatter:wrap_chained_method_calls chop_always + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RequiredRelationships { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class DefaultBehaviorDbContext : DbContext { public DbSet Customers { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorFakers.cs index 0363ddda5b..05431b5d55 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorFakers.cs @@ -2,6 +2,9 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RequiredRelationships { internal sealed class DefaultBehaviorFakers : FakerContainer @@ -9,21 +12,18 @@ internal sealed class DefaultBehaviorFakers : FakerContainer private readonly Lazy> _orderFaker = new Lazy>(() => new Faker() .UseSeed(GetFakerSeed()) - .RuleFor(order => order.Amount, f => f.Finance.Amount()) - ); + .RuleFor(order => order.Amount, f => f.Finance.Amount())); private readonly Lazy> _customerFaker = new Lazy>(() => new Faker() .UseSeed(GetFakerSeed()) - .RuleFor(customer => customer.EmailAddress, f => f.Person.Email) - ); + .RuleFor(customer => customer.EmailAddress, f => f.Person.Email)); private readonly Lazy> _shipmentFaker = new Lazy>(() => new Faker() .UseSeed(GetFakerSeed()) .RuleFor(shipment => shipment.TrackAndTraceCode, f => f.Commerce.Ean13()) - .RuleFor(shipment => shipment.ShippedAt, f => f.Date.Past()) - ); + .RuleFor(shipment => shipment.ShippedAt, f => f.Date.Past())); public Faker Orders => _orderFaker.Value; public Faker Customers => _customerFaker.Value; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs index 9fda109f2e..f810bde4ed 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs @@ -42,7 +42,7 @@ public async Task Cannot_create_dependent_side_of_required_ManyToOne_relationshi } }; - var route = "/orders"; + const string route = "/orders"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -50,10 +50,12 @@ public async Task Cannot_create_dependent_side_of_required_ManyToOne_relationshi // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); - responseDocument.GetErrorStatusCode().Should().Be(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].Title.Should().Be("An unhandled error occurred while processing this request."); - responseDocument.Errors[0].Detail.Should().Be("Failed to persist changes in the underlying data store."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + error.Title.Should().Be("An unhandled error occurred while processing this request."); + error.Detail.Should().Be("Failed to persist changes in the underlying data store."); } [Fact] @@ -74,7 +76,7 @@ public async Task Cannot_create_dependent_side_of_required_OneToOne_relationship } }; - var route = "/shipments"; + const string route = "/shipments"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -82,10 +84,12 @@ public async Task Cannot_create_dependent_side_of_required_OneToOne_relationship // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); - responseDocument.GetErrorStatusCode().Should().Be(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].Title.Should().Be("An unhandled error occurred while processing this request."); - responseDocument.Errors[0].Detail.Should().Be("Failed to persist changes in the underlying data store."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + error.Title.Should().Be("An unhandled error occurred while processing this request."); + error.Detail.Should().Be("Failed to persist changes in the underlying data store."); } [Fact] @@ -113,10 +117,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var existingCustomerInDatabase = await dbContext.Customers.FindAsync(existingOrder.Customer.Id); + var existingCustomerInDatabase = await dbContext.Customers.FirstWithIdOrDefaultAsync(existingOrder.Customer.Id); existingCustomerInDatabase.Should().BeNull(); - var existingOrderInDatabase = await dbContext.Orders.FindAsync(existingOrder.Id); + var existingOrderInDatabase = await dbContext.Orders.FirstWithIdOrDefaultAsync(existingOrder.Id); existingOrderInDatabase.Should().BeNull(); }); } @@ -147,13 +151,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var existingOrderInDatabase = await dbContext.Orders.FindAsync(existingOrder.Id); + var existingOrderInDatabase = await dbContext.Orders.FirstWithIdOrDefaultAsync(existingOrder.Id); existingOrderInDatabase.Should().BeNull(); - var existingShipmentInDatabase = await dbContext.Shipments.FindAsync(existingOrder.Shipment.Id); + var existingShipmentInDatabase = await dbContext.Shipments.FirstWithIdOrDefaultAsync(existingOrder.Shipment.Id); existingShipmentInDatabase.Should().BeNull(); - var existingCustomerInDatabase = await dbContext.Customers.FindAsync(existingOrder.Customer.Id); + var existingCustomerInDatabase = await dbContext.Customers.FirstWithIdOrDefaultAsync(existingOrder.Customer.Id); existingCustomerInDatabase.Should().NotBeNull(); }); } @@ -196,10 +200,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.GetErrorStatusCode().Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].Title.Should().Be("Failed to clear a required relationship."); - responseDocument.Errors[0].Detail.Should().Be($"The relationship 'customer' of resource type 'orders' with ID '{existingOrder.StringId}' cannot be cleared because it is a required relationship."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Failed to clear a required relationship."); + error.Detail.Should().Be($"The relationship 'customer' of resource type 'orders' with ID '{existingOrder.StringId}' " + + "cannot be cleared because it is a required relationship."); } [Fact] @@ -229,10 +236,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.GetErrorStatusCode().Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].Title.Should().Be("Failed to clear a required relationship."); - responseDocument.Errors[0].Detail.Should().Be($"The relationship 'customer' of resource type 'orders' with ID '{existingOrder.StringId}' cannot be cleared because it is a required relationship."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Failed to clear a required relationship."); + error.Detail.Should().Be($"The relationship 'customer' of resource type 'orders' with ID '{existingOrder.StringId}' " + + "cannot be cleared because it is a required relationship."); } [Fact] @@ -273,10 +283,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.GetErrorStatusCode().Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].Title.Should().Be("Failed to clear a required relationship."); - responseDocument.Errors[0].Detail.Should().Be($"The relationship 'orders' of resource type 'customers' with ID '{existingOrder.StringId}' cannot be cleared because it is a required relationship."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Failed to clear a required relationship."); + error.Detail.Should().Be($"The relationship 'orders' of resource type 'customers' with ID '{existingOrder.StringId}' " + + "cannot be cleared because it is a required relationship."); } [Fact] @@ -306,10 +319,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.GetErrorStatusCode().Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].Title.Should().Be("Failed to clear a required relationship."); - responseDocument.Errors[0].Detail.Should().Be($"The relationship 'orders' of resource type 'customers' with ID '{existingOrder.StringId}' cannot be cleared because it is a required relationship."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Failed to clear a required relationship."); + error.Detail.Should().Be($"The relationship 'orders' of resource type 'customers' with ID '{existingOrder.StringId}' " + + "cannot be cleared because it is a required relationship."); } [Fact] @@ -346,10 +362,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.GetErrorStatusCode().Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].Title.Should().Be("Failed to clear a required relationship."); - responseDocument.Errors[0].Detail.Should().Be($"The relationship 'orders' of resource type 'customers' with ID '{existingOrder.StringId}' cannot be cleared because it is a required relationship."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Failed to clear a required relationship."); + error.Detail.Should().Be($"The relationship 'orders' of resource type 'customers' with ID '{existingOrder.StringId}' " + + "cannot be cleared because it is a required relationship."); } [Fact] @@ -397,10 +416,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); - responseDocument.GetErrorStatusCode().Should().Be(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].Title.Should().Be("An unhandled error occurred while processing this request."); - responseDocument.Errors[0].Detail.Should().StartWith("The property 'Id' on entity type 'Shipment' is part of a key and so cannot be modified or marked as modified."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + error.Title.Should().Be("An unhandled error occurred while processing this request."); + error.Detail.Should().StartWith("The property 'Id' on entity type 'Shipment' is part of a key and so cannot be modified or marked as modified."); } [Fact] @@ -437,10 +458,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); - responseDocument.GetErrorStatusCode().Should().Be(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].Title.Should().Be("An unhandled error occurred while processing this request."); - responseDocument.Errors[0].Detail.Should().StartWith("The property 'Id' on entity type 'Shipment' is part of a key and so cannot be modified or marked as modified."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + error.Title.Should().Be("An unhandled error occurred while processing this request."); + error.Detail.Should().StartWith("The property 'Id' on entity type 'Shipment' is part of a key and so cannot be modified or marked as modified."); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/Order.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/Order.cs index 3e57bae01e..7ddae205fa 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/Order.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/Order.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RequiredRelationships { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Order : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/Shipment.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/Shipment.cs index b79335b841..00257b5f0c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/Shipment.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/Shipment.cs @@ -1,9 +1,11 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RequiredRelationships { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Shipment : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/GiftCertificate.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/GiftCertificate.cs index 44436e03ee..ea508c877a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/GiftCertificate.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/GiftCertificate.cs @@ -1,11 +1,13 @@ using System; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using Microsoft.AspNetCore.Authentication; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceConstructorInjection { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class GiftCertificate : Identifiable { private readonly ISystemClock _systemClock; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/InjectionDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/InjectionDbContext.cs index 2b695dfd45..df174c7c6f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/InjectionDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/InjectionDbContext.cs @@ -1,9 +1,11 @@ -using System; +using JetBrains.Annotations; +using JsonApiDotNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceConstructorInjection { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class InjectionDbContext : DbContext { public ISystemClock SystemClock { get; } @@ -14,7 +16,9 @@ public sealed class InjectionDbContext : DbContext public InjectionDbContext(DbContextOptions options, ISystemClock systemClock) : base(options) { - SystemClock = systemClock ?? throw new ArgumentNullException(nameof(systemClock)); + ArgumentGuard.NotNull(systemClock, nameof(systemClock)); + + SystemClock = systemClock; } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/InjectionFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/InjectionFakers.cs index 35aea4b060..75c5dc6f92 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/InjectionFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/InjectionFakers.cs @@ -1,8 +1,12 @@ using System; using Bogus; +using JsonApiDotNetCore; using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceConstructorInjection { internal sealed class InjectionFakers : FakerContainer @@ -17,18 +21,20 @@ internal sealed class InjectionFakers : FakerContainer public InjectionFakers(IServiceProvider serviceProvider) { - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); + + _serviceProvider = serviceProvider; _lazyPostOfficeFaker = new Lazy>(() => new Faker() .UseSeed(GetFakerSeed()) - .CustomInstantiator(f => new PostOffice(ResolveDbContext())) + .CustomInstantiator(_ => new PostOffice(ResolveDbContext())) .RuleFor(postOffice => postOffice.Address, f => f.Address.FullAddress())); _lazyGiftCertificateFaker = new Lazy>(() => new Faker() .UseSeed(GetFakerSeed()) - .CustomInstantiator(f => new GiftCertificate(ResolveDbContext())) + .CustomInstantiator(_ => new GiftCertificate(ResolveDbContext())) .RuleFor(giftCertificate => giftCertificate.IssueDate, f => f.Date.PastOffset())); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/PostOffice.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/PostOffice.cs index e183760098..1980a1113c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/PostOffice.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/PostOffice.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using Microsoft.AspNetCore.Authentication; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceConstructorInjection { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class PostOffice : Identifiable { private readonly ISystemClock _systemClock; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/ResourceInjectionTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/ResourceInjectionTests.cs index 92b0783b89..f487f65bb7 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/ResourceInjectionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceConstructorInjection/ResourceInjectionTests.cs @@ -161,7 +161,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/giftCertificates?include=issuer"; + const string route = "/giftCertificates?include=issuer"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -184,8 +184,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { var certificateInDatabase = await dbContext.GiftCertificates - .Include(giftCertificate => giftCertificate.Issuer) - .FirstAsync(giftCertificate => giftCertificate.Id == newCertificateId); + .Include(certificate => certificate.Issuer) + .FirstWithIdAsync(newCertificateId); certificateInDatabase.IssueDate.Should().Be(newIssueDate); @@ -253,7 +253,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var officeInDatabase = await dbContext.PostOffice .Include(postOffice => postOffice.GiftCertificates) - .FirstAsync(postOffice => postOffice.Id == existingOffice.Id); + .FirstWithIdAsync(existingOffice.Id); officeInDatabase.Address.Should().Be(newAddress); @@ -286,8 +286,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var officeInDatabase = await dbContext.PostOffice - .FirstOrDefaultAsync(postOffice => postOffice.Id == existingOffice.Id); + var officeInDatabase = await dbContext.PostOffice.FirstWithIdOrDefaultAsync(existingOffice.Id); officeInDatabase.Should().BeNull(); }); @@ -297,7 +296,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_delete_unknown_resource() { // Arrange - var route = "/postOffices/99999999"; + const string route = "/postOffices/99999999"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -306,9 +305,11 @@ public async Task Cannot_delete_unknown_resource() httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'postOffices' with ID '99999999' does not exist."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be("Resource of type 'postOffices' with ID '99999999' does not exist."); } [Fact] @@ -352,7 +353,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var officeInDatabase = await dbContext.PostOffice .Include(postOffice => postOffice.GiftCertificates) - .FirstAsync(postOffice => postOffice.Id == existingOffice.Id); + .FirstWithIdAsync(existingOffice.Id); officeInDatabase.GiftCertificates.Should().HaveCount(2); }); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableDbContext.cs index 395d356313..cea928d6b4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class CallableDbContext : DbContext { public DbSet CallableResources { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResource.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResource.cs index e63ae24daa..73820a9364 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResource.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResource.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class CallableResource : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs index 7cfb920377..67d7fd5d61 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs @@ -2,6 +2,8 @@ using System.ComponentModel; using System.Linq; using System.Net; +using JetBrains.Annotations; +using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Queries.Expressions; @@ -11,10 +13,11 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class CallableResourceDefinition : JsonApiResourceDefinition { private readonly IUserRolesService _userRolesService; - private static readonly PageSize _maxPageSize = new PageSize(5); + private static readonly PageSize MaxPageSize = new PageSize(5); public CallableResourceDefinition(IResourceGraph resourceGraph, IUserRolesService userRolesService) : base(resourceGraph) { @@ -52,7 +55,7 @@ public override FilterExpression OnApplyFilter(FilterExpression existingFilter) return existingFilter == null ? (FilterExpression) isNotDeleted - : new LogicalExpression(LogicalOperator.And, new[] {isNotDeleted, existingFilter}); + : new LogicalExpression(LogicalOperator.And, ArrayFactory.Create(isNotDeleted, existingFilter)); } public override SortExpression OnApplySort(SortExpression existingSort) @@ -77,20 +80,24 @@ public override PaginationExpression OnApplyPagination(PaginationExpression exis if (existingPagination != null) { - var pageSize = existingPagination.PageSize?.Value <= _maxPageSize.Value ? existingPagination.PageSize : _maxPageSize; + var pageSize = existingPagination.PageSize?.Value <= MaxPageSize.Value ? existingPagination.PageSize : MaxPageSize; return new PaginationExpression(existingPagination.PageNumber, pageSize); } - return new PaginationExpression(PageNumber.ValueOne, _maxPageSize); + return new PaginationExpression(PageNumber.ValueOne, MaxPageSize); } public override SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExpression existingSparseFieldSet) { // Use case: always retrieve percentageComplete and never include riskLevel in responses. + // @formatter:keep_existing_linebreaks true + return existingSparseFieldSet .Including(resource => resource.PercentageComplete, ResourceGraph) .Excluding(resource => resource.RiskLevel, ResourceGraph); + + // @formatter:keep_existing_linebreaks restore } public override QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters() diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs index 5c54c0e658..5157569799 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs @@ -48,11 +48,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.CallableResources.Add(resource); - await dbContext.SaveChangesAsync(); }); - var route = "/callableResources?include=owner"; + const string route = "/callableResources?include=owner"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -61,8 +60,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Including owner is not permitted."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Including owner is not permitted."); + error.Detail.Should().BeNull(); } [Fact] @@ -97,11 +99,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); }); - var route = "/callableResources"; + const string route = "/callableResources"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -148,11 +149,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); }); - var route = "/callableResources?filter=equals(label,'B')"; + const string route = "/callableResources?filter=equals(label,'B')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -196,11 +196,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); }); - var route = "/callableResources"; + const string route = "/callableResources"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -244,11 +243,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); }); - var route = "/callableResources?sort=-createdAt,modifiedAt"; + const string route = "/callableResources?sort=-createdAt,modifiedAt"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -277,11 +275,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); }); - var route = "/callableResources?page[size]=8"; + const string route = "/callableResources?page[size]=8"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -305,7 +302,6 @@ public async Task Attribute_inclusion_from_resource_definition_is_applied_for_em await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.CallableResources.Add(resource); - await dbContext.SaveChangesAsync(); }); @@ -336,7 +332,6 @@ public async Task Attribute_inclusion_from_resource_definition_is_applied_for_no await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.CallableResources.Add(resource); - await dbContext.SaveChangesAsync(); }); @@ -369,7 +364,6 @@ public async Task Attribute_exclusion_from_resource_definition_is_applied_for_em await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.CallableResources.Add(resource); - await dbContext.SaveChangesAsync(); }); @@ -400,7 +394,6 @@ public async Task Attribute_exclusion_from_resource_definition_is_applied_for_no await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.CallableResources.Add(resource); - await dbContext.SaveChangesAsync(); }); @@ -451,11 +444,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); }); - var route = "/callableResources?isHighRisk=true"; + const string route = "/callableResources?isHighRisk=true"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -500,11 +492,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); }); - var route = "/callableResources?isHighRisk=false&filter=equals(label,'B')"; + const string route = "/callableResources?isHighRisk=false&filter=equals(label,'B')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -539,7 +530,6 @@ public async Task Queryable_parameter_handler_from_resource_definition_is_not_ap await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.CallableResources.Add(resource); - await dbContext.SaveChangesAsync(); }); @@ -552,10 +542,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Custom query string parameters cannot be used on nested resource endpoints."); - responseDocument.Errors[0].Detail.Should().Be("Query string parameter 'isHighRisk' cannot be used on a nested resource endpoint."); - responseDocument.Errors[0].Source.Parameter.Should().Be("isHighRisk"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Custom query string parameters cannot be used on nested resource endpoints."); + error.Detail.Should().Be("Query string parameter 'isHighRisk' cannot be used on a nested resource endpoint."); + error.Source.Parameter.Should().Be("isHighRisk"); } private sealed class FakeUserRolesService : IUserRolesService diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHookTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHookTests.cs index 62bd8491e3..f41738308a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHookTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHookTests.cs @@ -12,7 +12,6 @@ using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Definitions; using JsonApiDotNetCoreExample.Models; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using TestBuildingBlocks; @@ -50,12 +49,12 @@ public ResourceHookTests(ExampleIntegrationTestContext(p => new {p.Password, p.UserName}); - string requestBody = serializer.Serialize(user); + string requestBody = serializer.Serialize(newUser); - var route = "/api/v1/users"; + const string route = "/api/v1/users"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -67,14 +66,14 @@ public async Task Can_create_user_with_password() var document = JsonConvert.DeserializeObject(responseDocument); document.SingleData.Attributes.Should().NotContainKey("password"); - document.SingleData.Attributes["userName"].Should().Be(user.UserName); + document.SingleData.Attributes["userName"].Should().Be(newUser.UserName); await _testContext.RunOnDatabaseAsync(async dbContext => { - var userInDatabase = await dbContext.Users.FirstAsync(u => u.Id == responseUser.Id); + var userInDatabase = await dbContext.Users.FirstWithIdAsync(responseUser.Id); - userInDatabase.UserName.Should().Be(user.UserName); - userInDatabase.Password.Should().Be(user.Password); + userInDatabase.UserName.Should().Be(newUser.UserName); + userInDatabase.Password.Should().Be(newUser.Password); }); } @@ -82,20 +81,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_user_password() { // Arrange - var user = _fakers.User.Generate(); + var existingUser = _fakers.User.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { - dbContext.Users.Add(user); + dbContext.Users.Add(existingUser); await dbContext.SaveChangesAsync(); }); - user.Password = _fakers.User.Generate().Password; + existingUser.Password = _fakers.User.Generate().Password; var serializer = GetRequestSerializer(p => new {p.Password}); - string requestBody = serializer.Serialize(user); + string requestBody = serializer.Serialize(existingUser); - var route = $"/api/v1/users/{user.Id}"; + var route = $"/api/v1/users/{existingUser.Id}"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -104,13 +103,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.SingleData.Attributes.Should().NotContainKey("password"); - responseDocument.SingleData.Attributes["userName"].Should().Be(user.UserName); + responseDocument.SingleData.Attributes["userName"].Should().Be(existingUser.UserName); await _testContext.RunOnDatabaseAsync(async dbContext => { - var userInDatabase = await dbContext.Users.FirstAsync(u => u.Id == user.Id); + var userInDatabase = await dbContext.Users.FirstWithIdAsync(existingUser.Id); - userInDatabase.Password.Should().Be(user.Password); + userInDatabase.Password.Should().Be(existingUser.Password); }); } @@ -118,7 +117,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_block_access_to_resource_from_GetSingle_endpoint_using_BeforeRead_hook() { // Arrange - var route = "/api/v1/todoItems/1337"; + const string route = "/api/v1/todoItems/1337"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -127,16 +126,18 @@ public async Task Can_block_access_to_resource_from_GetSingle_endpoint_using_Bef httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("You are not allowed to update the author of todo items."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("You are not allowed to update the author of todo items."); + error.Detail.Should().BeNull(); } [Fact] public async Task Can_block_access_to_included_resource_using_BeforeRead_hook() { // Arrange - var route = "/api/v1/people/1?include=passport"; + const string route = "/api/v1/people/1?include=passport"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -145,9 +146,11 @@ public async Task Can_block_access_to_included_resource_using_BeforeRead_hook() httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("You are not allowed to include passports on individual persons."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("You are not allowed to include passports on individual persons."); + error.Detail.Should().BeNull(); } [Fact] @@ -172,9 +175,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("You are not allowed to see this article."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("You are not allowed to see this article."); + error.Detail.Should().BeNull(); } [Fact] @@ -182,7 +187,7 @@ public async Task Can_hide_primary_resource_from_result_set_from_GetAll_endpoint { // Arrange var articles = _fakers.Article.Generate(3); - string toBeExcluded = "This should not be included"; + const string toBeExcluded = "This should not be included"; articles[0].Caption = toBeExcluded; await _testContext.RunOnDatabaseAsync(async dbContext => @@ -191,7 +196,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/api/v1/articles"; + const string route = "/api/v1/articles"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -231,7 +236,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_hide_secondary_resource_from_ToMany_List_relationship_using_OnReturn_hook() { // Arrange - string toBeExcluded = "This should not be included"; + const string toBeExcluded = "This should not be included"; var author = _fakers.Author.Generate(); author.Articles = _fakers.Article.Generate(3); @@ -258,7 +263,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_hide_secondary_resource_from_ToMany_Set_relationship_using_OnReturn_hook() { // Arrange - string toBeExcluded = "This should not be included"; + const string toBeExcluded = "This should not be included"; var person = _fakers.Person.Generate(); person.TodoItems = _fakers.TodoItem.Generate(3).ToHashSet(); @@ -285,7 +290,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_hide_resource_from_included_HasManyThrough_relationship_using_OnReturn_hook() { // Arrange - string toBeExcluded = "This should not be included"; + const string toBeExcluded = "This should not be included"; var tags = _fakers.Tag.Generate(2); tags[0].Name = toBeExcluded; @@ -314,7 +319,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => options.DisableTopPagination = false; options.DisableChildrenPagination = true; - var route = "/api/v1/articles?include=tags"; + const string route = "/api/v1/articles?include=tags"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -358,7 +363,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/api/v1/people"; + const string route = "/api/v1/people"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -367,9 +372,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'people'."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'people'."); + error.Detail.Should().BeNull(); } [Fact] @@ -416,9 +423,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'passports'."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'passports'."); + error.Detail.Should().BeNull(); } [Fact] @@ -461,9 +470,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'passports'."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'passports'."); + error.Detail.Should().BeNull(); } [Fact] @@ -489,9 +500,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'people'."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'people'."); + error.Detail.Should().BeNull(); } [Fact] @@ -536,7 +549,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/api/v1/todoItems"; + const string route = "/api/v1/todoItems"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -545,9 +558,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'todoItems'."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'todoItems'."); + error.Detail.Should().BeNull(); } [Fact] @@ -605,9 +620,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'todoItems'."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'todoItems'."); + error.Detail.Should().BeNull(); } [Fact] @@ -634,9 +651,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'todoItems'."); - responseDocument.Errors[0].Detail.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); + error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'todoItems'."); + error.Detail.Should().BeNull(); } private IRequestSerializer GetRequestSerializer(Expression> attributes = null, diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHooksStartup.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHooksStartup.cs index 64685ba34e..57974b5ede 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHooksStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHooksStartup.cs @@ -1,19 +1,15 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCoreExampleTests.Startups; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class ResourceHooksStartup : TestableStartup where TDbContext : DbContext { - public ResourceHooksStartup(IConfiguration configuration) - : base(configuration) - { - } - public override void ConfigureServices(IServiceCollection services) { base.ConfigureServices(services); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/InheritanceDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/InheritanceDbContext.cs index 904b840c90..69f49b73b9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/InheritanceDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/InheritanceDbContext.cs @@ -1,8 +1,12 @@ +using JetBrains.Annotations; using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance.Models; using Microsoft.EntityFrameworkCore; +// @formatter:wrap_chained_method_calls chop_always + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class InheritanceDbContext : DbContext { public InheritanceDbContext(DbContextOptions options) : base(options) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/InheritanceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/InheritanceTests.cs index 602600b2b4..1542000fd5 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/InheritanceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/InheritanceTests.cs @@ -24,7 +24,7 @@ public InheritanceTests(ExampleIntegrationTestContext(route, requestBody); @@ -55,20 +55,19 @@ public async Task Can_create_resource_with_inherited_attributes() responseDocument.SingleData.Should().NotBeNull(); responseDocument.SingleData.Type.Should().Be("men"); - responseDocument.SingleData.Attributes["familyName"].Should().Be(man.FamilyName); - responseDocument.SingleData.Attributes["isRetired"].Should().Be(man.IsRetired); - responseDocument.SingleData.Attributes["hasBeard"].Should().Be(man.HasBeard); + responseDocument.SingleData.Attributes["familyName"].Should().Be(newMan.FamilyName); + responseDocument.SingleData.Attributes["isRetired"].Should().Be(newMan.IsRetired); + responseDocument.SingleData.Attributes["hasBeard"].Should().Be(newMan.HasBeard); var newManId = int.Parse(responseDocument.SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var manInDatabase = await dbContext.Men - .FirstAsync(m => m.Id == newManId); + var manInDatabase = await dbContext.Men.FirstWithIdAsync(newManId); - manInDatabase.FamilyName.Should().Be(man.FamilyName); - manInDatabase.IsRetired.Should().Be(man.IsRetired); - manInDatabase.HasBeard.Should().Be(man.HasBeard); + manInDatabase.FamilyName.Should().Be(newMan.FamilyName); + manInDatabase.IsRetired.Should().Be(newMan.IsRetired); + manInDatabase.HasBeard.Should().Be(newMan.HasBeard); }); } @@ -82,7 +81,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.CompanyHealthInsurances.Add(existingInsurance); - await dbContext.SaveChangesAsync(); }); @@ -105,7 +103,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/men"; + const string route = "/men"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -120,7 +118,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var manInDatabase = await dbContext.Men .Include(man => man.HealthInsurance) - .FirstAsync(man => man.Id == newManId); + .FirstWithIdAsync(newManId); manInDatabase.HealthInsurance.Should().BeOfType(); manInDatabase.HealthInsurance.Id.Should().Be(existingInsurance.Id); @@ -178,8 +176,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var manInDatabase = await dbContext.Men - .FirstAsync(man => man.Id == existingMan.Id); + var manInDatabase = await dbContext.Men.FirstWithIdAsync(existingMan.Id); manInDatabase.FamilyName.Should().Be(newMan.FamilyName); manInDatabase.IsRetired.Should().Be(newMan.IsRetired); @@ -198,7 +195,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTablesAsync(); dbContext.AddRange(existingMan, existingInsurance); - await dbContext.SaveChangesAsync(); }); @@ -225,7 +221,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var manInDatabase = await dbContext.Men .Include(man => man.HealthInsurance) - .FirstAsync(man => man.Id == existingMan.Id); + .FirstWithIdAsync(existingMan.Id); manInDatabase.HealthInsurance.Should().BeOfType(); manInDatabase.HealthInsurance.Id.Should().Be(existingInsurance.Id); @@ -243,7 +239,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTablesAsync(); dbContext.Humans.AddRange(existingFather, existingMother); - await dbContext.SaveChangesAsync(); }); @@ -274,7 +269,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/men"; + const string route = "/men"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -289,7 +284,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var manInDatabase = await dbContext.Men .Include(man => man.Parents) - .FirstAsync(man => man.Id == newManId); + .FirstWithIdAsync(newManId); manInDatabase.Parents.Should().HaveCount(2); manInDatabase.Parents.Should().ContainSingle(human => human is Man); @@ -309,7 +304,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTablesAsync(); dbContext.Humans.AddRange(existingChild, existingFather, existingMother); - await dbContext.SaveChangesAsync(); }); @@ -344,7 +338,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var manInDatabase = await dbContext.Men .Include(man => man.Parents) - .FirstAsync(man => man.Id == existingChild.Id); + .FirstWithIdAsync(existingChild.Id); manInDatabase.Parents.Should().HaveCount(2); manInDatabase.Parents.Should().ContainSingle(human => human is Man); @@ -363,7 +357,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTablesAsync(); dbContext.ContentItems.AddRange(existingBook, existingVideo); - await dbContext.SaveChangesAsync(); }); @@ -394,11 +387,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/men"; + const string route = "/men"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - + // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -407,11 +400,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var contentItems = await dbContext.HumanFavoriteContentItems .Where(favorite => favorite.Human.Id == newManId) .Select(favorite => favorite.ContentItem) .ToListAsync(); + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + contentItems.Should().HaveCount(2); contentItems.Should().ContainSingle(item => item is Book); contentItems.Should().ContainSingle(item => item is Video); @@ -430,7 +429,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTablesAsync(); dbContext.AddRange(existingBook, existingVideo, existingMan); - await dbContext.SaveChangesAsync(); }); @@ -463,11 +461,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + var contentItems = await dbContext.HumanFavoriteContentItems .Where(favorite => favorite.Human.Id == existingMan.Id) .Select(favorite => favorite.ContentItem) .ToListAsync(); + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + contentItems.Should().HaveCount(2); contentItems.Should().ContainSingle(item => item is Book); contentItems.Should().ContainSingle(item => item is Video); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Book.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Book.cs index 1c87e2f825..406456baa5 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Book.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Book.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Book : ContentItem { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/CompanyHealthInsurance.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/CompanyHealthInsurance.cs index cb1a8bcce8..4813c2b185 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/CompanyHealthInsurance.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/CompanyHealthInsurance.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class CompanyHealthInsurance : HealthInsurance { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/ContentItem.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/ContentItem.cs index 9f1509f98e..7eca0cab9a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/ContentItem.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/ContentItem.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public abstract class ContentItem : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/FamilyHealthInsurance.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/FamilyHealthInsurance.cs index 564e8f5701..ff38ba3094 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/FamilyHealthInsurance.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/FamilyHealthInsurance.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class FamilyHealthInsurance : HealthInsurance { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/HealthInsurance.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/HealthInsurance.cs index 4dc82d5cbf..eeaef53aa9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/HealthInsurance.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/HealthInsurance.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public abstract class HealthInsurance : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Human.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Human.cs index e4177e1c91..88086338c9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Human.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Human.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public abstract class Human : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/HumanFavoriteContentItem.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/HumanFavoriteContentItem.cs index 4fbbf6b698..b5ac783c76 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/HumanFavoriteContentItem.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/HumanFavoriteContentItem.cs @@ -1,13 +1,14 @@ +using JetBrains.Annotations; + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class HumanFavoriteContentItem { public int ContentItemId { get; set; } - public ContentItem ContentItem { get; set; } public int HumanId { get; set; } - public Human Human { get; set; } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Man.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Man.cs index 462dd0fc78..7c0acfaa35 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Man.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Man.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Man : Human { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Video.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Video.cs index 62cae2af08..dc34ebaf6c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Video.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Video.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Video : ContentItem { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Woman.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Woman.cs index 666fd53305..29007907a8 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Woman.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Woman.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance.Models { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Woman : Human { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Bed.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Bed.cs index 8c84293143..a576e6c903 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Bed.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Bed.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RestrictedControllers { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Bed : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Chair.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Chair.cs index 1948563f01..4f58b57233 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Chair.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Chair.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RestrictedControllers { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Chair : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/DisableQueryStringTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/DisableQueryStringTests.cs index 873add19b1..f6ec44d781 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/DisableQueryStringTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/DisableQueryStringTests.cs @@ -30,7 +30,7 @@ public DisableQueryStringTests(ExampleIntegrationTestContext(route); @@ -39,17 +39,19 @@ public async Task Cannot_sort_if_query_string_parameter_is_blocked_by_controller httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - responseDocument.Errors[0].Detail.Should().Be("The parameter 'sort' cannot be used at this endpoint."); - responseDocument.Errors[0].Source.Parameter.Should().Be("sort"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); + error.Detail.Should().Be("The parameter 'sort' cannot be used at this endpoint."); + error.Source.Parameter.Should().Be("sort"); } [Fact] public async Task Cannot_paginate_if_query_string_parameter_is_blocked_by_controller() { // Arrange - var route = "/sofas?page[number]=2"; + const string route = "/sofas?page[number]=2"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -58,17 +60,19 @@ public async Task Cannot_paginate_if_query_string_parameter_is_blocked_by_contro httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - responseDocument.Errors[0].Detail.Should().Be("The parameter 'page[number]' cannot be used at this endpoint."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); + error.Detail.Should().Be("The parameter 'page[number]' cannot be used at this endpoint."); + error.Source.Parameter.Should().Be("page[number]"); } [Fact] public async Task Cannot_use_custom_query_string_parameter_if_blocked_by_controller() { // Arrange - var route = "/beds?skipCache=true"; + const string route = "/beds?skipCache=true"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -77,10 +81,12 @@ public async Task Cannot_use_custom_query_string_parameter_if_blocked_by_control httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - responseDocument.Errors[0].Detail.Should().Be("The parameter 'skipCache' cannot be used at this endpoint."); - responseDocument.Errors[0].Source.Parameter.Should().Be("skipCache"); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); + error.Detail.Should().Be("The parameter 'skipCache' cannot be used at this endpoint."); + error.Source.Parameter.Should().Be("skipCache"); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs index b2725a9d18..47547faa57 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs @@ -23,7 +23,7 @@ public HttpReadOnlyTests(ExampleIntegrationTestContext(route); @@ -47,7 +47,7 @@ public async Task Cannot_create_resource() } }; - var route = "/beds"; + const string route = "/beds"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -56,9 +56,11 @@ public async Task Cannot_create_resource() httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); - responseDocument.Errors[0].Title.Should().Be("The request method is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Resource does not support POST requests."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); + error.Title.Should().Be("The request method is not allowed."); + error.Detail.Should().Be("Resource does not support POST requests."); } [Fact] @@ -94,9 +96,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); - responseDocument.Errors[0].Title.Should().Be("The request method is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Resource does not support PATCH requests."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); + error.Title.Should().Be("The request method is not allowed."); + error.Detail.Should().Be("Resource does not support PATCH requests."); } [Fact] @@ -120,9 +124,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); - responseDocument.Errors[0].Title.Should().Be("The request method is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Resource does not support DELETE requests."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); + error.Title.Should().Be("The request method is not allowed."); + error.Detail.Should().Be("Resource does not support DELETE requests."); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs index 26a8036d80..b2626252bc 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs @@ -23,7 +23,7 @@ public NoHttpDeleteTests(ExampleIntegrationTestContext(route); @@ -47,7 +47,7 @@ public async Task Can_create_resource() } }; - var route = "/sofas"; + const string route = "/sofas"; // Act var (httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody); @@ -110,9 +110,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); - responseDocument.Errors[0].Title.Should().Be("The request method is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Resource does not support DELETE requests."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); + error.Title.Should().Be("The request method is not allowed."); + error.Detail.Should().Be("Resource does not support DELETE requests."); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs index b3acd90b77..3a421083d3 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs @@ -23,7 +23,7 @@ public NoHttpPatchTests(ExampleIntegrationTestContext(route); @@ -47,7 +47,7 @@ public async Task Can_create_resource() } }; - var route = "/chairs"; + const string route = "/chairs"; // Act var (httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody); @@ -89,9 +89,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); - responseDocument.Errors[0].Title.Should().Be("The request method is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Resource does not support PATCH requests."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); + error.Title.Should().Be("The request method is not allowed."); + error.Detail.Should().Be("Resource does not support PATCH requests."); } [Fact] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs index 4486093ca0..cff7168f62 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs @@ -23,7 +23,7 @@ public NoHttpPostTests(ExampleIntegrationTestContext(route); @@ -47,7 +47,7 @@ public async Task Cannot_create_resource() } }; - var route = "/tables"; + const string route = "/tables"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -56,9 +56,11 @@ public async Task Cannot_create_resource() httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); - responseDocument.Errors[0].Title.Should().Be("The request method is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Resource does not support POST requests."); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); + error.Title.Should().Be("The request method is not allowed."); + error.Detail.Should().Be("Resource does not support POST requests."); } [Fact] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/RestrictionDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/RestrictionDbContext.cs index 190df5f4d1..c26e120450 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/RestrictionDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/RestrictionDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RestrictedControllers { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class RestrictionDbContext : DbContext { public DbSet Tables { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/RestrictionFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/RestrictionFakers.cs index b81d66dda5..2f8e844154 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/RestrictionFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/RestrictionFakers.cs @@ -2,6 +2,9 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RestrictedControllers { internal sealed class RestrictionFakers : FakerContainer diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/SkipCacheQueryStringParameterReader.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/SkipCacheQueryStringParameterReader.cs index a165357740..f129f5121e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/SkipCacheQueryStringParameterReader.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/SkipCacheQueryStringParameterReader.cs @@ -1,4 +1,5 @@ using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.QueryStrings; @@ -8,18 +9,19 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RestrictedControllers { public sealed class SkipCacheQueryStringParameterReader : IQueryStringParameterReader { - private const string _skipCacheParameterName = "skipCache"; + private const string SkipCacheParameterName = "skipCache"; + [UsedImplicitly] public bool SkipCache { get; private set; } public bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - return !disableQueryStringAttribute.ParameterNames.Contains(_skipCacheParameterName); + return !disableQueryStringAttribute.ParameterNames.Contains(SkipCacheParameterName); } public bool CanRead(string parameterName) { - return parameterName == _skipCacheParameterName; + return parameterName == SkipCacheParameterName; } public void Read(string parameterName, StringValues parameterValue) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Sofa.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Sofa.cs index c812e24b01..a126d7d0cc 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Sofa.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Sofa.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RestrictedControllers { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Sofa : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Table.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Table.cs index 6d07ba65d6..a9d2325ae2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Table.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/Table.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RestrictedControllers { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Table : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/Meeting.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/Meeting.cs index 6a3092004f..147eaa24c9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/Meeting.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/Meeting.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Serialization { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Meeting : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/MeetingAttendee.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/MeetingAttendee.cs index dd79ed029b..86628469c5 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/MeetingAttendee.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/MeetingAttendee.cs @@ -1,9 +1,11 @@ -using System; +using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Serialization { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class MeetingAttendee : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationDbContext.cs index 730ea37d42..87e39675a7 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Serialization { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class SerializationDbContext : DbContext { public DbSet Meetings { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationFakers.cs index 1bda09f4ae..acaeafa90b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationFakers.cs @@ -2,11 +2,14 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Serialization { internal sealed class SerializationFakers : FakerContainer { - private static readonly TimeSpan[] _meetingDurations = + private static readonly TimeSpan[] MeetingDurations = { TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30), @@ -19,7 +22,7 @@ internal sealed class SerializationFakers : FakerContainer .UseSeed(GetFakerSeed()) .RuleFor(meeting => meeting.Title, f => f.Lorem.Word()) .RuleFor(meeting => meeting.StartTime, f => TruncateToWholeMilliseconds(f.Date.FutureOffset())) - .RuleFor(meeting => meeting.Duration, f => f.PickRandom(_meetingDurations)) + .RuleFor(meeting => meeting.Duration, f => f.PickRandom(MeetingDurations)) .RuleFor(meeting => meeting.Latitude, f => f.Address.Latitude()) .RuleFor(meeting => meeting.Longitude, f => f.Address.Longitude())); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs index 76d0ccf298..88e083b766 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs @@ -49,7 +49,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/meetings?include=attendees"; + const string route = "/meetings?include=attendees"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -457,7 +457,7 @@ public async Task Can_create_resource_with_side_effects() } }; - var route = "/meetings"; + const string route = "/meetings"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Company.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Company.cs index 77128d4dbc..1e4dad6f61 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Company.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Company.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Company : Identifiable, ISoftDeletable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Department.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Department.cs index 06154b5a84..db2ec47c68 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Department.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Department.cs @@ -1,8 +1,10 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Department : Identifiable, ISoftDeletable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionDbContext.cs index df5c8cdcf5..01bdfe6ba6 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionDbContext.cs @@ -1,7 +1,9 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class SoftDeletionDbContext : DbContext { public DbSet Companies { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionResourceDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionResourceDefinition.cs index b3e32ba74c..095bdaf02f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionResourceDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionResourceDefinition.cs @@ -1,11 +1,14 @@ using System.Linq; +using JetBrains.Annotations; +using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion { - public class SoftDeletionResourceDefinition : JsonApiResourceDefinition + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class SoftDeletionResourceDefinition : JsonApiResourceDefinition where TResource : class, IIdentifiable, ISoftDeletable { private readonly IResourceGraph _resourceGraph; @@ -26,8 +29,7 @@ public override FilterExpression OnApplyFilter(FilterExpression existingFilter) return existingFilter == null ? (FilterExpression) isNotSoftDeleted - : new LogicalExpression(LogicalOperator.And, new[] {isNotSoftDeleted, existingFilter}); + : new LogicalExpression(LogicalOperator.And, ArrayFactory.Create(isNotSoftDeleted, existingFilter)); } - } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs index 2dcd98ea40..93b4d68c18 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs @@ -48,11 +48,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Departments.AddRange(departments); - await dbContext.SaveChangesAsync(); }); - var route = "/departments"; + const string route = "/departments"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -89,11 +88,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Departments.AddRange(departments); - await dbContext.SaveChangesAsync(); }); - var route = "/departments?filter=startsWith(name,'S')"; + const string route = "/departments?filter=startsWith(name,'S')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -118,7 +116,6 @@ public async Task Cannot_get_deleted_primary_resource_by_ID() await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.Departments.Add(department); - await dbContext.SaveChangesAsync(); }); @@ -131,10 +128,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be($"Resource of type 'departments' with ID '{department.StringId}' does not exist."); - responseDocument.Errors[0].Source.Parameter.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be($"Resource of type 'departments' with ID '{department.StringId}' does not exist."); + error.Source.Parameter.Should().BeNull(); } [Fact] @@ -160,7 +159,6 @@ public async Task Can_get_secondary_resources() await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.Companies.Add(company); - await dbContext.SaveChangesAsync(); }); @@ -195,7 +193,6 @@ public async Task Cannot_get_secondary_resources_for_deleted_parent() await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.Companies.Add(company); - await dbContext.SaveChangesAsync(); }); @@ -208,10 +205,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be($"Resource of type 'companies' with ID '{company.StringId}' does not exist."); - responseDocument.Errors[0].Source.Parameter.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be($"Resource of type 'companies' with ID '{company.StringId}' does not exist."); + error.Source.Parameter.Should().BeNull(); } [Fact] @@ -254,11 +253,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); dbContext.Companies.AddRange(companies); - await dbContext.SaveChangesAsync(); }); - var route = "/companies?include=departments"; + const string route = "/companies?include=departments"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -298,7 +296,6 @@ public async Task Can_get_relationship() await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.Companies.Add(company); - await dbContext.SaveChangesAsync(); }); @@ -333,7 +330,6 @@ public async Task Cannot_get_relationship_for_deleted_parent() await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.Companies.Add(company); - await dbContext.SaveChangesAsync(); }); @@ -346,10 +342,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be($"Resource of type 'companies' with ID '{company.StringId}' does not exist."); - responseDocument.Errors[0].Source.Parameter.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be($"Resource of type 'companies' with ID '{company.StringId}' does not exist."); + error.Source.Parameter.Should().BeNull(); } [Fact] @@ -364,7 +362,6 @@ public async Task Cannot_update_deleted_resource() await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.Companies.Add(company); - await dbContext.SaveChangesAsync(); }); @@ -390,10 +387,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be($"Resource of type 'companies' with ID '{company.StringId}' does not exist."); - responseDocument.Errors[0].Source.Parameter.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be($"Resource of type 'companies' with ID '{company.StringId}' does not exist."); + error.Source.Parameter.Should().BeNull(); } [Fact] @@ -415,7 +414,6 @@ public async Task Cannot_update_relationship_for_deleted_parent() await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.Companies.Add(company); - await dbContext.SaveChangesAsync(); }); @@ -433,10 +431,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); - responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be($"Resource of type 'companies' with ID '{company.StringId}' does not exist."); - responseDocument.Errors[0].Source.Parameter.Should().BeNull(); + + var error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.NotFound); + error.Title.Should().Be("The requested resource does not exist."); + error.Detail.Should().Be($"Resource of type 'companies' with ID '{company.StringId}' does not exist."); + error.Source.Parameter.Should().BeNull(); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs index a4193f46b0..713f3b6280 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs @@ -42,7 +42,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/maps?filter=equals(id,'00000000-0000-0000-0000-000000000000')"; + const string route = "/maps?filter=equals(id,'00000000-0000-0000-0000-000000000000')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -71,7 +71,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/maps/00000000-0000-0000-0000-000000000000?include=game"; + const string route = "/maps/00000000-0000-0000-0000-000000000000?include=game"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -111,7 +111,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/maps"; + const string route = "/maps"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -125,8 +125,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var mapInDatabase = await dbContext.Maps - .FirstAsync(map => map.Id == Guid.Empty); + var mapInDatabase = await dbContext.Maps.FirstWithIdAsync((Guid?)Guid.Empty); mapInDatabase.Should().NotBeNull(); mapInDatabase.Name.Should().Be(newName); @@ -162,7 +161,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/maps/00000000-0000-0000-0000-000000000000"; + const string route = "/maps/00000000-0000-0000-0000-000000000000"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -174,8 +173,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var mapInDatabase = await dbContext.Maps - .FirstAsync(game => game.Id == Guid.Empty); + var mapInDatabase = await dbContext.Maps.FirstWithIdAsync((Guid?)Guid.Empty); mapInDatabase.Should().NotBeNull(); mapInDatabase.Name.Should().Be(newName); @@ -216,7 +214,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var gameInDatabase = await dbContext.Games .Include(game => game.ActiveMap) - .FirstAsync(game => game.Id == existingGame.Id); + .FirstWithIdAsync(existingGame.Id); gameInDatabase.Should().NotBeNull(); gameInDatabase.ActiveMap.Should().BeNull(); @@ -262,7 +260,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var gameInDatabase = await dbContext.Games .Include(game => game.ActiveMap) - .FirstAsync(game => game.Id == existingGame.Id); + .FirstWithIdAsync(existingGame.Id); gameInDatabase.Should().NotBeNull(); gameInDatabase.ActiveMap.Id.Should().Be(Guid.Empty); @@ -309,7 +307,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var gameInDatabase = await dbContext.Games .Include(game => game.ActiveMap) - .FirstAsync(game => game.Id == existingGame.Id); + .FirstWithIdAsync(existingGame.Id); gameInDatabase.Should().NotBeNull(); gameInDatabase.ActiveMap.Id.Should().Be(Guid.Empty); @@ -350,7 +348,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var gameInDatabase = await dbContext.Games .Include(game => game.Maps) - .FirstAsync(game => game.Id == existingGame.Id); + .FirstWithIdAsync(existingGame.Id); gameInDatabase.Should().NotBeNull(); gameInDatabase.Maps.Should().BeEmpty(); @@ -399,7 +397,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var gameInDatabase = await dbContext.Games .Include(game => game.Maps) - .FirstAsync(game => game.Id == existingGame.Id); + .FirstWithIdAsync(existingGame.Id); gameInDatabase.Should().NotBeNull(); gameInDatabase.Maps.Should().HaveCount(1); @@ -450,7 +448,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var gameInDatabase = await dbContext.Games .Include(game => game.Maps) - .FirstAsync(game => game.Id == existingGame.Id); + .FirstWithIdAsync(existingGame.Id); gameInDatabase.Should().NotBeNull(); gameInDatabase.Maps.Should().HaveCount(1); @@ -501,7 +499,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var gameInDatabase = await dbContext.Games .Include(game => game.Maps) - .FirstAsync(game => game.Id == existingGame.Id); + .FirstWithIdAsync(existingGame.Id); gameInDatabase.Should().NotBeNull(); gameInDatabase.Maps.Should().HaveCount(2); @@ -550,7 +548,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var gameInDatabase = await dbContext.Games .Include(game => game.Maps) - .FirstAsync(game => game.Id == existingGame.Id); + .FirstWithIdAsync(existingGame.Id); gameInDatabase.Should().NotBeNull(); gameInDatabase.Maps.Should().HaveCount(1); @@ -572,7 +570,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/maps/00000000-0000-0000-0000-000000000000"; + const string route = "/maps/00000000-0000-0000-0000-000000000000"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -584,8 +582,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var gameInDatabase = await dbContext.Maps - .FirstOrDefaultAsync(map => map.Id == existingMap.Id); + var gameInDatabase = await dbContext.Maps.FirstWithIdOrDefaultAsync(existingMap.Id); gameInDatabase.Should().BeNull(); }); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/Game.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/Game.cs index 46269627ef..721da9d377 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/Game.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/Game.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ZeroKeys { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Game : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/Map.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/Map.cs index 994f2d0d2f..21ffe008a6 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/Map.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/Map.cs @@ -1,9 +1,11 @@ using System; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ZeroKeys { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Map : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/Player.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/Player.cs index 656bfd61a9..3d5a6236cf 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/Player.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/Player.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ZeroKeys { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Player : Identifiable { [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs index a50dec9bc0..0939ce692a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs @@ -41,7 +41,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/games?filter=equals(id,'0')"; + const string route = "/games?filter=equals(id,'0')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -69,7 +69,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/games/0?include=activePlayers"; + const string route = "/games/0?include=activePlayers"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -109,7 +109,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/games"; + const string route = "/games"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -124,8 +124,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var gameInDatabase = await dbContext.Games - .FirstAsync(game => game.Id == 0); + var gameInDatabase = await dbContext.Games.FirstWithIdAsync((int?)0); gameInDatabase.Should().NotBeNull(); gameInDatabase.Title.Should().Be(newTitle); @@ -161,7 +160,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/games/0"; + const string route = "/games/0"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -175,8 +174,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var gameInDatabase = await dbContext.Games - .FirstAsync(game => game.Id == 0); + var gameInDatabase = await dbContext.Games.FirstWithIdAsync((int?)0); gameInDatabase.Should().NotBeNull(); gameInDatabase.Title.Should().Be(newTitle); @@ -217,7 +215,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var playerInDatabase = await dbContext.Players .Include(player => player.ActiveGame) - .FirstAsync(player => player.Id == existingPlayer.Id); + .FirstWithIdAsync(existingPlayer.Id); playerInDatabase.Should().NotBeNull(); playerInDatabase.ActiveGame.Should().BeNull(); @@ -263,7 +261,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var playerInDatabase = await dbContext.Players .Include(player => player.ActiveGame) - .FirstAsync(player => player.Id == existingPlayer.Id); + .FirstWithIdAsync(existingPlayer.Id); playerInDatabase.Should().NotBeNull(); playerInDatabase.ActiveGame.Id.Should().Be(0); @@ -310,7 +308,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var playerInDatabase = await dbContext.Players .Include(player => player.ActiveGame) - .FirstAsync(player => player.Id == existingPlayer.Id); + .FirstWithIdAsync(existingPlayer.Id); playerInDatabase.Should().NotBeNull(); playerInDatabase.ActiveGame.Id.Should().Be(0); @@ -351,7 +349,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var playerInDatabase = await dbContext.Players .Include(player => player.RecentlyPlayed) - .FirstAsync(player => player.Id == existingPlayer.Id); + .FirstWithIdAsync(existingPlayer.Id); playerInDatabase.Should().NotBeNull(); playerInDatabase.RecentlyPlayed.Should().BeEmpty(); @@ -400,7 +398,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var playerInDatabase = await dbContext.Players .Include(player => player.RecentlyPlayed) - .FirstAsync(player => player.Id == existingPlayer.Id); + .FirstWithIdAsync(existingPlayer.Id); playerInDatabase.Should().NotBeNull(); playerInDatabase.RecentlyPlayed.Should().HaveCount(1); @@ -451,7 +449,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var playerInDatabase = await dbContext.Players .Include(player => player.RecentlyPlayed) - .FirstAsync(player => player.Id == existingPlayer.Id); + .FirstWithIdAsync(existingPlayer.Id); playerInDatabase.Should().NotBeNull(); playerInDatabase.RecentlyPlayed.Should().HaveCount(1); @@ -502,7 +500,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var playerInDatabase = await dbContext.Players .Include(player => player.RecentlyPlayed) - .FirstAsync(player => player.Id == existingPlayer.Id); + .FirstWithIdAsync(existingPlayer.Id); playerInDatabase.Should().NotBeNull(); playerInDatabase.RecentlyPlayed.Should().HaveCount(2); @@ -551,7 +549,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { var playerInDatabase = await dbContext.Players .Include(player => player.RecentlyPlayed) - .FirstAsync(player => player.Id == existingPlayer.Id); + .FirstWithIdAsync(existingPlayer.Id); playerInDatabase.Should().NotBeNull(); playerInDatabase.RecentlyPlayed.Should().HaveCount(1); @@ -573,7 +571,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/games/0"; + const string route = "/games/0"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -585,8 +583,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var gameInDatabase = await dbContext.Games - .FirstOrDefaultAsync(game => game.Id == existingGame.Id); + var gameInDatabase = await dbContext.Games.FirstWithIdOrDefaultAsync(existingGame.Id); gameInDatabase.Should().BeNull(); }); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroKeyDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroKeyDbContext.cs index b119f7e26a..9fa95d2860 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroKeyDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroKeyDbContext.cs @@ -1,7 +1,11 @@ +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +// @formatter:wrap_chained_method_calls chop_always + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ZeroKeys { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ZeroKeyDbContext : DbContext { public DbSet Games { get; set; } @@ -14,8 +18,6 @@ public ZeroKeyDbContext(DbContextOptions options) : base(optio protected override void OnModelCreating(ModelBuilder builder) { - base.OnModelCreating(builder); - builder.Entity() .HasMany(game => game.Maps) .WithOne(map => map.Game); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroKeyFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroKeyFakers.cs index 300e76382b..9db5425952 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroKeyFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroKeyFakers.cs @@ -2,6 +2,9 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ZeroKeys { internal sealed class ZeroKeyFakers : FakerContainer diff --git a/test/JsonApiDotNetCoreExampleTests/ServiceCollectionExtensions.cs b/test/JsonApiDotNetCoreExampleTests/ServiceCollectionExtensions.cs index e8eb3fa54e..b0ec80ebe3 100644 --- a/test/JsonApiDotNetCoreExampleTests/ServiceCollectionExtensions.cs +++ b/test/JsonApiDotNetCoreExampleTests/ServiceCollectionExtensions.cs @@ -1,10 +1,10 @@ -using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Startups; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.Extensions.DependencyInjection; namespace JsonApiDotNetCoreExampleTests { - public static class ServiceCollectionExtensions + internal static class ServiceCollectionExtensions { public static void AddControllersFromExampleProject(this IServiceCollection services) { diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/AbsoluteLinksInApiNamespaceStartup.cs b/test/JsonApiDotNetCoreExampleTests/Startups/AbsoluteLinksInApiNamespaceStartup.cs index a9950b3da7..261947f6e4 100644 --- a/test/JsonApiDotNetCoreExampleTests/Startups/AbsoluteLinksInApiNamespaceStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/Startups/AbsoluteLinksInApiNamespaceStartup.cs @@ -1,17 +1,13 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; namespace JsonApiDotNetCoreExampleTests.Startups { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class AbsoluteLinksInApiNamespaceStartup : TestableStartup where TDbContext : DbContext { - public AbsoluteLinksInApiNamespaceStartup(IConfiguration configuration) - : base(configuration) - { - } - protected override void SetJsonApiOptions(JsonApiOptions options) { base.SetJsonApiOptions(options); diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/AbsoluteLinksNoNamespaceStartup.cs b/test/JsonApiDotNetCoreExampleTests/Startups/AbsoluteLinksNoNamespaceStartup.cs index 636a84ca14..b3cd8df056 100644 --- a/test/JsonApiDotNetCoreExampleTests/Startups/AbsoluteLinksNoNamespaceStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/Startups/AbsoluteLinksNoNamespaceStartup.cs @@ -1,17 +1,13 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; namespace JsonApiDotNetCoreExampleTests.Startups { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class AbsoluteLinksNoNamespaceStartup : TestableStartup where TDbContext : DbContext { - public AbsoluteLinksNoNamespaceStartup(IConfiguration configuration) - : base(configuration) - { - } - protected override void SetJsonApiOptions(JsonApiOptions options) { base.SetJsonApiOptions(options); diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/ModelStateValidationStartup.cs b/test/JsonApiDotNetCoreExampleTests/Startups/ModelStateValidationStartup.cs index 8816d19f21..4a814722a8 100644 --- a/test/JsonApiDotNetCoreExampleTests/Startups/ModelStateValidationStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/Startups/ModelStateValidationStartup.cs @@ -1,17 +1,13 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; namespace JsonApiDotNetCoreExampleTests.Startups { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class ModelStateValidationStartup : TestableStartup where TDbContext : DbContext { - public ModelStateValidationStartup(IConfiguration configuration) - : base(configuration) - { - } - protected override void SetJsonApiOptions(JsonApiOptions options) { base.SetJsonApiOptions(options); diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/RelativeLinksInApiNamespaceStartup.cs b/test/JsonApiDotNetCoreExampleTests/Startups/RelativeLinksInApiNamespaceStartup.cs index f8e3b18814..092a3c1ef1 100644 --- a/test/JsonApiDotNetCoreExampleTests/Startups/RelativeLinksInApiNamespaceStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/Startups/RelativeLinksInApiNamespaceStartup.cs @@ -1,17 +1,13 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; namespace JsonApiDotNetCoreExampleTests.Startups { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class RelativeLinksInApiNamespaceStartup : TestableStartup where TDbContext : DbContext { - public RelativeLinksInApiNamespaceStartup(IConfiguration configuration) - : base(configuration) - { - } - protected override void SetJsonApiOptions(JsonApiOptions options) { base.SetJsonApiOptions(options); diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/RelativeLinksNoNamespaceStartup.cs b/test/JsonApiDotNetCoreExampleTests/Startups/RelativeLinksNoNamespaceStartup.cs index 365f3454ca..8623301d7a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Startups/RelativeLinksNoNamespaceStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/Startups/RelativeLinksNoNamespaceStartup.cs @@ -1,17 +1,13 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; namespace JsonApiDotNetCoreExampleTests.Startups { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class RelativeLinksNoNamespaceStartup : TestableStartup where TDbContext : DbContext { - public RelativeLinksNoNamespaceStartup(IConfiguration configuration) - : base(configuration) - { - } - protected override void SetJsonApiOptions(JsonApiOptions options) { base.SetJsonApiOptions(options); diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/TestableStartup.cs b/test/JsonApiDotNetCoreExampleTests/Startups/TestableStartup.cs index 3d07ea8187..a486046b4e 100644 --- a/test/JsonApiDotNetCoreExampleTests/Startups/TestableStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/Startups/TestableStartup.cs @@ -1,9 +1,8 @@ using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Startups; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -13,10 +12,6 @@ namespace JsonApiDotNetCoreExampleTests.Startups public class TestableStartup : EmptyStartup where TDbContext : DbContext { - public TestableStartup(IConfiguration configuration) : base(configuration) - { - } - public override void ConfigureServices(IServiceCollection services) { services.AddJsonApi(SetJsonApiOptions); diff --git a/test/JsonApiDotNetCoreExampleTests/UnitTests/Links/LinkInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/UnitTests/Links/LinkInclusionTests.cs index 1134e7782b..14beb27a4f 100644 --- a/test/JsonApiDotNetCoreExampleTests/UnitTests/Links/LinkInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/UnitTests/Links/LinkInclusionTests.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; @@ -60,10 +61,7 @@ public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInReso TopLevelLinks = linksInResourceContext }; - var resourceGraph = new ResourceGraph(new[] - { - exampleResourceContext - }); + var resourceGraph = new ResourceGraph(exampleResourceContext.AsArray()); var request = new JsonApiRequest { @@ -162,10 +160,7 @@ public void Applies_cascading_settings_for_resource_links(LinkTypes linksInResou ResourceLinks = linksInResourceContext }; - var resourceGraph = new ResourceGraph(new[] - { - exampleResourceContext - }); + var resourceGraph = new ResourceGraph(exampleResourceContext.AsArray()); var request = new JsonApiRequest(); @@ -330,10 +325,7 @@ public void Applies_cascading_settings_for_relationship_links(LinkTypes linksInR RelationshipLinks = linksInResourceContext }; - var resourceGraph = new ResourceGraph(new[] - { - exampleResourceContext - }); + var resourceGraph = new ResourceGraph(exampleResourceContext.AsArray()); var request = new JsonApiRequest(); diff --git a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/BaseParseTests.cs b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/BaseParseTests.cs index 1c4be2a4ca..8eee4e52d5 100644 --- a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/BaseParseTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/BaseParseTests.cs @@ -15,6 +15,9 @@ protected BaseParseTests() { Options = new JsonApiOptions(); + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + ResourceGraph = new ResourceGraphBuilder(Options, NullLoggerFactory.Instance) .Add() .Add() @@ -24,6 +27,9 @@ protected BaseParseTests() .Add() .Build(); + // @formatter:wrap_chained_method_calls restore + // @formatter:keep_existing_linebreaks restore + Request = new JsonApiRequest { PrimaryResource = ResourceGraph.GetResourceContext(), diff --git a/test/MultiDbContextTests/ResourceTests.cs b/test/MultiDbContextTests/ResourceTests.cs index eace446619..7dab27aaf1 100644 --- a/test/MultiDbContextTests/ResourceTests.cs +++ b/test/MultiDbContextTests/ResourceTests.cs @@ -23,7 +23,7 @@ public ResourceTests(WebApplicationFactory factory) public async Task Can_get_ResourceAs() { // Arrange - var route = "/resourceAs"; + const string route = "/resourceAs"; // Act var (httpResponse, responseDocument) = await ExecuteGetAsync(route); @@ -39,7 +39,7 @@ public async Task Can_get_ResourceAs() public async Task Can_get_ResourceBs() { // Arrange - var route = "/resourceBs"; + const string route = "/resourceBs"; // Act var (httpResponse, responseDocument) = await ExecuteGetAsync(route); diff --git a/test/NoEntityFrameworkTests/WorkItemTests.cs b/test/NoEntityFrameworkTests/WorkItemTests.cs index c78119d226..54b8928b58 100644 --- a/test/NoEntityFrameworkTests/WorkItemTests.cs +++ b/test/NoEntityFrameworkTests/WorkItemTests.cs @@ -33,7 +33,7 @@ await RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/api/v1/workItems"; + const string route = "/api/v1/workItems"; // Act var (httpResponse, responseDocument) = await ExecuteGetAsync(route); @@ -95,7 +95,7 @@ public async Task Can_create_WorkItem() } }; - var route = "/api/v1/workItems/"; + const string route = "/api/v1/workItems/"; // Act var (httpResponse, responseDocument) = await ExecutePostAsync(route, requestBody); diff --git a/test/TestBuildingBlocks/FakeLoggerFactory.cs b/test/TestBuildingBlocks/FakeLoggerFactory.cs index f474e6a2e3..f62dea7e71 100644 --- a/test/TestBuildingBlocks/FakeLoggerFactory.cs +++ b/test/TestBuildingBlocks/FakeLoggerFactory.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using JetBrains.Annotations; using Microsoft.Extensions.Logging; namespace TestBuildingBlocks { + [PublicAPI] public sealed class FakeLoggerFactory : ILoggerFactory, ILoggerProvider { public FakeLogger Logger { get; } diff --git a/test/TestBuildingBlocks/FrozenSystemClock.cs b/test/TestBuildingBlocks/FrozenSystemClock.cs index 91a361624e..3cca8ff3be 100644 --- a/test/TestBuildingBlocks/FrozenSystemClock.cs +++ b/test/TestBuildingBlocks/FrozenSystemClock.cs @@ -5,9 +5,9 @@ namespace TestBuildingBlocks { public sealed class FrozenSystemClock : ISystemClock { - private static readonly DateTimeOffset _defaultTime = + private static readonly DateTimeOffset DefaultTime = new DateTimeOffset(new DateTime(2000, 1, 1, 1, 1, 1), TimeSpan.FromHours(1)); - public DateTimeOffset UtcNow { get; set; } = _defaultTime; + public DateTimeOffset UtcNow { get; set; } = DefaultTime; } } diff --git a/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs b/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs index c8be07454a..69b733d868 100644 --- a/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs +++ b/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs @@ -3,11 +3,13 @@ using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Primitives; +using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace TestBuildingBlocks { + [PublicAPI] public static class HttpResponseMessageExtensions { public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance) @@ -25,6 +27,8 @@ public HttpResponseMessageAssertions(HttpResponseMessage instance) Subject = instance; } + // ReSharper disable once UnusedMethodReturnValue.Global + [CustomAssertion] public AndConstraint HaveStatusCode(HttpStatusCode statusCode) { if (Subject.StatusCode != statusCode) diff --git a/test/TestBuildingBlocks/IntegrationTest.cs b/test/TestBuildingBlocks/IntegrationTest.cs index 8992bc6997..17e2b8af1a 100644 --- a/test/TestBuildingBlocks/IntegrationTest.cs +++ b/test/TestBuildingBlocks/IntegrationTest.cs @@ -60,7 +60,7 @@ public abstract class IntegrationTest ExecuteRequestAsync(HttpMethod method, string requestUrl, object requestBody, string contentType, IEnumerable acceptHeaders) { - var request = new HttpRequestMessage(method, requestUrl); + using var request = new HttpRequestMessage(method, requestUrl); string requestText = SerializeRequest(requestBody); if (!string.IsNullOrEmpty(requestText)) diff --git a/test/TestBuildingBlocks/IntegrationTestConfiguration.cs b/test/TestBuildingBlocks/IntegrationTestConfiguration.cs index 32014a9630..0ae06299c4 100644 --- a/test/TestBuildingBlocks/IntegrationTestConfiguration.cs +++ b/test/TestBuildingBlocks/IntegrationTestConfiguration.cs @@ -2,7 +2,7 @@ namespace TestBuildingBlocks { - public static class IntegrationTestConfiguration + internal static class IntegrationTestConfiguration { // Because our tests often deserialize incoming responses into weakly-typed string-to-object dictionaries (as part of ResourceObject), // Newtonsoft.JSON is unable to infer the target type in such cases. So we steer a bit using explicit configuration. diff --git a/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs b/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs index d13759e1b6..c217b97f87 100644 --- a/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs +++ b/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs @@ -1,14 +1,19 @@ using System; using FluentAssertions; +using FluentAssertions.Numeric; using FluentAssertions.Primitives; +using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace TestBuildingBlocks { + [PublicAPI] public static class ObjectAssertionsExtensions { - private static readonly JsonSerializerSettings _deserializationSettings = new JsonSerializerSettings + private const decimal NumericPrecision = 0.00000000001M; + + private static readonly JsonSerializerSettings DeserializationSettings = new JsonSerializerSettings { Formatting = Formatting.Indented }; @@ -18,6 +23,7 @@ public static class ObjectAssertionsExtensions /// whose value is returned as in JSON:API response body /// because of . /// + [CustomAssertion] public static void BeCloseTo(this ObjectAssertions source, DateTimeOffset? expected, string because = "", params object[] becauseArgs) { @@ -37,14 +43,36 @@ public static void BeCloseTo(this ObjectAssertions source, DateTimeOffset? expec } } + /// + /// Same as , but with default precision. + /// + [CustomAssertion] + public static AndConstraint> BeApproximately(this NumericAssertions parent, + decimal expectedValue, string because = "", params object[] becauseArgs) + { + return parent.BeApproximately(expectedValue, NumericPrecision, because, becauseArgs); + } + + /// + /// Same as , but with default precision. + /// + [CustomAssertion] + public static AndConstraint> BeApproximately( + this NullableNumericAssertions parent, decimal? expectedValue, string because = "", + params object[] becauseArgs) + { + return parent.BeApproximately(expectedValue, NumericPrecision, because, becauseArgs); + } + /// /// Used to assert on a JSON-formatted string, ignoring differences in insignificant whitespace and line endings. /// + [CustomAssertion] public static void BeJson(this StringAssertions source, string expected, string because = "", params object[] becauseArgs) { - var sourceToken = JsonConvert.DeserializeObject(source.Subject, _deserializationSettings); - var expectedToken = JsonConvert.DeserializeObject(expected, _deserializationSettings); + var sourceToken = JsonConvert.DeserializeObject(source.Subject, DeserializationSettings); + var expectedToken = JsonConvert.DeserializeObject(expected, DeserializationSettings); string sourceText = sourceToken?.ToString(); string expectedText = expectedToken?.ToString(); diff --git a/test/TestBuildingBlocks/QueryableExtensions.cs b/test/TestBuildingBlocks/QueryableExtensions.cs new file mode 100644 index 0000000000..7fb767cdce --- /dev/null +++ b/test/TestBuildingBlocks/QueryableExtensions.cs @@ -0,0 +1,25 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JsonApiDotNetCore.Resources; +using Microsoft.EntityFrameworkCore; + +namespace TestBuildingBlocks +{ + public static class QueryableExtensions + { + public static Task FirstWithIdAsync(this IQueryable resources, TId id, + CancellationToken cancellationToken = default) + where TResource : IIdentifiable + { + return resources.FirstAsync(resource => Equals(resource.Id, id), cancellationToken); + } + + public static Task FirstWithIdOrDefaultAsync(this IQueryable resources, TId id, + CancellationToken cancellationToken = default) + where TResource : IIdentifiable + { + return resources.FirstOrDefaultAsync(resource => Equals(resource.Id, id), cancellationToken); + } + } +} diff --git a/test/UnitTests/Builders/ResourceGraphBuilder_Tests.cs b/test/UnitTests/Builders/ResourceGraphBuilderTests.cs similarity index 89% rename from test/UnitTests/Builders/ResourceGraphBuilder_Tests.cs rename to test/UnitTests/Builders/ResourceGraphBuilderTests.cs index 34535d9d5c..bda43cdd6a 100644 --- a/test/UnitTests/Builders/ResourceGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ResourceGraphBuilderTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -9,15 +10,16 @@ using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace UnitTests +namespace UnitTests.Builders { - public sealed class ResourceGraphBuilder_Tests + public sealed class ResourceGraphBuilderTests { private sealed class NonDbResource : Identifiable { } private sealed class DbResource : Identifiable { } - private class TestContext : DbContext + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + private sealed class TestContext : DbContext { public DbSet DbResources { get; set; } @@ -75,7 +77,7 @@ public void Attrs_Without_Names_Specified_Will_Use_Configured_Formatter() // Assert var resource = resourceGraph.GetResourceContext(typeof(TestResource)); - Assert.Contains(resource.Attributes, (i) => i.PublicName == "compoundAttribute"); + Assert.Contains(resource.Attributes, i => i.PublicName == "compoundAttribute"); } [Fact] @@ -94,6 +96,7 @@ public void Relationships_Without_Names_Specified_Will_Use_Configured_Formatter( Assert.Equal("relatedResources", resource.Relationships.Single(r => !(r is HasOneAttribute)).PublicName); } + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class TestResource : Identifiable { [Attr] @@ -106,6 +109,6 @@ public sealed class TestResource : Identifiable public ISet RelatedResources { get; set; } } - public class RelatedResource : Identifiable { } + public sealed class RelatedResource : Identifiable { } } } diff --git a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs b/test/UnitTests/Controllers/BaseJsonApiControllerTests.cs similarity index 97% rename from test/UnitTests/Controllers/BaseJsonApiController_Tests.cs rename to test/UnitTests/Controllers/BaseJsonApiControllerTests.cs index a2a63c638a..076a13e442 100644 --- a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs +++ b/test/UnitTests/Controllers/BaseJsonApiControllerTests.cs @@ -2,6 +2,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Errors; @@ -15,16 +16,17 @@ using Moq; using Xunit; -namespace UnitTests +namespace UnitTests.Controllers { - public sealed class BaseJsonApiController_Tests + public sealed class BaseJsonApiControllerTests { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Resource : Identifiable { [Attr] public string TestAttribute { get; set; } } - public sealed class ResourceController : BaseJsonApiController + private sealed class ResourceController : BaseJsonApiController { public ResourceController( IJsonApiOptions options, diff --git a/test/UnitTests/Controllers/CoreJsonApiControllerTests.cs b/test/UnitTests/Controllers/CoreJsonApiControllerTests.cs deleted file mode 100644 index 9fbe8275f3..0000000000 --- a/test/UnitTests/Controllers/CoreJsonApiControllerTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Net; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.AspNetCore.Mvc; -using Xunit; - -namespace UnitTests -{ - public sealed class CoreJsonApiControllerTests : CoreJsonApiController - { - [Fact] - public void Errors_Correctly_Infers_Status_Code() - { - // Arrange - var errors422 = new List - { - new Error(HttpStatusCode.UnprocessableEntity) {Title = "bad specific"}, - new Error(HttpStatusCode.UnprocessableEntity) {Title = "bad other specific"} - }; - - var errors400 = new List - { - new Error(HttpStatusCode.OK) {Title = "weird"}, - new Error(HttpStatusCode.BadRequest) {Title = "bad"}, - new Error(HttpStatusCode.UnprocessableEntity) {Title = "bad specific"} - }; - - var errors500 = new List - { - new Error(HttpStatusCode.OK) {Title = "weird"}, - new Error(HttpStatusCode.BadRequest) {Title = "bad"}, - new Error(HttpStatusCode.UnprocessableEntity) {Title = "bad specific"}, - new Error(HttpStatusCode.InternalServerError) {Title = "really bad"}, - new Error(HttpStatusCode.BadGateway) {Title = "really bad specific"} - }; - - // Act - var result422 = Error(errors422); - var result400 = Error(errors400); - var result500 = Error(errors500); - - // Assert - var response422 = Assert.IsType(result422); - var response400 = Assert.IsType(result400); - var response500 = Assert.IsType(result500); - - Assert.Equal((int)HttpStatusCode.UnprocessableEntity, response422.StatusCode); - Assert.Equal((int)HttpStatusCode.BadRequest, response400.StatusCode); - Assert.Equal((int)HttpStatusCode.InternalServerError, response500.StatusCode); - } - } -} diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs similarity index 95% rename from test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs rename to test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs index 533e69b5a0..57009509cf 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; @@ -21,7 +22,7 @@ namespace UnitTests.Extensions { - public sealed class IServiceCollectionExtensionsTests + public sealed class ServiceCollectionExtensionsTests { [Fact] public void AddJsonApiInternals_Adds_All_Required_Services() @@ -186,9 +187,12 @@ public void AddJsonApi_With_Context_Uses_Resource_Type_Name_If_NoOtherSpecified( Assert.Equal("intResources", resource.PublicName); } - public sealed class IntResource : Identifiable { } - public class GuidResource : Identifiable { } + private sealed class IntResource : Identifiable { } + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + private sealed class GuidResource : Identifiable { } + + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] private sealed class IntResourceService : IResourceService { public Task> GetAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); @@ -203,6 +207,7 @@ private sealed class IntResourceService : IResourceService public Task RemoveFromToManyRelationshipAsync(int primaryId, string relationshipName, ISet secondaryResourceIds, CancellationToken cancellationToken) => throw new NotImplementedException(); } + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] private sealed class GuidResourceService : IResourceService { public Task> GetAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); @@ -217,6 +222,7 @@ private sealed class GuidResourceService : IResourceService public Task RemoveFromToManyRelationshipAsync(Guid primaryId, string relationshipName, ISet secondaryResourceIds, CancellationToken cancellationToken) => throw new NotImplementedException(); } + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] private sealed class IntResourceRepository : IResourceRepository { public Task> GetAsync(QueryLayer layer, CancellationToken cancellationToken) => throw new NotImplementedException(); @@ -231,6 +237,7 @@ private sealed class IntResourceRepository : IResourceRepository public Task RemoveFromToManyRelationshipAsync(IntResource primaryResource, ISet secondaryResourceIds, CancellationToken cancellationToken) => throw new NotImplementedException(); } + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] private sealed class GuidResourceRepository : IResourceRepository { public Task> GetAsync(QueryLayer layer, CancellationToken cancellationToken) => throw new NotImplementedException(); @@ -245,7 +252,8 @@ private sealed class GuidResourceRepository : IResourceRepository secondaryResourceIds, CancellationToken cancellationToken) => throw new NotImplementedException(); } - public class TestContext : DbContext + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + private sealed class TestContext : DbContext { public TestContext(DbContextOptions options) : base(options) { diff --git a/test/UnitTests/Graph/BaseType.cs b/test/UnitTests/Graph/BaseType.cs new file mode 100644 index 0000000000..c2ee6f5fe8 --- /dev/null +++ b/test/UnitTests/Graph/BaseType.cs @@ -0,0 +1,6 @@ +// ReSharper disable UnusedTypeParameter + +namespace UnitTests.Graph +{ + internal class BaseType { } +} diff --git a/test/UnitTests/Graph/DerivedType.cs b/test/UnitTests/Graph/DerivedType.cs new file mode 100644 index 0000000000..c9c12ecd97 --- /dev/null +++ b/test/UnitTests/Graph/DerivedType.cs @@ -0,0 +1,4 @@ +namespace UnitTests.Graph +{ + internal sealed class DerivedType : BaseType { } +} diff --git a/test/UnitTests/Graph/IGenericInterface.cs b/test/UnitTests/Graph/IGenericInterface.cs new file mode 100644 index 0000000000..648a8e8ea4 --- /dev/null +++ b/test/UnitTests/Graph/IGenericInterface.cs @@ -0,0 +1,6 @@ +// ReSharper disable UnusedTypeParameter + +namespace UnitTests.Graph +{ + internal interface IGenericInterface { } +} diff --git a/test/UnitTests/Graph/Implementation.cs b/test/UnitTests/Graph/Implementation.cs new file mode 100644 index 0000000000..08a61c229e --- /dev/null +++ b/test/UnitTests/Graph/Implementation.cs @@ -0,0 +1,4 @@ +namespace UnitTests.Graph +{ + internal sealed class Implementation : IGenericInterface { } +} diff --git a/test/UnitTests/Graph/Model.cs b/test/UnitTests/Graph/Model.cs new file mode 100644 index 0000000000..cd2830f7f8 --- /dev/null +++ b/test/UnitTests/Graph/Model.cs @@ -0,0 +1,6 @@ +using JsonApiDotNetCore.Resources; + +namespace UnitTests.Graph +{ + internal sealed class Model : Identifiable { } +} diff --git a/test/UnitTests/Graph/ResourceDescriptorAssemblyCacheTests.cs b/test/UnitTests/Graph/ResourceDescriptorAssemblyCacheTests.cs index d533400a3e..5ceac23ecd 100644 --- a/test/UnitTests/Graph/ResourceDescriptorAssemblyCacheTests.cs +++ b/test/UnitTests/Graph/ResourceDescriptorAssemblyCacheTests.cs @@ -1,12 +1,11 @@ using System.Linq; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; -using UnitTests.Internal; using Xunit; namespace UnitTests.Graph { - public class ResourceDescriptorAssemblyCacheTests + public sealed class ResourceDescriptorAssemblyCacheTests { [Fact] public void GetResourceDescriptorsPerAssembly_Locates_Identifiable_Resource() diff --git a/test/UnitTests/Graph/TypeLocator_Tests.cs b/test/UnitTests/Graph/TypeLocatorTests.cs similarity index 87% rename from test/UnitTests/Graph/TypeLocator_Tests.cs rename to test/UnitTests/Graph/TypeLocatorTests.cs index a5f5aa1803..4c3c055b64 100644 --- a/test/UnitTests/Graph/TypeLocator_Tests.cs +++ b/test/UnitTests/Graph/TypeLocatorTests.cs @@ -1,11 +1,9 @@ -using System; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; using Xunit; -namespace UnitTests.Internal +namespace UnitTests.Graph { - public sealed class TypeLocator_Tests + public sealed class TypeLocatorTests { [Fact] public void GetGenericInterfaceImplementation_Gets_Implementation() @@ -99,7 +97,7 @@ public void TryGetResourceDescriptor_Returns_Type_If_Type_Is_IIdentifiable() public void TryGetResourceDescriptor_Returns_False_If_Type_Is_IIdentifiable() { // Arrange - var resourceType = typeof(String); + var resourceType = typeof(string); // Act var descriptor = TypeLocator.TryGetResourceDescriptor(resourceType); @@ -108,12 +106,4 @@ public void TryGetResourceDescriptor_Returns_False_If_Type_Is_IIdentifiable() Assert.Null(descriptor); } } - - public interface IGenericInterface { } - public sealed class Implementation : IGenericInterface { } - - public class BaseType { } - public sealed class DerivedType : BaseType { } - - public sealed class Model : Identifiable { } } diff --git a/test/UnitTests/Internal/ErrorDocumentTests.cs b/test/UnitTests/Internal/ErrorDocumentTests.cs index 4a7dd40a24..e202d847e3 100644 --- a/test/UnitTests/Internal/ErrorDocumentTests.cs +++ b/test/UnitTests/Internal/ErrorDocumentTests.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Linq; using System.Net; +using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; using Xunit; @@ -7,26 +8,23 @@ namespace UnitTests.Internal { public sealed class ErrorDocumentTests { - [Fact] - public void Can_GetStatusCode() + // @formatter:wrap_array_initializer_style wrap_if_long + [Theory] + [InlineData(new[] { HttpStatusCode.UnprocessableEntity }, HttpStatusCode.UnprocessableEntity)] + [InlineData(new[] { HttpStatusCode.UnprocessableEntity, HttpStatusCode.UnprocessableEntity }, HttpStatusCode.UnprocessableEntity)] + [InlineData(new[] { HttpStatusCode.UnprocessableEntity, HttpStatusCode.Unauthorized }, HttpStatusCode.BadRequest)] + [InlineData(new[] { HttpStatusCode.UnprocessableEntity, HttpStatusCode.BadGateway }, HttpStatusCode.InternalServerError)] + // @formatter:wrap_array_initializer_style restore + public void ErrorDocument_GetErrorStatusCode_IsCorrect(HttpStatusCode[] errorCodes, HttpStatusCode expected) { - List errors = new List(); + // Arrange + var document = new ErrorDocument(errorCodes.Select(code => new Error(code))); - // Add First 422 error - errors.Add(new Error(HttpStatusCode.UnprocessableEntity) {Title = "Something wrong"}); - Assert.Equal(HttpStatusCode.UnprocessableEntity, new ErrorDocument(errors).GetErrorStatusCode()); + // Act + var status = document.GetErrorStatusCode(); - // Add a second 422 error - errors.Add(new Error(HttpStatusCode.UnprocessableEntity) {Title = "Something else wrong"}); - Assert.Equal(HttpStatusCode.UnprocessableEntity, new ErrorDocument(errors).GetErrorStatusCode()); - - // Add 4xx error not 422 - errors.Add(new Error(HttpStatusCode.Unauthorized) {Title = "Unauthorized"}); - Assert.Equal(HttpStatusCode.BadRequest, new ErrorDocument(errors).GetErrorStatusCode()); - - // Add 5xx error not 4xx - errors.Add(new Error(HttpStatusCode.BadGateway) {Title = "Not good"}); - Assert.Equal(HttpStatusCode.InternalServerError, new ErrorDocument(errors).GetErrorStatusCode()); + // Assert + status.Should().Be(expected); } } } diff --git a/test/UnitTests/Internal/ResourceGraphBuilderTests.cs b/test/UnitTests/Internal/ResourceGraphBuilderTests.cs index ba1fc670b1..68f5a86efe 100644 --- a/test/UnitTests/Internal/ResourceGraphBuilderTests.cs +++ b/test/UnitTests/Internal/ResourceGraphBuilderTests.cs @@ -1,5 +1,6 @@ using System.Linq; using Castle.DynamicProxy; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using Microsoft.EntityFrameworkCore; @@ -75,15 +76,17 @@ public void GetResourceContext_Yields_Right_Type_For_Identifiable() Assert.Equal(typeof(Bar), result.ResourceType); } - private class Foo { } + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + private sealed class Foo { } - private class TestContext : DbContext + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + private sealed class TestContext : DbContext { public DbSet Foos { get; set; } } + // ReSharper disable once ClassCanBeSealed.Global + // ReSharper disable once MemberCanBePrivate.Global public class Bar : Identifiable { } - } - } diff --git a/test/UnitTests/Internal/TypeHelper_Tests.cs b/test/UnitTests/Internal/TypeHelperTests.cs similarity index 88% rename from test/UnitTests/Internal/TypeHelper_Tests.cs rename to test/UnitTests/Internal/TypeHelperTests.cs index 89ec11da09..2a9c6b1f5b 100644 --- a/test/UnitTests/Internal/TypeHelper_Tests.cs +++ b/test/UnitTests/Internal/TypeHelperTests.cs @@ -6,13 +6,13 @@ namespace UnitTests.Internal { - public sealed class TypeHelper_Tests + public sealed class TypeHelperTests { [Fact] public void Can_Convert_DateTimeOffsets() { // Arrange - var dto = new DateTimeOffset(new DateTime(2002, 2,2), TimeSpan.FromHours(4));; + var dto = new DateTimeOffset(new DateTime(2002, 2, 2), TimeSpan.FromHours(4)); var formattedString = dto.ToString("O"); // Act @@ -26,7 +26,7 @@ public void Can_Convert_DateTimeOffsets() public void Bad_DateTimeOffset_String_Throws() { // Arrange - var formattedString = "this_is_not_a_valid_dto"; + const string formattedString = "this_is_not_a_valid_dto"; // Act // Assert @@ -37,7 +37,7 @@ public void Bad_DateTimeOffset_String_Throws() public void Can_Convert_Enums() { // Arrange - var formattedString = "1"; + const string formattedString = "1"; // Act var result = TypeHelper.ConvertType(formattedString, typeof(TestEnum)); @@ -50,10 +50,7 @@ public void Can_Convert_Enums() public void ConvertType_Returns_Value_If_Type_Is_Same() { // Arrange - var val = new ComplexType - { - Property = 1 - }; + var val = new ComplexType(); var type = val.GetType(); @@ -68,10 +65,7 @@ public void ConvertType_Returns_Value_If_Type_Is_Same() public void ConvertType_Returns_Value_If_Type_Is_Assignable() { // Arrange - var val = new ComplexType - { - Property = 1 - }; + var val = new ComplexType(); var baseType = typeof(BaseType); var iType = typeof(IType); @@ -123,10 +117,10 @@ public void Can_Convert_TimeSpans() } [Fact] - public void Bad_TimeSpanString_Throws() + public void Bad_TimeSpanString_Throws() { // Arrange - var formattedString = "this_is_not_a_valid_timespan"; + const string formattedString = "this_is_not_a_valid_timespan"; // Act/assert Assert.Throws(() => TypeHelper.ConvertType(formattedString, typeof(TimeSpan))); @@ -179,7 +173,6 @@ private enum TestEnum private sealed class ComplexType : BaseType { - public int Property { get; set; } } private class BaseType : IType diff --git a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs index f11f6b3f27..cfdfd8d85c 100644 --- a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs +++ b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs @@ -18,7 +18,7 @@ public sealed class JsonApiMiddlewareTests public async Task ParseUrlBase_ObfuscatedIdClass_ShouldSetIdCorrectly() { // Arrange - var id = "ABC123ABC"; + const string id = "ABC123ABC"; var configuration = GetConfiguration($"/obfuscatedIdModel/{id}", action: "GetAsync", id: id); var request = configuration.Request; @@ -33,7 +33,7 @@ public async Task ParseUrlBase_ObfuscatedIdClass_ShouldSetIdCorrectly() public async Task ParseUrlBase_UrlHasPrimaryIdSet_ShouldSetupRequestWithSameId() { // Arrange - var id = "123"; + const string id = "123"; var configuration = GetConfiguration($"/users/{id}", id: id); var request = configuration.Request; @@ -70,12 +70,12 @@ public async Task ParseUrlBase_UrlHasNegativePrimaryIdAndTypeIsInt_ShouldNotThro private sealed class InvokeConfiguration { - public JsonApiMiddleware MiddleWare; - public HttpContext HttpContext; - public Mock ControllerResourceMapping; - public Mock Options; - public JsonApiRequest Request; - public Mock ResourceGraph; + public JsonApiMiddleware MiddleWare { get; set; } + public HttpContext HttpContext { get; set; } + public Mock ControllerResourceMapping { get; set; } + public Mock Options { get; set; } + public JsonApiRequest Request { get; set; } + public Mock ResourceGraph { get; set; } } private Task RunMiddlewareTask(InvokeConfiguration holder) { @@ -84,7 +84,7 @@ private Task RunMiddlewareTask(InvokeConfiguration holder) var options = holder.Options.Object; var request = holder.Request; var resourceGraph = holder.ResourceGraph.Object; - return holder.MiddleWare.Invoke(context, controllerResourceMapping, options, request, resourceGraph); + return holder.MiddleWare.InvokeAsync(context, controllerResourceMapping, options, request, resourceGraph); } private InvokeConfiguration GetConfiguration(string path, string resourceName = "users", string action = "", string id =null, Type relType = null) { @@ -92,11 +92,11 @@ private InvokeConfiguration GetConfiguration(string path, string resourceName = { throw new ArgumentException("Path should start with a '/'"); } - var middleware = new JsonApiMiddleware(httpContext => + var middleware = new JsonApiMiddleware(_ => { return Task.Run(() => Console.WriteLine("finished")); }); - var forcedNamespace = "api/v1"; + const string forcedNamespace = "api/v1"; var mockMapping = new Mock(); mockMapping.Setup(x => x.GetResourceTypeForController(It.IsAny())).Returns(typeof(string)); diff --git a/test/UnitTests/Middleware/JsonApiRequestTests.cs b/test/UnitTests/Middleware/JsonApiRequestTests.cs index 62139617ec..70b7b70268 100644 --- a/test/UnitTests/Middleware/JsonApiRequestTests.cs +++ b/test/UnitTests/Middleware/JsonApiRequestTests.cs @@ -57,7 +57,7 @@ public async Task Sets_request_properties_correctly(string requestMethod, string var middleware = new JsonApiMiddleware(_ => Task.CompletedTask); // Act - await middleware.Invoke(httpContext, controllerResourceMappingMock.Object, options, request, resourceGraph); + await middleware.InvokeAsync(httpContext, controllerResourceMappingMock.Object, options, request, resourceGraph); // Assert request.IsCollection.Should().Be(expectIsCollection); diff --git a/test/UnitTests/Models/RelationshipDataTests.cs b/test/UnitTests/Models/RelationshipDataTests.cs index 4fc3d2f98c..0455c7ee82 100644 --- a/test/UnitTests/Models/RelationshipDataTests.cs +++ b/test/UnitTests/Models/RelationshipDataTests.cs @@ -34,7 +34,7 @@ public void Setting_ExposeData_To_JArray_Sets_ManyData() { // Arrange var relationshipData = new RelationshipEntry(); - var relationshipsJson = @"[ + const string relationshipsJson = @"[ { ""type"": ""authors"", ""id"": ""9"" @@ -78,7 +78,7 @@ public void Setting_ExposeData_To_JObject_Sets_SingleData() { // Arrange var relationshipData = new RelationshipEntry(); - var relationshipJson = @"{ + const string relationshipJson = @"{ ""id"": ""9"", ""type"": ""authors"" }"; diff --git a/test/UnitTests/Models/ResourceConstructionTests.cs b/test/UnitTests/Models/ResourceConstructionTests.cs index 0e8077ca79..0265fbb659 100644 --- a/test/UnitTests/Models/ResourceConstructionTests.cs +++ b/test/UnitTests/Models/ResourceConstructionTests.cs @@ -4,7 +4,6 @@ using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCoreExample.Data; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; using Moq; @@ -15,9 +14,9 @@ namespace UnitTests.Models { public sealed class ResourceConstructionTests { - public Mock _requestMock; - public Mock _mockHttpContextAccessor; - + private readonly Mock _requestMock; + private readonly Mock _mockHttpContextAccessor; + public ResourceConstructionTests() { _mockHttpContextAccessor = new Mock(); @@ -123,36 +122,4 @@ public void When_resource_has_constructor_with_string_parameter_it_must_fail() exception.Message); } } - - public class ResourceWithoutConstructor : Identifiable - { - } - - public class ResourceWithDbContextConstructor : Identifiable - { - public AppDbContext AppDbContext { get; } - - public ResourceWithDbContextConstructor(AppDbContext appDbContext) - { - AppDbContext = appDbContext ?? throw new ArgumentNullException(nameof(appDbContext)); - } - } - - public class ResourceWithThrowingConstructor : Identifiable - { - public ResourceWithThrowingConstructor() - { - throw new ArgumentException("Failed to initialize."); - } - } - - public class ResourceWithStringConstructor : Identifiable - { - public string Text { get; } - - public ResourceWithStringConstructor(string text) - { - Text = text ?? throw new ArgumentNullException(nameof(text)); - } - } } diff --git a/test/UnitTests/Models/ResourceWithStringConstructor.cs b/test/UnitTests/Models/ResourceWithStringConstructor.cs new file mode 100644 index 0000000000..8b166f50e4 --- /dev/null +++ b/test/UnitTests/Models/ResourceWithStringConstructor.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore; +using JsonApiDotNetCore.Resources; + +namespace UnitTests.Models +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class ResourceWithStringConstructor : Identifiable + { + public string Text { get; } + + public ResourceWithStringConstructor(string text) + { + ArgumentGuard.NotNull(text, nameof(text)); + + Text = text; + } + } +} diff --git a/test/UnitTests/Models/ResourceWithThrowingConstructor.cs b/test/UnitTests/Models/ResourceWithThrowingConstructor.cs new file mode 100644 index 0000000000..b1fe9f6d48 --- /dev/null +++ b/test/UnitTests/Models/ResourceWithThrowingConstructor.cs @@ -0,0 +1,15 @@ +using System; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; + +namespace UnitTests.Models +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class ResourceWithThrowingConstructor : Identifiable + { + public ResourceWithThrowingConstructor() + { + throw new ArgumentException("Failed to initialize."); + } + } +} diff --git a/test/UnitTests/Models/ResourceWithoutConstructor.cs b/test/UnitTests/Models/ResourceWithoutConstructor.cs new file mode 100644 index 0000000000..521e01405e --- /dev/null +++ b/test/UnitTests/Models/ResourceWithoutConstructor.cs @@ -0,0 +1,8 @@ +using JsonApiDotNetCore.Resources; + +namespace UnitTests.Models +{ + internal sealed class ResourceWithoutConstructor : Identifiable + { + } +} diff --git a/test/UnitTests/ResourceHooks/DiscoveryTests.cs b/test/UnitTests/ResourceHooks/DiscoveryTests.cs index 3985ba327e..be9c137f9b 100644 --- a/test/UnitTests/ResourceHooks/DiscoveryTests.cs +++ b/test/UnitTests/ResourceHooks/DiscoveryTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Hooks.Internal.Discovery; @@ -13,33 +14,36 @@ namespace UnitTests.ResourceHooks { public sealed class DiscoveryTests { - public class Dummy : Identifiable { } + public sealed class Dummy : Identifiable { } + + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class DummyResourceDefinition : ResourceHooksDefinition { public DummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) { } - public override IEnumerable BeforeDelete(IResourceHashSet affected, ResourcePipeline pipeline) { return affected; } + public override IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) { return resources; } public override void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) { } } private IServiceProvider MockProvider(object service) where TResource : class, IIdentifiable { var services = new ServiceCollection(); - services.AddScoped((_) => (ResourceHooksDefinition)service); + services.AddScoped(_ => (ResourceHooksDefinition)service); return services.BuildServiceProvider(); } [Fact] public void HookDiscovery_StandardResourceDefinition_CanDiscover() { - // Arrange & act + // Act var hookConfig = new HooksDiscovery(MockProvider(new DummyResourceDefinition())); + // Assert Assert.Contains(ResourceHook.BeforeDelete, hookConfig.ImplementedHooks); Assert.Contains(ResourceHook.AfterDelete, hookConfig.ImplementedHooks); } - public class AnotherDummy : Identifiable { } + public sealed class AnotherDummy : Identifiable { } public abstract class ResourceDefinitionBase : ResourceHooksDefinition where T : class, IIdentifiable { protected ResourceDefinitionBase(IResourceGraph resourceGraph) : base(resourceGraph) { } @@ -47,6 +51,7 @@ protected ResourceDefinitionBase(IResourceGraph resourceGraph) : base(resourceGr public override void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) { } } + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class AnotherDummyResourceDefinition : ResourceDefinitionBase { public AnotherDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) { } @@ -55,19 +60,22 @@ public AnotherDummyResourceDefinition() : base(new ResourceGraphBuilder(new Json [Fact] public void HookDiscovery_InheritanceSubclass_CanDiscover() { - // Arrange & act + // Act var hookConfig = new HooksDiscovery(MockProvider(new AnotherDummyResourceDefinition())); + // Assert Assert.Contains(ResourceHook.BeforeDelete, hookConfig.ImplementedHooks); Assert.Contains(ResourceHook.AfterDelete, hookConfig.ImplementedHooks); } - public class YetAnotherDummy : Identifiable { } + public sealed class YetAnotherDummy : Identifiable { } + + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class YetAnotherDummyResourceDefinition : ResourceHooksDefinition { public YetAnotherDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) { } - public override IEnumerable BeforeDelete(IResourceHashSet affected, ResourcePipeline pipeline) { return affected; } + public override IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) { return resources; } [LoadDatabaseValues(false)] public override void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) { } @@ -76,18 +84,17 @@ public override void AfterDelete(HashSet resources, ResourcePip [Fact] public void HookDiscovery_WronglyUsedLoadDatabaseValueAttribute_ThrowsJsonApiSetupException() { - // assert - Assert.Throws(() => - { - // Arrange & act - new HooksDiscovery(MockProvider(new YetAnotherDummyResourceDefinition())); - }); + // Act + Action action = () => _ = new HooksDiscovery(MockProvider(new YetAnotherDummyResourceDefinition())); + + // Assert + Assert.Throws(action); } [Fact] public void HookDiscovery_InheritanceWithGenericSubclass_CanDiscover() { - // Arrange & act + // Act var hookConfig = new HooksDiscovery(MockProvider(new GenericDummyResourceDefinition())); // Assert @@ -95,6 +102,7 @@ public void HookDiscovery_InheritanceWithGenericSubclass_CanDiscover() Assert.Contains(ResourceHook.AfterDelete, hookConfig.ImplementedHooks); } + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class GenericDummyResourceDefinition : ResourceHooksDefinition where TResource : class, IIdentifiable { public GenericDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) { } diff --git a/test/UnitTests/ResourceHooks/Dummy.cs b/test/UnitTests/ResourceHooks/Dummy.cs new file mode 100644 index 0000000000..821fdb2bbc --- /dev/null +++ b/test/UnitTests/ResourceHooks/Dummy.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.ResourceHooks +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Dummy : Identifiable + { + public string SomeUpdatedProperty { get; set; } + public string SomeNotUpdatedProperty { get; set; } + + [HasOne] + public ToOne FirstToOne { get; set; } + [HasOne] + public ToOne SecondToOne { get; set; } + [HasMany] + public ISet ToManies { get; set; } + } +} diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs b/test/UnitTests/ResourceHooks/Executor/Create/AfterCreateTests.cs similarity index 98% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs rename to test/UnitTests/ResourceHooks/Executor/Create/AfterCreateTests.cs index 30d391a394..bf4aa9b8f8 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs +++ b/test/UnitTests/ResourceHooks/Executor/Create/AfterCreateTests.cs @@ -4,7 +4,7 @@ using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor.Create { public sealed class AfterCreateTests : HooksTestsSetup { diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs b/test/UnitTests/ResourceHooks/Executor/Create/BeforeCreateTests.cs similarity index 85% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs rename to test/UnitTests/ResourceHooks/Executor/Create/BeforeCreateTests.cs index 2f580128a9..83aa1bd0bf 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs +++ b/test/UnitTests/ResourceHooks/Executor/Create/BeforeCreateTests.cs @@ -4,18 +4,18 @@ using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor.Create { public sealed class BeforeCreateTests : HooksTestsSetup { - private readonly ResourceHook[] targetHooks = { ResourceHook.BeforeCreate, ResourceHook.BeforeUpdateRelationship }; + private readonly ResourceHook[] _targetHooks = { ResourceHook.BeforeCreate, ResourceHook.BeforeUpdateRelationship }; [Fact] public void BeforeCreate() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); @@ -33,7 +33,7 @@ public void BeforeCreate_Without_Parent_Hook_Implemented() { // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); @@ -49,7 +49,7 @@ public void BeforeCreate_Without_Parent_Hook_Implemented() public void BeforeCreate_Without_Child_Hook_Implemented() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs b/test/UnitTests/ResourceHooks/Executor/Create/BeforeCreateWithDbValuesTests.cs similarity index 87% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs rename to test/UnitTests/ResourceHooks/Executor/Create/BeforeCreateWithDbValuesTests.cs index b8ad10bd11..20d2ed0a43 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs +++ b/test/UnitTests/ResourceHooks/Executor/Create/BeforeCreateWithDbValuesTests.cs @@ -7,32 +7,32 @@ using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor.Create { - public sealed class BeforeCreate_WithDbValues_Tests : HooksTestsSetup + public sealed class BeforeCreateWithDbValuesTests : HooksTestsSetup { private readonly ResourceHook[] _targetHooks = { ResourceHook.BeforeCreate, ResourceHook.BeforeImplicitUpdateRelationship, ResourceHook.BeforeUpdateRelationship }; private readonly ResourceHook[] _targetHooksNoImplicit = { ResourceHook.BeforeCreate, ResourceHook.BeforeUpdateRelationship }; - private const string _description = "DESCRIPTION"; - private const string _lastName = "NAME"; + private const string Description = "DESCRIPTION"; + private const string LastName = "NAME"; private readonly string _personId; private readonly List _todoList; private readonly DbContextOptions _options; - public BeforeCreate_WithDbValues_Tests() + public BeforeCreateWithDbValuesTests() { _todoList = CreateTodoWithToOnePerson(); _todoList[0].Id = 0; - _todoList[0].Description = _description; + _todoList[0].Description = Description; var person = _todoList[0].OneToOnePerson; - person.LastName = _lastName; + person.LastName = LastName; _personId = person.Id.ToString(); - var implicitTodo = _todoFaker.Generate(); + var implicitTodo = TodoFaker.Generate(); implicitTodo.Id += 1000; implicitTodo.OneToOnePerson = person; - implicitTodo.Description = _description + _description; + implicitTodo.Description = Description + Description; _options = InitInMemoryDb(context => { @@ -56,14 +56,14 @@ public void BeforeCreate() hookExecutor.BeforeCreate(_todoList, ResourcePipeline.Post); // Assert - todoResourceMock.Verify(rd => rd.BeforeCreate(It.Is>((resources) => TodoCheck(resources, _description)), ResourcePipeline.Post), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeCreate(It.Is>(resources => TodoCheck(resources, Description)), ResourcePipeline.Post), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, _personId)), It.IsAny>(), ResourcePipeline.Post), Times.Once()); todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship( - It.Is>(rh => TodoCheckRelationships(rh, _description + _description)), + It.Is>(rh => TodoCheckRelationships(rh, Description + Description)), ResourcePipeline.Post), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -101,9 +101,9 @@ public void BeforeCreate_Without_Child_Hook_Implemented() hookExecutor.BeforeCreate(_todoList, ResourcePipeline.Post); // Assert - todoResourceMock.Verify(rd => rd.BeforeCreate(It.Is>((resources) => TodoCheck(resources, _description)), ResourcePipeline.Post), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeCreate(It.Is>(resources => TodoCheck(resources, Description)), ResourcePipeline.Post), Times.Once()); todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship( - It.Is>(rh => TodoCheckRelationships(rh, _description + _description)), + It.Is>(rh => TodoCheckRelationships(rh, Description + Description)), ResourcePipeline.Post), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -121,7 +121,7 @@ public void BeforeCreate_NoImplicit() hookExecutor.BeforeCreate(_todoList, ResourcePipeline.Post); // Assert - todoResourceMock.Verify(rd => rd.BeforeCreate(It.Is>((resources) => TodoCheck(resources, _description)), ResourcePipeline.Post), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeCreate(It.Is>(resources => TodoCheck(resources, Description)), ResourcePipeline.Post), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, _personId)), It.IsAny>(), @@ -162,7 +162,7 @@ public void BeforeCreate_NoImplicit_Without_Child_Hook_Implemented() hookExecutor.BeforeCreate(_todoList, ResourcePipeline.Post); // Assert - todoResourceMock.Verify(rd => rd.BeforeCreate(It.Is>((resources) => TodoCheck(resources, _description)), ResourcePipeline.Post), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeCreate(It.Is>(resources => TodoCheck(resources, Description)), ResourcePipeline.Post), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs b/test/UnitTests/ResourceHooks/Executor/Delete/AfterDeleteTests.cs similarity index 74% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs rename to test/UnitTests/ResourceHooks/Executor/Delete/AfterDeleteTests.cs index af85d5d487..16478d2b04 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs +++ b/test/UnitTests/ResourceHooks/Executor/Delete/AfterDeleteTests.cs @@ -4,18 +4,18 @@ using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor.Delete { public sealed class AfterDeleteTests : HooksTestsSetup { - private readonly ResourceHook[] targetHooks = { ResourceHook.AfterDelete }; + private readonly ResourceHook[] _targetHooks = { ResourceHook.AfterDelete }; [Fact] public void AfterDelete() { // Arrange - var discovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var (_, hookExecutor, resourceDefinitionMock) = CreateTestObjects(discovery); + var discovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var (hookExecutor, resourceDefinitionMock) = CreateTestObjects(discovery); var todoList = CreateTodoWithOwner(); // Act @@ -31,7 +31,7 @@ public void AfterDelete_Without_Any_Hook_Implemented() { // Arrange var discovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - var (_, hookExecutor, resourceDefinitionMock) = CreateTestObjects(discovery); + var (hookExecutor, resourceDefinitionMock) = CreateTestObjects(discovery); var todoList = CreateTodoWithOwner(); // Act diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs b/test/UnitTests/ResourceHooks/Executor/Delete/BeforeDeleteTests.cs similarity index 73% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs rename to test/UnitTests/ResourceHooks/Executor/Delete/BeforeDeleteTests.cs index b904846ec3..8435f6f9bc 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs +++ b/test/UnitTests/ResourceHooks/Executor/Delete/BeforeDeleteTests.cs @@ -3,18 +3,18 @@ using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor.Delete { public sealed class BeforeDeleteTests : HooksTestsSetup { - private readonly ResourceHook[] targetHooks = { ResourceHook.BeforeDelete }; + private readonly ResourceHook[] _targetHooks = { ResourceHook.BeforeDelete }; [Fact] public void BeforeDelete() { // Arrange - var discovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var (_, hookExecutor, resourceDefinitionMock) = CreateTestObjects(discovery); + var discovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var (hookExecutor, resourceDefinitionMock) = CreateTestObjects(discovery); var todoList = CreateTodoWithOwner(); // Act @@ -30,7 +30,7 @@ public void BeforeDelete_Without_Any_Hook_Implemented() { // Arrange var discovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - var (_, hookExecutor, resourceDefinitionMock) = CreateTestObjects(discovery); + var (hookExecutor, resourceDefinitionMock) = CreateTestObjects(discovery); var todoList = CreateTodoWithOwner(); // Act diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs b/test/UnitTests/ResourceHooks/Executor/Delete/BeforeDeleteWithDbValuesTests.cs similarity index 58% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs rename to test/UnitTests/ResourceHooks/Executor/Delete/BeforeDeleteWithDbValuesTests.cs index 5500fb9c2a..37f94ff1e1 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs +++ b/test/UnitTests/ResourceHooks/Executor/Delete/BeforeDeleteWithDbValuesTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore; using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -7,27 +8,27 @@ using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor.Delete { - public sealed class BeforeDelete_WithDbValues_Tests : HooksTestsSetup + public sealed class BeforeDeleteWithDbValuesTests : HooksTestsSetup { - private readonly ResourceHook[] targetHooks = { ResourceHook.BeforeDelete, ResourceHook.BeforeImplicitUpdateRelationship, ResourceHook.BeforeUpdateRelationship }; + private readonly ResourceHook[] _targetHooks = { ResourceHook.BeforeDelete, ResourceHook.BeforeImplicitUpdateRelationship, ResourceHook.BeforeUpdateRelationship }; - private readonly DbContextOptions options; - private readonly Person person; - public BeforeDelete_WithDbValues_Tests() + private readonly DbContextOptions _options; + private readonly Person _person; + public BeforeDeleteWithDbValuesTests() { - person = _personFaker.Generate(); - var todo1 = _todoFaker.Generate(); - var todo2 = _todoFaker.Generate(); - var passport = _passportFaker.Generate(); + _person = PersonFaker.Generate(); + var todo1 = TodoFaker.Generate(); + var todo2 = TodoFaker.Generate(); + var passport = PassportFaker.Generate(); - person.Passport = passport; - person.TodoItems = new HashSet { todo1 }; - person.StakeHolderTodoItem = todo2; - options = InitInMemoryDb(context => + _person.Passport = passport; + _person.TodoItems = new HashSet { todo1 }; + _person.StakeHolderTodoItem = todo2; + _options = InitInMemoryDb(context => { - context.Set().Add(person); + context.Set().Add(_person); context.SaveChanges(); }); } @@ -36,17 +37,17 @@ public BeforeDelete_WithDbValues_Tests() public void BeforeDelete() { // Arrange - var personDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); - var todoDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); - var passportDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); - var (_, hookExecutor, personResourceMock, todoResourceMock, passportResourceMock) = CreateTestObjects(personDiscovery, todoDiscovery, passportDiscovery, repoDbContextOptions: options); + var personDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); + var passportDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); + var (_, hookExecutor, personResourceMock, todoResourceMock, passportResourceMock) = CreateTestObjects(personDiscovery, todoDiscovery, passportDiscovery, repoDbContextOptions: _options); // Act - hookExecutor.BeforeDelete(new List { person }, ResourcePipeline.Delete); + hookExecutor.BeforeDelete(_person.AsList(), ResourcePipeline.Delete); // Assert personResourceMock.Verify(rd => rd.BeforeDelete(It.IsAny>(), It.IsAny()), Times.Once()); - todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitTodos(rh)), ResourcePipeline.Delete), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitTodoItems(rh)), ResourcePipeline.Delete), Times.Once()); passportResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitPassports(rh)), ResourcePipeline.Delete), Times.Once()); VerifyNoOtherCalls(personResourceMock, todoResourceMock, passportResourceMock); } @@ -56,15 +57,15 @@ public void BeforeDelete_No_Parent_Hooks() { // Arrange var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - var todoDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); - var passportDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); - var (_, hookExecutor, personResourceMock, todoResourceMock, passportResourceMock) = CreateTestObjects(personDiscovery, todoDiscovery, passportDiscovery, repoDbContextOptions: options); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); + var passportDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); + var (_, hookExecutor, personResourceMock, todoResourceMock, passportResourceMock) = CreateTestObjects(personDiscovery, todoDiscovery, passportDiscovery, repoDbContextOptions: _options); // Act - hookExecutor.BeforeDelete(new List { person }, ResourcePipeline.Delete); + hookExecutor.BeforeDelete(_person.AsList(), ResourcePipeline.Delete); // Assert - todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitTodos(rh)), ResourcePipeline.Delete), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitTodoItems(rh)), ResourcePipeline.Delete), Times.Once()); passportResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitPassports(rh)), ResourcePipeline.Delete), Times.Once()); VerifyNoOtherCalls(personResourceMock, todoResourceMock, passportResourceMock); } @@ -73,23 +74,23 @@ public void BeforeDelete_No_Parent_Hooks() public void BeforeDelete_No_Children_Hooks() { // Arrange - var personDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); + var personDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var passportDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - var (_, hookExecutor, personResourceMock, todoResourceMock, passportResourceMock) = CreateTestObjects(personDiscovery, todoDiscovery, passportDiscovery, repoDbContextOptions: options); + var (_, hookExecutor, personResourceMock, todoResourceMock, passportResourceMock) = CreateTestObjects(personDiscovery, todoDiscovery, passportDiscovery, repoDbContextOptions: _options); // Act - hookExecutor.BeforeDelete(new List { person }, ResourcePipeline.Delete); + hookExecutor.BeforeDelete(_person.AsList(), ResourcePipeline.Delete); // Assert personResourceMock.Verify(rd => rd.BeforeDelete(It.IsAny>(), It.IsAny()), Times.Once()); VerifyNoOtherCalls(personResourceMock, todoResourceMock, passportResourceMock); } - private bool CheckImplicitTodos(IRelationshipsDictionary rh) + private bool CheckImplicitTodoItems(IRelationshipsDictionary rh) { - var todos = rh.GetByRelationship(); - return todos.Count == 2; + var todoItems = rh.GetByRelationship(); + return todoItems.Count == 2; } private bool CheckImplicitPassports(IRelationshipsDictionary rh) diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs b/test/UnitTests/ResourceHooks/Executor/IdentifiableManyToManyOnReturnTests.cs similarity index 77% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs rename to test/UnitTests/ResourceHooks/Executor/IdentifiableManyToManyOnReturnTests.cs index 0b3c0aa874..faf79c8c34 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs +++ b/test/UnitTests/ResourceHooks/Executor/IdentifiableManyToManyOnReturnTests.cs @@ -5,19 +5,19 @@ using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor { - public sealed class IdentifiableManyToMany_OnReturnTests : HooksTestsSetup + public sealed class IdentifiableManyToManyOnReturnTests : HooksTestsSetup { - private readonly ResourceHook[] targetHooks = { ResourceHook.OnReturn }; + private readonly ResourceHook[] _targetHooks = { ResourceHook.OnReturn }; [Fact] public void OnReturn() { // Arrange - var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); - var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); + var joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); var (articles, joins, tags) = CreateIdentifiableManyToManyData(); @@ -26,8 +26,8 @@ public void OnReturn() // Assert articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); - joinResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.Get), Times.Once()); - tagResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); + joinResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.Get), Times.Once()); + tagResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); } @@ -35,9 +35,9 @@ public void OnReturn() public void OnReturn_GetRelationship() { // Arrange - var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); - var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); + var joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); var (articles, joins, tags) = CreateIdentifiableManyToManyData(); @@ -45,9 +45,9 @@ public void OnReturn_GetRelationship() hookExecutor.OnReturn(articles, ResourcePipeline.GetRelationship); // Assert - articleResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(articles).Any()), ResourcePipeline.GetRelationship), Times.Once()); - joinResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.GetRelationship), Times.Once()); - tagResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.GetRelationship), Times.Once()); + articleResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(articles).Any()), ResourcePipeline.GetRelationship), Times.Once()); + joinResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.GetRelationship), Times.Once()); + tagResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.GetRelationship), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); } @@ -56,8 +56,8 @@ public void OnReturn_Without_Parent_Hook_Implemented() { // Arrange var articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); - var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); var (articles, joins, tags) = CreateIdentifiableManyToManyData(); @@ -65,8 +65,8 @@ public void OnReturn_Without_Parent_Hook_Implemented() hookExecutor.OnReturn(articles, ResourcePipeline.Get); // Assert - joinResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.Get), Times.Once()); - tagResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); + joinResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.Get), Times.Once()); + tagResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); } @@ -74,9 +74,9 @@ public void OnReturn_Without_Parent_Hook_Implemented() public void OnReturn_Without_Children_Hooks_Implemented() { // Arrange - var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); + var articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); var (articles, _, tags) = CreateIdentifiableManyToManyData(); @@ -86,7 +86,7 @@ public void OnReturn_Without_Children_Hooks_Implemented() // Assert articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); - tagResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); + tagResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); } @@ -94,8 +94,8 @@ public void OnReturn_Without_Children_Hooks_Implemented() public void OnReturn_Without_Grand_Children_Hooks_Implemented() { // Arrange - var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); - var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); + var joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); var (articles, joins, _) = CreateIdentifiableManyToManyData(); @@ -105,7 +105,7 @@ public void OnReturn_Without_Grand_Children_Hooks_Implemented() // Assert articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); - joinResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.Get), Times.Once()); + joinResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.Get), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); } @@ -113,7 +113,7 @@ public void OnReturn_Without_Grand_Children_Hooks_Implemented() public void OnReturn_Without_Any_Descendant_Hooks_Implemented() { // Arrange - var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); + var articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs b/test/UnitTests/ResourceHooks/Executor/ManyToManyOnReturnTests.cs similarity index 68% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs rename to test/UnitTests/ResourceHooks/Executor/ManyToManyOnReturnTests.cs index e4db309781..4b2e51d566 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs +++ b/test/UnitTests/ResourceHooks/Executor/ManyToManyOnReturnTests.cs @@ -1,21 +1,22 @@ using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore; using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Models; using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor { - public sealed class ManyToMany_OnReturnTests : HooksTestsSetup + public sealed class ManyToManyOnReturnTests : HooksTestsSetup { - private readonly ResourceHook[] targetHooks = { ResourceHook.OnReturn }; + private readonly ResourceHook[] _targetHooks = { ResourceHook.OnReturn }; - private (List
, List, List) CreateDummyData() + private (List
, List) CreateDummyData() { - var tagsSubset = _tagFaker.Generate(3); - var joinsSubSet = _articleTagFaker.Generate(3); - var articleTagsSubset = _articleFaker.Generate(); + var tagsSubset = TagFaker.Generate(3); + var joinsSubSet = ArticleTagFaker.Generate(3); + var articleTagsSubset = ArticleFaker.Generate(); articleTagsSubset.ArticleTags = joinsSubSet.ToHashSet(); for (int i = 0; i < 3; i++) { @@ -23,10 +24,10 @@ public sealed class ManyToMany_OnReturnTests : HooksTestsSetup joinsSubSet[i].Tag = tagsSubset[i]; } - var allTags = _tagFaker.Generate(3).Concat(tagsSubset).ToList(); - var completeJoin = _articleTagFaker.Generate(6); + var allTags = TagFaker.Generate(3).Concat(tagsSubset).ToList(); + var completeJoin = ArticleTagFaker.Generate(6); - var articleWithAllTags = _articleFaker.Generate(); + var articleWithAllTags = ArticleFaker.Generate(); articleWithAllTags.ArticleTags = completeJoin.ToHashSet(); for (int i = 0; i < 6; i++) @@ -35,27 +36,25 @@ public sealed class ManyToMany_OnReturnTests : HooksTestsSetup completeJoin[i].Tag = allTags[i]; } - var allJoins = joinsSubSet.Concat(completeJoin).ToList(); - - var articles = new List
{ articleTagsSubset, articleWithAllTags }; - return (articles, allJoins, allTags); + var articles = ArrayFactory.Create(articleTagsSubset, articleWithAllTags).ToList(); + return (articles, allTags); } [Fact] public void OnReturn() { // Arrange - var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); - var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); + var tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - var (articles, _, tags) = CreateDummyData(); + var (articles, tags) = CreateDummyData(); // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); // Assert articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); - tagResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); + tagResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); VerifyNoOtherCalls(articleResourceMock, tagResourceMock); } @@ -64,15 +63,15 @@ public void OnReturn_Without_Parent_Hook_Implemented() { // Arrange var articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); - var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - var (articles, _, tags) = CreateDummyData(); + var (articles, tags) = CreateDummyData(); // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); // Assert - tagResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); + tagResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); VerifyNoOtherCalls(articleResourceMock, tagResourceMock); } @@ -80,10 +79,10 @@ public void OnReturn_Without_Parent_Hook_Implemented() public void OnReturn_Without_Children_Hooks_Implemented() { // Arrange - var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); + var articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - var (articles, _, _) = CreateDummyData(); + var (articles, _) = CreateDummyData(); // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); @@ -101,7 +100,7 @@ public void OnReturn_Without_Any_Hook_Implemented() var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - var (articles, _, _) = CreateDummyData(); + var (articles, _) = CreateDummyData(); // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/BeforeReadTests.cs b/test/UnitTests/ResourceHooks/Executor/Read/BeforeReadTests.cs similarity index 79% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/BeforeReadTests.cs rename to test/UnitTests/ResourceHooks/Executor/Read/BeforeReadTests.cs index a967bc2c9d..c5be40aa74 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/BeforeReadTests.cs +++ b/test/UnitTests/ResourceHooks/Executor/Read/BeforeReadTests.cs @@ -1,22 +1,20 @@ -using System.Collections.Generic; using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Queries; using JsonApiDotNetCoreExample.Models; using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor.Read { public sealed class BeforeReadTests : HooksTestsSetup { - private readonly ResourceHook[] targetHooks = { ResourceHook.BeforeRead }; + private readonly ResourceHook[] _targetHooks = { ResourceHook.BeforeRead }; [Fact] public void BeforeRead() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var (_, hookExecutor, todoResourceMock) = CreateTestObjects(todoDiscovery); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var (hookExecutor, todoResourceMock) = CreateTestObjects(todoDiscovery); // Act hookExecutor.BeforeRead(ResourcePipeline.Get); @@ -30,14 +28,14 @@ public void BeforeRead() public void BeforeReadWithInclusion() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (constraintsMock, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); // eg a call on api/todoItems?include=owner,assignee,stakeHolders var relationshipsChains = GetIncludedRelationshipsChains("owner", "assignee", "stakeHolders"); - constraintsMock.Setup(x => x.GetEnumerator()).Returns(new List(ConvertInclusionChains(relationshipsChains)).GetEnumerator()); + constraintsMock.Setup(x => x.GetEnumerator()).Returns(ConvertInclusionChains(relationshipsChains).GetEnumerator()); // Act hookExecutor.BeforeRead(ResourcePipeline.Get); @@ -51,15 +49,15 @@ public void BeforeReadWithInclusion() public void BeforeReadWithNestedInclusion() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var passportDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var passportDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (constraintsMock, hookExecutor, todoResourceMock, ownerResourceMock, passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); // eg a call on api/todoItems?include=owner.passport,assignee,stakeHolders var relationshipsChains = GetIncludedRelationshipsChains("owner.passport", "assignee", "stakeHolders"); - constraintsMock.Setup(x => x.GetEnumerator()).Returns(new List(ConvertInclusionChains(relationshipsChains)).GetEnumerator()); + constraintsMock.Setup(x => x.GetEnumerator()).Returns(ConvertInclusionChains(relationshipsChains).GetEnumerator()); // Act hookExecutor.BeforeRead(ResourcePipeline.Get); @@ -76,14 +74,14 @@ public void BeforeReadWithNestedInclusion_No_Parent_Hook_Implemented() { // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var passportDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var passportDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (constraintsMock, hookExecutor, todoResourceMock, ownerResourceMock, passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); // eg a call on api/todoItems?include=owner.passport,assignee,stakeHolders var relationshipsChains = GetIncludedRelationshipsChains("owner.passport", "assignee", "stakeHolders"); - constraintsMock.Setup(x => x.GetEnumerator()).Returns(new List(ConvertInclusionChains(relationshipsChains)).GetEnumerator()); + constraintsMock.Setup(x => x.GetEnumerator()).Returns(ConvertInclusionChains(relationshipsChains).GetEnumerator()); // Act hookExecutor.BeforeRead(ResourcePipeline.Get); @@ -97,15 +95,15 @@ public void BeforeReadWithNestedInclusion_No_Parent_Hook_Implemented() public void BeforeReadWithNestedInclusion_No_Child_Hook_Implemented() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - var passportDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var passportDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (constraintsMock, hookExecutor, todoResourceMock, ownerResourceMock, passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); // eg a call on api/todoItems?include=owner.passport,assignee,stakeHolders var relationshipsChains = GetIncludedRelationshipsChains("owner.passport", "assignee", "stakeHolders"); - constraintsMock.Setup(x => x.GetEnumerator()).Returns(new List(ConvertInclusionChains(relationshipsChains)).GetEnumerator()); + constraintsMock.Setup(x => x.GetEnumerator()).Returns(ConvertInclusionChains(relationshipsChains).GetEnumerator()); // Act hookExecutor.BeforeRead(ResourcePipeline.Get); @@ -119,15 +117,15 @@ public void BeforeReadWithNestedInclusion_No_Child_Hook_Implemented() public void BeforeReadWithNestedInclusion_No_Grandchild_Hook_Implemented() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var passportDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var (constraintsMock, hookExecutor, todoResourceMock, ownerResourceMock, passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); // eg a call on api/todoItems?include=owner.passport,assignee,stakeHolders var relationshipsChains = GetIncludedRelationshipsChains("owner.passport", "assignee", "stakeHolders"); - constraintsMock.Setup(x => x.GetEnumerator()).Returns(new List(ConvertInclusionChains(relationshipsChains)).GetEnumerator()); + constraintsMock.Setup(x => x.GetEnumerator()).Returns(ConvertInclusionChains(relationshipsChains).GetEnumerator()); // Act hookExecutor.BeforeRead(ResourcePipeline.Get); @@ -150,7 +148,7 @@ public void BeforeReadWithNestedInclusion_Without_Any_Hook_Implemented() // eg a call on api/todoItems?include=owner.passport,assignee,stakeHolders var relationshipsChains = GetIncludedRelationshipsChains("owner.passport", "assignee", "stakeHolders"); - constraintsMock.Setup(x => x.GetEnumerator()).Returns(new List(ConvertInclusionChains(relationshipsChains)).GetEnumerator()); + constraintsMock.Setup(x => x.GetEnumerator()).Returns(ConvertInclusionChains(relationshipsChains).GetEnumerator()); // Act hookExecutor.BeforeRead(ResourcePipeline.Get); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs b/test/UnitTests/ResourceHooks/Executor/Read/IdentifiableManyToManyAfterReadTests.cs similarity index 79% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs rename to test/UnitTests/ResourceHooks/Executor/Read/IdentifiableManyToManyAfterReadTests.cs index e91e3a6ebd..beaaff4faa 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs +++ b/test/UnitTests/ResourceHooks/Executor/Read/IdentifiableManyToManyAfterReadTests.cs @@ -5,19 +5,19 @@ using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor.Read { - public sealed class IdentifiableManyToMany_AfterReadTests : HooksTestsSetup + public sealed class IdentifiableManyToManyAfterReadTests : HooksTestsSetup { - private readonly ResourceHook[] targetHooks = { ResourceHook.AfterRead }; + private readonly ResourceHook[] _targetHooks = { ResourceHook.AfterRead }; [Fact] public void AfterRead() { // Arrange - var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); - var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); + var joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); var (articles, joins, tags) = CreateIdentifiableManyToManyData(); @@ -26,8 +26,8 @@ public void AfterRead() // Assert articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); - joinResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.Get, true), Times.Once()); - tagResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); + joinResourceMock.Verify(rd => rd.AfterRead(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.Get, true), Times.Once()); + tagResourceMock.Verify(rd => rd.AfterRead(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); } @@ -36,8 +36,8 @@ public void AfterRead_Without_Parent_Hook_Implemented() { // Arrange var articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); - var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); var (articles, joins, tags) = CreateIdentifiableManyToManyData(); @@ -45,8 +45,8 @@ public void AfterRead_Without_Parent_Hook_Implemented() hookExecutor.AfterRead(articles, ResourcePipeline.Get); // Assert - joinResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.Get, true), Times.Once()); - tagResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); + joinResourceMock.Verify(rd => rd.AfterRead(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.Get, true), Times.Once()); + tagResourceMock.Verify(rd => rd.AfterRead(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); } @@ -54,9 +54,9 @@ public void AfterRead_Without_Parent_Hook_Implemented() public void AfterRead_Without_Children_Hooks_Implemented() { // Arrange - var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); + var articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); @@ -67,7 +67,7 @@ public void AfterRead_Without_Children_Hooks_Implemented() // Assert articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); - tagResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); + tagResourceMock.Verify(rd => rd.AfterRead(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); } @@ -75,8 +75,8 @@ public void AfterRead_Without_Children_Hooks_Implemented() public void AfterRead_Without_Grand_Children_Hooks_Implemented() { // Arrange - var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); - var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); + var joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); var (articles, joins, _) = CreateIdentifiableManyToManyData(); @@ -86,7 +86,7 @@ public void AfterRead_Without_Grand_Children_Hooks_Implemented() // Assert articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); - joinResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.Get, true), Times.Once()); + joinResourceMock.Verify(rd => rd.AfterRead(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.Get, true), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); } @@ -94,7 +94,7 @@ public void AfterRead_Without_Grand_Children_Hooks_Implemented() public void AfterRead_Without_Any_Descendant_Hooks_Implemented() { // Arrange - var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); + var articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs b/test/UnitTests/ResourceHooks/Executor/Read/ManyToManyAfterReadTests.cs similarity index 75% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs rename to test/UnitTests/ResourceHooks/Executor/Read/ManyToManyAfterReadTests.cs index 7ca674ea65..e13f25c209 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs +++ b/test/UnitTests/ResourceHooks/Executor/Read/ManyToManyAfterReadTests.cs @@ -5,27 +5,27 @@ using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor.Read { - public sealed class ManyToMany_AfterReadTests : HooksTestsSetup + public sealed class ManyToManyAfterReadTests : HooksTestsSetup { - private readonly ResourceHook[] targetHooks = { ResourceHook.AfterRead }; + private readonly ResourceHook[] _targetHooks = { ResourceHook.AfterRead }; [Fact] public void AfterRead() { // Arrange - var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); - var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); + var tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - var (articles, _, tags) = CreateManyToManyData(); + var (articles, tags) = CreateManyToManyData(); // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); // Assert articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); - tagResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); + tagResourceMock.Verify(rd => rd.AfterRead(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); VerifyNoOtherCalls(articleResourceMock, tagResourceMock); } @@ -34,15 +34,15 @@ public void AfterRead_Without_Parent_Hook_Implemented() { // Arrange var articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); - var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - var (articles, _, tags) = CreateManyToManyData(); + var (articles, tags) = CreateManyToManyData(); // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); // Assert - tagResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); + tagResourceMock.Verify(rd => rd.AfterRead(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); VerifyNoOtherCalls(articleResourceMock, tagResourceMock); } @@ -50,10 +50,10 @@ public void AfterRead_Without_Parent_Hook_Implemented() public void AfterRead_Without_Children_Hooks_Implemented() { // Arrange - var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); + var articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - var (articles, _, _) = CreateManyToManyData(); + var (articles, _) = CreateManyToManyData(); // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); @@ -70,7 +70,7 @@ public void AfterRead_Without_Any_Hook_Implemented() var articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - var (articles, _, _) = CreateManyToManyData(); + var (articles, _) = CreateManyToManyData(); // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs b/test/UnitTests/ResourceHooks/Executor/SameResourceTypeTests.cs similarity index 67% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs rename to test/UnitTests/ResourceHooks/Executor/SameResourceTypeTests.cs index eb963e38f2..be5186ea1b 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs +++ b/test/UnitTests/ResourceHooks/Executor/SameResourceTypeTests.cs @@ -1,21 +1,22 @@ using System.Collections.Generic; +using JsonApiDotNetCore; using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Models; using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor { public sealed class SameResourceTypeTests : HooksTestsSetup { - private readonly ResourceHook[] targetHooks = { ResourceHook.OnReturn }; + private readonly ResourceHook[] _targetHooks = { ResourceHook.OnReturn }; [Fact] public void Resource_Has_Multiple_Relations_To_Same_Type() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var person1 = new Person(); var todo = new TodoItem { Owner = person1 }; @@ -23,7 +24,7 @@ public void Resource_Has_Multiple_Relations_To_Same_Type() todo.Assignee = person2; var person3 = new Person { StakeHolderTodoItem = todo }; todo.StakeHolders = new HashSet { person3 }; - var todoList = new List { todo }; + var todoList = todo.AsList(); // Act hookExecutor.OnReturn(todoList, ResourcePipeline.Post); @@ -38,12 +39,12 @@ public void Resource_Has_Multiple_Relations_To_Same_Type() public void Resource_Has_Cyclic_Relations() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var (_, hookExecutor, todoResourceMock) = CreateTestObjects(todoDiscovery); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var (hookExecutor, todoResourceMock) = CreateTestObjects(todoDiscovery); var todo = new TodoItem(); todo.ParentTodo = todo; - todo.ChildrenTodos = new List { todo }; - var todoList = new List { todo }; + todo.ChildTodoItems = todo.AsList(); + var todoList = todo.AsList(); // Act hookExecutor.OnReturn(todoList, ResourcePipeline.Post); @@ -57,17 +58,17 @@ public void Resource_Has_Cyclic_Relations() public void Resource_Has_Nested_Cyclic_Relations() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var (_, hookExecutor, todoResourceMock) = CreateTestObjects(todoDiscovery); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var (hookExecutor, todoResourceMock) = CreateTestObjects(todoDiscovery); var rootTodo = new TodoItem { Id = 1 }; var child = new TodoItem { ParentTodo = rootTodo, Id = 2 }; - rootTodo.ChildrenTodos = new List { child }; + rootTodo.ChildTodoItems = child.AsList(); var grandChild = new TodoItem { ParentTodo = child, Id = 3 }; - child.ChildrenTodos = new List { grandChild }; + child.ChildTodoItems = grandChild.AsList(); var greatGrandChild = new TodoItem { ParentTodo = grandChild, Id = 4 }; - grandChild.ChildrenTodos = new List { greatGrandChild }; - greatGrandChild.ChildrenTodos = new List { rootTodo }; - var todoList = new List { rootTodo }; + grandChild.ChildTodoItems = greatGrandChild.AsList(); + greatGrandChild.ChildTodoItems = rootTodo.AsList(); + var todoList = rootTodo.AsList(); // Act hookExecutor.OnReturn(todoList, ResourcePipeline.Post); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs b/test/UnitTests/ResourceHooks/Executor/Update/AfterUpdateTests.cs similarity index 84% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs rename to test/UnitTests/ResourceHooks/Executor/Update/AfterUpdateTests.cs index c4402b809c..e8a2127335 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs +++ b/test/UnitTests/ResourceHooks/Executor/Update/AfterUpdateTests.cs @@ -4,18 +4,18 @@ using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor.Update { public sealed class AfterUpdateTests : HooksTestsSetup { - private readonly ResourceHook[] targetHooks = { ResourceHook.AfterUpdate, ResourceHook.AfterUpdateRelationship }; + private readonly ResourceHook[] _targetHooks = { ResourceHook.AfterUpdate, ResourceHook.AfterUpdateRelationship }; [Fact] public void AfterUpdate() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); @@ -33,7 +33,7 @@ public void AfterUpdate_Without_Parent_Hook_Implemented() { // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); @@ -49,7 +49,7 @@ public void AfterUpdate_Without_Parent_Hook_Implemented() public void AfterUpdate_Without_Child_Hook_Implemented() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs b/test/UnitTests/ResourceHooks/Executor/Update/BeforeUpdateTests.cs similarity index 85% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs rename to test/UnitTests/ResourceHooks/Executor/Update/BeforeUpdateTests.cs index 7a299416c3..6ed75e9d19 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs +++ b/test/UnitTests/ResourceHooks/Executor/Update/BeforeUpdateTests.cs @@ -4,18 +4,18 @@ using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor.Update { public sealed class BeforeUpdateTests : HooksTestsSetup { - private readonly ResourceHook[] targetHooks = { ResourceHook.BeforeUpdate, ResourceHook.BeforeUpdateRelationship }; + private readonly ResourceHook[] _targetHooks = { ResourceHook.BeforeUpdate, ResourceHook.BeforeUpdateRelationship }; [Fact] public void BeforeUpdate() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); + var personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); @@ -33,7 +33,7 @@ public void BeforeUpdate_Without_Parent_Hook_Implemented() { // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); @@ -49,7 +49,7 @@ public void BeforeUpdate_Without_Parent_Hook_Implemented() public void BeforeUpdate_Without_Child_Hook_Implemented() { // Arrange - var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); + var todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs b/test/UnitTests/ResourceHooks/Executor/Update/BeforeUpdateWithDbValuesTests.cs similarity index 86% rename from test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs rename to test/UnitTests/ResourceHooks/Executor/Update/BeforeUpdateWithDbValuesTests.cs index 83f7f21dbe..1e92475a0d 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs +++ b/test/UnitTests/ResourceHooks/Executor/Update/BeforeUpdateWithDbValuesTests.cs @@ -7,20 +7,20 @@ using Moq; using Xunit; -namespace UnitTests.ResourceHooks +namespace UnitTests.ResourceHooks.Executor.Update { - public sealed class BeforeUpdate_WithDbValues_Tests : HooksTestsSetup + public sealed class BeforeUpdateWithDbValuesTests : HooksTestsSetup { private readonly ResourceHook[] _targetHooks = { ResourceHook.BeforeUpdate, ResourceHook.BeforeImplicitUpdateRelationship, ResourceHook.BeforeUpdateRelationship }; private readonly ResourceHook[] _targetHooksNoImplicit = { ResourceHook.BeforeUpdate, ResourceHook.BeforeUpdateRelationship }; - private const string _description = "DESCRIPTION"; - private const string _lastName = "NAME"; + private const string Description = "DESCRIPTION"; + private const string LastName = "NAME"; private readonly string _personId; private readonly List _todoList; private readonly DbContextOptions _options; - public BeforeUpdate_WithDbValues_Tests() + public BeforeUpdateWithDbValuesTests() { _todoList = CreateTodoWithToOnePerson(); @@ -29,14 +29,14 @@ public BeforeUpdate_WithDbValues_Tests() _personId = personId.ToString(); var implicitPersonId = personId + 10000; - var implicitTodo = _todoFaker.Generate(); + var implicitTodo = TodoFaker.Generate(); implicitTodo.Id += 1000; - implicitTodo.OneToOnePerson = new Person {Id = personId, LastName = _lastName}; - implicitTodo.Description = _description + _description; + implicitTodo.OneToOnePerson = new Person {Id = personId, LastName = LastName}; + implicitTodo.Description = Description + Description; _options = InitInMemoryDb(context => { - context.Set().Add(new TodoItem {Id = todoId, OneToOnePerson = new Person {Id = implicitPersonId, LastName = _lastName + _lastName}, Description = _description}); + context.Set().Add(new TodoItem {Id = todoId, OneToOnePerson = new Person {Id = implicitPersonId, LastName = LastName + LastName}, Description = Description}); context.Set().Add(implicitTodo); context.SaveChanges(); }); @@ -54,18 +54,18 @@ public void BeforeUpdate() hookExecutor.BeforeUpdate(_todoList, ResourcePipeline.Patch); // Assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, _description)), ResourcePipeline.Patch), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>(diff => TodoCheckDiff(diff, Description)), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, _personId)), - It.Is>(rh => PersonCheck(_lastName, rh)), + It.Is>(rh => PersonCheck(LastName, rh)), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship( - It.Is>(rh => PersonCheck(_lastName + _lastName, rh)), + It.Is>(rh => PersonCheck(LastName + LastName, rh)), ResourcePipeline.Patch), Times.Once()); todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship( - It.Is>(rh => TodoCheck(rh, _description + _description)), + It.Is>(rh => TodoCheck(rh, Description + Description)), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -80,16 +80,16 @@ public void BeforeUpdate_Deleting_Relationship() var personDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); var (_, ufMock, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: _options); - ufMock.Setup(c => c.Relationships).Returns(_resourceGraph.GetRelationships((TodoItem t) => t.OneToOnePerson).ToHashSet); + ufMock.Setup(c => c.Relationships).Returns(ResourceGraph.GetRelationships((TodoItem t) => t.OneToOnePerson).ToHashSet); // Act var todoList = new List { new TodoItem { Id = _todoList[0].Id } }; hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); // Assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, _description)), ResourcePipeline.Patch), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>(diff => TodoCheckDiff(diff, Description)), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship( - It.Is>(rh => PersonCheck(_lastName + _lastName, rh)), + It.Is>(rh => PersonCheck(LastName + LastName, rh)), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -110,11 +110,11 @@ public void BeforeUpdate_Without_Parent_Hook_Implemented() // Assert ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, _personId)), - It.Is>(rh => PersonCheck(_lastName, rh)), + It.Is>(rh => PersonCheck(LastName, rh)), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship( - It.Is>(rh => PersonCheck(_lastName + _lastName, rh)), + It.Is>(rh => PersonCheck(LastName + LastName, rh)), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -132,9 +132,9 @@ public void BeforeUpdate_Without_Child_Hook_Implemented() hookExecutor.BeforeUpdate(_todoList, ResourcePipeline.Patch); // Assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, _description)), ResourcePipeline.Patch), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>(diff => TodoCheckDiff(diff, Description)), ResourcePipeline.Patch), Times.Once()); todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship( - It.Is>(rh => TodoCheck(rh, _description + _description)), + It.Is>(rh => TodoCheck(rh, Description + Description)), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -152,7 +152,7 @@ public void BeforeUpdate_NoImplicit() hookExecutor.BeforeUpdate(_todoList, ResourcePipeline.Patch); // Assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, _description)), ResourcePipeline.Patch), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>(diff => TodoCheckDiff(diff, Description)), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, _personId)), It.IsAny>(), @@ -175,7 +175,7 @@ public void BeforeUpdate_NoImplicit_Without_Parent_Hook_Implemented() // Assert ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, _personId)), - It.Is>(rh => PersonCheck(_lastName, rh)), + It.Is>(rh => PersonCheck(LastName, rh)), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -193,7 +193,7 @@ public void BeforeUpdate_NoImplicit_Without_Child_Hook_Implemented() hookExecutor.BeforeUpdate(_todoList, ResourcePipeline.Patch); // Assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, _description)), ResourcePipeline.Patch), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>(diff => TodoCheckDiff(diff, Description)), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } diff --git a/test/UnitTests/ResourceHooks/HooksDummyData.cs b/test/UnitTests/ResourceHooks/HooksDummyData.cs new file mode 100644 index 0000000000..1a5d88a365 --- /dev/null +++ b/test/UnitTests/ResourceHooks/HooksDummyData.cs @@ -0,0 +1,141 @@ +using System.Collections.Generic; +using System.Linq; +using Bogus; +using JsonApiDotNetCore; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCoreExample.Models; +using Microsoft.Extensions.Logging.Abstractions; +using Person = JsonApiDotNetCoreExample.Models.Person; + +namespace UnitTests.ResourceHooks +{ + public class HooksDummyData + { + protected IResourceGraph ResourceGraph { get; } + protected ResourceHook[] NoHooks { get; } = new ResourceHook[0]; + protected ResourceHook[] EnableDbValues { get; } = { ResourceHook.BeforeUpdate, ResourceHook.BeforeUpdateRelationship }; + protected ResourceHook[] DisableDbValues { get; } = new ResourceHook[0]; + protected readonly Faker TodoFaker; + protected readonly Faker PersonFaker; + protected readonly Faker
ArticleFaker; + protected readonly Faker TagFaker; + protected readonly Faker ArticleTagFaker; + private readonly Faker _identifiableArticleTagFaker; + protected readonly Faker PassportFaker; + + protected HooksDummyData() + { + ResourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) + .Add() + .Add() + .Add() + .Add
() + .Add() + .Add() + .Build(); + + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + + TodoFaker = new Faker() + .RuleFor(x => x.Id, f => f.UniqueIndex + 1); + + PersonFaker = new Faker() + .RuleFor(x => x.Id, f => f.UniqueIndex + 1); + + ArticleFaker = new Faker
() + .RuleFor(x => x.Id, f => f.UniqueIndex + 1); + + TagFaker = new Faker() + .RuleFor(x => x.Id, f => f.UniqueIndex + 1); + + ArticleTagFaker = new Faker(); + + _identifiableArticleTagFaker = new Faker() + .RuleFor(x => x.Id, f => f.UniqueIndex + 1); + + PassportFaker = new Faker() + .RuleFor(x => x.Id, f => f.UniqueIndex + 1); + + // @formatter:wrap_chained_method_calls restore + // @formatter:keep_existing_linebreaks restore + } + + protected List CreateTodoWithToOnePerson() + { + var todoItem = TodoFaker.Generate(); + var person = PersonFaker.Generate(); + var todoList = todoItem.AsList(); + person.OneToOneTodoItem = todoItem; + todoItem.OneToOnePerson = person; + return todoList; + } + + protected HashSet CreateTodoWithOwner() + { + var todoItem = TodoFaker.Generate(); + var person = PersonFaker.Generate(); + var todoList = new HashSet { todoItem }; + person.AssignedTodoItems = todoList; + todoItem.Owner = person; + return todoList; + } + + protected (List
, List) CreateManyToManyData() + { + var tagsSubset = TagFaker.Generate(3); + var joinsSubSet = ArticleTagFaker.Generate(3); + var articleTagsSubset = ArticleFaker.Generate(); + articleTagsSubset.ArticleTags = joinsSubSet.ToHashSet(); + for (int i = 0; i < 3; i++) + { + joinsSubSet[i].Article = articleTagsSubset; + joinsSubSet[i].Tag = tagsSubset[i]; + } + + var allTags = TagFaker.Generate(3).Concat(tagsSubset).ToList(); + var completeJoin = ArticleTagFaker.Generate(6); + + var articleWithAllTags = ArticleFaker.Generate(); + articleWithAllTags.ArticleTags = completeJoin.ToHashSet(); + + for (int i = 0; i < 6; i++) + { + completeJoin[i].Article = articleWithAllTags; + completeJoin[i].Tag = allTags[i]; + } + + var articles = ArrayFactory.Create(articleTagsSubset, articleWithAllTags).ToList(); + return (articles, allTags); + } + + protected (List
, List, List) CreateIdentifiableManyToManyData() + { + var tagsSubset = TagFaker.Generate(3); + var joinsSubSet = _identifiableArticleTagFaker.Generate(3); + var articleTagsSubset = ArticleFaker.Generate(); + articleTagsSubset.IdentifiableArticleTags = joinsSubSet.ToHashSet(); + for (int i = 0; i < 3; i++) + { + joinsSubSet[i].Article = articleTagsSubset; + joinsSubSet[i].Tag = tagsSubset[i]; + } + var allTags = TagFaker.Generate(3).Concat(tagsSubset).ToList(); + var completeJoin = _identifiableArticleTagFaker.Generate(6); + + var articleWithAllTags = ArticleFaker.Generate(); + articleWithAllTags.IdentifiableArticleTags = joinsSubSet.ToHashSet(); + + for (int i = 0; i < 6; i++) + { + completeJoin[i].Article = articleWithAllTags; + completeJoin[i].Tag = allTags[i]; + } + + var allJoins = joinsSubSet.Concat(completeJoin).ToList(); + var articles = ArrayFactory.Create(articleTagsSubset, articleWithAllTags).ToList(); + return (articles, allJoins, allTags); + } + } +} diff --git a/test/UnitTests/ResourceHooks/HooksTestsSetup.cs b/test/UnitTests/ResourceHooks/HooksTestsSetup.cs new file mode 100644 index 0000000000..d02671435c --- /dev/null +++ b/test/UnitTests/ResourceHooks/HooksTestsSetup.cs @@ -0,0 +1,319 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Hooks.Internal; +using JsonApiDotNetCore.Hooks.Internal.Discovery; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Hooks.Internal.Traversal; +using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; + +namespace UnitTests.ResourceHooks +{ + public class HooksTestsSetup : HooksDummyData + { + private (Mock, Mock>, Mock, IJsonApiOptions) CreateMocks() + { + var pfMock = new Mock(); + var ufMock = new Mock(); + + var constraintsMock = new Mock>(); + constraintsMock.Setup(x => x.GetEnumerator()).Returns(Enumerable.Empty().GetEnumerator()); + + var optionsMock = new JsonApiOptions { LoadDatabaseValues = false }; + return (ufMock, constraintsMock, pfMock, optionsMock); + } + + internal (ResourceHookExecutor, Mock>) CreateTestObjects(IHooksDiscovery primaryDiscovery = null) + where TPrimary : class, IIdentifiable + { + // creates the resource definition mock and corresponding ImplementedHooks discovery instance + var primaryResource = CreateResourceDefinition(); + + // mocking the genericServiceFactory and JsonApiContext and wiring them up. + var (ufMock, constraintsMock, gpfMock, options) = CreateMocks(); + + SetupProcessorFactoryForResourceDefinition(gpfMock, primaryResource.Object, primaryDiscovery); + + var execHelper = new HookExecutorHelper(gpfMock.Object, ResourceGraph, options); + var traversalHelper = new TraversalHelper(ResourceGraph, ufMock.Object); + var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, ResourceGraph); + + return (hookExecutor, primaryResource); + } + + protected (Mock>, Mock, IResourceHookExecutor, Mock>, Mock>) + CreateTestObjects( + IHooksDiscovery primaryDiscovery = null, + IHooksDiscovery secondaryDiscovery = null, + DbContextOptions repoDbContextOptions = null + ) + where TPrimary : class, IIdentifiable + where TSecondary : class, IIdentifiable + { + // creates the resource definition mock and corresponding for a given set of discoverable hooks + var primaryResource = CreateResourceDefinition(); + var secondaryResource = CreateResourceDefinition(); + + // mocking the genericServiceFactory and JsonApiContext and wiring them up. + var (ufMock, constraintsMock, gpfMock, options) = CreateMocks(); + + var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions) : null; + + var resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) + .Add() + .Add() + .Build(); + + SetupProcessorFactoryForResourceDefinition(gpfMock, primaryResource.Object, primaryDiscovery, dbContext, resourceGraph); + SetupProcessorFactoryForResourceDefinition(gpfMock, secondaryResource.Object, secondaryDiscovery, dbContext, resourceGraph); + + var execHelper = new HookExecutorHelper(gpfMock.Object, ResourceGraph, options); + var traversalHelper = new TraversalHelper(ResourceGraph, ufMock.Object); + var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, ResourceGraph); + + return (constraintsMock, ufMock, hookExecutor, primaryResource, secondaryResource); + } + + protected (Mock>, IResourceHookExecutor, Mock>, Mock>, Mock>) + CreateTestObjects( + IHooksDiscovery primaryDiscovery = null, + IHooksDiscovery firstSecondaryDiscovery = null, + IHooksDiscovery secondSecondaryDiscovery = null, + DbContextOptions repoDbContextOptions = null + ) + where TPrimary : class, IIdentifiable + where TFirstSecondary : class, IIdentifiable + where TSecondSecondary : class, IIdentifiable + { + // creates the resource definition mock and corresponding for a given set of discoverable hooks + var primaryResource = CreateResourceDefinition(); + var firstSecondaryResource = CreateResourceDefinition(); + var secondSecondaryResource = CreateResourceDefinition(); + + // mocking the genericServiceFactory and JsonApiContext and wiring them up. + var (ufMock, constraintsMock, gpfMock, options) = CreateMocks(); + + var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions) : null; + + var resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) + .Add() + .Add() + .Add() + .Build(); + + SetupProcessorFactoryForResourceDefinition(gpfMock, primaryResource.Object, primaryDiscovery, dbContext, resourceGraph); + SetupProcessorFactoryForResourceDefinition(gpfMock, firstSecondaryResource.Object, firstSecondaryDiscovery, dbContext, resourceGraph); + SetupProcessorFactoryForResourceDefinition(gpfMock, secondSecondaryResource.Object, secondSecondaryDiscovery, dbContext, resourceGraph); + + var execHelper = new HookExecutorHelper(gpfMock.Object, ResourceGraph, options); + var traversalHelper = new TraversalHelper(ResourceGraph, ufMock.Object); + var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, ResourceGraph); + + return (constraintsMock, hookExecutor, primaryResource, firstSecondaryResource, secondSecondaryResource); + } + + protected IHooksDiscovery SetDiscoverableHooks(ResourceHook[] implementedHooks, params ResourceHook[] enableDbValuesHooks) + where TResource : class, IIdentifiable + { + var mock = new Mock>(); + mock.Setup(discovery => discovery.ImplementedHooks) + .Returns(implementedHooks); + + if (!enableDbValuesHooks.Any()) + { + mock.Setup(discovery => discovery.DatabaseValuesDisabledHooks) + .Returns(enableDbValuesHooks); + } + mock.Setup(discovery => discovery.DatabaseValuesEnabledHooks) + .Returns(ResourceHook.BeforeImplicitUpdateRelationship.AsEnumerable().Concat(enableDbValuesHooks).ToArray()); + + return mock.Object; + } + + protected void VerifyNoOtherCalls(params dynamic[] resourceMocks) + { + foreach (var mock in resourceMocks) + { + mock.VerifyNoOtherCalls(); + } + } + + protected DbContextOptions InitInMemoryDb(Action seeder) + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "repository_mock") + .Options; + + using var context = new AppDbContext(options); + + seeder(context); + ResolveInverseRelationships(context); + + return options; + } + + private void MockHooks(Mock> resourceDefinition) where TModel : class, IIdentifiable + { + resourceDefinition + .Setup(rd => rd.BeforeCreate(It.IsAny>(), It.IsAny())) + .Returns, ResourcePipeline>((resources, _) => resources) + .Verifiable(); + resourceDefinition + .Setup(rd => rd.BeforeRead(It.IsAny(), It.IsAny(), It.IsAny())) + .Verifiable(); + resourceDefinition + .Setup(rd => rd.BeforeUpdate(It.IsAny>(), It.IsAny())) + .Returns, ResourcePipeline>((resources, _) => resources) + .Verifiable(); + resourceDefinition + .Setup(rd => rd.BeforeDelete(It.IsAny>(), It.IsAny())) + .Returns, ResourcePipeline>((resources, _) => resources) + .Verifiable(); + resourceDefinition + .Setup(rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), It.IsAny())) + .Returns, IRelationshipsDictionary, ResourcePipeline>((ids, _, __) => ids) + .Verifiable(); + resourceDefinition + .Setup(rd => rd.BeforeImplicitUpdateRelationship(It.IsAny>(), It.IsAny())) + .Verifiable(); + resourceDefinition + .Setup(rd => rd.OnReturn(It.IsAny>(), It.IsAny())) + .Returns, ResourcePipeline>((resources, _) => resources) + .Verifiable(); + resourceDefinition + .Setup(rd => rd.AfterCreate(It.IsAny>(), It.IsAny())) + .Verifiable(); + resourceDefinition + .Setup(rd => rd.AfterRead(It.IsAny>(), It.IsAny(), It.IsAny())) + .Verifiable(); + resourceDefinition + .Setup(rd => rd.AfterUpdate(It.IsAny>(), It.IsAny())) + .Verifiable(); + resourceDefinition + .Setup(rd => rd.AfterDelete(It.IsAny>(), It.IsAny(), It.IsAny())) + .Verifiable(); + } + + private void SetupProcessorFactoryForResourceDefinition( + Mock processorFactory, + IResourceHookContainer modelResource, + IHooksDiscovery discovery, + AppDbContext dbContext = null, + IResourceGraph resourceGraph = null + ) + where TModel : class, IIdentifiable + { + processorFactory.Setup(c => c.Get(typeof(ResourceHooksDefinition<>), typeof(TModel))) + .Returns(modelResource); + + processorFactory.Setup(c => c.Get(typeof(IHooksDiscovery<>), typeof(TModel))) + .Returns(discovery); + + if (dbContext != null) + { + var idType = TypeHelper.GetIdType(typeof(TModel)); + if (idType == typeof(int)) + { + IResourceReadRepository repo = CreateTestRepository(dbContext, resourceGraph); + processorFactory.Setup(c => c.Get>(typeof(IResourceReadRepository<,>), typeof(TModel), typeof(int))).Returns(repo); + } + else + { + throw new TypeLoadException("Test not set up properly"); + } + + } + } + + private IResourceReadRepository CreateTestRepository(AppDbContext dbContext, IResourceGraph resourceGraph) + where TModel : class, IIdentifiable + { + var serviceProvider = ((IInfrastructure) dbContext).Instance; + var resourceFactory = new ResourceFactory(serviceProvider); + IDbContextResolver resolver = CreateTestDbResolver(dbContext); + var targetedFields = new TargetedFields(); + + return new EntityFrameworkCoreRepository(targetedFields, resolver, resourceGraph, resourceFactory, + Enumerable.Empty(), NullLoggerFactory.Instance); + } + + private IDbContextResolver CreateTestDbResolver(AppDbContext dbContext) + { + var mock = new Mock(); + mock.Setup(r => r.GetContext()).Returns(dbContext); + return mock.Object; + } + + private void ResolveInverseRelationships(AppDbContext context) + { + var dbContextResolvers = new DbContextResolver(context).AsEnumerable(); + var inverseRelationships = new InverseNavigationResolver(ResourceGraph, dbContextResolvers); + inverseRelationships.Resolve(); + } + + private Mock> CreateResourceDefinition() + where TModel : class, IIdentifiable + { + var resourceDefinition = new Mock>(); + MockHooks(resourceDefinition); + return resourceDefinition; + } + + protected List> GetIncludedRelationshipsChains(params string[] chains) + { + var parsedChains = new List>(); + + foreach (var chain in chains) + { + parsedChains.Add(GetIncludedRelationshipsChain(chain)); + } + + return parsedChains; + } + + private List GetIncludedRelationshipsChain(string chain) + { + var parsedChain = new List(); + var resourceContext = ResourceGraph.GetResourceContext(); + var splitPath = chain.Split('.'); + foreach (var requestedRelationship in splitPath) + { + var relationship = resourceContext.Relationships.Single(r => r.PublicName == requestedRelationship); + parsedChain.Add(relationship); + resourceContext = ResourceGraph.GetResourceContext(relationship.RightType); + } + return parsedChain; + } + + protected IEnumerable ConvertInclusionChains(List> inclusionChains) + { + var expressionsInScope = new List(); + + if (inclusionChains != null) + { + var chains = inclusionChains.Select(relationships => new ResourceFieldChainExpression(relationships)).ToList(); + var includeExpression = IncludeChainConverter.FromRelationshipChains(chains); + expressionsInScope.Add(new ExpressionInScope(null, includeExpression)); + } + + var mock = new Mock(); + mock.Setup(x => x.GetConstraints()).Returns(expressionsInScope); + + IQueryConstraintProvider includeConstraintProvider = mock.Object; + return includeConstraintProvider.AsEnumerable(); + } + + } +} diff --git a/test/UnitTests/ResourceHooks/NotTargeted.cs b/test/UnitTests/ResourceHooks/NotTargeted.cs new file mode 100644 index 0000000000..cff5f41d19 --- /dev/null +++ b/test/UnitTests/ResourceHooks/NotTargeted.cs @@ -0,0 +1,6 @@ +using JsonApiDotNetCore.Resources; + +namespace UnitTests.ResourceHooks +{ + internal sealed class NotTargeted : Identifiable { } +} diff --git a/test/UnitTests/ResourceHooks/RelationshipDictionaryTests.cs b/test/UnitTests/ResourceHooks/RelationshipDictionaryTests.cs index f431a8a604..732899dff7 100644 --- a/test/UnitTests/ResourceHooks/RelationshipDictionaryTests.cs +++ b/test/UnitTests/ResourceHooks/RelationshipDictionaryTests.cs @@ -1,76 +1,60 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using JetBrains.Annotations; using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using Xunit; namespace UnitTests.ResourceHooks { - public sealed class Dummy : Identifiable + public sealed class RelationshipDictionaryTests { - public string SomeUpdatedProperty { get; set; } - public string SomeNotUpdatedProperty { get; set; } - - [HasOne] - public ToOne FirstToOne { get; set; } - [HasOne] - public ToOne SecondToOne { get; set; } - [HasMany] - public ISet ToManies { get; set; } - } + private readonly HasOneAttribute _firstToOneAttr; + private readonly HasOneAttribute _secondToOneAttr; + private readonly HasManyAttribute _toManyAttr; - public class NotTargeted : Identifiable { } - public sealed class ToMany : Identifiable { } - public sealed class ToOne : Identifiable { } + private readonly Dictionary> _relationships = new Dictionary>(); + private readonly HashSet _firstToOnesResources = new HashSet { new Dummy { Id = 1 }, new Dummy { Id = 2 }, new Dummy { Id = 3 } }; + private readonly HashSet _secondToOnesResources = new HashSet { new Dummy { Id = 4 }, new Dummy { Id = 5 }, new Dummy { Id = 6 } }; + private readonly HashSet _toManiesResources = new HashSet { new Dummy { Id = 7 }, new Dummy { Id = 8 }, new Dummy { Id = 9 } }; + private readonly HashSet _noRelationshipsResources = new HashSet { new Dummy { Id = 10 }, new Dummy { Id = 11 }, new Dummy { Id = 12 } }; + private readonly HashSet _allResources; - public sealed class RelationshipDictionaryTests - { - public readonly HasOneAttribute FirstToOneAttr; - public readonly HasOneAttribute SecondToOneAttr; - public readonly HasManyAttribute ToManyAttr; - - public readonly Dictionary> Relationships = new Dictionary>(); - public readonly HashSet FirstToOnesResources = new HashSet { new Dummy { Id = 1 }, new Dummy { Id = 2 }, new Dummy { Id = 3 } }; - public readonly HashSet SecondToOnesResources = new HashSet { new Dummy { Id = 4 }, new Dummy { Id = 5 }, new Dummy { Id = 6 } }; - public readonly HashSet ToManiesResources = new HashSet { new Dummy { Id = 7 }, new Dummy { Id = 8 }, new Dummy { Id = 9 } }; - public readonly HashSet NoRelationshipsResources = new HashSet { new Dummy { Id = 10 }, new Dummy { Id = 11 }, new Dummy { Id = 12 } }; - public readonly HashSet AllResources; public RelationshipDictionaryTests() { - FirstToOneAttr = new HasOneAttribute + _firstToOneAttr = new HasOneAttribute { PublicName = "firstToOne", LeftType = typeof(Dummy), RightType = typeof(ToOne), Property = typeof(Dummy).GetProperty(nameof(Dummy.FirstToOne)) }; - SecondToOneAttr = new HasOneAttribute + _secondToOneAttr = new HasOneAttribute { PublicName = "secondToOne", LeftType = typeof(Dummy), RightType = typeof(ToOne), Property = typeof(Dummy).GetProperty(nameof(Dummy.SecondToOne)) }; - ToManyAttr = new HasManyAttribute + _toManyAttr = new HasManyAttribute { PublicName = "toManies", LeftType = typeof(Dummy), RightType = typeof(ToMany), Property = typeof(Dummy).GetProperty(nameof(Dummy.ToManies)) }; - Relationships.Add(FirstToOneAttr, FirstToOnesResources); - Relationships.Add(SecondToOneAttr, SecondToOnesResources); - Relationships.Add(ToManyAttr, ToManiesResources); - AllResources = new HashSet(FirstToOnesResources.Union(SecondToOnesResources).Union(ToManiesResources).Union(NoRelationshipsResources)); + _relationships.Add(_firstToOneAttr, _firstToOnesResources); + _relationships.Add(_secondToOneAttr, _secondToOnesResources); + _relationships.Add(_toManyAttr, _toManiesResources); + _allResources = new HashSet(_firstToOnesResources.Union(_secondToOnesResources).Union(_toManiesResources).Union(_noRelationshipsResources)); } [Fact] public void RelationshipsDictionary_GetByRelationships() { // Arrange - RelationshipsDictionary relationshipsDictionary = new RelationshipsDictionary(Relationships); + RelationshipsDictionary relationshipsDictionary = new RelationshipsDictionary(_relationships); // Act Dictionary> toOnes = relationshipsDictionary.GetByRelationship(); @@ -85,7 +69,7 @@ public void RelationshipsDictionary_GetByRelationships() public void RelationshipsDictionary_GetAffected() { // Arrange - RelationshipsDictionary relationshipsDictionary = new RelationshipsDictionary(Relationships); + RelationshipsDictionary relationshipsDictionary = new RelationshipsDictionary(_relationships); // Act var affectedThroughFirstToOne = relationshipsDictionary.GetAffected(d => d.FirstToOne).ToList(); @@ -93,16 +77,16 @@ public void RelationshipsDictionary_GetAffected() var affectedThroughToMany = relationshipsDictionary.GetAffected(d => d.ToManies).ToList(); // Assert - affectedThroughFirstToOne.ForEach(resource => Assert.Contains(resource, FirstToOnesResources)); - affectedThroughSecondToOne.ForEach(resource => Assert.Contains(resource, SecondToOnesResources)); - affectedThroughToMany.ForEach(resource => Assert.Contains(resource, ToManiesResources)); + affectedThroughFirstToOne.ForEach(resource => Assert.Contains(resource, _firstToOnesResources)); + affectedThroughSecondToOne.ForEach(resource => Assert.Contains(resource, _secondToOnesResources)); + affectedThroughToMany.ForEach(resource => Assert.Contains(resource, _toManiesResources)); } [Fact] public void ResourceHashSet_GetByRelationships() { // Arrange - ResourceHashSet resources = new ResourceHashSet(AllResources, Relationships); + ResourceHashSet resources = new ResourceHashSet(_allResources, _relationships); // Act Dictionary> toOnes = resources.GetByRelationship(); @@ -113,7 +97,7 @@ public void ResourceHashSet_GetByRelationships() // Assert AssertRelationshipDictionaryGetters(allRelationships, toOnes, toManies, notTargeted); var allResourcesWithAffectedRelationships = allRelationships.SelectMany(kvp => kvp.Value).ToList(); - NoRelationshipsResources.ToList().ForEach(e => + _noRelationshipsResources.ToList().ForEach(e => { Assert.DoesNotContain(e, allResourcesWithAffectedRelationships); }); @@ -123,8 +107,8 @@ public void ResourceHashSet_GetByRelationships() public void ResourceDiff_GetByRelationships() { // Arrange - var dbResources = new HashSet(AllResources.Select(e => new Dummy { Id = e.Id }).ToList()); - DiffableResourceHashSet diffs = new DiffableResourceHashSet(AllResources, dbResources, Relationships, null); + var dbResources = new HashSet(_allResources.Select(e => new Dummy { Id = e.Id }).ToList()); + DiffableResourceHashSet diffs = new DiffableResourceHashSet(_allResources, dbResources, _relationships, null); // Act Dictionary> toOnes = diffs.GetByRelationship(); @@ -135,7 +119,7 @@ public void ResourceDiff_GetByRelationships() // Assert AssertRelationshipDictionaryGetters(allRelationships, toOnes, toManies, notTargeted); var allResourcesWithAffectedRelationships = allRelationships.SelectMany(kvp => kvp.Value).ToList(); - NoRelationshipsResources.ToList().ForEach(e => + _noRelationshipsResources.ToList().ForEach(e => { Assert.DoesNotContain(e, allResourcesWithAffectedRelationships); }); @@ -143,7 +127,7 @@ public void ResourceDiff_GetByRelationships() var requestResourcesFromDiff = diffs; requestResourcesFromDiff.ToList().ForEach(e => { - Assert.Contains(e, AllResources); + Assert.Contains(e, _allResources); }); var databaseResourcesFromDiff = diffs.GetDiffs().Select(d => d.DatabaseValue); databaseResourcesFromDiff.ToList().ForEach(e => @@ -156,15 +140,15 @@ public void ResourceDiff_GetByRelationships() public void ResourceDiff_Loops_Over_Diffs() { // Arrange - var dbResources = new HashSet(AllResources.Select(e => new Dummy { Id = e.Id })); - DiffableResourceHashSet diffs = new DiffableResourceHashSet(AllResources, dbResources, Relationships, null); + var dbResources = new HashSet(_allResources.Select(e => new Dummy { Id = e.Id })); + DiffableResourceHashSet diffs = new DiffableResourceHashSet(_allResources, dbResources, _relationships, null); // Assert & act foreach (ResourceDiffPair diff in diffs.GetDiffs()) { Assert.Equal(diff.Resource.Id, diff.DatabaseValue.Id); Assert.NotEqual(diff.Resource, diff.DatabaseValue); - Assert.Contains(diff.Resource, AllResources); + Assert.Contains(diff.Resource, _allResources); Assert.Contains(diff.DatabaseValue, dbResources); } } @@ -173,8 +157,8 @@ public void ResourceDiff_Loops_Over_Diffs() public void ResourceDiff_GetAffected_Relationships() { // Arrange - var dbResources = new HashSet(AllResources.Select(e => new Dummy { Id = e.Id })); - DiffableResourceHashSet diffs = new DiffableResourceHashSet(AllResources, dbResources, Relationships, null); + var dbResources = new HashSet(_allResources.Select(e => new Dummy { Id = e.Id })); + DiffableResourceHashSet diffs = new DiffableResourceHashSet(_allResources, dbResources, _relationships, null); // Act var affectedThroughFirstToOne = diffs.GetAffected(d => d.FirstToOne).ToList(); @@ -182,21 +166,21 @@ public void ResourceDiff_GetAffected_Relationships() var affectedThroughToMany = diffs.GetAffected(d => d.ToManies).ToList(); // Assert - affectedThroughFirstToOne.ForEach(resource => Assert.Contains(resource, FirstToOnesResources)); - affectedThroughSecondToOne.ForEach(resource => Assert.Contains(resource, SecondToOnesResources)); - affectedThroughToMany.ForEach(resource => Assert.Contains(resource, ToManiesResources)); + affectedThroughFirstToOne.ForEach(resource => Assert.Contains(resource, _firstToOnesResources)); + affectedThroughSecondToOne.ForEach(resource => Assert.Contains(resource, _secondToOnesResources)); + affectedThroughToMany.ForEach(resource => Assert.Contains(resource, _toManiesResources)); } [Fact] public void ResourceDiff_GetAffected_Attributes() { // Arrange - var dbResources = new HashSet(AllResources.Select(e => new Dummy { Id = e.Id })); + var dbResources = new HashSet(_allResources.Select(e => new Dummy { Id = e.Id })); var updatedAttributes = new Dictionary> { - { typeof(Dummy).GetProperty("SomeUpdatedProperty"), AllResources } + { typeof(Dummy).GetProperty(nameof(Dummy.SomeUpdatedProperty))!, _allResources } }; - DiffableResourceHashSet diffs = new DiffableResourceHashSet(AllResources, dbResources, Relationships, updatedAttributes); + DiffableResourceHashSet diffs = new DiffableResourceHashSet(_allResources, dbResources, _relationships, updatedAttributes); // Act var affectedThroughSomeUpdatedProperty = diffs.GetAffected(d => d.SomeUpdatedProperty); @@ -207,29 +191,30 @@ public void ResourceDiff_GetAffected_Attributes() Assert.Empty(affectedThroughSomeNotUpdatedProperty); } + [AssertionMethod] private void AssertRelationshipDictionaryGetters(Dictionary> relationshipsDictionary, - Dictionary> toOnes, - Dictionary> toManies, - Dictionary> notTargeted) + Dictionary> toOnes, + Dictionary> toManies, + Dictionary> notTargeted) { - Assert.Contains(FirstToOneAttr, toOnes.Keys); - Assert.Contains(SecondToOneAttr, toOnes.Keys); - Assert.Contains(ToManyAttr, toManies.Keys); + Assert.Contains(_firstToOneAttr, toOnes.Keys); + Assert.Contains(_secondToOneAttr, toOnes.Keys); + Assert.Contains(_toManyAttr, toManies.Keys); Assert.Equal(relationshipsDictionary.Keys.Count, toOnes.Keys.Count + toManies.Keys.Count + notTargeted.Keys.Count); - toOnes[FirstToOneAttr].ToList().ForEach(resource => + toOnes[_firstToOneAttr].ToList().ForEach(resource => { - Assert.Contains(resource, FirstToOnesResources); + Assert.Contains(resource, _firstToOnesResources); }); - toOnes[SecondToOneAttr].ToList().ForEach(resource => + toOnes[_secondToOneAttr].ToList().ForEach(resource => { - Assert.Contains(resource, SecondToOnesResources); + Assert.Contains(resource, _secondToOnesResources); }); - toManies[ToManyAttr].ToList().ForEach(resource => + toManies[_toManyAttr].ToList().ForEach(resource => { - Assert.Contains(resource, ToManiesResources); + Assert.Contains(resource, _toManiesResources); }); Assert.Empty(notTargeted); } diff --git a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs deleted file mode 100644 index ef87eb4f4a..0000000000 --- a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs +++ /dev/null @@ -1,438 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Bogus; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Hooks.Internal.Traversal; -using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Repositories; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCoreExample.Data; -using JsonApiDotNetCoreExample.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Person = JsonApiDotNetCoreExample.Models.Person; - -namespace UnitTests.ResourceHooks -{ - public class HooksDummyData - { - protected IResourceGraph _resourceGraph; - protected ResourceHook[] NoHooks = new ResourceHook[0]; - protected ResourceHook[] EnableDbValues = { ResourceHook.BeforeUpdate, ResourceHook.BeforeUpdateRelationship }; - protected ResourceHook[] DisableDbValues = new ResourceHook[0]; - protected readonly Faker _personFaker; - protected readonly Faker _todoFaker; - protected readonly Faker _tagFaker; - protected readonly Faker
_articleFaker; - protected readonly Faker _articleTagFaker; - protected readonly Faker _identifiableArticleTagFaker; - protected readonly Faker _passportFaker; - - public HooksDummyData() - { - _resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) - .Add() - .Add() - .Add() - .Add
() - .Add() - .Add() - .Build(); - - _todoFaker = new Faker().Rules((f, i) => i.Id = f.UniqueIndex + 1); - _personFaker = new Faker().Rules((f, i) => i.Id = f.UniqueIndex + 1); - - _articleFaker = new Faker
().Rules((f, i) => i.Id = f.UniqueIndex + 1); - _articleTagFaker = new Faker(); - _identifiableArticleTagFaker = new Faker().Rules((f, i) => i.Id = f.UniqueIndex + 1); - _tagFaker = new Faker() - .Rules((f, i) => i.Id = f.UniqueIndex + 1); - - _passportFaker = new Faker() - .Rules((f, i) => i.Id = f.UniqueIndex + 1); - } - - protected List CreateTodoWithToOnePerson() - { - var todoItem = _todoFaker.Generate(); - var person = _personFaker.Generate(); - var todoList = new List { todoItem }; - person.OneToOneTodoItem = todoItem; - todoItem.OneToOnePerson = person; - return todoList; - } - - protected HashSet CreateTodoWithOwner() - { - var todoItem = _todoFaker.Generate(); - var person = _personFaker.Generate(); - var todoList = new HashSet { todoItem }; - person.AssignedTodoItems = todoList; - todoItem.Owner = person; - return todoList; - } - - protected (List
, List, List) CreateManyToManyData() - { - var tagsSubset = _tagFaker.Generate(3); - var joinsSubSet = _articleTagFaker.Generate(3); - var articleTagsSubset = _articleFaker.Generate(); - articleTagsSubset.ArticleTags = joinsSubSet.ToHashSet(); - for (int i = 0; i < 3; i++) - { - joinsSubSet[i].Article = articleTagsSubset; - joinsSubSet[i].Tag = tagsSubset[i]; - } - - var allTags = _tagFaker.Generate(3).Concat(tagsSubset).ToList(); - var completeJoin = _articleTagFaker.Generate(6); - - var articleWithAllTags = _articleFaker.Generate(); - articleWithAllTags.ArticleTags = completeJoin.ToHashSet(); - - for (int i = 0; i < 6; i++) - { - completeJoin[i].Article = articleWithAllTags; - completeJoin[i].Tag = allTags[i]; - } - - var allJoins = joinsSubSet.Concat(completeJoin).ToList(); - - var articles = new List
{ articleTagsSubset, articleWithAllTags }; - return (articles, allJoins, allTags); - } - - protected (List
, List, List) CreateIdentifiableManyToManyData() - { - var tagsSubset = _tagFaker.Generate(3); - var joinsSubSet = _identifiableArticleTagFaker.Generate(3); - var articleTagsSubset = _articleFaker.Generate(); - articleTagsSubset.IdentifiableArticleTags = joinsSubSet.ToHashSet(); - for (int i = 0; i < 3; i++) - { - joinsSubSet[i].Article = articleTagsSubset; - joinsSubSet[i].Tag = tagsSubset[i]; - } - var allTags = _tagFaker.Generate(3).Concat(tagsSubset).ToList(); - var completeJoin = _identifiableArticleTagFaker.Generate(6); - - var articleWithAllTags = _articleFaker.Generate(); - articleWithAllTags.IdentifiableArticleTags = joinsSubSet.ToHashSet(); - - for (int i = 0; i < 6; i++) - { - completeJoin[i].Article = articleWithAllTags; - completeJoin[i].Tag = allTags[i]; - } - - var allJoins = joinsSubSet.Concat(completeJoin).ToList(); - var articles = new List
{ articleTagsSubset, articleWithAllTags }; - return (articles, allJoins, allTags); - } - } - - public class HooksTestsSetup : HooksDummyData - { - private (Mock, Mock>, Mock, IJsonApiOptions) CreateMocks() - { - var pfMock = new Mock(); - var ufMock = new Mock(); - - var constraintsMock = new Mock>(); - constraintsMock.Setup(x => x.GetEnumerator()).Returns(new List(new IQueryConstraintProvider[0]).GetEnumerator()); - - var optionsMock = new JsonApiOptions { LoadDatabaseValues = false }; - return (ufMock, constraintsMock, pfMock, optionsMock); - } - - internal (Mock>, ResourceHookExecutor, Mock>) CreateTestObjects(IHooksDiscovery primaryDiscovery = null) - where TPrimary : class, IIdentifiable - { - // creates the resource definition mock and corresponding ImplementedHooks discovery instance - var primaryResource = CreateResourceDefinition(primaryDiscovery); - - // mocking the genericServiceFactory and JsonApiContext and wiring them up. - var (ufMock, constraintsMock, gpfMock, options) = CreateMocks(); - - SetupProcessorFactoryForResourceDefinition(gpfMock, primaryResource.Object, primaryDiscovery); - - var execHelper = new HookExecutorHelper(gpfMock.Object, _resourceGraph, options); - var traversalHelper = new TraversalHelper(_resourceGraph, ufMock.Object); - var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, _resourceGraph); - - return (constraintsMock, hookExecutor, primaryResource); - } - - protected (Mock>, Mock, IResourceHookExecutor, Mock>, Mock>) - CreateTestObjects( - IHooksDiscovery primaryDiscovery = null, - IHooksDiscovery secondaryDiscovery = null, - DbContextOptions repoDbContextOptions = null - ) - where TPrimary : class, IIdentifiable - where TSecondary : class, IIdentifiable - { - // creates the resource definition mock and corresponding for a given set of discoverable hooks - var primaryResource = CreateResourceDefinition(primaryDiscovery); - var secondaryResource = CreateResourceDefinition(secondaryDiscovery); - - // mocking the genericServiceFactory and JsonApiContext and wiring them up. - var (ufMock, constraintsMock, gpfMock, options) = CreateMocks(); - - var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions) : null; - - var resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) - .Add() - .Add() - .Build(); - - SetupProcessorFactoryForResourceDefinition(gpfMock, primaryResource.Object, primaryDiscovery, dbContext, resourceGraph); - SetupProcessorFactoryForResourceDefinition(gpfMock, secondaryResource.Object, secondaryDiscovery, dbContext, resourceGraph); - - var execHelper = new HookExecutorHelper(gpfMock.Object, _resourceGraph, options); - var traversalHelper = new TraversalHelper(_resourceGraph, ufMock.Object); - var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, _resourceGraph); - - return (constraintsMock, ufMock, hookExecutor, primaryResource, secondaryResource); - } - - protected (Mock>, IResourceHookExecutor, Mock>, Mock>, Mock>) - CreateTestObjects( - IHooksDiscovery primaryDiscovery = null, - IHooksDiscovery firstSecondaryDiscovery = null, - IHooksDiscovery secondSecondaryDiscovery = null, - DbContextOptions repoDbContextOptions = null - ) - where TPrimary : class, IIdentifiable - where TFirstSecondary : class, IIdentifiable - where TSecondSecondary : class, IIdentifiable - { - // creates the resource definition mock and corresponding for a given set of discoverable hooks - var primaryResource = CreateResourceDefinition(primaryDiscovery); - var firstSecondaryResource = CreateResourceDefinition(firstSecondaryDiscovery); - var secondSecondaryResource = CreateResourceDefinition(secondSecondaryDiscovery); - - // mocking the genericServiceFactory and JsonApiContext and wiring them up. - var (ufMock, constraintsMock, gpfMock, options) = CreateMocks(); - - var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions) : null; - - var resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) - .Add() - .Add() - .Add() - .Build(); - - SetupProcessorFactoryForResourceDefinition(gpfMock, primaryResource.Object, primaryDiscovery, dbContext, resourceGraph); - SetupProcessorFactoryForResourceDefinition(gpfMock, firstSecondaryResource.Object, firstSecondaryDiscovery, dbContext, resourceGraph); - SetupProcessorFactoryForResourceDefinition(gpfMock, secondSecondaryResource.Object, secondSecondaryDiscovery, dbContext, resourceGraph); - - var execHelper = new HookExecutorHelper(gpfMock.Object, _resourceGraph, options); - var traversalHelper = new TraversalHelper(_resourceGraph, ufMock.Object); - var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, _resourceGraph); - - return (constraintsMock, hookExecutor, primaryResource, firstSecondaryResource, secondSecondaryResource); - } - - protected IHooksDiscovery SetDiscoverableHooks(ResourceHook[] implementedHooks, params ResourceHook[] enableDbValuesHooks) - where TResource : class, IIdentifiable - { - var mock = new Mock>(); - mock.Setup(discovery => discovery.ImplementedHooks) - .Returns(implementedHooks); - - if (!enableDbValuesHooks.Any()) - { - mock.Setup(discovery => discovery.DatabaseValuesDisabledHooks) - .Returns(enableDbValuesHooks); - } - mock.Setup(discovery => discovery.DatabaseValuesEnabledHooks) - .Returns(new[] { ResourceHook.BeforeImplicitUpdateRelationship }.Concat(enableDbValuesHooks).ToArray()); - - return mock.Object; - } - - protected void VerifyNoOtherCalls(params dynamic[] resourceMocks) - { - foreach (var mock in resourceMocks) - { - mock.VerifyNoOtherCalls(); - } - } - - protected DbContextOptions InitInMemoryDb(Action seeder) - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: "repository_mock") - .Options; - - using (var context = new AppDbContext(options)) - { - seeder(context); - ResolveInverseRelationships(context); - } - return options; - } - - private void MockHooks(Mock> resourceDefinition) where TModel : class, IIdentifiable - { - resourceDefinition - .Setup(rd => rd.BeforeCreate(It.IsAny>(), It.IsAny())) - .Returns, ResourcePipeline>((resources, context) => resources) - .Verifiable(); - resourceDefinition - .Setup(rd => rd.BeforeRead(It.IsAny(), It.IsAny(), It.IsAny())) - .Verifiable(); - resourceDefinition - .Setup(rd => rd.BeforeUpdate(It.IsAny>(), It.IsAny())) - .Returns, ResourcePipeline>((resources, context) => resources) - .Verifiable(); - resourceDefinition - .Setup(rd => rd.BeforeDelete(It.IsAny>(), It.IsAny())) - .Returns, ResourcePipeline>((resources, context) => resources) - .Verifiable(); - resourceDefinition - .Setup(rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), It.IsAny())) - .Returns, IRelationshipsDictionary, ResourcePipeline>((ids, context, helper) => ids) - .Verifiable(); - resourceDefinition - .Setup(rd => rd.BeforeImplicitUpdateRelationship(It.IsAny>(), It.IsAny())) - .Verifiable(); - resourceDefinition - .Setup(rd => rd.OnReturn(It.IsAny>(), It.IsAny())) - .Returns, ResourcePipeline>((resources, context) => resources) - .Verifiable(); - resourceDefinition - .Setup(rd => rd.AfterCreate(It.IsAny>(), It.IsAny())) - .Verifiable(); - resourceDefinition - .Setup(rd => rd.AfterRead(It.IsAny>(), It.IsAny(), It.IsAny())) - .Verifiable(); - resourceDefinition - .Setup(rd => rd.AfterUpdate(It.IsAny>(), It.IsAny())) - .Verifiable(); - resourceDefinition - .Setup(rd => rd.AfterDelete(It.IsAny>(), It.IsAny(), It.IsAny())) - .Verifiable(); - } - - private void SetupProcessorFactoryForResourceDefinition( - Mock processorFactory, - IResourceHookContainer modelResource, - IHooksDiscovery discovery, - AppDbContext dbContext = null, - IResourceGraph resourceGraph = null - ) - where TModel : class, IIdentifiable - { - processorFactory.Setup(c => c.Get(typeof(ResourceHooksDefinition<>), typeof(TModel))) - .Returns(modelResource); - - processorFactory.Setup(c => c.Get(typeof(IHooksDiscovery<>), typeof(TModel))) - .Returns(discovery); - - if (dbContext != null) - { - var idType = TypeHelper.GetIdType(typeof(TModel)); - if (idType == typeof(int)) - { - IResourceReadRepository repo = CreateTestRepository(dbContext, resourceGraph); - processorFactory.Setup(c => c.Get>(typeof(IResourceReadRepository<,>), typeof(TModel), typeof(int))).Returns(repo); - } - else - { - throw new TypeLoadException("Test not set up properly"); - } - - } - } - - private IResourceReadRepository CreateTestRepository(AppDbContext dbContext, IResourceGraph resourceGraph) - where TModel : class, IIdentifiable - { - var serviceProvider = ((IInfrastructure) dbContext).Instance; - var resourceFactory = new ResourceFactory(serviceProvider); - IDbContextResolver resolver = CreateTestDbResolver(dbContext); - var targetedFields = new TargetedFields(); - - return new EntityFrameworkCoreRepository(targetedFields, resolver, resourceGraph, resourceFactory, - new List(), NullLoggerFactory.Instance); - } - - private IDbContextResolver CreateTestDbResolver(AppDbContext dbContext) - { - var mock = new Mock(); - mock.Setup(r => r.GetContext()).Returns(dbContext); - return mock.Object; - } - - private void ResolveInverseRelationships(AppDbContext context) - { - var dbContextResolvers = new[] {new DbContextResolver(context)}; - var inverseRelationships = new InverseNavigationResolver(_resourceGraph, dbContextResolvers); - inverseRelationships.Resolve(); - } - - private Mock> CreateResourceDefinition - (IHooksDiscovery discovery - ) - where TModel : class, IIdentifiable - { - var resourceDefinition = new Mock>(); - MockHooks(resourceDefinition); - return resourceDefinition; - } - - protected List> GetIncludedRelationshipsChains(params string[] chains) - { - var parsedChains = new List>(); - - foreach (var chain in chains) - parsedChains.Add(GetIncludedRelationshipsChain(chain)); - - return parsedChains; - } - - protected List GetIncludedRelationshipsChain(string chain) - { - var parsedChain = new List(); - var resourceContext = _resourceGraph.GetResourceContext(); - var splitPath = chain.Split('.'); - foreach (var requestedRelationship in splitPath) - { - var relationship = resourceContext.Relationships.Single(r => r.PublicName == requestedRelationship); - parsedChain.Add(relationship); - resourceContext = _resourceGraph.GetResourceContext(relationship.RightType); - } - return parsedChain; - } - - protected IEnumerable ConvertInclusionChains(List> inclusionChains) - { - var expressionsInScope = new List(); - - if (inclusionChains != null) - { - var chains = inclusionChains.Select(relationships => new ResourceFieldChainExpression(relationships)).ToList(); - var includeExpression = IncludeChainConverter.FromRelationshipChains(chains); - expressionsInScope.Add(new ExpressionInScope(null, includeExpression)); - } - - var mock = new Mock(); - mock.Setup(x => x.GetConstraints()).Returns(expressionsInScope); - - IQueryConstraintProvider includeConstraintProvider = mock.Object; - return new List {includeConstraintProvider}; - } - - } -} diff --git a/test/UnitTests/ResourceHooks/ToMany.cs b/test/UnitTests/ResourceHooks/ToMany.cs new file mode 100644 index 0000000000..b77442f911 --- /dev/null +++ b/test/UnitTests/ResourceHooks/ToMany.cs @@ -0,0 +1,6 @@ +using JsonApiDotNetCore.Resources; + +namespace UnitTests.ResourceHooks +{ + public sealed class ToMany : Identifiable { } +} \ No newline at end of file diff --git a/test/UnitTests/ResourceHooks/ToOne.cs b/test/UnitTests/ResourceHooks/ToOne.cs new file mode 100644 index 0000000000..06f085aa4b --- /dev/null +++ b/test/UnitTests/ResourceHooks/ToOne.cs @@ -0,0 +1,6 @@ +using JsonApiDotNetCore.Resources; + +namespace UnitTests.ResourceHooks +{ + public sealed class ToOne : Identifiable { } +} \ No newline at end of file diff --git a/test/UnitTests/Serialization/Client/RequestSerializerTests.cs b/test/UnitTests/Serialization/Client/RequestSerializerTests.cs index e5a2d696ce..449338a508 100644 --- a/test/UnitTests/Serialization/Client/RequestSerializerTests.cs +++ b/test/UnitTests/Serialization/Client/RequestSerializerTests.cs @@ -14,8 +14,8 @@ public sealed class RequestSerializerTests : SerializerTestsSetup public RequestSerializerTests() { - var builder = new ResourceObjectBuilder(_resourceGraph, new ResourceObjectBuilderSettings()); - _serializer = new RequestSerializer(_resourceGraph, builder); + var builder = new ResourceObjectBuilder(ResourceGraph, new ResourceObjectBuilderSettings()); + _serializer = new RequestSerializer(ResourceGraph, builder); } [Fact] @@ -28,8 +28,7 @@ public void SerializeSingle_ResourceWithDefaultTargetFields_CanBuild() string serialized = _serializer.Serialize(resource); // Assert - var expectedFormatted = - @"{ + const string expectedFormatted = @"{ ""data"":{ ""type"":""testResource"", ""id"":""1"", @@ -53,14 +52,13 @@ public void SerializeSingle_ResourceWithTargetedSetAttributes_CanBuild() { // Arrange var resource = new TestResource { Id = 1, StringField = "value", NullableIntField = 123 }; - _serializer.AttributesToSerialize = _resourceGraph.GetAttributes(tr => tr.StringField); + _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); // Act string serialized = _serializer.Serialize(resource); // Assert - var expectedFormatted = - @"{ + const string expectedFormatted = @"{ ""data"":{ ""type"":""testResource"", ""id"":""1"", @@ -78,14 +76,13 @@ public void SerializeSingle_NoIdWithTargetedSetAttributes_CanBuild() { // Arrange var resourceNoId = new TestResource { Id = 0, StringField = "value", NullableIntField = 123 }; - _serializer.AttributesToSerialize = _resourceGraph.GetAttributes(tr => tr.StringField); + _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); // Act string serialized = _serializer.Serialize(resourceNoId); // Assert - var expectedFormatted = - @"{ + const string expectedFormatted = @"{ ""data"":{ ""type"":""testResource"", ""attributes"":{ @@ -103,14 +100,13 @@ public void SerializeSingle_ResourceWithoutTargetedAttributes_CanBuild() { // Arrange var resource = new TestResource { Id = 1, StringField = "value", NullableIntField = 123 }; - _serializer.AttributesToSerialize = _resourceGraph.GetAttributes(tr => new { }); + _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(_ => new { }); // Act string serialized = _serializer.Serialize(resource); // Assert - var expectedFormatted = - @"{ + const string expectedFormatted = @"{ ""data"":{ ""type"":""testResource"", ""id"":""1"" @@ -130,13 +126,12 @@ public void SerializeSingle_ResourceWithTargetedRelationships_CanBuild() PopulatedToOne = new OneToOneDependent { Id = 10 }, PopulatedToManies = new HashSet { new OneToManyDependent { Id = 20 } } }; - _serializer.RelationshipsToSerialize = _resourceGraph.GetRelationships(tr => new { tr.EmptyToOne, tr.EmptyToManies, tr.PopulatedToOne, tr.PopulatedToManies }); + _serializer.RelationshipsToSerialize = ResourceGraph.GetRelationships(tr => new { tr.EmptyToOne, tr.EmptyToManies, tr.PopulatedToOne, tr.PopulatedToManies }); // Act string serialized = _serializer.Serialize(resourceWithRelationships); // Assert - var expectedFormatted = - @"{ + const string expectedFormatted = @"{ ""data"":{ ""type"":""multiPrincipals"", ""attributes"":{ @@ -179,14 +174,13 @@ public void SerializeMany_ResourcesWithTargetedAttributes_CanBuild() new TestResource { Id = 1, StringField = "value1", NullableIntField = 123 }, new TestResource { Id = 2, StringField = "value2", NullableIntField = 123 } }; - _serializer.AttributesToSerialize = _resourceGraph.GetAttributes(tr => tr.StringField); + _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); // Act string serialized = _serializer.Serialize(resources); // Assert - var expectedFormatted = - @"{ + const string expectedFormatted = @"{ ""data"":[ { ""type"":""testResource"", @@ -212,14 +206,13 @@ public void SerializeMany_ResourcesWithTargetedAttributes_CanBuild() public void SerializeSingle_Null_CanBuild() { // Arrange - _serializer.AttributesToSerialize = _resourceGraph.GetAttributes(tr => tr.StringField); + _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); // Act string serialized = _serializer.Serialize((IIdentifiable) null); // Assert - var expectedFormatted = - @"{ + const string expectedFormatted = @"{ ""data"":null }"; var expected = Regex.Replace(expectedFormatted, @"\s+", ""); @@ -230,15 +223,13 @@ public void SerializeSingle_Null_CanBuild() public void SerializeMany_EmptyList_CanBuild() { // Arrange - var resources = new List(); - _serializer.AttributesToSerialize = _resourceGraph.GetAttributes(tr => tr.StringField); + _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); // Act - string serialized = _serializer.Serialize(resources); + string serialized = _serializer.Serialize(new List()); // Assert - var expectedFormatted = - @"{ + const string expectedFormatted = @"{ ""data"":[] }"; var expected = Regex.Replace(expectedFormatted, @"\s+", ""); diff --git a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs index bfe862d0ca..7573a6ff9a 100644 --- a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs +++ b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs @@ -17,7 +17,7 @@ public sealed class ResponseDeserializerTests : DeserializerTestsSetup public ResponseDeserializerTests() { - _deserializer = new ResponseDeserializer(_resourceGraph, new ResourceFactory(new ServiceContainer())); + _deserializer = new ResponseDeserializer(ResourceGraph, new ResourceFactory(new ServiceContainer())); _linkValues.Add("self", "http://example.com/articles"); _linkValues.Add("next", "http://example.com/articles?page[number]=2"); _linkValues.Add("last", "http://example.com/articles?page[number]=10"); @@ -29,7 +29,7 @@ public void DeserializeSingle_EmptyResponseWithMeta_CanDeserialize() // Arrange var content = new Document { - Meta = new Dictionary { { "foo", "bar" } } + Meta = new Dictionary { ["foo"] = "bar" } }; var body = JsonConvert.SerializeObject(content); @@ -114,21 +114,21 @@ public void DeserializeSingle_MultipleDependentRelationshipsWithIncluded_CanDese content.SingleData.Relationships.Add("populatedToManies", CreateRelationshipData("oneToManyDependents", isToManyData: true)); content.SingleData.Relationships.Add("emptyToOne", CreateRelationshipData()); content.SingleData.Relationships.Add("emptyToManies", CreateRelationshipData(isToManyData: true)); - var toOneAttributeValue = "populatedToOne member content"; - var toManyAttributeValue = "populatedToManies member content"; + const string toOneAttributeValue = "populatedToOne member content"; + const string toManyAttributeValue = "populatedToManies member content"; content.Included = new List { new ResourceObject { Type = "oneToOneDependents", Id = "10", - Attributes = new Dictionary { {"attributeMember", toOneAttributeValue } } + Attributes = new Dictionary { ["attributeMember"] = toOneAttributeValue } }, new ResourceObject { Type = "oneToManyDependents", Id = "10", - Attributes = new Dictionary { {"attributeMember", toManyAttributeValue } } + Attributes = new Dictionary { ["attributeMember"] = toManyAttributeValue } } }; var body = JsonConvert.SerializeObject(content); @@ -157,21 +157,21 @@ public void DeserializeSingle_MultiplePrincipalRelationshipsWithIncluded_CanDese content.SingleData.Relationships.Add("populatedToMany", CreateRelationshipData("oneToManyPrincipals")); content.SingleData.Relationships.Add("emptyToOne", CreateRelationshipData()); content.SingleData.Relationships.Add("emptyToMany", CreateRelationshipData()); - var toOneAttributeValue = "populatedToOne member content"; - var toManyAttributeValue = "populatedToManies member content"; + const string toOneAttributeValue = "populatedToOne member content"; + const string toManyAttributeValue = "populatedToManies member content"; content.Included = new List { new ResourceObject { Type = "oneToOnePrincipals", Id = "10", - Attributes = new Dictionary { {"attributeMember", toOneAttributeValue } } + Attributes = new Dictionary { ["attributeMember"] = toOneAttributeValue } }, new ResourceObject { Type = "oneToManyPrincipals", Id = "10", - Attributes = new Dictionary { {"attributeMember", toManyAttributeValue } } + Attributes = new Dictionary { ["attributeMember"] = toManyAttributeValue } } }; var body = JsonConvert.SerializeObject(content); @@ -196,22 +196,22 @@ public void DeserializeSingle_NestedIncluded_CanDeserialize() // Arrange var content = CreateDocumentWithRelationships("multiPrincipals"); content.SingleData.Relationships.Add("populatedToManies", CreateRelationshipData("oneToManyDependents", isToManyData: true)); - var toManyAttributeValue = "populatedToManies member content"; - var nestedIncludeAttributeValue = "nested include member content"; + const string toManyAttributeValue = "populatedToManies member content"; + const string nestedIncludeAttributeValue = "nested include member content"; content.Included = new List { new ResourceObject { Type = "oneToManyDependents", Id = "10", - Attributes = new Dictionary { {"attributeMember", toManyAttributeValue } }, - Relationships = new Dictionary { { "principal", CreateRelationshipData("oneToManyPrincipals") } } + Attributes = new Dictionary { ["attributeMember"] = toManyAttributeValue }, + Relationships = new Dictionary { ["principal"] = CreateRelationshipData("oneToManyPrincipals") } }, new ResourceObject { Type = "oneToManyPrincipals", Id = "10", - Attributes = new Dictionary { {"attributeMember", nestedIncludeAttributeValue } } + Attributes = new Dictionary { ["attributeMember"] = nestedIncludeAttributeValue } } }; var body = JsonConvert.SerializeObject(content); @@ -238,30 +238,30 @@ public void DeserializeSingle_DeeplyNestedIncluded_CanDeserialize() // Arrange var content = CreateDocumentWithRelationships("multiPrincipals"); content.SingleData.Relationships.Add("multi", CreateRelationshipData("multiPrincipals")); - var includedAttributeValue = "multi member content"; - var nestedIncludedAttributeValue = "nested include member content"; - var deeplyNestedIncludedAttributeValue = "deeply nested member content"; + const string includedAttributeValue = "multi member content"; + const string nestedIncludedAttributeValue = "nested include member content"; + const string deeplyNestedIncludedAttributeValue = "deeply nested member content"; content.Included = new List { new ResourceObject { Type = "multiPrincipals", Id = "10", - Attributes = new Dictionary { {"attributeMember", includedAttributeValue } }, - Relationships = new Dictionary { { "populatedToManies", CreateRelationshipData("oneToManyDependents", isToManyData: true) } } + Attributes = new Dictionary { ["attributeMember"] = includedAttributeValue }, + Relationships = new Dictionary { ["populatedToManies"] = CreateRelationshipData("oneToManyDependents", isToManyData: true) } }, new ResourceObject { Type = "oneToManyDependents", Id = "10", - Attributes = new Dictionary { {"attributeMember", nestedIncludedAttributeValue } }, - Relationships = new Dictionary { { "principal", CreateRelationshipData("oneToManyPrincipals") } } + Attributes = new Dictionary { ["attributeMember"] = nestedIncludedAttributeValue }, + Relationships = new Dictionary { ["principal"] = CreateRelationshipData("oneToManyPrincipals") } }, new ResourceObject { Type = "oneToManyPrincipals", Id = "10", - Attributes = new Dictionary { {"attributeMember", deeplyNestedIncludedAttributeValue } } + Attributes = new Dictionary { ["attributeMember"] = deeplyNestedIncludedAttributeValue } } }; var body = JsonConvert.SerializeObject(content); @@ -289,30 +289,30 @@ public void DeserializeList_DeeplyNestedIncluded_CanDeserialize() // Arrange var content = new Document { Data = new List { CreateDocumentWithRelationships("multiPrincipals").SingleData } }; content.ManyData[0].Relationships.Add("multi", CreateRelationshipData("multiPrincipals")); - var includedAttributeValue = "multi member content"; - var nestedIncludedAttributeValue = "nested include member content"; - var deeplyNestedIncludedAttributeValue = "deeply nested member content"; + const string includedAttributeValue = "multi member content"; + const string nestedIncludedAttributeValue = "nested include member content"; + const string deeplyNestedIncludedAttributeValue = "deeply nested member content"; content.Included = new List { new ResourceObject { Type = "multiPrincipals", Id = "10", - Attributes = new Dictionary { {"attributeMember", includedAttributeValue } }, - Relationships = new Dictionary { { "populatedToManies", CreateRelationshipData("oneToManyDependents", isToManyData: true) } } + Attributes = new Dictionary { ["attributeMember"] = includedAttributeValue }, + Relationships = new Dictionary { ["populatedToManies"] = CreateRelationshipData("oneToManyDependents", isToManyData: true) } }, new ResourceObject { Type = "oneToManyDependents", Id = "10", - Attributes = new Dictionary { {"attributeMember", nestedIncludedAttributeValue } }, - Relationships = new Dictionary { { "principal", CreateRelationshipData("oneToManyPrincipals") } } + Attributes = new Dictionary { ["attributeMember"] = nestedIncludedAttributeValue }, + Relationships = new Dictionary { ["principal"] = CreateRelationshipData("oneToManyPrincipals") } }, new ResourceObject { Type = "oneToManyPrincipals", Id = "10", - Attributes = new Dictionary { {"attributeMember", deeplyNestedIncludedAttributeValue } } + Attributes = new Dictionary { ["attributeMember"] = deeplyNestedIncludedAttributeValue } } }; var body = JsonConvert.SerializeObject(content); @@ -348,19 +348,19 @@ public void DeserializeSingle_ResourceWithInheritanceAndInclusions_CanDeserializ { Type = "firstDerivedModels", Id = "10", - Attributes = new Dictionary { { "firstProperty", "true" } } + Attributes = new Dictionary { ["firstProperty"] = "true" } }, new ResourceObject { Type = "secondDerivedModels", Id = "11", - Attributes = new Dictionary { { "secondProperty", "false" } } + Attributes = new Dictionary { ["secondProperty"] = "false" } }, new ResourceObject { Type = "firstDerivedModels", Id = "20", - Attributes = new Dictionary { { "firstProperty", "true" } } + Attributes = new Dictionary { ["firstProperty"] = "true" } } }; var body = JsonConvert.SerializeObject(content); diff --git a/test/UnitTests/Serialization/Common/DocumentBuilderTests.cs b/test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs similarity index 78% rename from test/UnitTests/Serialization/Common/DocumentBuilderTests.cs rename to test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs index 1023998c15..9e21fc2f62 100644 --- a/test/UnitTests/Serialization/Common/DocumentBuilderTests.cs +++ b/test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using JsonApiDotNetCore; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; @@ -7,7 +8,7 @@ using UnitTests.TestModels; using Xunit; -namespace UnitTests.Serialization.Serializer +namespace UnitTests.Serialization.Common { public sealed class BaseDocumentBuilderTests : SerializerTestsSetup { @@ -25,7 +26,7 @@ public BaseDocumentBuilderTests() public void ResourceToDocument_NullResource_CanBuild() { // Act - var document = _builder.Build((TestResource) null); + var document = _builder.PublicBuild((TestResource) null); // Assert Assert.Null(document.Data); @@ -36,11 +37,8 @@ public void ResourceToDocument_NullResource_CanBuild() [Fact] public void ResourceToDocument_EmptyList_CanBuild() { - // Arrange - var resources = new List(); - // Act - var document = _builder.Build(resources); + var document = _builder.PublicBuild(new List()); // Assert Assert.NotNull(document.Data); @@ -55,7 +53,7 @@ public void ResourceToDocument_SingleResource_CanBuild() IIdentifiable dummy = new DummyResource(); // Act - var document = _builder.Build(dummy); + var document = _builder.PublicBuild(dummy); // Assert Assert.NotNull(document.Data); @@ -66,17 +64,17 @@ public void ResourceToDocument_SingleResource_CanBuild() public void ResourceToDocument_ResourceList_CanBuild() { // Arrange - var resources = new List { new DummyResource(), new DummyResource() }; + var resources = ArrayFactory.Create(new DummyResource(), new DummyResource()); // Act - var document = _builder.Build(resources); + var document = _builder.PublicBuild(resources); var data = (List)document.Data; // Assert Assert.Equal(2, data.Count); } - public sealed class DummyResource : Identifiable + private sealed class DummyResource : Identifiable { } } diff --git a/test/UnitTests/Serialization/Common/DocumentParserTests.cs b/test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs similarity index 94% rename from test/UnitTests/Serialization/Common/DocumentParserTests.cs rename to test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs index 2d02c60c51..7933daaa6a 100644 --- a/test/UnitTests/Serialization/Common/DocumentParserTests.cs +++ b/test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs @@ -9,7 +9,7 @@ using UnitTests.TestModels; using Xunit; -namespace UnitTests.Serialization.Deserializer +namespace UnitTests.Serialization.Common { public sealed class BaseDocumentParserTests : DeserializerTestsSetup { @@ -17,7 +17,7 @@ public sealed class BaseDocumentParserTests : DeserializerTestsSetup public BaseDocumentParserTests() { - _deserializer = new TestDeserializer(_resourceGraph, new ResourceFactory(new ServiceContainer())); + _deserializer = new TestDeserializer(ResourceGraph, new ResourceFactory(new ServiceContainer())); } [Fact] @@ -116,7 +116,7 @@ public void DeserializeAttributes_VariousDataTypes_CanDeserialize(string member, Id = "1", Attributes = new Dictionary { - { member, value } + [member] = value } } }; @@ -133,7 +133,7 @@ public void DeserializeAttributes_VariousDataTypes_CanDeserialize(string member, var resource = (TestResource)_deserializer.Deserialize(body); // Assert - var pi = _resourceGraph.GetResourceContext("testResource").Attributes.Single(attr => attr.PublicName == member).Property; + var pi = ResourceGraph.GetResourceContext("testResource").Attributes.Single(attr => attr.PublicName == member).Property; var deserializedValue = pi.GetValue(resource); if (member == "intField") @@ -174,7 +174,10 @@ public void DeserializeAttributes_ComplexType_CanDeserialize() Id = "1", Attributes = new Dictionary { - { "complexField", new Dictionary { {"compoundName", "testName" } } } // this is not right + ["complexField"] = new Dictionary + { + ["compoundName"] = "testName" + } } } }; @@ -200,7 +203,13 @@ public void DeserializeAttributes_ComplexListType_CanDeserialize() Id = "1", Attributes = new Dictionary { - { "complexFields", new [] { new Dictionary { {"compoundName", "testName" } } } } + ["complexFields"] = new[] + { + new Dictionary + { + ["compoundName"] = "testName" + } + } } } }; diff --git a/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs index a6d06382af..95168a31d6 100644 --- a/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs @@ -6,7 +6,7 @@ using UnitTests.TestModels; using Xunit; -namespace UnitTests.Serialization.Serializer +namespace UnitTests.Serialization.Common { public sealed class ResourceObjectBuilderTests : SerializerTestsSetup { @@ -14,7 +14,7 @@ public sealed class ResourceObjectBuilderTests : SerializerTestsSetup public ResourceObjectBuilderTests() { - _builder = new ResourceObjectBuilder(_resourceGraph, new ResourceObjectBuilderSettings()); + _builder = new ResourceObjectBuilder(ResourceGraph, new ResourceObjectBuilderSettings()); } [Fact] @@ -56,7 +56,7 @@ public void ResourceToResourceObject_ResourceWithIncludedAttrs_CanBuild(string s { // Arrange var resource = new TestResource { StringField = stringFieldValue, NullableIntField = intFieldValue }; - var attrs = _resourceGraph.GetAttributes(tr => new { tr.StringField, tr.NullableIntField }); + var attrs = ResourceGraph.GetAttributes(tr => new { tr.StringField, tr.NullableIntField }); // Act var resourceObject = _builder.Build(resource, attrs); @@ -112,7 +112,7 @@ public void ResourceWithRelationshipsToResourceObject_WithIncludedRelationshipsA PopulatedToOne = new OneToOneDependent { Id = 10 }, PopulatedToManies = new HashSet { new OneToManyDependent { Id = 20 } } }; - var relationships = _resourceGraph.GetRelationships(tr => new { tr.PopulatedToManies, tr.PopulatedToOne, tr.EmptyToOne, tr.EmptyToManies }); + var relationships = ResourceGraph.GetRelationships(tr => new { tr.PopulatedToManies, tr.PopulatedToOne, tr.EmptyToOne, tr.EmptyToManies }); // Act var resourceObject = _builder.Build(resource, relationships: relationships); @@ -136,7 +136,7 @@ public void ResourceWithRelationshipsToResourceObject_DeviatingForeignKeyWhileRe { // Arrange var resource = new OneToOneDependent { Principal = new OneToOnePrincipal { Id = 10 }, PrincipalId = 123 }; - var relationships = _resourceGraph.GetRelationships(tr => tr.Principal); + var relationships = ResourceGraph.GetRelationships(tr => tr.Principal); // Act var resourceObject = _builder.Build(resource, relationships: relationships); @@ -153,7 +153,7 @@ public void ResourceWithRelationshipsToResourceObject_DeviatingForeignKeyAndNoNa { // Arrange var resource = new OneToOneDependent { Principal = null, PrincipalId = 123 }; - var relationships = _resourceGraph.GetRelationships(tr => tr.Principal); + var relationships = ResourceGraph.GetRelationships(tr => tr.Principal); // Act var resourceObject = _builder.Build(resource, relationships: relationships); @@ -167,7 +167,7 @@ public void ResourceWithRequiredRelationshipsToResourceObject_DeviatingForeignKe { // Arrange var resource = new OneToOneRequiredDependent { Principal = new OneToOnePrincipal { Id = 10 }, PrincipalId = 123 }; - var relationships = _resourceGraph.GetRelationships(tr => tr.Principal); + var relationships = ResourceGraph.GetRelationships(tr => tr.Principal); // Act var resourceObject = _builder.Build(resource, relationships: relationships); diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs index 2f8d4a053f..f22f9406a5 100644 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -11,12 +12,12 @@ namespace UnitTests.Serialization { public class DeserializerTestsSetup : SerializationTestsSetupBase { - public Mock _mockHttpContextAccessor; + protected Mock MockHttpContextAccessor { get; } - public DeserializerTestsSetup() + protected DeserializerTestsSetup() { - _mockHttpContextAccessor = new Mock(); - _mockHttpContextAccessor.Setup(mock => mock.HttpContext).Returns(new DefaultHttpContext()); + MockHttpContextAccessor = new Mock(); + MockHttpContextAccessor.Setup(mock => mock.HttpContext).Returns(new DefaultHttpContext()); } protected sealed class TestDeserializer : BaseDeserializer { @@ -52,19 +53,18 @@ protected Document CreateDocumentWithRelationships(string primaryType) protected RelationshipEntry CreateRelationshipData(string relatedType = null, bool isToManyData = false, string id = "10") { - var data = new RelationshipEntry(); + var entry = new RelationshipEntry(); var rio = relatedType == null ? null : new ResourceIdentifierObject { Id = id, Type = relatedType }; if (isToManyData) { - data.Data = new List(); - if (relatedType != null) ((List)data.Data).Add(rio); + entry.Data = relatedType != null ? rio.AsList() : new List(); } else { - data.Data = rio; + entry.Data = rio; } - return data; + return entry; } protected Document CreateTestResourceDocument() @@ -77,11 +77,11 @@ protected Document CreateTestResourceDocument() Id = "1", Attributes = new Dictionary { - { "stringField", "some string" }, - { "intField", 1 }, - { "nullableIntField", null }, - { "guidField", "1a68be43-cc84-4924-a421-7f4d614b7781" }, - { "dateTimeField", "9/11/2019 11:41:40 AM" } + ["stringField"] = "some string", + ["intField"] = 1, + ["nullableIntField"] = null, + ["guidField"] = "1a68be43-cc84-4924-a421-7f4d614b7781", + ["dateTimeField"] = "9/11/2019 11:41:40 AM" } } }; diff --git a/test/UnitTests/Serialization/SerializationTestsSetupBase.cs b/test/UnitTests/Serialization/SerializationTestsSetupBase.cs index 03685f31dc..5b20e8764c 100644 --- a/test/UnitTests/Serialization/SerializationTestsSetupBase.cs +++ b/test/UnitTests/Serialization/SerializationTestsSetupBase.cs @@ -8,34 +8,45 @@ namespace UnitTests.Serialization { public class SerializationTestsSetupBase { - protected IResourceGraph _resourceGraph; - protected readonly Faker _foodFaker; - protected readonly Faker _songFaker; - protected readonly Faker
_articleFaker; - protected readonly Faker _blogFaker; - protected readonly Faker _personFaker; + protected IResourceGraph ResourceGraph { get; } + protected Faker FoodFaker { get; } + protected Faker SongFaker { get; } + protected Faker
ArticleFaker { get; } + protected Faker BlogFaker { get; } + protected Faker PersonFaker { get; } - public SerializationTestsSetupBase() + protected SerializationTestsSetupBase() { - _resourceGraph = BuildGraph(); - _articleFaker = new Faker
() - .RuleFor(f => f.Title, f => f.Hacker.Phrase()) - .RuleFor(f => f.Id, f => f.UniqueIndex + 1); - _personFaker = new Faker() - .RuleFor(f => f.Name, f => f.Person.FullName) - .RuleFor(f => f.Id, f => f.UniqueIndex + 1); - _blogFaker = new Faker() - .RuleFor(f => f.Title, f => f.Hacker.Phrase()) - .RuleFor(f => f.Id, f => f.UniqueIndex + 1); - _songFaker = new Faker() - .RuleFor(f => f.Title, f => f.Lorem.Sentence()) - .RuleFor(f => f.Id, f => f.UniqueIndex + 1); - _foodFaker = new Faker() - .RuleFor(f => f.Dish, f => f.Lorem.Sentence()) - .RuleFor(f => f.Id, f => f.UniqueIndex + 1); + ResourceGraph = BuildGraph(); + + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + + ArticleFaker = new Faker
() + .RuleFor(f => f.Title, f => f.Hacker.Phrase()) + .RuleFor(f => f.Id, f => f.UniqueIndex + 1); + + PersonFaker = new Faker() + .RuleFor(f => f.Name, f => f.Person.FullName) + .RuleFor(f => f.Id, f => f.UniqueIndex + 1); + + BlogFaker = new Faker() + .RuleFor(f => f.Title, f => f.Hacker.Phrase()) + .RuleFor(f => f.Id, f => f.UniqueIndex + 1); + + SongFaker = new Faker() + .RuleFor(f => f.Title, f => f.Lorem.Sentence()) + .RuleFor(f => f.Id, f => f.UniqueIndex + 1); + + FoodFaker = new Faker() + .RuleFor(f => f.Dish, f => f.Lorem.Sentence()) + .RuleFor(f => f.Id, f => f.UniqueIndex + 1); + + // @formatter:wrap_chained_method_calls restore + // @formatter:keep_existing_linebreaks restore } - protected IResourceGraph BuildGraph() + private IResourceGraph BuildGraph() { var resourceGraphBuilder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); resourceGraphBuilder.Add("testResource"); @@ -62,8 +73,8 @@ protected IResourceGraph BuildGraph() resourceGraphBuilder.Add(); resourceGraphBuilder.Add(); resourceGraphBuilder.Add(); - + return resourceGraphBuilder.Build(); } - } + } } diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs index 7251e2fbe1..d856f48d81 100644 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; @@ -15,12 +16,13 @@ namespace UnitTests.Serialization { public class SerializerTestsSetup : SerializationTestsSetupBase { - protected readonly TopLevelLinks _dummyTopLevelLinks; - protected readonly ResourceLinks _dummyResourceLinks; - protected readonly RelationshipLinks _dummyRelationshipLinks; - public SerializerTestsSetup() + protected readonly TopLevelLinks DummyTopLevelLinks; + protected readonly ResourceLinks DummyResourceLinks; + protected readonly RelationshipLinks DummyRelationshipLinks; + + protected SerializerTestsSetup() { - _dummyTopLevelLinks = new TopLevelLinks + DummyTopLevelLinks = new TopLevelLinks { Self = "http://www.dummy.com/dummy-self-link", Next = "http://www.dummy.com/dummy-next-link", @@ -28,39 +30,39 @@ public SerializerTestsSetup() First = "http://www.dummy.com/dummy-first-link", Last = "http://www.dummy.com/dummy-last-link" }; - _dummyResourceLinks = new ResourceLinks + DummyResourceLinks = new ResourceLinks { Self = "http://www.dummy.com/dummy-resource-self-link" }; - _dummyRelationshipLinks = new RelationshipLinks + DummyRelationshipLinks = new RelationshipLinks { Related = "http://www.dummy.com/dummy-relationship-related-link", Self = "http://www.dummy.com/dummy-relationship-self-link" }; } - protected ResponseSerializer GetResponseSerializer(List> inclusionChains = null, Dictionary metaDict = null, TopLevelLinks topLinks = null, ResourceLinks resourceLinks = null, RelationshipLinks relationshipLinks = null) where T : class, IIdentifiable + protected ResponseSerializer GetResponseSerializer(IEnumerable> inclusionChains = null, Dictionary metaDict = null, TopLevelLinks topLinks = null, ResourceLinks resourceLinks = null, RelationshipLinks relationshipLinks = null) where T : class, IIdentifiable { var meta = GetMetaBuilder(metaDict); var link = GetLinkBuilder(topLinks, resourceLinks, relationshipLinks); var includeConstraints = GetIncludeConstraints(inclusionChains); var includedBuilder = GetIncludedBuilder(); var fieldsToSerialize = GetSerializableFields(); - ResponseResourceObjectBuilder resourceObjectBuilder = new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, _resourceGraph, GetResourceDefinitionAccessor(), GetSerializerSettingsProvider()); + ResponseResourceObjectBuilder resourceObjectBuilder = new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, GetResourceDefinitionAccessor(), GetSerializerSettingsProvider()); return new ResponseSerializer(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, new JsonApiOptions()); } - protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(List> inclusionChains = null, ResourceLinks resourceLinks = null, RelationshipLinks relationshipLinks = null) + protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(IEnumerable> inclusionChains = null, ResourceLinks resourceLinks = null, RelationshipLinks relationshipLinks = null) { var link = GetLinkBuilder(null, resourceLinks, relationshipLinks); var includeConstraints = GetIncludeConstraints(inclusionChains); var includedBuilder = GetIncludedBuilder(); - return new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, _resourceGraph, GetResourceDefinitionAccessor(), GetSerializerSettingsProvider()); + return new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, GetResourceDefinitionAccessor(), GetSerializerSettingsProvider()); } private IIncludedResourceObjectBuilder GetIncludedBuilder() { - return new IncludedResourceObjectBuilder(GetSerializableFields(), GetLinkBuilder(), _resourceGraph, Enumerable.Empty(), GetResourceDefinitionAccessor(), GetSerializerSettingsProvider()); + return new IncludedResourceObjectBuilder(GetSerializableFields(), GetLinkBuilder(), ResourceGraph, Enumerable.Empty(), GetResourceDefinitionAccessor(), GetSerializerSettingsProvider()); } protected IResourceObjectBuilderSettingsProvider GetSerializerSettingsProvider() @@ -70,13 +72,13 @@ protected IResourceObjectBuilderSettingsProvider GetSerializerSettingsProvider() return mock.Object; } - protected IResourceDefinitionAccessor GetResourceDefinitionAccessor() + private IResourceDefinitionAccessor GetResourceDefinitionAccessor() { var mock = new Mock(); return mock.Object; } - protected IMetaBuilder GetMetaBuilder(Dictionary meta = null) + private IMetaBuilder GetMetaBuilder(Dictionary meta = null) { var mock = new Mock(); mock.Setup(m => m.Build()).Returns(meta); @@ -95,18 +97,18 @@ protected ILinkBuilder GetLinkBuilder(TopLevelLinks top = null, ResourceLinks re protected IFieldsToSerialize GetSerializableFields() { var mock = new Mock(); - mock.Setup(m => m.GetAttributes(It.IsAny())).Returns(t => _resourceGraph.GetResourceContext(t).Attributes); - mock.Setup(m => m.GetRelationships(It.IsAny())).Returns(t => _resourceGraph.GetResourceContext(t).Relationships); + mock.Setup(m => m.GetAttributes(It.IsAny())).Returns(t => ResourceGraph.GetResourceContext(t).Attributes); + mock.Setup(m => m.GetRelationships(It.IsAny())).Returns(t => ResourceGraph.GetResourceContext(t).Relationships); return mock.Object; } - protected IEnumerable GetIncludeConstraints(List> inclusionChains = null) + private IEnumerable GetIncludeConstraints(IEnumerable> inclusionChains = null) { var expressionsInScope = new List(); if (inclusionChains != null) { - var chains = inclusionChains.Select(relationships => new ResourceFieldChainExpression(relationships)).ToList(); + var chains = inclusionChains.Select(relationships => new ResourceFieldChainExpression(relationships.ToArray())).ToList(); var includeExpression = IncludeChainConverter.FromRelationshipChains(chains); expressionsInScope.Add(new ExpressionInScope(null, includeExpression)); } @@ -115,7 +117,7 @@ protected IEnumerable GetIncludeConstraints(List x.GetConstraints()).Returns(expressionsInScope); IQueryConstraintProvider includeConstraintProvider = mock.Object; - return new List {includeConstraintProvider}; + return includeConstraintProvider.AsEnumerable(); } /// @@ -126,14 +128,14 @@ protected sealed class TestSerializer : BaseSerializer { public TestSerializer(IResourceObjectBuilder resourceObjectBuilder) : base(resourceObjectBuilder) { } - public new Document Build(IIdentifiable resource, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) + public Document PublicBuild(IIdentifiable resource, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) { - return base.Build(resource, attributes, relationships); + return Build(resource, attributes, relationships); } - public new Document Build(IReadOnlyCollection resources, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) + public Document PublicBuild(IReadOnlyCollection resources, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) { - return base.Build(resources, attributes, relationships); + return Build(resources, attributes, relationships); } } } diff --git a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs index 00886e89cd..3e8eb9747b 100644 --- a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs @@ -41,7 +41,7 @@ public void BuildIncluded_DeeplyNestedCircularChainOfManyData_BuildsWithoutDupli { // Arrange var (article, author, _, _, _) = GetAuthorChainInstances(); - var secondArticle = _articleFaker.Generate(); + var secondArticle = ArticleFaker.Generate(); secondArticle.Author = author; var builder = GetBuilder(); @@ -63,7 +63,7 @@ public void BuildIncluded_OverlappingDeeplyNestedCircularChains_CanBuild() var (article, author, _, reviewer, reviewerFood) = GetAuthorChainInstances(); var sharedBlog = author.Blogs.First(); var sharedBlogAuthor = reviewer; - var (_, _, _, authorSong) = GetReviewerChainInstances(article, sharedBlog, sharedBlogAuthor); + var authorSong = GetReviewerChainInstances(article, sharedBlog, sharedBlogAuthor); var reviewerChain = GetIncludedRelationshipsChain("reviewer.blogs.author.favoriteSong"); var builder = GetBuilder(); @@ -80,51 +80,53 @@ public void BuildIncluded_OverlappingDeeplyNestedCircularChains_CanBuild() var nonOverlappingBlogs = result.Where(ro => ro.Type == "blogs" && ro.Id != sharedBlog.StringId).ToList(); foreach (var blog in nonOverlappingBlogs) + { Assert.Single(blog.Relationships.Keys); + } Assert.Equal(authorSong.StringId, sharedBlogAuthor.FavoriteSong.StringId); Assert.Equal(reviewerFood.StringId, sharedBlogAuthor.FavoriteFood.StringId); } - private (Person, Song, Person, Song) GetReviewerChainInstances(Article article, Blog sharedBlog, Person sharedBlogAuthor) + private Song GetReviewerChainInstances(Article article, Blog sharedBlog, Person sharedBlogAuthor) { - var reviewer = _personFaker.Generate(); + var reviewer = PersonFaker.Generate(); article.Reviewer = reviewer; - var blogs = _blogFaker.Generate(1); + var blogs = BlogFaker.Generate(1); blogs.Add(sharedBlog); reviewer.Blogs = blogs.ToHashSet(); blogs[0].Author = reviewer; - var author = _personFaker.Generate(); + var author = PersonFaker.Generate(); blogs[1].Author = sharedBlogAuthor; - var authorSong = _songFaker.Generate(); + var authorSong = SongFaker.Generate(); author.FavoriteSong = authorSong; sharedBlogAuthor.FavoriteSong = authorSong; - var reviewerSong = _songFaker.Generate(); + var reviewerSong = SongFaker.Generate(); reviewer.FavoriteSong = reviewerSong; - return (reviewer, reviewerSong, author, authorSong); + return authorSong; } private (Article, Person, Food, Person, Food) GetAuthorChainInstances() { - var article = _articleFaker.Generate(); - var author = _personFaker.Generate(); + var article = ArticleFaker.Generate(); + var author = PersonFaker.Generate(); article.Author = author; - var blogs = _blogFaker.Generate(2); + var blogs = BlogFaker.Generate(2); author.Blogs = blogs.ToHashSet(); blogs[0].Reviewer = author; - var reviewer = _personFaker.Generate(); + var reviewer = PersonFaker.Generate(); blogs[1].Reviewer = reviewer; - var authorFood = _foodFaker.Generate(); + var authorFood = FoodFaker.Generate(); author.FavoriteFood = authorFood; - var reviewerFood = _foodFaker.Generate(); + var reviewerFood = FoodFaker.Generate(); reviewer.FavoriteFood = reviewerFood; return (article, author, authorFood, reviewer, reviewerFood); @@ -133,8 +135,8 @@ public void BuildIncluded_OverlappingDeeplyNestedCircularChains_CanBuild() [Fact] public void BuildIncluded_DuplicateChildrenMultipleChains_OnceInOutput() { - var person = _personFaker.Generate(); - var articles = _articleFaker.Generate(5); + var person = PersonFaker.Generate(); + var articles = ArticleFaker.Generate(5); articles.ForEach(a => a.Author = person); articles.ForEach(a => a.Reviewer = person); var builder = GetBuilder(); @@ -155,13 +157,13 @@ public void BuildIncluded_DuplicateChildrenMultipleChains_OnceInOutput() private List GetIncludedRelationshipsChain(string chain) { var parsedChain = new List(); - var resourceContext = _resourceGraph.GetResourceContext
(); + var resourceContext = ResourceGraph.GetResourceContext
(); var splitPath = chain.Split('.'); foreach (var requestedRelationship in splitPath) { var relationship = resourceContext.Relationships.Single(r => r.PublicName == requestedRelationship); parsedChain.Add(relationship); - resourceContext = _resourceGraph.GetResourceContext(relationship.RightType); + resourceContext = ResourceGraph.GetResourceContext(relationship.RightType); } return parsedChain; } @@ -172,7 +174,7 @@ private IncludedResourceObjectBuilder GetBuilder() var links = GetLinkBuilder(); var accessor = new Mock().Object; - return new IncludedResourceObjectBuilder(fields, links, _resourceGraph, Enumerable.Empty(), accessor, GetSerializerSettingsProvider()); + return new IncludedResourceObjectBuilder(fields, links, ResourceGraph, Enumerable.Empty(), accessor, GetSerializerSettingsProvider()); } } } diff --git a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs index da492a2938..295b13fdb3 100644 --- a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs +++ b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs @@ -19,7 +19,7 @@ public sealed class RequestDeserializerTests : DeserializerTestsSetup private readonly Mock _requestMock = new Mock(); public RequestDeserializerTests() { - _deserializer = new RequestDeserializer(_resourceGraph, new ResourceFactory(new ServiceContainer()), _fieldsManagerMock.Object, _mockHttpContextAccessor.Object, _requestMock.Object, new JsonApiOptions()); + _deserializer = new RequestDeserializer(ResourceGraph, new ResourceFactory(new ServiceContainer()), _fieldsManagerMock.Object, MockHttpContextAccessor.Object, _requestMock.Object, new JsonApiOptions()); } [Fact] diff --git a/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs index bc50c2ec93..7586ed17a1 100644 --- a/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore; using JsonApiDotNetCore.Resources.Annotations; using UnitTests.TestModels; using Xunit; @@ -9,11 +10,11 @@ namespace UnitTests.Serialization.Server public sealed class ResponseResourceObjectBuilderTests : SerializerTestsSetup { private readonly List _relationshipsForBuild; - private const string _relationshipName = "dependents"; + private const string RelationshipName = "dependents"; public ResponseResourceObjectBuilderTests() { - _relationshipsForBuild = _resourceGraph.GetRelationships(e => new { e.Dependents }).ToList(); + _relationshipsForBuild = ResourceGraph.GetRelationships(e => new { e.Dependents }).ToList(); } [Fact] @@ -21,13 +22,13 @@ public void Build_RelationshipNotIncludedAndLinksEnabled_RelationshipEntryWithLi { // Arrange var resource = new OneToManyPrincipal { Id = 10 }; - var builder = GetResponseResourceObjectBuilder(relationshipLinks: _dummyRelationshipLinks); + var builder = GetResponseResourceObjectBuilder(relationshipLinks: DummyRelationshipLinks); // Act var resourceObject = builder.Build(resource, relationships: _relationshipsForBuild); // Assert - Assert.True(resourceObject.Relationships.TryGetValue(_relationshipName, out var entry)); + Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out var entry)); Assert.Equal("http://www.dummy.com/dummy-relationship-self-link", entry.Links.Self); Assert.Equal("http://www.dummy.com/dummy-relationship-related-link", entry.Links.Related); Assert.False(entry.IsPopulated); @@ -52,13 +53,13 @@ public void Build_RelationshipIncludedAndLinksDisabled_RelationshipEntryWithData { // Arrange var resource = new OneToManyPrincipal { Id = 10, Dependents = new HashSet { new OneToManyDependent { Id = 20 } } }; - var builder = GetResponseResourceObjectBuilder(inclusionChains: new List> { _relationshipsForBuild } ); + var builder = GetResponseResourceObjectBuilder(inclusionChains: _relationshipsForBuild.AsEnumerable()); // Act var resourceObject = builder.Build(resource, relationships: _relationshipsForBuild); // Assert - Assert.True(resourceObject.Relationships.TryGetValue(_relationshipName, out var entry)); + Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out var entry)); Assert.Null(entry.Links); Assert.True(entry.IsPopulated); Assert.Equal("20", entry.ManyData.Single().Id); @@ -69,13 +70,13 @@ public void Build_RelationshipIncludedAndLinksEnabled_RelationshipEntryWithDataA { // Arrange var resource = new OneToManyPrincipal { Id = 10, Dependents = new HashSet { new OneToManyDependent { Id = 20 } } }; - var builder = GetResponseResourceObjectBuilder(inclusionChains: new List> { _relationshipsForBuild }, relationshipLinks: _dummyRelationshipLinks); + var builder = GetResponseResourceObjectBuilder(inclusionChains: _relationshipsForBuild.AsEnumerable(), relationshipLinks: DummyRelationshipLinks); // Act var resourceObject = builder.Build(resource, relationships: _relationshipsForBuild); // Assert - Assert.True(resourceObject.Relationships.TryGetValue(_relationshipName, out var entry)); + Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out var entry)); Assert.Equal("http://www.dummy.com/dummy-relationship-self-link", entry.Links.Self); Assert.Equal("http://www.dummy.com/dummy-relationship-related-link", entry.Links.Related); Assert.True(entry.IsPopulated); diff --git a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs index 9749d95ac9..fad17e27cd 100644 --- a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs +++ b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Net; using System.Text.RegularExpressions; -using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; using UnitTests.TestModels; @@ -23,7 +23,7 @@ public void SerializeSingle_ResourceWithDefaultTargetFields_CanSerialize() string serialized = serializer.SerializeSingle(resource); // Assert - var expectedFormatted = @"{ + const string expectedFormatted = @"{ ""data"":{ ""type"":""testResource"", ""id"":""1"", @@ -52,10 +52,10 @@ public void SerializeMany_ResourceWithDefaultTargetFields_CanSerialize() var serializer = GetResponseSerializer(); // Act - string serialized = serializer.SerializeMany(new List { resource }); + string serialized = serializer.SerializeMany(resource.AsArray()); // Assert - var expectedFormatted = @"{ + const string expectedFormatted = @"{ ""data"":[{ ""type"":""testResource"", ""id"":""1"", @@ -85,14 +85,14 @@ public void SerializeSingle_ResourceWithIncludedRelationships_CanSerialize() PopulatedToOne = new OneToOneDependent { Id = 10 }, PopulatedToManies = new HashSet { new OneToManyDependent { Id = 20 } } }; - var chain = _resourceGraph.GetRelationships().Select(r => new List { r }).ToList(); + var chain = ResourceGraph.GetRelationships().Select(r => r.AsEnumerable()).ToList(); var serializer = GetResponseSerializer(inclusionChains: chain); // Act string serialized = serializer.SerializeSingle(resource); // Assert - var expectedFormatted = @"{ + const string expectedFormatted = @"{ ""data"":{ ""type"":""multiPrincipals"", ""id"":""1"", @@ -147,13 +147,16 @@ public void SerializeSingle_ResourceWithDeeplyIncludedRelationships_CanSerialize PopulatedToManies = new HashSet { includedResource } }; - var chains = _resourceGraph.GetRelationships() + var chains = ResourceGraph.GetRelationships() .Select(r => { - var chain = new List {r}; + var chain = r.AsList(); if (r.PublicName != "populatedToManies") - return new List {r}; - chain.AddRange(_resourceGraph.GetRelationships()); + { + return chain; + } + + chain.AddRange(ResourceGraph.GetRelationships()); return chain; }).ToList(); @@ -163,7 +166,7 @@ public void SerializeSingle_ResourceWithDeeplyIncludedRelationships_CanSerialize string serialized = serializer.SerializeSingle(resource); // Assert - var expectedFormatted = @"{ + const string expectedFormatted = @"{ ""data"":{ ""type"":""multiPrincipals"", ""id"":""10"", @@ -233,7 +236,7 @@ public void SerializeSingle_Null_CanSerialize() string serialized = serializer.SerializeSingle(null); // Assert - var expectedFormatted = @"{ ""data"": null }"; + const string expectedFormatted = @"{ ""data"": null }"; var expected = Regex.Replace(expectedFormatted, @"\s+", ""); Assert.Equal(expected, serialized); } @@ -243,11 +246,12 @@ public void SerializeList_EmptyList_CanSerialize() { // Arrange var serializer = GetResponseSerializer(); + // Act string serialized = serializer.SerializeMany(new List()); // Assert - var expectedFormatted = @"{ ""data"": [] }"; + const string expectedFormatted = @"{ ""data"": [] }"; var expected = Regex.Replace(expectedFormatted, @"\s+", ""); Assert.Equal(expected, serialized); } @@ -257,13 +261,13 @@ public void SerializeSingle_ResourceWithLinksEnabled_CanSerialize() { // Arrange var resource = new OneToManyPrincipal { Id = 10 }; - var serializer = GetResponseSerializer(topLinks: _dummyTopLevelLinks, relationshipLinks: _dummyRelationshipLinks, resourceLinks: _dummyResourceLinks); + var serializer = GetResponseSerializer(topLinks: DummyTopLevelLinks, relationshipLinks: DummyRelationshipLinks, resourceLinks: DummyResourceLinks); // Act string serialized = serializer.SerializeSingle(resource); // Assert - var expectedFormatted = @"{ + const string expectedFormatted = @"{ ""links"":{ ""self"":""http://www.dummy.com/dummy-self-link"", ""first"":""http://www.dummy.com/dummy-first-link"", @@ -299,7 +303,7 @@ public void SerializeSingle_ResourceWithLinksEnabled_CanSerialize() public void SerializeSingle_ResourceWithMeta_IncludesMetaInResult() { // Arrange - var meta = new Dictionary { { "test", "meta" } }; + var meta = new Dictionary { ["test"] = "meta" }; var resource = new OneToManyPrincipal { Id = 10 }; var serializer = GetResponseSerializer(metaDict: meta); @@ -307,7 +311,7 @@ public void SerializeSingle_ResourceWithMeta_IncludesMetaInResult() string serialized = serializer.SerializeSingle(resource); // Assert - var expectedFormatted = @"{ + const string expectedFormatted = @"{ ""meta"":{ ""test"": ""meta"" }, ""data"":{ ""type"":""oneToManyPrincipals"", @@ -326,14 +330,14 @@ public void SerializeSingle_ResourceWithMeta_IncludesMetaInResult() public void SerializeSingle_NullWithLinksAndMeta_StillShowsLinksAndMeta() { // Arrange - var meta = new Dictionary { { "test", "meta" } }; - var serializer = GetResponseSerializer(metaDict: meta, topLinks: _dummyTopLevelLinks, relationshipLinks: _dummyRelationshipLinks, resourceLinks: _dummyResourceLinks); + var meta = new Dictionary { ["test"] = "meta" }; + var serializer = GetResponseSerializer(metaDict: meta, topLinks: DummyTopLevelLinks, relationshipLinks: DummyRelationshipLinks, resourceLinks: DummyResourceLinks); // Act string serialized = serializer.SerializeSingle(null); // Assert - var expectedFormatted = @"{ + const string expectedFormatted = @"{ ""meta"":{ ""test"": ""meta"" }, ""links"":{ ""self"":""http://www.dummy.com/dummy-self-link"", diff --git a/test/UnitTests/TestModels.cs b/test/UnitTests/TestModels.cs deleted file mode 100644 index 54d23e8458..0000000000 --- a/test/UnitTests/TestModels.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace UnitTests.TestModels -{ - public sealed class TestResource : Identifiable - { - [Attr] public string StringField { get; set; } - [Attr] public DateTime DateTimeField { get; set; } - [Attr] public DateTime? NullableDateTimeField { get; set; } - [Attr] public int IntField { get; set; } - [Attr] public int? NullableIntField { get; set; } - [Attr] public Guid GuidField { get; set; } - [Attr] public ComplexType ComplexField { get; set; } - - } - - public class TestResourceWithList : Identifiable - { - [Attr] public List ComplexFields { get; set; } - } - - public class ComplexType - { - public string CompoundName { get; set; } - } - - public class TestResourceWithAbstractRelationship : Identifiable - { - [HasOne] public BaseModel ToOne { get; set; } - [HasMany] public List ToMany { get; set; } - } - - public abstract class BaseModel : Identifiable { } - - public class FirstDerivedModel : BaseModel - { - [Attr] public bool FirstProperty { get; set; } - } - public class SecondDerivedModel : BaseModel - { - [Attr] public bool SecondProperty { get; set; } - } - - public sealed class OneToOnePrincipal : IdentifiableWithAttribute - { - [HasOne] public OneToOneDependent Dependent { get; set; } - } - - public sealed class OneToOneDependent : IdentifiableWithAttribute - { - [HasOne] public OneToOnePrincipal Principal { get; set; } - public int? PrincipalId { get; set; } - } - - public sealed class OneToOneRequiredDependent : IdentifiableWithAttribute - { - [HasOne] public OneToOnePrincipal Principal { get; set; } - public int PrincipalId { get; set; } - } - - public sealed class OneToManyDependent : IdentifiableWithAttribute - { - [HasOne] public OneToManyPrincipal Principal { get; set; } - public int? PrincipalId { get; set; } - } - - public class OneToManyRequiredDependent : IdentifiableWithAttribute - { - [HasOne] public OneToManyPrincipal Principal { get; set; } - public int PrincipalId { get; set; } - } - - public sealed class OneToManyPrincipal : IdentifiableWithAttribute - { - [HasMany] public ISet Dependents { get; set; } - } - - public class IdentifiableWithAttribute : Identifiable - { - [Attr] public string AttributeMember { get; set; } - } - - public sealed class MultipleRelationshipsPrincipalPart : IdentifiableWithAttribute - { - [HasOne] public OneToOneDependent PopulatedToOne { get; set; } - [HasOne] public OneToOneDependent EmptyToOne { get; set; } - [HasMany] public ISet PopulatedToManies { get; set; } - [HasMany] public ISet EmptyToManies { get; set; } - [HasOne] public MultipleRelationshipsPrincipalPart Multi { get; set; } - } - - public class MultipleRelationshipsDependentPart : IdentifiableWithAttribute - { - [HasOne] public OneToOnePrincipal PopulatedToOne { get; set; } - public int PopulatedToOneId { get; set; } - [HasOne] public OneToOnePrincipal EmptyToOne { get; set; } - public int? EmptyToOneId { get; set; } - [HasOne] public OneToManyPrincipal PopulatedToMany { get; set; } - public int PopulatedToManyId { get; set; } - [HasOne] public OneToManyPrincipal EmptyToMany { get; set; } - public int? EmptyToManyId { get; set; } - } - - public class Article : Identifiable - { - [Attr] public string Title { get; set; } - [HasOne] public Person Reviewer { get; set; } - [HasOne] public Person Author { get; set; } - - [HasOne(CanInclude = false)] public Person CannotInclude { get; set; } - } - - public class Person : Identifiable - { - [Attr] public string Name { get; set; } - [HasMany] public ISet Blogs { get; set; } - [HasOne] public Food FavoriteFood { get; set; } - [HasOne] public Song FavoriteSong { get; set; } - } - - public class Blog : Identifiable - { - [Attr] public string Title { get; set; } - [HasOne] public Person Reviewer { get; set; } - [HasOne] public Person Author { get; set; } - } - - public class Food : Identifiable - { - [Attr] public string Dish { get; set; } - } - - public class Song : Identifiable - { - [Attr] public string Title { get; set; } - } -} diff --git a/test/UnitTests/TestModels/Article.cs b/test/UnitTests/TestModels/Article.cs new file mode 100644 index 0000000000..0731286f59 --- /dev/null +++ b/test/UnitTests/TestModels/Article.cs @@ -0,0 +1,16 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Article : Identifiable + { + [Attr] public string Title { get; set; } + [HasOne] public Person Reviewer { get; set; } + [HasOne] public Person Author { get; set; } + + [HasOne(CanInclude = false)] public Person CannotInclude { get; set; } + } +} diff --git a/test/UnitTests/TestModels/BaseModel.cs b/test/UnitTests/TestModels/BaseModel.cs new file mode 100644 index 0000000000..5ec5d635f9 --- /dev/null +++ b/test/UnitTests/TestModels/BaseModel.cs @@ -0,0 +1,6 @@ +using JsonApiDotNetCore.Resources; + +namespace UnitTests.TestModels +{ + public abstract class BaseModel : Identifiable { } +} \ No newline at end of file diff --git a/test/UnitTests/TestModels/Blog.cs b/test/UnitTests/TestModels/Blog.cs new file mode 100644 index 0000000000..fcc61891b8 --- /dev/null +++ b/test/UnitTests/TestModels/Blog.cs @@ -0,0 +1,14 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Blog : Identifiable + { + [Attr] public string Title { get; set; } + [HasOne] public Person Reviewer { get; set; } + [HasOne] public Person Author { get; set; } + } +} diff --git a/test/UnitTests/TestModels/ComplexType.cs b/test/UnitTests/TestModels/ComplexType.cs new file mode 100644 index 0000000000..a8748203e6 --- /dev/null +++ b/test/UnitTests/TestModels/ComplexType.cs @@ -0,0 +1,10 @@ +using JetBrains.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class ComplexType + { + public string CompoundName { get; set; } + } +} diff --git a/test/UnitTests/TestModels/FirstDerivedModel.cs b/test/UnitTests/TestModels/FirstDerivedModel.cs new file mode 100644 index 0000000000..b381cf0857 --- /dev/null +++ b/test/UnitTests/TestModels/FirstDerivedModel.cs @@ -0,0 +1,11 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class FirstDerivedModel : BaseModel + { + [Attr] public bool FirstProperty { get; set; } + } +} diff --git a/test/UnitTests/TestModels/Food.cs b/test/UnitTests/TestModels/Food.cs new file mode 100644 index 0000000000..81e08dd055 --- /dev/null +++ b/test/UnitTests/TestModels/Food.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Food : Identifiable + { + [Attr] public string Dish { get; set; } + } +} diff --git a/test/UnitTests/TestModels/IdentifiableWithAttribute.cs b/test/UnitTests/TestModels/IdentifiableWithAttribute.cs new file mode 100644 index 0000000000..a46fa1e9b3 --- /dev/null +++ b/test/UnitTests/TestModels/IdentifiableWithAttribute.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public class IdentifiableWithAttribute : Identifiable + { + [Attr] public string AttributeMember { get; set; } + } +} diff --git a/test/UnitTests/TestModels/MultipleRelationshipsDependentPart.cs b/test/UnitTests/TestModels/MultipleRelationshipsDependentPart.cs new file mode 100644 index 0000000000..0d2ef40933 --- /dev/null +++ b/test/UnitTests/TestModels/MultipleRelationshipsDependentPart.cs @@ -0,0 +1,18 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class MultipleRelationshipsDependentPart : IdentifiableWithAttribute + { + [HasOne] public OneToOnePrincipal PopulatedToOne { get; set; } + public int PopulatedToOneId { get; set; } + [HasOne] public OneToOnePrincipal EmptyToOne { get; set; } + public int? EmptyToOneId { get; set; } + [HasOne] public OneToManyPrincipal PopulatedToMany { get; set; } + public int PopulatedToManyId { get; set; } + [HasOne] public OneToManyPrincipal EmptyToMany { get; set; } + public int? EmptyToManyId { get; set; } + } +} diff --git a/test/UnitTests/TestModels/MultipleRelationshipsPrincipalPart.cs b/test/UnitTests/TestModels/MultipleRelationshipsPrincipalPart.cs new file mode 100644 index 0000000000..0374ba1971 --- /dev/null +++ b/test/UnitTests/TestModels/MultipleRelationshipsPrincipalPart.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class MultipleRelationshipsPrincipalPart : IdentifiableWithAttribute + { + [HasOne] public OneToOneDependent PopulatedToOne { get; set; } + [HasOne] public OneToOneDependent EmptyToOne { get; set; } + [HasMany] public ISet PopulatedToManies { get; set; } + [HasMany] public ISet EmptyToManies { get; set; } + [HasOne] public MultipleRelationshipsPrincipalPart Multi { get; set; } + } +} diff --git a/test/UnitTests/TestModels/OneToManyDependent.cs b/test/UnitTests/TestModels/OneToManyDependent.cs new file mode 100644 index 0000000000..32ac04e77e --- /dev/null +++ b/test/UnitTests/TestModels/OneToManyDependent.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class OneToManyDependent : IdentifiableWithAttribute + { + [HasOne] public OneToManyPrincipal Principal { get; set; } + public int? PrincipalId { get; set; } + } +} diff --git a/test/UnitTests/TestModels/OneToManyPrincipal.cs b/test/UnitTests/TestModels/OneToManyPrincipal.cs new file mode 100644 index 0000000000..39e0bd931b --- /dev/null +++ b/test/UnitTests/TestModels/OneToManyPrincipal.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class OneToManyPrincipal : IdentifiableWithAttribute + { + [HasMany] public ISet Dependents { get; set; } + } +} diff --git a/test/UnitTests/TestModels/OneToManyRequiredDependent.cs b/test/UnitTests/TestModels/OneToManyRequiredDependent.cs new file mode 100644 index 0000000000..89491b8065 --- /dev/null +++ b/test/UnitTests/TestModels/OneToManyRequiredDependent.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class OneToManyRequiredDependent : IdentifiableWithAttribute + { + [HasOne] public OneToManyPrincipal Principal { get; set; } + public int PrincipalId { get; set; } + } +} diff --git a/test/UnitTests/TestModels/OneToOneDependent.cs b/test/UnitTests/TestModels/OneToOneDependent.cs new file mode 100644 index 0000000000..7a8b0d34f7 --- /dev/null +++ b/test/UnitTests/TestModels/OneToOneDependent.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class OneToOneDependent : IdentifiableWithAttribute + { + [HasOne] public OneToOnePrincipal Principal { get; set; } + public int? PrincipalId { get; set; } + } +} diff --git a/test/UnitTests/TestModels/OneToOnePrincipal.cs b/test/UnitTests/TestModels/OneToOnePrincipal.cs new file mode 100644 index 0000000000..b5678a0e69 --- /dev/null +++ b/test/UnitTests/TestModels/OneToOnePrincipal.cs @@ -0,0 +1,11 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class OneToOnePrincipal : IdentifiableWithAttribute + { + [HasOne] public OneToOneDependent Dependent { get; set; } + } +} diff --git a/test/UnitTests/TestModels/OneToOneRequiredDependent.cs b/test/UnitTests/TestModels/OneToOneRequiredDependent.cs new file mode 100644 index 0000000000..2c4672ef82 --- /dev/null +++ b/test/UnitTests/TestModels/OneToOneRequiredDependent.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class OneToOneRequiredDependent : IdentifiableWithAttribute + { + [HasOne] public OneToOnePrincipal Principal { get; set; } + public int PrincipalId { get; set; } + } +} diff --git a/test/UnitTests/TestModels/Person.cs b/test/UnitTests/TestModels/Person.cs new file mode 100644 index 0000000000..58f616ccdd --- /dev/null +++ b/test/UnitTests/TestModels/Person.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Person : Identifiable + { + [Attr] public string Name { get; set; } + [HasMany] public ISet Blogs { get; set; } + [HasOne] public Food FavoriteFood { get; set; } + [HasOne] public Song FavoriteSong { get; set; } + } +} diff --git a/test/UnitTests/TestModels/SecondDerivedModel.cs b/test/UnitTests/TestModels/SecondDerivedModel.cs new file mode 100644 index 0000000000..3f04f9d7ee --- /dev/null +++ b/test/UnitTests/TestModels/SecondDerivedModel.cs @@ -0,0 +1,11 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class SecondDerivedModel : BaseModel + { + [Attr] public bool SecondProperty { get; set; } + } +} diff --git a/test/UnitTests/TestModels/Song.cs b/test/UnitTests/TestModels/Song.cs new file mode 100644 index 0000000000..ea7cd5f78d --- /dev/null +++ b/test/UnitTests/TestModels/Song.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Song : Identifiable + { + [Attr] public string Title { get; set; } + } +} diff --git a/test/UnitTests/TestModels/TestResource.cs b/test/UnitTests/TestModels/TestResource.cs new file mode 100644 index 0000000000..f5d8f9b90d --- /dev/null +++ b/test/UnitTests/TestModels/TestResource.cs @@ -0,0 +1,20 @@ +using System; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class TestResource : Identifiable + { + [Attr] public string StringField { get; set; } + [Attr] public DateTime DateTimeField { get; set; } + [Attr] public DateTime? NullableDateTimeField { get; set; } + [Attr] public int IntField { get; set; } + [Attr] public int? NullableIntField { get; set; } + [Attr] public Guid GuidField { get; set; } + [Attr] public ComplexType ComplexField { get; set; } + + } +} diff --git a/test/UnitTests/TestModels/TestResourceWithAbstractRelationship.cs b/test/UnitTests/TestModels/TestResourceWithAbstractRelationship.cs new file mode 100644 index 0000000000..f7db45eb51 --- /dev/null +++ b/test/UnitTests/TestModels/TestResourceWithAbstractRelationship.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class TestResourceWithAbstractRelationship : Identifiable + { + [HasOne] public BaseModel ToOne { get; set; } + [HasMany] public List ToMany { get; set; } + } +} diff --git a/test/UnitTests/TestModels/TestResourceWithList.cs b/test/UnitTests/TestModels/TestResourceWithList.cs new file mode 100644 index 0000000000..e9995208d5 --- /dev/null +++ b/test/UnitTests/TestModels/TestResourceWithList.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace UnitTests.TestModels +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class TestResourceWithList : Identifiable + { + [Attr] public List ComplexFields { get; set; } + } +}