diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 4e19c6f25c..eb2be47056 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -158,6 +158,7 @@ public void ConfigureServiceContainer(ICollection dbContextTypes) _services.AddScoped(); _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); _services.AddScoped(); + _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs index 1d3dea04ad..d3fde6d0cf 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Resources.Annotations; @@ -32,6 +33,11 @@ public IReadOnlyCollection GetRelationshipChains(I { ArgumentGuard.NotNull(include, nameof(include)); + if (!include.Elements.Any()) + { + return Array.Empty(); + } + var converter = new IncludeToChainsConverter(); converter.Visit(include, null); diff --git a/src/JsonApiDotNetCore/Queries/Internal/EvaluatedIncludeCache.cs b/src/JsonApiDotNetCore/Queries/Internal/EvaluatedIncludeCache.cs new file mode 100644 index 0000000000..1e9202827b --- /dev/null +++ b/src/JsonApiDotNetCore/Queries/Internal/EvaluatedIncludeCache.cs @@ -0,0 +1,22 @@ +using JsonApiDotNetCore.Queries.Expressions; + +namespace JsonApiDotNetCore.Queries.Internal +{ + /// + internal sealed class EvaluatedIncludeCache : IEvaluatedIncludeCache + { + private IncludeExpression _include; + + /// + public void Set(IncludeExpression include) + { + _include = include; + } + + /// + public IncludeExpression Get() + { + return _include; + } + } +} diff --git a/src/JsonApiDotNetCore/Queries/Internal/IEvaluatedIncludeCache.cs b/src/JsonApiDotNetCore/Queries/Internal/IEvaluatedIncludeCache.cs new file mode 100644 index 0000000000..d7c924f066 --- /dev/null +++ b/src/JsonApiDotNetCore/Queries/Internal/IEvaluatedIncludeCache.cs @@ -0,0 +1,23 @@ +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Queries.Internal +{ + /// + /// Provides in-memory storage for the evaluated inclusion tree within a request. This tree is produced from query string and resource definition + /// callbacks. The cache enables the serialization layer to take changes from into + /// account. + /// + public interface IEvaluatedIncludeCache + { + /// + /// Stores the evaluated inclusion tree for later usage. + /// + void Set(IncludeExpression include); + + /// + /// Gets the evaluated inclusion tree that was stored earlier. + /// + IncludeExpression Get(); + } +} diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index 58fc650321..981ea9d71d 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -20,11 +20,12 @@ public class QueryLayerComposer : IQueryLayerComposer private readonly IJsonApiOptions _options; private readonly IPaginationContext _paginationContext; private readonly ITargetedFields _targetedFields; + private readonly IEvaluatedIncludeCache _evaluatedIncludeCache; private readonly SparseFieldSetCache _sparseFieldSetCache; public QueryLayerComposer(IEnumerable constraintProviders, IResourceContextProvider resourceContextProvider, IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiOptions options, IPaginationContext paginationContext, - ITargetedFields targetedFields) + ITargetedFields targetedFields, IEvaluatedIncludeCache evaluatedIncludeCache) { ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); @@ -32,6 +33,7 @@ public QueryLayerComposer(IEnumerable constraintProvid ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(paginationContext, nameof(paginationContext)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); + ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache)); _constraintProviders = constraintProviders; _resourceContextProvider = resourceContextProvider; @@ -39,6 +41,7 @@ public QueryLayerComposer(IEnumerable constraintProvid _options = options; _paginationContext = paginationContext; _targetedFields = targetedFields; + _evaluatedIncludeCache = evaluatedIncludeCache; _sparseFieldSetCache = new SparseFieldSetCache(_constraintProviders, resourceDefinitionAccessor); } @@ -72,6 +75,8 @@ public QueryLayer ComposeFromConstraints(ResourceContext requestResource) QueryLayer topLayer = ComposeTopLayer(constraints, requestResource); topLayer.Include = ComposeChildren(topLayer, constraints); + _evaluatedIncludeCache.Set(topLayer.Include); + return topLayer; } @@ -159,12 +164,15 @@ private IReadOnlyCollection ProcessIncludeSet(IReadOnl // @formatter:wrap_chained_method_calls restore ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(includeElement.Relationship.RightType); + bool isToManyRelationship = includeElement.Relationship is HasManyAttribute; var child = new QueryLayer(resourceContext) { - Filter = GetFilter(expressionsInCurrentScope, resourceContext), - Sort = GetSort(expressionsInCurrentScope, resourceContext), - Pagination = ((JsonApiOptions)_options).DisableChildrenPagination ? null : GetPagination(expressionsInCurrentScope, resourceContext), + Filter = isToManyRelationship ? GetFilter(expressionsInCurrentScope, resourceContext) : null, + Sort = isToManyRelationship ? GetSort(expressionsInCurrentScope, resourceContext) : null, + Pagination = isToManyRelationship + ? ((JsonApiOptions)_options).DisableChildrenPagination ? null : GetPagination(expressionsInCurrentScope, resourceContext) + : null, Projection = GetProjectionForSparseAttributeSet(resourceContext) }; diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs index b73777855f..d5eb7a4110 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs @@ -56,7 +56,7 @@ public virtual void Read(string parameterName, StringValues parameterValue) private object GetQueryableHandler(string parameterName) { - Type resourceType = _request.PrimaryResource.ResourceType; + Type resourceType = (_request.SecondaryResource ?? _request.PrimaryResource).ResourceType; object handler = _resourceDefinitionAccessor.GetQueryableHandlerForQueryStringParameter(resourceType, parameterName); if (handler != null && _request.Kind != EndpointKind.Primary) diff --git a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs index 74aac437be..3498d74a7a 100644 --- a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs @@ -37,11 +37,17 @@ public class JsonApiResourceDefinition : IResourceDefinition + /// Provides metadata for the resource type . + /// + protected ResourceContext ResourceContext { get; } + public JsonApiResourceDefinition(IResourceGraph resourceGraph) { ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ResourceGraph = resourceGraph; + ResourceContext = resourceGraph.GetResourceContext(); } /// diff --git a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs index c41ac48195..9f22dcbe3f 100644 --- a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs @@ -4,6 +4,7 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Queries.Internal; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; @@ -21,6 +22,7 @@ public sealed class AtomicOperationsResponseSerializer : BaseSerializer, IJsonAp private readonly ILinkBuilder _linkBuilder; private readonly IFieldsToSerialize _fieldsToSerialize; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; + private readonly IEvaluatedIncludeCache _evaluatedIncludeCache; private readonly IJsonApiRequest _request; private readonly IJsonApiOptions _options; @@ -28,13 +30,15 @@ public sealed class AtomicOperationsResponseSerializer : BaseSerializer, IJsonAp public string ContentType { get; } = HeaderConstants.AtomicOperationsMediaType; public AtomicOperationsResponseSerializer(IResourceObjectBuilder resourceObjectBuilder, IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, - IFieldsToSerialize fieldsToSerialize, IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiRequest request, IJsonApiOptions options) + IFieldsToSerialize fieldsToSerialize, IResourceDefinitionAccessor resourceDefinitionAccessor, IEvaluatedIncludeCache evaluatedIncludeCache, + IJsonApiRequest request, IJsonApiOptions options) : base(resourceObjectBuilder) { ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder)); ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize)); ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); + ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache)); ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(options, nameof(options)); @@ -42,6 +46,7 @@ public AtomicOperationsResponseSerializer(IResourceObjectBuilder resourceObjectB _linkBuilder = linkBuilder; _fieldsToSerialize = fieldsToSerialize; _resourceDefinitionAccessor = resourceDefinitionAccessor; + _evaluatedIncludeCache = evaluatedIncludeCache; _request = request; _options = options; } @@ -93,6 +98,7 @@ private AtomicResultObject SerializeOperation(OperationContainer operation) { _request.CopyFrom(operation.Request); _fieldsToSerialize.ResetCache(); + _evaluatedIncludeCache.Set(null); _resourceDefinitionAccessor.OnSerialize(operation.Resource); diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs index f8b6bed8e3..62e36f1e37 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs @@ -5,7 +5,6 @@ using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Queries.Internal; -using JsonApiDotNetCore.QueryStrings; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; @@ -17,28 +16,31 @@ public class ResponseResourceObjectBuilder : ResourceObjectBuilder { private static readonly IncludeChainConverter IncludeChainConverter = new IncludeChainConverter(); + private readonly ILinkBuilder _linkBuilder; private readonly IIncludedResourceObjectBuilder _includedBuilder; - private readonly IEnumerable _constraintProviders; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; - private readonly ILinkBuilder _linkBuilder; + private readonly IEvaluatedIncludeCache _evaluatedIncludeCache; private readonly SparseFieldSetCache _sparseFieldSetCache; + private RelationshipAttribute _requestRelationship; public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, IEnumerable constraintProviders, IResourceContextProvider resourceContextProvider, - IResourceDefinitionAccessor resourceDefinitionAccessor, IResourceObjectBuilderSettingsProvider settingsProvider) + IResourceDefinitionAccessor resourceDefinitionAccessor, IResourceObjectBuilderSettingsProvider settingsProvider, + IEvaluatedIncludeCache evaluatedIncludeCache) : base(resourceContextProvider, settingsProvider.Get()) { ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); ArgumentGuard.NotNull(includedBuilder, nameof(includedBuilder)); ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); + ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache)); _linkBuilder = linkBuilder; _includedBuilder = includedBuilder; - _constraintProviders = constraintProviders; _resourceDefinitionAccessor = resourceDefinitionAccessor; - _sparseFieldSetCache = new SparseFieldSetCache(_constraintProviders, resourceDefinitionAccessor); + _evaluatedIncludeCache = evaluatedIncludeCache; + _sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); } public RelationshipEntry Build(IIdentifiable resource, RelationshipAttribute requestRelationship) @@ -72,7 +74,7 @@ protected override RelationshipEntry GetRelationshipData(RelationshipAttribute r ArgumentGuard.NotNull(resource, nameof(resource)); RelationshipEntry relationshipEntry = null; - IReadOnlyCollection> relationshipChains = GetInclusionChain(relationship); + IReadOnlyCollection> relationshipChains = GetInclusionChainsStartingWith(relationship); if (Equals(relationship, _requestRelationship) || relationshipChains.Any()) { @@ -116,35 +118,24 @@ private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship) } /// - /// Inspects the included relationship chains (see to see if should be - /// included or not. + /// Inspects the included relationship chains and selects the ones that starts with the specified relationship. /// - private IReadOnlyCollection> GetInclusionChain(RelationshipAttribute relationship) + private IReadOnlyCollection> GetInclusionChainsStartingWith(RelationshipAttribute relationship) { - // @formatter:wrap_chained_method_calls chop_always - // @formatter:keep_existing_linebreaks true - - ResourceFieldChainExpression[] chains = _constraintProviders - .SelectMany(provider => provider.GetConstraints()) - .Select(expressionInScope => expressionInScope.Expression) - .OfType() - .SelectMany(IncludeChainConverter.GetRelationshipChains) - .ToArray(); - - // @formatter:keep_existing_linebreaks restore - // @formatter:wrap_chained_method_calls restore + IncludeExpression include = _evaluatedIncludeCache.Get() ?? IncludeExpression.Empty; + IReadOnlyCollection chains = IncludeChainConverter.GetRelationshipChains(include); - var inclusionChain = new List>(); + var inclusionChains = new List>(); foreach (ResourceFieldChainExpression chain in chains) { if (chain.Fields.First().Equals(relationship)) { - inclusionChain.Add(chain.Fields.Cast().ToArray()); + inclusionChains.Add(chain.Fields.Cast().ToArray()); } } - return inclusionChain; + return inclusionChains; } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs index 2ccefe3670..191bca53dc 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs @@ -24,7 +24,6 @@ public sealed class TelevisionBroadcastDefinition : JsonApiResourceDefinition _constraintProviders; - private readonly ResourceContext _broadcastContext; private DateTimeOffset? _storedArchivedAt; @@ -35,7 +34,6 @@ public TelevisionBroadcastDefinition(IResourceGraph resourceGraph, TelevisionDbC _dbContext = dbContext; _request = request; _constraintProviders = constraintProviders; - _broadcastContext = resourceGraph.GetResourceContext(); } public override FilterExpression OnApplyFilter(FilterExpression existingFilter) @@ -46,8 +44,7 @@ public override FilterExpression OnApplyFilter(FilterExpression existingFilter) if (IsReturningCollectionOfTelevisionBroadcasts() && !HasFilterOnArchivedAt(existingFilter)) { - AttrAttribute archivedAtAttribute = - _broadcastContext.Attributes.Single(attr => attr.Property.Name == nameof(TelevisionBroadcast.ArchivedAt)); + AttrAttribute archivedAtAttribute = ResourceContext.Attributes.Single(attr => attr.Property.Name == nameof(TelevisionBroadcast.ArchivedAt)); var archivedAtChain = new ResourceFieldChainExpression(archivedAtAttribute); @@ -71,7 +68,7 @@ private bool IsRequestingCollectionOfTelevisionBroadcasts() { if (_request.IsCollection) { - if (_request.PrimaryResource == _broadcastContext || _request.SecondaryResource == _broadcastContext) + if (_request.PrimaryResource == ResourceContext || _request.SecondaryResource == ResourceContext) { return true; } @@ -97,7 +94,7 @@ private bool IsIncludingCollectionOfTelevisionBroadcasts() foreach (IncludeElementExpression includeElement in includeElements) { - if (includeElement.Relationship is HasManyAttribute && includeElement.Relationship.RightType == _broadcastContext.ResourceType) + if (includeElement.Relationship is HasManyAttribute && includeElement.Relationship.RightType == ResourceContext.ResourceType) { return true; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs index 453cd86714..bae079ffcb 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs @@ -7,6 +7,7 @@ using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample.Controllers; using JsonApiDotNetCoreExampleTests.Startups; +using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -27,13 +28,20 @@ public AtomicResourceMetaTests(ExampleIntegrationTestContext(); services.AddResourceDefinition(); + + services.AddSingleton(); }); + + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + hitCounter.Reset(); } [Fact] public async Task Returns_resource_meta_in_create_resource_with_side_effects() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + string newTitle1 = _fakers.MusicTrack.Generate().Title; string newTitle2 = _fakers.MusicTrack.Generate().Title; @@ -86,12 +94,20 @@ public async Task Returns_resource_meta_in_create_resource_with_side_effects() responseDocument.Results[1].SingleData.Meta.Should().HaveCount(1); responseDocument.Results[1].SingleData.Meta["Copyright"].Should().Be("(C) 1994. All rights reserved."); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(MusicTrack), ResourceDefinitionHitCounter.ExtensibilityPoint.GetMeta), + (typeof(MusicTrack), ResourceDefinitionHitCounter.ExtensibilityPoint.GetMeta) + }, options => options.WithStrictOrdering()); } [Fact] public async Task Returns_resource_meta_in_update_resource_with_side_effects() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + TextLanguage existingLanguage = _fakers.TextLanguage.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -131,6 +147,11 @@ 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(TextLanguageMetaDefinition.NoticeText); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(TextLanguage), ResourceDefinitionHitCounter.ExtensibilityPoint.GetMeta) + }, options => options.WithStrictOrdering()); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs index 8c9546742e..822e614a95 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs @@ -9,13 +9,18 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Meta [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class MusicTrackMetaDefinition : JsonApiResourceDefinition { - public MusicTrackMetaDefinition(IResourceGraph resourceGraph) + private readonly ResourceDefinitionHitCounter _hitCounter; + + public MusicTrackMetaDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) : base(resourceGraph) { + _hitCounter = hitCounter; } public override IDictionary GetMeta(MusicTrack resource) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.GetMeta); + return new Dictionary { ["Copyright"] = $"(C) {resource.ReleasedAt.Year}. All rights reserved." diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs index f59c5d3991..c1c2be73b4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs @@ -11,13 +11,18 @@ public sealed class TextLanguageMetaDefinition : JsonApiResourceDefinition GetMeta(TextLanguage resource) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.GetMeta); + return new Dictionary { ["Notice"] = NoticeText diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationHitCounter.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationHitCounter.cs deleted file mode 100644 index baff791fbc..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationHitCounter.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.ResourceDefinitions.Serialization -{ - public sealed class AtomicSerializationHitCounter - { - internal int DeserializeCount { get; private set; } - internal int SerializeCount { get; private set; } - - internal void Reset() - { - DeserializeCount = 0; - SerializeCount = 0; - } - - internal void IncrementDeserializeCount() - { - DeserializeCount++; - } - - internal void IncrementSerializeCount() - { - SerializeCount++; - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs index f7a71cfd6b..c0e2408d63 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs @@ -31,11 +31,11 @@ public AtomicSerializationResourceDefinitionTests(ExampleIntegrationTestContext< { services.AddResourceDefinition(); - services.AddSingleton(); + services.AddSingleton(); services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>)); }); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); hitCounter.Reset(); } @@ -43,6 +43,8 @@ public AtomicSerializationResourceDefinitionTests(ExampleIntegrationTestContext< public async Task Transforms_on_create_resource_with_side_effects() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + List newCompanies = _fakers.RecordCompany.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -112,15 +114,21 @@ await _testContext.RunOnDatabaseAsync(async dbContext => companiesInDatabase[1].CountryOfResidence.Should().Be(newCompanies[1].CountryOfResidence); }); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); - hitCounter.DeserializeCount.Should().Be(2); - hitCounter.SerializeCount.Should().Be(2); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(RecordCompany), ResourceDefinitionHitCounter.ExtensibilityPoint.OnDeserialize), + (typeof(RecordCompany), ResourceDefinitionHitCounter.ExtensibilityPoint.OnDeserialize), + (typeof(RecordCompany), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize), + (typeof(RecordCompany), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize) + }, options => options.WithStrictOrdering()); } [Fact] public async Task Skips_on_create_resource_with_ToOne_relationship() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); string newTrackTitle = _fakers.MusicTrack.Generate().Title; @@ -172,15 +180,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results.Should().HaveCount(1); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(0); + hitCounter.HitExtensibilityPoints.Should().BeEmpty(); } [Fact] public async Task Transforms_on_update_resource_with_side_effects() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + List existingCompanies = _fakers.RecordCompany.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -250,15 +258,21 @@ await _testContext.RunOnDatabaseAsync(async dbContext => companiesInDatabase[1].CountryOfResidence.Should().Be(existingCompanies[1].CountryOfResidence); }); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); - hitCounter.DeserializeCount.Should().Be(2); - hitCounter.SerializeCount.Should().Be(2); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(RecordCompany), ResourceDefinitionHitCounter.ExtensibilityPoint.OnDeserialize), + (typeof(RecordCompany), ResourceDefinitionHitCounter.ExtensibilityPoint.OnDeserialize), + (typeof(RecordCompany), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize), + (typeof(RecordCompany), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize) + }, options => options.WithStrictOrdering()); } [Fact] public async Task Skips_on_update_resource_with_ToOne_relationship() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); RecordCompany existingCompany = _fakers.RecordCompany.Generate(); @@ -309,15 +323,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results.Should().HaveCount(1); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(0); + hitCounter.HitExtensibilityPoints.Should().BeEmpty(); } [Fact] public async Task Skips_on_update_ToOne_relationship() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); RecordCompany existingCompany = _fakers.RecordCompany.Generate(); @@ -359,9 +373,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(0); + hitCounter.HitExtensibilityPoints.Should().BeEmpty(); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/RecordCompanyDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/RecordCompanyDefinition.cs index 2d899b27b3..df221f521a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/RecordCompanyDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/RecordCompanyDefinition.cs @@ -7,9 +7,9 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Resour [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class RecordCompanyDefinition : JsonApiResourceDefinition { - private readonly AtomicSerializationHitCounter _hitCounter; + private readonly ResourceDefinitionHitCounter _hitCounter; - public RecordCompanyDefinition(IResourceGraph resourceGraph, AtomicSerializationHitCounter hitCounter) + public RecordCompanyDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) : base(resourceGraph) { _hitCounter = hitCounter; @@ -17,7 +17,7 @@ public RecordCompanyDefinition(IResourceGraph resourceGraph, AtomicSerialization public override void OnDeserialize(RecordCompany resource) { - _hitCounter.IncrementDeserializeCount(); + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnDeserialize); if (!string.IsNullOrEmpty(resource.Name)) { @@ -27,7 +27,7 @@ public override void OnDeserialize(RecordCompany resource) public override void OnSerialize(RecordCompany resource) { - _hitCounter.IncrementSerializeCount(); + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize); if (!string.IsNullOrEmpty(resource.CountryOfResidence)) { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs index b745a39f7b..db9c793038 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs @@ -28,19 +28,25 @@ public AtomicSparseFieldSetResourceDefinitionTests(ExampleIntegrationTestContext testContext.ConfigureServicesAfterStartup(services => { - services.AddSingleton(); services.AddResourceDefinition(); + + services.AddSingleton(); + services.AddSingleton(); services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>)); }); + + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + hitCounter.Reset(); } [Fact] public async Task Hides_text_in_create_resource_with_side_effects() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var provider = _testContext.Factory.Services.GetRequiredService(); provider.CanViewText = false; - provider.HitCount = 0; List newLyrics = _fakers.Lyric.Generate(2); @@ -94,16 +100,23 @@ public async Task Hides_text_in_create_resource_with_side_effects() responseDocument.Results[1].SingleData.Attributes["format"].Should().Be(newLyrics[1].Format); responseDocument.Results[1].SingleData.Attributes.Should().NotContainKey("text"); - provider.HitCount.Should().Be(4); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Lyric), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Lyric), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Lyric), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Lyric), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) + }, options => options.WithStrictOrdering()); } [Fact] public async Task Hides_text_in_update_resource_with_side_effects() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var provider = _testContext.Factory.Services.GetRequiredService(); provider.CanViewText = false; - provider.HitCount = 0; List existingLyrics = _fakers.Lyric.Generate(2); @@ -161,7 +174,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[1].SingleData.Attributes["format"].Should().Be(existingLyrics[1].Format); responseDocument.Results[1].SingleData.Attributes.Should().NotContainKey("text"); - provider.HitCount.Should().Be(4); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Lyric), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Lyric), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Lyric), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Lyric), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) + }, options => options.WithStrictOrdering()); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/LyricPermissionProvider.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/LyricPermissionProvider.cs index 63620c991a..5593e187d3 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/LyricPermissionProvider.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/LyricPermissionProvider.cs @@ -3,6 +3,5 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Resour public sealed class LyricPermissionProvider { internal bool CanViewText { get; set; } - internal int HitCount { get; set; } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/LyricTextDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/LyricTextDefinition.cs index 7603fd4588..a1943625ea 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/LyricTextDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/LyricTextDefinition.cs @@ -9,16 +9,18 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Resour public sealed class LyricTextDefinition : JsonApiResourceDefinition { private readonly LyricPermissionProvider _lyricPermissionProvider; + private readonly ResourceDefinitionHitCounter _hitCounter; - public LyricTextDefinition(IResourceGraph resourceGraph, LyricPermissionProvider lyricPermissionProvider) + public LyricTextDefinition(IResourceGraph resourceGraph, LyricPermissionProvider lyricPermissionProvider, ResourceDefinitionHitCounter hitCounter) : base(resourceGraph) { _lyricPermissionProvider = lyricPermissionProvider; + _hitCounter = hitCounter; } public override SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExpression existingSparseFieldSet) { - _lyricPermissionProvider.HitCount++; + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet); return _lyricPermissionProvider.CanViewText ? base.OnApplySparseFieldSet(existingSparseFieldSet) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs index bdf5efec57..f968dc1840 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs @@ -6,6 +6,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExampleTests.Startups; +using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -26,13 +27,19 @@ public ResourceMetaTests(ExampleIntegrationTestContext { services.AddResourceDefinition(); + services.AddSingleton(); }); + + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + hitCounter.Reset(); } [Fact] public async Task Returns_resource_meta_from_ResourceDefinition() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + List tickets = _fakers.SupportTicket.Generate(3); tickets[0].Description = "Critical: " + tickets[0].Description; tickets[2].Description = "Critical: " + tickets[2].Description; @@ -56,12 +63,21 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.ManyData[0].Meta.Should().ContainKey("hasHighPriority"); responseDocument.ManyData[1].Meta.Should().BeNull(); responseDocument.ManyData[2].Meta.Should().ContainKey("hasHighPriority"); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(SupportTicket), ResourceDefinitionHitCounter.ExtensibilityPoint.GetMeta), + (typeof(SupportTicket), ResourceDefinitionHitCounter.ExtensibilityPoint.GetMeta), + (typeof(SupportTicket), ResourceDefinitionHitCounter.ExtensibilityPoint.GetMeta) + }, options => options.WithStrictOrdering()); } [Fact] public async Task Returns_resource_meta_from_ResourceDefinition_in_included_resources() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + ProductFamily family = _fakers.ProductFamily.Generate(); family.Tickets = _fakers.SupportTicket.Generate(1); family.Tickets[0].Description = "Critical: " + family.Tickets[0].Description; @@ -84,6 +100,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.SingleData.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Meta.Should().ContainKey("hasHighPriority"); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(SupportTicket), ResourceDefinitionHitCounter.ExtensibilityPoint.GetMeta) + }, options => options.WithStrictOrdering()); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketDefinition.cs index 53304cb09f..cb68f863ec 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketDefinition.cs @@ -9,13 +9,18 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class SupportTicketDefinition : JsonApiResourceDefinition { - public SupportTicketDefinition(IResourceGraph resourceGraph) + private readonly ResourceDefinitionHitCounter _hitCounter; + + public SupportTicketDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) : base(resourceGraph) { + _hitCounter = hitCounter; } public override IDictionary GetMeta(SupportTicket resource) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.GetMeta); + if (resource.Description != null && resource.Description.StartsWith("Critical:", StringComparison.Ordinal)) { return new Dictionary diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetGroupDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetGroupDefinition.cs index f0faad036c..803afbf723 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetGroupDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetGroupDefinition.cs @@ -12,16 +12,21 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Microservices.FireAndFo public sealed class FireForgetGroupDefinition : MessagingGroupDefinition { private readonly MessageBroker _messageBroker; + private readonly ResourceDefinitionHitCounter _hitCounter; private DomainGroup _groupToDelete; - public FireForgetGroupDefinition(IResourceGraph resourceGraph, FireForgetDbContext dbContext, MessageBroker messageBroker) - : base(resourceGraph, dbContext.Users, dbContext.Groups) + public FireForgetGroupDefinition(IResourceGraph resourceGraph, FireForgetDbContext dbContext, MessageBroker messageBroker, + ResourceDefinitionHitCounter hitCounter) + : base(resourceGraph, dbContext.Users, dbContext.Groups, hitCounter) { _messageBroker = messageBroker; + _hitCounter = hitCounter; } public override async Task OnWritingAsync(DomainGroup group, OperationKind operationKind, CancellationToken cancellationToken) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync); + if (operationKind == OperationKind.DeleteResource) { _groupToDelete = await base.GetGroupToDeleteAsync(group.Id, cancellationToken); @@ -30,6 +35,8 @@ public override async Task OnWritingAsync(DomainGroup group, OperationKind opera public override Task OnWriteSucceededAsync(DomainGroup group, OperationKind operationKind, CancellationToken cancellationToken) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync); + return FinishWriteAsync(group, operationKind, cancellationToken); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs index 452d267128..1c22f1834a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs @@ -18,6 +18,9 @@ public sealed partial class FireForgetTests public async Task Create_group_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + string newGroupName = _fakers.DomainGroup.Generate().Name; var requestBody = new @@ -43,11 +46,17 @@ public async Task Create_group_sends_messages() responseDocument.SingleData.Should().NotBeNull(); responseDocument.SingleData.Attributes["name"].Should().Be(newGroupName); - Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); messageBroker.SentMessages.Should().HaveCount(1); + Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + var content = messageBroker.SentMessages[0].GetContentAs(); content.GroupId.Should().Be(newGroupId); content.GroupName.Should().Be(newGroupName); @@ -57,6 +66,9 @@ public async Task Create_group_sends_messages() public async Task Create_group_with_users_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUserWithoutGroup = _fakers.DomainUser.Generate(); DomainUser existingUserWithOtherGroup = _fakers.DomainUser.Generate(); @@ -112,11 +124,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.SingleData.Should().NotBeNull(); responseDocument.SingleData.Attributes["name"].Should().Be(newGroupName); - Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToManyRelationshipAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); messageBroker.SentMessages.Should().HaveCount(3); + Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + var content1 = messageBroker.SentMessages[0].GetContentAs(); content1.GroupId.Should().Be(newGroupId); content1.GroupName.Should().Be(newGroupName); @@ -135,6 +154,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_group_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); string newGroupName = _fakers.DomainGroup.Generate().Name; @@ -168,7 +190,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(1); var content = messageBroker.SentMessages[0].GetContentAs(); @@ -181,6 +209,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_group_with_users_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); DomainUser existingUserWithoutGroup = _fakers.DomainUser.Generate(); @@ -243,7 +274,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToManyRelationshipAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(3); var content1 = messageBroker.SentMessages[0].GetContentAs(); @@ -264,6 +302,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Delete_group_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -282,7 +323,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(1); var content = messageBroker.SentMessages[0].GetContentAs(); @@ -293,6 +339,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Delete_group_with_users_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); existingGroup.Users = _fakers.DomainUser.Generate(1).ToHashSet(); @@ -312,7 +361,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(2); var content1 = messageBroker.SentMessages[0].GetContentAs(); @@ -327,6 +381,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Replace_users_in_group_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); DomainUser existingUserWithoutGroup = _fakers.DomainUser.Generate(); @@ -378,7 +435,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToManyRelationshipAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(3); var content1 = messageBroker.SentMessages[0].GetContentAs(); @@ -399,6 +463,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Add_users_to_group_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); DomainUser existingUserWithoutGroup = _fakers.DomainUser.Generate(); @@ -442,7 +509,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnAddToRelationshipAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(2); var content1 = messageBroker.SentMessages[0].GetContentAs(); @@ -459,6 +532,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Remove_users_from_group_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); DomainUser existingUserWithSameGroup1 = _fakers.DomainUser.Generate(); @@ -495,7 +571,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnRemoveFromRelationshipAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(1); var content = messageBroker.SentMessages[0].GetContentAs(); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs index 7c0fe9c3fb..bbbfc12da8 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs @@ -17,6 +17,9 @@ public sealed partial class FireForgetTests public async Task Create_user_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + string newLoginName = _fakers.DomainUser.Generate().LoginName; string newDisplayName = _fakers.DomainUser.Generate().DisplayName; @@ -45,11 +48,17 @@ public async Task Create_user_sends_messages() responseDocument.SingleData.Attributes["loginName"].Should().Be(newLoginName); responseDocument.SingleData.Attributes["displayName"].Should().Be(newDisplayName); - Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); messageBroker.SentMessages.Should().HaveCount(1); + Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); + var content = messageBroker.SentMessages[0].GetContentAs(); content.UserId.Should().Be(newUserId); content.UserLoginName.Should().Be(newLoginName); @@ -60,6 +69,9 @@ public async Task Create_user_sends_messages() public async Task Create_user_in_group_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); string newLoginName = _fakers.DomainUser.Generate().LoginName; @@ -105,11 +117,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.SingleData.Attributes["loginName"].Should().Be(newLoginName); responseDocument.SingleData.Attributes["displayName"].Should().BeNull(); - Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); messageBroker.SentMessages.Should().HaveCount(2); + Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); + var content1 = messageBroker.SentMessages[0].GetContentAs(); content1.UserId.Should().Be(newUserId); content1.UserLoginName.Should().Be(newLoginName); @@ -124,6 +143,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_user_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); string newLoginName = _fakers.DomainUser.Generate().LoginName; @@ -159,7 +181,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(2); var content1 = messageBroker.SentMessages[0].GetContentAs(); @@ -177,6 +205,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_user_clear_group_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); existingUser.Group = _fakers.DomainGroup.Generate(); @@ -218,7 +249,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(2); var content1 = messageBroker.SentMessages[0].GetContentAs(); @@ -235,6 +273,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_user_add_to_group_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); DomainGroup existingGroup = _fakers.DomainGroup.Generate(); @@ -280,7 +321,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(2); var content1 = messageBroker.SentMessages[0].GetContentAs(); @@ -297,6 +345,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_user_move_to_group_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); existingUser.Group = _fakers.DomainGroup.Generate(); @@ -344,7 +395,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(2); var content1 = messageBroker.SentMessages[0].GetContentAs(); @@ -362,6 +420,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Delete_user_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -380,7 +441,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(1); var content = messageBroker.SentMessages[0].GetContentAs(); @@ -391,6 +457,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Delete_user_in_group_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); existingUser.Group = _fakers.DomainGroup.Generate(); @@ -410,7 +479,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(2); var content1 = messageBroker.SentMessages[0].GetContentAs(); @@ -425,6 +499,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Clear_group_from_user_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); existingUser.Group = _fakers.DomainGroup.Generate(); @@ -449,7 +526,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(1); var content = messageBroker.SentMessages[0].GetContentAs(); @@ -461,6 +545,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Assign_group_to_user_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); DomainGroup existingGroup = _fakers.DomainGroup.Generate(); @@ -489,7 +576,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(1); var content = messageBroker.SentMessages[0].GetContentAs(); @@ -501,6 +595,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Replace_group_for_user_sends_messages() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); existingUser.Group = _fakers.DomainGroup.Generate(); @@ -531,7 +628,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().HaveCount(1); var content = messageBroker.SentMessages[0].GetContentAs(); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs index db834ac227..b36fae4173 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs @@ -30,16 +30,23 @@ public FireForgetTests(ExampleIntegrationTestContext(); services.AddSingleton(); + services.AddSingleton(); }); var messageBroker = _testContext.Factory.Services.GetRequiredService(); messageBroker.Reset(); + + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + hitCounter.Reset(); } [Fact] public async Task Does_not_send_message_on_write_error() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); + string missingUserId = Guid.NewGuid().ToString(); string route = "/domainUsers/" + missingUserId; @@ -57,7 +64,11 @@ public async Task Does_not_send_message_on_write_error() error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'domainUsers' with ID '{missingUserId}' does not exist."); - var messageBroker = _testContext.Factory.Services.GetRequiredService(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + messageBroker.SentMessages.Should().BeEmpty(); } @@ -65,6 +76,8 @@ public async Task Does_not_send_message_on_write_error() public async Task Does_not_rollback_on_message_delivery_error() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var messageBroker = _testContext.Factory.Services.GetRequiredService(); messageBroker.SimulateFailure = true; @@ -92,6 +105,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Message delivery failed."); error.Detail.Should().BeNull(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync) + }, options => options.WithStrictOrdering()); + + messageBroker.SentMessages.Should().HaveCount(1); + await _testContext.RunOnDatabaseAsync(async dbContext => { DomainUser user = await dbContext.Users.FirstWithIdOrDefaultAsync(existingUser.Id); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetUserDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetUserDefinition.cs index e07bf899e7..72df77a2e2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetUserDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetUserDefinition.cs @@ -12,16 +12,21 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Microservices.FireAndFo public sealed class FireForgetUserDefinition : MessagingUserDefinition { private readonly MessageBroker _messageBroker; + private readonly ResourceDefinitionHitCounter _hitCounter; private DomainUser _userToDelete; - public FireForgetUserDefinition(IResourceGraph resourceGraph, FireForgetDbContext dbContext, MessageBroker messageBroker) - : base(resourceGraph, dbContext.Users) + public FireForgetUserDefinition(IResourceGraph resourceGraph, FireForgetDbContext dbContext, MessageBroker messageBroker, + ResourceDefinitionHitCounter hitCounter) + : base(resourceGraph, dbContext.Users, hitCounter) { _messageBroker = messageBroker; + _hitCounter = hitCounter; } public override async Task OnWritingAsync(DomainUser user, OperationKind operationKind, CancellationToken cancellationToken) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync); + if (operationKind == OperationKind.DeleteResource) { _userToDelete = await base.GetUserToDeleteAsync(user.Id, cancellationToken); @@ -30,6 +35,8 @@ public override async Task OnWritingAsync(DomainUser user, OperationKind operati public override Task OnWriteSucceededAsync(DomainUser user, OperationKind operationKind, CancellationToken cancellationToken) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync); + return FinishWriteAsync(user, operationKind, cancellationToken); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/MessageBroker.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/MessageBroker.cs index 1de7fed1a1..594697b8a4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/MessageBroker.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/MessageBroker.cs @@ -22,6 +22,9 @@ internal void Reset() internal Task PostMessageAsync(OutgoingMessage message, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + SentMessages.Add(message); + if (SimulateFailure) { throw new JsonApiException(new Error(HttpStatusCode.ServiceUnavailable) @@ -30,10 +33,6 @@ internal Task PostMessageAsync(OutgoingMessage message, CancellationToken cancel }); } - cancellationToken.ThrowIfCancellationRequested(); - - SentMessages.Add(message); - return Task.CompletedTask; } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingGroupDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingGroupDefinition.cs index 9d99fe6793..cac44d1c03 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingGroupDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingGroupDefinition.cs @@ -16,19 +16,24 @@ public abstract class MessagingGroupDefinition : JsonApiResourceDefinition _userSet; private readonly DbSet _groupSet; + private readonly ResourceDefinitionHitCounter _hitCounter; private readonly List _pendingMessages = new List(); private string _beforeGroupName; - protected MessagingGroupDefinition(IResourceGraph resourceGraph, DbSet userSet, DbSet groupSet) + protected MessagingGroupDefinition(IResourceGraph resourceGraph, DbSet userSet, DbSet groupSet, + ResourceDefinitionHitCounter hitCounter) : base(resourceGraph) { _userSet = userSet; _groupSet = groupSet; + _hitCounter = hitCounter; } public override Task OnPrepareWriteAsync(DomainGroup group, OperationKind operationKind, CancellationToken cancellationToken) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync); + if (operationKind == OperationKind.CreateResource) { group.Id = Guid.NewGuid(); @@ -44,6 +49,8 @@ public override Task OnPrepareWriteAsync(DomainGroup group, OperationKind operat public override async Task OnSetToManyRelationshipAsync(DomainGroup group, HasManyAttribute hasManyRelationship, ISet rightResourceIds, OperationKind operationKind, CancellationToken cancellationToken) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToManyRelationshipAsync); + if (hasManyRelationship.Property.Name == nameof(DomainGroup.Users)) { HashSet rightUserIds = rightResourceIds.Select(resource => (Guid)resource.GetTypedId()).ToHashSet(); @@ -98,6 +105,8 @@ public override async Task OnSetToManyRelationshipAsync(DomainGroup group, HasMa public override async Task OnAddToRelationshipAsync(Guid groupId, HasManyAttribute hasManyRelationship, ISet rightResourceIds, CancellationToken cancellationToken) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnAddToRelationshipAsync); + if (hasManyRelationship.Property.Name == nameof(DomainGroup.Users)) { HashSet rightUserIds = rightResourceIds.Select(resource => (Guid)resource.GetTypedId()).ToHashSet(); @@ -138,6 +147,8 @@ public override async Task OnAddToRelationshipAsync(Guid groupId, HasManyAttribu public override Task OnRemoveFromRelationshipAsync(DomainGroup group, HasManyAttribute hasManyRelationship, ISet rightResourceIds, CancellationToken cancellationToken) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnRemoveFromRelationshipAsync); + if (hasManyRelationship.Property.Name == nameof(DomainGroup.Users)) { HashSet rightUserIds = rightResourceIds.Select(resource => (Guid)resource.GetTypedId()).ToHashSet(); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingUserDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingUserDefinition.cs index 5b2dc27b7f..2e48d9e3d7 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingUserDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingUserDefinition.cs @@ -14,19 +14,23 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Microservices public abstract class MessagingUserDefinition : JsonApiResourceDefinition { private readonly DbSet _userSet; + private readonly ResourceDefinitionHitCounter _hitCounter; private readonly List _pendingMessages = new List(); private string _beforeLoginName; private string _beforeDisplayName; - protected MessagingUserDefinition(IResourceGraph resourceGraph, DbSet userSet) + protected MessagingUserDefinition(IResourceGraph resourceGraph, DbSet userSet, ResourceDefinitionHitCounter hitCounter) : base(resourceGraph) { _userSet = userSet; + _hitCounter = hitCounter; } public override Task OnPrepareWriteAsync(DomainUser user, OperationKind operationKind, CancellationToken cancellationToken) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync); + if (operationKind == OperationKind.CreateResource) { user.Id = Guid.NewGuid(); @@ -43,6 +47,8 @@ public override Task OnPrepareWriteAsync(DomainUser user, OperationKind operatio public override Task OnSetToOneRelationshipAsync(DomainUser user, HasOneAttribute hasOneRelationship, IIdentifiable rightResourceId, OperationKind operationKind, CancellationToken cancellationToken) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync); + if (hasOneRelationship.Property.Name == nameof(DomainUser.Group)) { var afterGroupId = (Guid?)rightResourceId?.GetTypedId(); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxGroupDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxGroupDefinition.cs index 52245827bc..61e685859b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxGroupDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxGroupDefinition.cs @@ -11,16 +11,20 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Microservices.Transacti [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class OutboxGroupDefinition : MessagingGroupDefinition { + private readonly ResourceDefinitionHitCounter _hitCounter; private readonly DbSet _outboxMessageSet; - public OutboxGroupDefinition(IResourceGraph resourceGraph, OutboxDbContext dbContext) - : base(resourceGraph, dbContext.Users, dbContext.Groups) + public OutboxGroupDefinition(IResourceGraph resourceGraph, OutboxDbContext dbContext, ResourceDefinitionHitCounter hitCounter) + : base(resourceGraph, dbContext.Users, dbContext.Groups, hitCounter) { + _hitCounter = hitCounter; _outboxMessageSet = dbContext.OutboxMessages; } public override Task OnWritingAsync(DomainGroup group, OperationKind operationKind, CancellationToken cancellationToken) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync); + return FinishWriteAsync(group, operationKind, cancellationToken); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs index a3bf1ad6b1..cc71a19f6d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs @@ -8,6 +8,7 @@ using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExampleTests.IntegrationTests.Microservices.Messages; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -19,6 +20,8 @@ public sealed partial class OutboxTests public async Task Create_group_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + string newGroupName = _fakers.DomainGroup.Generate().Name; await _testContext.RunOnDatabaseAsync(async dbContext => @@ -49,6 +52,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.SingleData.Should().NotBeNull(); responseDocument.SingleData.Attributes["name"].Should().Be(newGroupName); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -66,6 +75,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Create_group_with_users_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUserWithoutGroup = _fakers.DomainUser.Generate(); DomainUser existingUserWithOtherGroup = _fakers.DomainUser.Generate(); @@ -122,6 +133,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.SingleData.Should().NotBeNull(); responseDocument.SingleData.Attributes["name"].Should().Be(newGroupName); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToManyRelationshipAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -148,6 +166,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_group_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); string newGroupName = _fakers.DomainGroup.Generate().Name; @@ -182,6 +202,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -198,6 +224,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_group_with_users_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); DomainUser existingUserWithoutGroup = _fakers.DomainUser.Generate(); @@ -261,6 +289,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToManyRelationshipAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -285,6 +320,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Delete_group_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -304,6 +341,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -318,6 +360,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Delete_group_with_users_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); existingGroup.Users = _fakers.DomainUser.Generate(1).ToHashSet(); @@ -338,6 +382,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -356,6 +405,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Replace_users_in_group_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); DomainUser existingUserWithoutGroup = _fakers.DomainUser.Generate(); @@ -408,6 +459,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToManyRelationshipAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -432,6 +490,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Add_users_to_group_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); DomainUser existingUserWithoutGroup = _fakers.DomainUser.Generate(); @@ -476,6 +536,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnAddToRelationshipAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -496,6 +562,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Remove_users_from_group_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); DomainUser existingUserWithSameGroup1 = _fakers.DomainUser.Generate(); @@ -533,6 +601,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnRemoveFromRelationshipAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs index 599d99d2a1..d392a4fb32 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs @@ -8,6 +8,7 @@ using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExampleTests.IntegrationTests.Microservices.Messages; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -19,6 +20,8 @@ public sealed partial class OutboxTests public async Task Create_user_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + string newLoginName = _fakers.DomainUser.Generate().LoginName; string newDisplayName = _fakers.DomainUser.Generate().DisplayName; @@ -52,6 +55,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.SingleData.Attributes["loginName"].Should().Be(newLoginName); responseDocument.SingleData.Attributes["displayName"].Should().Be(newDisplayName); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -70,6 +79,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Create_user_in_group_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); string newLoginName = _fakers.DomainUser.Generate().LoginName; @@ -116,6 +127,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.SingleData.Attributes["loginName"].Should().Be(newLoginName); responseDocument.SingleData.Attributes["displayName"].Should().BeNull(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -138,6 +156,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_user_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); string newLoginName = _fakers.DomainUser.Generate().LoginName; @@ -174,6 +194,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -195,6 +221,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_user_clear_group_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); existingUser.Group = _fakers.DomainGroup.Generate(); @@ -237,6 +265,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -257,6 +292,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_user_add_to_group_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); DomainGroup existingGroup = _fakers.DomainGroup.Generate(); @@ -303,6 +340,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -323,6 +367,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_user_move_to_group_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); existingUser.Group = _fakers.DomainGroup.Generate(); @@ -371,6 +417,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -392,6 +445,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Delete_user_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -411,6 +466,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -425,6 +485,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Delete_user_in_group_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); existingUser.Group = _fakers.DomainGroup.Generate(); @@ -445,6 +507,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -463,6 +530,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Clear_group_from_user_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); existingUser.Group = _fakers.DomainGroup.Generate(); @@ -488,6 +557,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -503,6 +579,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Assign_group_to_user_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); DomainGroup existingGroup = _fakers.DomainGroup.Generate(); @@ -532,6 +610,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); @@ -547,6 +632,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Replace_group_for_user_writes_to_outbox() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainUser existingUser = _fakers.DomainUser.Generate(); existingUser.Group = _fakers.DomainGroup.Generate(); @@ -578,6 +665,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync), + (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs index a351cefbfe..875af7adf9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs @@ -10,6 +10,7 @@ using JsonApiDotNetCoreExampleTests.IntegrationTests.Microservices.Messages; using JsonApiDotNetCoreExampleTests.Startups; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -33,13 +34,20 @@ public OutboxTests(ExampleIntegrationTestContext(); services.AddResourceDefinition(); + + services.AddSingleton(); }); + + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + hitCounter.Reset(); } [Fact] public async Task Does_not_add_to_outbox_on_write_error() { // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + DomainGroup existingGroup = _fakers.DomainGroup.Generate(); DomainUser existingUser = _fakers.DomainUser.Generate(); @@ -85,6 +93,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'domainUsers' with ID '{missingUserId}' in relationship 'users' does not exist."); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnAddToRelationshipAsync), + (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) + }, options => options.WithStrictOrdering()); + await _testContext.RunOnDatabaseAsync(async dbContext => { List messages = await dbContext.OutboxMessages.OrderBy(message => message.Id).ToListAsync(); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxUserDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxUserDefinition.cs index deae0a9fba..88b8a2e865 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxUserDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxUserDefinition.cs @@ -11,16 +11,20 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Microservices.Transacti [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class OutboxUserDefinition : MessagingUserDefinition { + private readonly ResourceDefinitionHitCounter _hitCounter; private readonly DbSet _outboxMessageSet; - public OutboxUserDefinition(IResourceGraph resourceGraph, OutboxDbContext dbContext) - : base(resourceGraph, dbContext.Users) + public OutboxUserDefinition(IResourceGraph resourceGraph, OutboxDbContext dbContext, ResourceDefinitionHitCounter hitCounter) + : base(resourceGraph, dbContext.Users, hitCounter) { + _hitCounter = hitCounter; _outboxMessageSet = dbContext.OutboxMessages; } public override Task OnWritingAsync(DomainUser user, OperationKind operationKind, CancellationToken cancellationToken) { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync); + return FinishWriteAsync(user, operationKind, cancellationToken); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs index 05ac775540..f7aa2c00a4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs @@ -149,7 +149,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_filter_on_HasOne_relationship() { // Arrange - List posts = _fakers.BlogPost.Generate(2); + List posts = _fakers.BlogPost.Generate(3); posts[0].Author = _fakers.WebAccount.Generate(); posts[0].Author.UserName = "Conner"; posts[1].Author = _fakers.WebAccount.Generate(); @@ -162,7 +162,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - const string route = "/blogPosts?include=author&filter=equals(author.userName,'Smith')"; + const string route = "/blogPosts?include=author&filter=or(equals(author.userName,'Smith'),equals(author,null))"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -170,9 +170,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.Included.Should().HaveCount(1); + responseDocument.ManyData.Should().HaveCount(2); + responseDocument.ManyData.Should().ContainSingle(post => post.Id == posts[1].StringId); + responseDocument.ManyData.Should().ContainSingle(post => post.Id == posts[2].StringId); + responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(posts[1].Author.StringId); } @@ -327,8 +329,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.ManyData.Should().HaveCount(1); - responseDocument.Included.Should().HaveCount(1); + responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(blog.Posts[1].StringId); } @@ -357,8 +359,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.SingleData.Should().NotBeNull(); - responseDocument.Included.Should().HaveCount(1); + responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(blog.Owner.Posts[1].StringId); } @@ -408,8 +410,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.ManyData.Should().HaveCount(2); - responseDocument.Included.Should().HaveCount(1); + responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(posts[1].BlogPostLabels.First().Label.StringId); } @@ -439,8 +441,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.ManyData.Should().HaveCount(1); - responseDocument.Included.Should().HaveCount(2); + responseDocument.Included.Should().HaveCount(2); responseDocument.Included[0].Id.Should().Be(blog.Owner.StringId); responseDocument.Included[1].Id.Should().Be(blog.Owner.Posts[1].StringId); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitionHitCounter.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitionHitCounter.cs new file mode 100644 index 0000000000..fb09066912 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitionHitCounter.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests +{ + /// + /// This is used solely in our tests, so we can assert which calls were made. + /// + public sealed class ResourceDefinitionHitCounter + { + internal IList<(Type, ExtensibilityPoint)> HitExtensibilityPoints { get; } = new List<(Type, ExtensibilityPoint)>(); + + internal void TrackInvocation(ExtensibilityPoint extensibilityPoint) + where TResource : IIdentifiable + { + HitExtensibilityPoints.Add((typeof(TResource), extensibilityPoint)); + } + + internal void Reset() + { + HitExtensibilityPoints.Clear(); + } + + internal enum ExtensibilityPoint + { + OnApplyIncludes, + OnApplyFilter, + OnApplySort, + OnApplyPagination, + OnApplySparseFieldSet, + OnRegisterQueryableHandlersForQueryStringParameters, + GetMeta, + OnPrepareWriteAsync, + OnSetToOneRelationshipAsync, + OnSetToManyRelationshipAsync, + OnAddToRelationshipAsync, + OnRemoveFromRelationshipAsync, + OnWritingAsync, + OnWriteSucceededAsync, + OnDeserialize, + OnSerialize + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/CallableResource.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/CallableResource.cs deleted file mode 100644 index c54bd08317..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/CallableResource.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class CallableResource : Identifiable - { - [Attr] - public string Label { get; set; } - - [Attr] - public int PercentageComplete { get; set; } - - [Attr] - public string Status => $"{PercentageComplete}% completed."; - - [Attr] - public int RiskLevel { get; set; } - - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowSort)] - public DateTime CreatedAt { get; set; } - - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowSort)] - public DateTime ModifiedAt { get; set; } - - [Attr(Capabilities = AttrCapabilities.None)] - public bool IsDeleted { get; set; } - - [HasMany] - public ICollection Children { get; set; } - - [HasOne] - public CallableResource Owner { get; set; } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/CallableResourceDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/CallableResourceDefinition.cs deleted file mode 100644 index 4f00b100b5..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/CallableResourceDefinition.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Net; -using JetBrains.Annotations; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.Extensions.Primitives; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading -{ - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class CallableResourceDefinition : JsonApiResourceDefinition - { - private static readonly PageSize MaxPageSize = new PageSize(5); - private readonly IUserRolesService _userRolesService; - - public CallableResourceDefinition(IResourceGraph resourceGraph, IUserRolesService userRolesService) - : base(resourceGraph) - { - // This constructor will be resolved from the container, which means - // you can take on any dependency that is also defined in the container. - - _userRolesService = userRolesService; - } - - public override IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes) - { - // Use case: prevent including owner if user has insufficient permissions. - - if (!_userRolesService.AllowIncludeOwner && existingIncludes.Any(include => include.Relationship.Property.Name == nameof(CallableResource.Owner))) - { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) - { - Title = "Including owner is not permitted." - }); - } - - return existingIncludes; - } - - public override FilterExpression OnApplyFilter(FilterExpression existingFilter) - { - // Use case: automatically exclude deleted resources for all requests. - - ResourceContext resourceContext = ResourceGraph.GetResourceContext(); - AttrAttribute isDeletedAttribute = resourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(CallableResource.IsDeleted)); - - var isNotDeleted = new ComparisonExpression(ComparisonOperator.Equals, new ResourceFieldChainExpression(isDeletedAttribute), - new LiteralConstantExpression(bool.FalseString)); - - return existingFilter == null - ? (FilterExpression)isNotDeleted - : new LogicalExpression(LogicalOperator.And, ArrayFactory.Create(isNotDeleted, existingFilter)); - } - - public override SortExpression OnApplySort(SortExpression existingSort) - { - // Use case: set a default sort order when none was specified in query string. - - if (existingSort != null) - { - return existingSort; - } - - return CreateSortExpressionFromLambda(new PropertySortOrder - { - (resource => resource.Label, ListSortDirection.Ascending), - (resource => resource.ModifiedAt, ListSortDirection.Descending) - }); - } - - public override PaginationExpression OnApplyPagination(PaginationExpression existingPagination) - { - // Use case: enforce a page size of 5 or less for this resource type. - - if (existingPagination != null) - { - PageSize pageSize = existingPagination.PageSize?.Value <= MaxPageSize.Value ? existingPagination.PageSize : MaxPageSize; - return new PaginationExpression(existingPagination.PageNumber, pageSize); - } - - 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() - { - // Use case: 'isHighRisk' query string parameter can be used to add extra filter on IQueryable. - - return new QueryStringParameterHandlers - { - ["isHighRisk"] = FilterByHighRisk - }; - } - - private static IQueryable FilterByHighRisk(IQueryable source, StringValues parameterValue) - { - bool isFilterOnHighRisk = bool.Parse(parameterValue); - return isFilterOnHighRisk ? source.Where(resource => resource.RiskLevel >= 5) : source.Where(resource => resource.RiskLevel < 5); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/IClientSettingsProvider.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/IClientSettingsProvider.cs new file mode 100644 index 0000000000..e5ed1386b9 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/IClientSettingsProvider.cs @@ -0,0 +1,9 @@ +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading +{ + public interface IClientSettingsProvider + { + bool IsIncludePlanetMoonsBlocked { get; } + bool ArePlanetsWithPrivateNameHidden { get; } + bool IsMoonOrbitingPlanetAutoIncluded { get; } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/IUserRolesService.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/IUserRolesService.cs deleted file mode 100644 index ceb2e170ab..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/IUserRolesService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading -{ - public interface IUserRolesService - { - bool AllowIncludeOwner { get; } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/Moon.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/Moon.cs new file mode 100644 index 0000000000..afdca8b7ce --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/Moon.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Moon : Identifiable + { + [Attr] + public string Name { get; set; } + + [Attr] + public decimal SolarRadius { get; set; } + + [HasOne] + public Planet OrbitsAround { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs new file mode 100644 index 0000000000..96152e3025 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading +{ + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class MoonDefinition : JsonApiResourceDefinition + { + private readonly IClientSettingsProvider _clientSettingsProvider; + private readonly ResourceDefinitionHitCounter _hitCounter; + + public MoonDefinition(IResourceGraph resourceGraph, IClientSettingsProvider clientSettingsProvider, ResourceDefinitionHitCounter hitCounter) + : base(resourceGraph) + { + // This constructor will be resolved from the container, which means + // you can take on any dependency that is also defined in the container. + + _clientSettingsProvider = clientSettingsProvider; + _hitCounter = hitCounter; + } + + public override IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes) + { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes); + + if (!_clientSettingsProvider.IsMoonOrbitingPlanetAutoIncluded || + existingIncludes.Any(include => include.Relationship.Property.Name == nameof(Moon.OrbitsAround))) + { + return existingIncludes; + } + + RelationshipAttribute orbitsAroundRelationship = + ResourceContext.Relationships.Single(relationship => relationship.Property.Name == nameof(Moon.OrbitsAround)); + + return new List(existingIncludes) + { + new IncludeElementExpression(orbitsAroundRelationship) + }; + } + + public override QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters() + { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnRegisterQueryableHandlersForQueryStringParameters); + + return new QueryStringParameterHandlers + { + ["isLargerThanTheSun"] = FilterByRadius + }; + } + + private static IQueryable FilterByRadius(IQueryable source, StringValues parameterValue) + { + bool isFilterOnLargerThan = bool.Parse(parameterValue); + return isFilterOnLargerThan ? source.Where(moon => moon.SolarRadius > 1m) : source.Where(moon => moon.SolarRadius <= 1m); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/CallableResourcesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/MoonsController.cs similarity index 57% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/CallableResourcesController.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/MoonsController.cs index 82707df6dc..fe0e1e9cd6 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/CallableResourcesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/MoonsController.cs @@ -5,9 +5,9 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading { - public sealed class CallableResourcesController : JsonApiController + public sealed class MoonsController : JsonApiController { - public CallableResourcesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) + public MoonsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/Planet.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/Planet.cs new file mode 100644 index 0000000000..d9f51627c0 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/Planet.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Planet : Identifiable + { + [Attr] + public string PublicName { get; set; } + + [Attr] + public string PrivateName { get; set; } + + [Attr] + public bool HasRingSystem { get; set; } + + [Attr] + public decimal SolarMass { get; set; } + + [HasMany] + public ISet Moons { get; set; } + + [HasOne] + public Star BelongsTo { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs new file mode 100644 index 0000000000..de0da36670 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using JetBrains.Annotations; +using JsonApiDotNetCore; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading +{ + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class PlanetDefinition : JsonApiResourceDefinition + { + private readonly IClientSettingsProvider _clientSettingsProvider; + private readonly ResourceDefinitionHitCounter _hitCounter; + + public PlanetDefinition(IResourceGraph resourceGraph, IClientSettingsProvider clientSettingsProvider, ResourceDefinitionHitCounter hitCounter) + : base(resourceGraph) + { + // This constructor will be resolved from the container, which means + // you can take on any dependency that is also defined in the container. + + _clientSettingsProvider = clientSettingsProvider; + _hitCounter = hitCounter; + } + + public override IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes) + { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes); + + if (_clientSettingsProvider.IsIncludePlanetMoonsBlocked && + existingIncludes.Any(include => include.Relationship.Property.Name == nameof(Planet.Moons))) + { + throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + { + Title = "Including moons is not permitted." + }); + } + + return existingIncludes; + } + + public override FilterExpression OnApplyFilter(FilterExpression existingFilter) + { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyFilter); + + if (_clientSettingsProvider.ArePlanetsWithPrivateNameHidden) + { + AttrAttribute privateNameAttribute = ResourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Planet.PrivateName)); + + FilterExpression hasNoPrivateName = new ComparisonExpression(ComparisonOperator.Equals, new ResourceFieldChainExpression(privateNameAttribute), + new NullConstantExpression()); + + return existingFilter == null + ? hasNoPrivateName + : new LogicalExpression(LogicalOperator.And, ArrayFactory.Create(hasNoPrivateName, existingFilter)); + } + + return existingFilter; + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/PlanetsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/PlanetsController.cs new file mode 100644 index 0000000000..d96f0fd2ef --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/PlanetsController.cs @@ -0,0 +1,15 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading +{ + public sealed class PlanetsController : JsonApiController + { + public PlanetsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionQueryCallbackTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionQueryCallbackTests.cs deleted file mode 100644 index 0f7e8e157a..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionQueryCallbackTests.cs +++ /dev/null @@ -1,561 +0,0 @@ -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using FluentAssertions; -using FluentAssertions.Extensions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreExampleTests.Startups; -using Microsoft.Extensions.DependencyInjection; -using TestBuildingBlocks; -using Xunit; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading -{ - public sealed class ResourceDefinitionQueryCallbackTests - : IClassFixture, CallableDbContext>> - { - private readonly ExampleIntegrationTestContext, CallableDbContext> _testContext; - - public ResourceDefinitionQueryCallbackTests(ExampleIntegrationTestContext, CallableDbContext> testContext) - { - _testContext = testContext; - - testContext.UseController(); - - testContext.ConfigureServicesAfterStartup(services => - { - services.AddResourceDefinition(); - services.AddSingleton(); - }); - - var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); - options.IncludeTotalResourceCount = true; - } - - [Fact] - public async Task Include_from_resource_definition_has_blocked_capability() - { - // Arrange - var userRolesService = (FakeUserRolesService)_testContext.Factory.Services.GetRequiredService(); - userRolesService.AllowIncludeOwner = false; - - var resource = new CallableResource - { - Label = "A", - IsDeleted = false - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - await dbContext.ClearTableAsync(); - dbContext.CallableResources.Add(resource); - await dbContext.SaveChangesAsync(); - }); - - const string route = "/callableResources?include=owner"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Including owner is not permitted."); - error.Detail.Should().BeNull(); - } - - [Fact] - public async Task Filter_from_resource_definition_is_applied() - { - // Arrange - var resources = new List - { - new CallableResource - { - Label = "A", - IsDeleted = true - }, - new CallableResource - { - Label = "A", - IsDeleted = false - }, - new CallableResource - { - Label = "B", - IsDeleted = true - }, - new CallableResource - { - Label = "B", - IsDeleted = false - } - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - await dbContext.ClearTableAsync(); - dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); - }); - - const string route = "/callableResources"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(resources[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(resources[3].StringId); - - responseDocument.Meta["totalResources"].Should().Be(2); - } - - [Fact] - public async Task Filter_from_resource_definition_and_query_string_are_applied() - { - // Arrange - var resources = new List - { - new CallableResource - { - Label = "A", - IsDeleted = true - }, - new CallableResource - { - Label = "A", - IsDeleted = false - }, - new CallableResource - { - Label = "B", - IsDeleted = true - }, - new CallableResource - { - Label = "B", - IsDeleted = false - } - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - await dbContext.ClearTableAsync(); - dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); - }); - - const string route = "/callableResources?filter=equals(label,'B')"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(resources[3].StringId); - - responseDocument.Meta["totalResources"].Should().Be(1); - } - - [Fact] - public async Task Sort_from_resource_definition_is_applied() - { - // Arrange - var resources = new List - { - new CallableResource - { - Label = "A", - CreatedAt = 1.January(2001), - ModifiedAt = 15.January(2001) - }, - new CallableResource - { - Label = "A", - CreatedAt = 1.January(2001), - ModifiedAt = 15.December(2001) - }, - new CallableResource - { - Label = "B", - CreatedAt = 1.February(2001), - ModifiedAt = 15.January(2001) - } - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - await dbContext.ClearTableAsync(); - dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); - }); - - const string route = "/callableResources"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(resources[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(resources[0].StringId); - responseDocument.ManyData[2].Id.Should().Be(resources[2].StringId); - } - - [Fact] - public async Task Sort_from_query_string_is_applied() - { - // Arrange - var resources = new List - { - new CallableResource - { - Label = "A", - CreatedAt = 1.January(2001), - ModifiedAt = 15.January(2001) - }, - new CallableResource - { - Label = "A", - CreatedAt = 1.January(2001), - ModifiedAt = 15.December(2001) - }, - new CallableResource - { - Label = "B", - CreatedAt = 1.February(2001), - ModifiedAt = 15.January(2001) - } - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - await dbContext.ClearTableAsync(); - dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); - }); - - const string route = "/callableResources?sort=-createdAt,modifiedAt"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(resources[2].StringId); - responseDocument.ManyData[1].Id.Should().Be(resources[0].StringId); - responseDocument.ManyData[2].Id.Should().Be(resources[1].StringId); - } - - [Fact] - public async Task Page_size_from_resource_definition_is_applied() - { - // Arrange - var resources = new List(); - - for (int index = 0; index < 10; index++) - { - resources.Add(new CallableResource()); - } - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - await dbContext.ClearTableAsync(); - dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); - }); - - const string route = "/callableResources?page[size]=8"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.ManyData.Should().HaveCount(5); - } - - [Fact] - public async Task Attribute_inclusion_from_resource_definition_is_applied_for_empty_query_string() - { - // Arrange - var resource = new CallableResource - { - Label = "X", - PercentageComplete = 5 - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.CallableResources.Add(resource); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/callableResources/{resource.StringId}"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(resource.StringId); - responseDocument.SingleData.Attributes["label"].Should().Be(resource.Label); - responseDocument.SingleData.Attributes["percentageComplete"].Should().Be(resource.PercentageComplete); - } - - [Fact] - public async Task Attribute_inclusion_from_resource_definition_is_applied_for_non_empty_query_string() - { - // Arrange - var resource = new CallableResource - { - Label = "X", - PercentageComplete = 5 - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.CallableResources.Add(resource); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/callableResources/{resource.StringId}?fields[callableResources]=label,status"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(resource.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(2); - responseDocument.SingleData.Attributes["label"].Should().Be(resource.Label); - responseDocument.SingleData.Attributes["status"].Should().Be("5% completed."); - responseDocument.SingleData.Relationships.Should().BeNull(); - } - - [Fact] - public async Task Attribute_exclusion_from_resource_definition_is_applied_for_empty_query_string() - { - // Arrange - var resource = new CallableResource - { - Label = "X", - RiskLevel = 3 - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.CallableResources.Add(resource); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/callableResources/{resource.StringId}"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(resource.StringId); - responseDocument.SingleData.Attributes["label"].Should().Be(resource.Label); - responseDocument.SingleData.Attributes.Should().NotContainKey("riskLevel"); - } - - [Fact] - public async Task Attribute_exclusion_from_resource_definition_is_applied_for_non_empty_query_string() - { - // Arrange - var resource = new CallableResource - { - Label = "X", - RiskLevel = 3 - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.CallableResources.Add(resource); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/callableResources/{resource.StringId}?fields[callableResources]=label,riskLevel"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(resource.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["label"].Should().Be(resource.Label); - responseDocument.SingleData.Relationships.Should().BeNull(); - } - - [Fact] - public async Task Queryable_parameter_handler_from_resource_definition_is_applied() - { - // Arrange - var resources = new List - { - new CallableResource - { - Label = "A", - RiskLevel = 3 - }, - new CallableResource - { - Label = "A", - RiskLevel = 8 - }, - new CallableResource - { - Label = "B", - RiskLevel = 3 - }, - new CallableResource - { - Label = "B", - RiskLevel = 8 - } - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - await dbContext.ClearTableAsync(); - dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); - }); - - const string route = "/callableResources?isHighRisk=true"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(resources[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(resources[3].StringId); - } - - [Fact] - public async Task Queryable_parameter_handler_from_resource_definition_and_query_string_filter_are_applied() - { - // Arrange - var resources = new List - { - new CallableResource - { - Label = "A", - RiskLevel = 3 - }, - new CallableResource - { - Label = "A", - RiskLevel = 8 - }, - new CallableResource - { - Label = "B", - RiskLevel = 3 - }, - new CallableResource - { - Label = "B", - RiskLevel = 8 - } - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - await dbContext.ClearTableAsync(); - dbContext.CallableResources.AddRange(resources); - await dbContext.SaveChangesAsync(); - }); - - const string route = "/callableResources?isHighRisk=false&filter=equals(label,'B')"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(resources[2].StringId); - } - - [Fact] - public async Task Queryable_parameter_handler_from_resource_definition_is_not_applied_on_secondary_request() - { - // Arrange - var resource = new CallableResource - { - RiskLevel = 3, - Children = new List - { - new CallableResource - { - RiskLevel = 3 - }, - new CallableResource - { - RiskLevel = 8 - } - } - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.CallableResources.Add(resource); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/callableResources/{resource.StringId}/children?isHighRisk=true"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - - Error 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 - { - public bool AllowIncludeOwner { get; set; } = true; - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs new file mode 100644 index 0000000000..9261f41e29 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs @@ -0,0 +1,624 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreExampleTests.Startups; +using Microsoft.Extensions.DependencyInjection; +using TestBuildingBlocks; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading +{ + public sealed class ResourceDefinitionReadTests : IClassFixture, UniverseDbContext>> + { + private readonly ExampleIntegrationTestContext, UniverseDbContext> _testContext; + private readonly UniverseFakers _fakers = new UniverseFakers(); + + public ResourceDefinitionReadTests(ExampleIntegrationTestContext, UniverseDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.UseController(); + testContext.UseController(); + + testContext.ConfigureServicesAfterStartup(services => + { + services.AddResourceDefinition(); + services.AddResourceDefinition(); + services.AddResourceDefinition(); + + services.AddSingleton(); + services.AddSingleton(); + }); + + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); + options.IncludeTotalResourceCount = true; + + var settingsProvider = (TestClientSettingsProvider)testContext.Factory.Services.GetRequiredService(); + settingsProvider.ResetToDefaults(); + + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + hitCounter.Reset(); + } + + [Fact] + public async Task Include_from_resource_definition_is_blocked() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + var settingsProvider = (TestClientSettingsProvider)_testContext.Factory.Services.GetRequiredService(); + settingsProvider.BlockIncludePlanetMoons(); + + Planet planet = _fakers.Planet.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.Planets.Add(planet); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/planets?include=moons"; + + // Act + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + + Error error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error.Title.Should().Be("Including moons is not permitted."); + error.Detail.Should().BeNull(); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Planet), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyFilter), + (typeof(Planet), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyFilter), + (typeof(Planet), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Include_from_resource_definition_is_added() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + var settingsProvider = (TestClientSettingsProvider)_testContext.Factory.Services.GetRequiredService(); + settingsProvider.AutoIncludeOrbitingPlanetForMoons(); + + Moon moon = _fakers.Moon.Generate(); + moon.OrbitsAround = _fakers.Planet.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Moons.Add(moon); + await dbContext.SaveChangesAsync(); + }); + + string route = "/moons/" + moon.StringId; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Relationships["orbitsAround"].SingleData.Type.Should().Be("planets"); + responseDocument.SingleData.Relationships["orbitsAround"].SingleData.Id.Should().Be(moon.OrbitsAround.StringId); + + responseDocument.Included.Should().HaveCount(1); + responseDocument.Included[0].Type.Should().Be("planets"); + responseDocument.Included[0].Id.Should().Be(moon.OrbitsAround.StringId); + responseDocument.Included[0].Attributes["publicName"].Should().Be(moon.OrbitsAround.PublicName); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Moon), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Filter_from_resource_definition_is_applied() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + var settingsProvider = (TestClientSettingsProvider)_testContext.Factory.Services.GetRequiredService(); + settingsProvider.HidePlanetsWithPrivateName(); + + List planets = _fakers.Planet.Generate(4); + planets[0].PrivateName = "A"; + planets[2].PrivateName = "B"; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.Planets.AddRange(planets); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/planets"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(2); + responseDocument.ManyData[0].Id.Should().Be(planets[1].StringId); + responseDocument.ManyData[1].Id.Should().Be(planets[3].StringId); + + responseDocument.Meta["totalResources"].Should().Be(2); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Planet), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyFilter), + (typeof(Planet), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyFilter), + (typeof(Planet), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Filter_from_resource_definition_and_query_string_are_applied() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + var settingsProvider = (TestClientSettingsProvider)_testContext.Factory.Services.GetRequiredService(); + settingsProvider.HidePlanetsWithPrivateName(); + + List planets = _fakers.Planet.Generate(4); + + planets[0].HasRingSystem = true; + planets[0].PrivateName = "A"; + + planets[1].HasRingSystem = false; + planets[1].PrivateName = "B"; + + planets[2].HasRingSystem = true; + + planets[3].HasRingSystem = false; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.Planets.AddRange(planets); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/planets?filter=equals(hasRingSystem,'false')"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(planets[3].StringId); + + responseDocument.Meta["totalResources"].Should().Be(1); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Planet), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyFilter), + (typeof(Planet), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyFilter), + (typeof(Planet), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Sort_from_resource_definition_is_applied() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + List stars = _fakers.Star.Generate(3); + + stars[0].SolarMass = 500m; + stars[0].SolarRadius = 1m; + + stars[1].SolarMass = 500m; + stars[1].SolarRadius = 10m; + + stars[2].SolarMass = 50m; + stars[2].SolarRadius = 15m; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.Stars.AddRange(stars); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/stars"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(3); + responseDocument.ManyData[0].Id.Should().Be(stars[1].StringId); + responseDocument.ManyData[1].Id.Should().Be(stars[0].StringId); + responseDocument.ManyData[2].Id.Should().Be(stars[2].StringId); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Sort_from_query_string_is_applied() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + List stars = _fakers.Star.Generate(3); + + stars[0].Name = "B"; + stars[0].SolarRadius = 10m; + + stars[1].Name = "B"; + stars[1].SolarRadius = 1m; + + stars[2].Name = "A"; + stars[2].SolarRadius = 15m; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.Stars.AddRange(stars); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/stars?sort=name,-solarRadius"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(3); + responseDocument.ManyData[0].Id.Should().Be(stars[2].StringId); + responseDocument.ManyData[1].Id.Should().Be(stars[0].StringId); + responseDocument.ManyData[2].Id.Should().Be(stars[1].StringId); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Page_size_from_resource_definition_is_applied() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + List stars = _fakers.Star.Generate(10); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.Stars.AddRange(stars); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/stars?page[size]=8"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(5); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Attribute_inclusion_from_resource_definition_is_applied_for_omitted_query_string() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + Star star = _fakers.Star.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Stars.Add(star); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/stars/{star.StringId}"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Id.Should().Be(star.StringId); + responseDocument.SingleData.Attributes["name"].Should().Be(star.Name); + responseDocument.SingleData.Attributes["kind"].Should().Be(star.Kind.ToString()); + responseDocument.SingleData.Relationships.Should().NotBeNull(); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Attribute_inclusion_from_resource_definition_is_applied_for_fields_query_string() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + Star star = _fakers.Star.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Stars.Add(star); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/stars/{star.StringId}?fields[stars]=name,solarRadius"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Id.Should().Be(star.StringId); + responseDocument.SingleData.Attributes.Should().HaveCount(2); + responseDocument.SingleData.Attributes["name"].Should().Be(star.Name); + responseDocument.SingleData.Attributes["solarRadius"].As().Should().BeApproximately(star.SolarRadius); + responseDocument.SingleData.Relationships.Should().BeNull(); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Attribute_exclusion_from_resource_definition_is_applied_for_omitted_query_string() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + Star star = _fakers.Star.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Stars.Add(star); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/stars/{star.StringId}"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Id.Should().Be(star.StringId); + responseDocument.SingleData.Attributes["name"].Should().Be(star.Name); + responseDocument.SingleData.Attributes.Should().NotContainKey("isVisibleFromEarth"); + responseDocument.SingleData.Relationships.Should().NotBeNull(); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Attribute_exclusion_from_resource_definition_is_applied_for_fields_query_string() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + Star star = _fakers.Star.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Stars.Add(star); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/stars/{star.StringId}?fields[stars]=name,isVisibleFromEarth"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Id.Should().Be(star.StringId); + responseDocument.SingleData.Attributes.Should().HaveCount(1); + responseDocument.SingleData.Attributes["name"].Should().Be(star.Name); + responseDocument.SingleData.Relationships.Should().BeNull(); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), + (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Queryable_parameter_handler_from_resource_definition_is_applied() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + List moons = _fakers.Moon.Generate(2); + + moons[0].SolarRadius = .5m; + moons[1].SolarRadius = 50m; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.Moons.AddRange(moons); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/moons?isLargerThanTheSun=true"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(moons[1].StringId); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Moon), ResourceDefinitionHitCounter.ExtensibilityPoint.OnRegisterQueryableHandlersForQueryStringParameters), + (typeof(Moon), ResourceDefinitionHitCounter.ExtensibilityPoint.OnRegisterQueryableHandlersForQueryStringParameters), + (typeof(Moon), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Queryable_parameter_handler_from_resource_definition_and_query_string_filter_are_applied() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + List moons = _fakers.Moon.Generate(4); + + moons[0].Name = "Alpha1"; + moons[0].SolarRadius = 1m; + + moons[1].Name = "Alpha2"; + moons[1].SolarRadius = 5m; + + moons[2].Name = "Beta1"; + moons[2].SolarRadius = 1m; + + moons[3].Name = "Beta2"; + moons[3].SolarRadius = 5m; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.Moons.AddRange(moons); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/moons?isLargerThanTheSun=false&filter=startsWith(name,'B')"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(moons[2].StringId); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Moon), ResourceDefinitionHitCounter.ExtensibilityPoint.OnRegisterQueryableHandlersForQueryStringParameters), + (typeof(Moon), ResourceDefinitionHitCounter.ExtensibilityPoint.OnRegisterQueryableHandlersForQueryStringParameters), + (typeof(Moon), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Queryable_parameter_handler_from_resource_definition_is_not_applied_on_secondary_request() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + Planet planet = _fakers.Planet.Generate(); + planet.Moons = _fakers.Moon.Generate(1).ToHashSet(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Planets.Add(planet); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/planets/{planet.StringId}/moons?isLargerThanTheSun=false"; + + // Act + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + + Error 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 'isLargerThanTheSun' cannot be used on a nested resource endpoint."); + error.Source.Parameter.Should().Be("isLargerThanTheSun"); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Moon), ResourceDefinitionHitCounter.ExtensibilityPoint.OnRegisterQueryableHandlersForQueryStringParameters) + }, options => options.WithStrictOrdering()); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/Star.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/Star.cs new file mode 100644 index 0000000000..328b1346ee --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/Star.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Star : Identifiable + { + [Attr] + public string Name { get; set; } + + [Attr] + public StarKind Kind { get; set; } + + [Attr] + public decimal SolarRadius { get; set; } + + [Attr] + public decimal SolarMass { get; set; } + + [Attr] + public bool IsVisibleFromEarth { get; set; } + + [HasMany] + public ISet Planets { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/StarDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/StarDefinition.cs new file mode 100644 index 0000000000..2ed3480dd3 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/StarDefinition.cs @@ -0,0 +1,67 @@ +using System.ComponentModel; +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading +{ + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class StarDefinition : JsonApiResourceDefinition + { + private readonly ResourceDefinitionHitCounter _hitCounter; + + public StarDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) + : base(resourceGraph) + { + // This constructor will be resolved from the container, which means + // you can take on any dependency that is also defined in the container. + + _hitCounter = hitCounter; + } + + public override SortExpression OnApplySort(SortExpression existingSort) + { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort); + + return existingSort ?? GetDefaultSortOrder(); + } + + private SortExpression GetDefaultSortOrder() + { + return CreateSortExpressionFromLambda(new PropertySortOrder + { + (star => star.SolarMass, ListSortDirection.Descending), + (star => star.SolarRadius, ListSortDirection.Descending) + }); + } + + public override PaginationExpression OnApplyPagination(PaginationExpression existingPagination) + { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination); + + var maxPageSize = new PageSize(5); + + if (existingPagination != null) + { + PageSize pageSize = existingPagination.PageSize?.Value <= maxPageSize.Value ? existingPagination.PageSize : maxPageSize; + return new PaginationExpression(existingPagination.PageNumber, pageSize); + } + + return new PaginationExpression(PageNumber.ValueOne, maxPageSize); + } + + public override SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExpression existingSparseFieldSet) + { + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet); + + // @formatter:keep_existing_linebreaks true + + return existingSparseFieldSet + .Including(star => star.Kind, ResourceGraph) + .Excluding(star => star.IsVisibleFromEarth, ResourceGraph); + + // @formatter:keep_existing_linebreaks restore + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/StarKind.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/StarKind.cs new file mode 100644 index 0000000000..0701442b41 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/StarKind.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public enum StarKind + { + Other, + RedDwarf, + MainSequence, + RedGiant + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/StarsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/StarsController.cs new file mode 100644 index 0000000000..483875106f --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/StarsController.cs @@ -0,0 +1,15 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading +{ + public sealed class StarsController : JsonApiController + { + public StarsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/TestClientSettingsProvider.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/TestClientSettingsProvider.cs new file mode 100644 index 0000000000..4005f1f29a --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/TestClientSettingsProvider.cs @@ -0,0 +1,31 @@ +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading +{ + internal sealed class TestClientSettingsProvider : IClientSettingsProvider + { + public bool IsIncludePlanetMoonsBlocked { get; private set; } + public bool ArePlanetsWithPrivateNameHidden { get; private set; } + public bool IsMoonOrbitingPlanetAutoIncluded { get; private set; } + + public void ResetToDefaults() + { + IsIncludePlanetMoonsBlocked = false; + ArePlanetsWithPrivateNameHidden = false; + IsMoonOrbitingPlanetAutoIncluded = false; + } + + public void BlockIncludePlanetMoons() + { + IsIncludePlanetMoonsBlocked = true; + } + + public void HidePlanetsWithPrivateName() + { + ArePlanetsWithPrivateNameHidden = true; + } + + public void AutoIncludeOrbitingPlanetForMoons() + { + IsMoonOrbitingPlanetAutoIncluded = true; + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/CallableDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/UniverseDbContext.cs similarity index 51% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/CallableDbContext.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/UniverseDbContext.cs index 3bac89e480..f4e1c87b61 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/CallableDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/UniverseDbContext.cs @@ -4,11 +4,13 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading { [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class CallableDbContext : DbContext + public sealed class UniverseDbContext : DbContext { - public DbSet CallableResources { get; set; } + public DbSet Stars { get; set; } + public DbSet Planets { get; set; } + public DbSet Moons { get; set; } - public CallableDbContext(DbContextOptions options) + public UniverseDbContext(DbContextOptions options) : base(options) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/UniverseFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/UniverseFakers.cs new file mode 100644 index 0000000000..3b9584fae7 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/UniverseFakers.cs @@ -0,0 +1,38 @@ +using System; +using Bogus; +using TestBuildingBlocks; + +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading +{ + internal sealed class UniverseFakers : FakerContainer + { + private readonly Lazy> _lazyStarFaker = new Lazy>(() => + new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(star => star.Name, faker => faker.Random.Word()) + .RuleFor(star => star.Kind, faker => faker.PickRandom()) + .RuleFor(star => star.SolarRadius, faker => faker.Random.Decimal(.01M, 1000M)) + .RuleFor(star => star.SolarMass, faker => faker.Random.Decimal(.001M, 100M)) + .RuleFor(star => star.IsVisibleFromEarth, faker => faker.Random.Bool())); + + private readonly Lazy> _lazyPlanetFaker = new Lazy>(() => + new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(planet => planet.PublicName, faker => faker.Random.Word()) + .RuleFor(planet => planet.HasRingSystem, faker => faker.Random.Bool()) + .RuleFor(planet => planet.SolarMass, faker => faker.Random.Decimal(.001M, 100M))); + + private readonly Lazy> _lazyMoonFaker = new Lazy>(() => + new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(moon => moon.Name, faker => faker.Random.Word()) + .RuleFor(moon => moon.SolarRadius, faker => faker.Random.Decimal(.01M, 1000M))); + + public Faker Star => _lazyStarFaker.Value; + public Faker Planet => _lazyPlanetFaker.Value; + public Faker Moon => _lazyMoonFaker.Value; + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Serialization/SerializationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Serialization/ResourceDefinitionSerializationTests.cs similarity index 84% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Serialization/SerializationTests.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Serialization/ResourceDefinitionSerializationTests.cs index 373867521f..f947e12a23 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Serialization/SerializationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Serialization/ResourceDefinitionSerializationTests.cs @@ -13,12 +13,13 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Serialization { - public sealed class SerializationTests : IClassFixture, SerializationDbContext>> + public sealed class ResourceDefinitionSerializationTests + : IClassFixture, SerializationDbContext>> { private readonly ExampleIntegrationTestContext, SerializationDbContext> _testContext; private readonly SerializationFakers _fakers = new SerializationFakers(); - public SerializationTests(ExampleIntegrationTestContext, SerializationDbContext> testContext) + public ResourceDefinitionSerializationTests(ExampleIntegrationTestContext, SerializationDbContext> testContext) { _testContext = testContext; @@ -30,12 +31,12 @@ public SerializationTests(ExampleIntegrationTestContext(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>)); }); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); hitCounter.Reset(); } @@ -44,7 +45,7 @@ public async Task Encrypts_on_get_primary_resources() { // Arrange var encryptionService = _testContext.Factory.Services.GetRequiredService(); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); List students = _fakers.Student.Generate(2); @@ -71,8 +72,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string socialSecurityNumber2 = encryptionService.Decrypt((string)responseDocument.ManyData[1].Attributes["socialSecurityNumber"]); socialSecurityNumber2.Should().Be(students[1].SocialSecurityNumber); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(2); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize), + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize) + }, options => options.WithStrictOrdering()); } [Fact] @@ -80,7 +84,7 @@ public async Task Encrypts_on_get_primary_resources_with_ToMany_include() { // Arrange var encryptionService = _testContext.Factory.Services.GetRequiredService(); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); List scholarships = _fakers.Scholarship.Generate(2); scholarships[0].Participants = _fakers.Student.Generate(2); @@ -117,8 +121,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string socialSecurityNumber4 = encryptionService.Decrypt((string)responseDocument.Included[3].Attributes["socialSecurityNumber"]); socialSecurityNumber4.Should().Be(scholarships[1].Participants[1].SocialSecurityNumber); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(4); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize), + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize), + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize), + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize) + }, options => options.WithStrictOrdering()); } [Fact] @@ -126,7 +135,7 @@ public async Task Encrypts_on_get_primary_resource_by_ID() { // Arrange var encryptionService = _testContext.Factory.Services.GetRequiredService(); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); Student student = _fakers.Student.Generate(); @@ -149,8 +158,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.SingleData.Attributes["socialSecurityNumber"]); socialSecurityNumber.Should().Be(student.SocialSecurityNumber); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(1); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize) + }, options => options.WithStrictOrdering()); } [Fact] @@ -158,7 +169,7 @@ public async Task Encrypts_on_get_secondary_resources() { // Arrange var encryptionService = _testContext.Factory.Services.GetRequiredService(); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); Scholarship scholarship = _fakers.Scholarship.Generate(); scholarship.Participants = _fakers.Student.Generate(2); @@ -185,8 +196,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string socialSecurityNumber2 = encryptionService.Decrypt((string)responseDocument.ManyData[1].Attributes["socialSecurityNumber"]); socialSecurityNumber2.Should().Be(scholarship.Participants[1].SocialSecurityNumber); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(2); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize), + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize) + }, options => options.WithStrictOrdering()); } [Fact] @@ -194,7 +208,7 @@ public async Task Encrypts_on_get_secondary_resource() { // Arrange var encryptionService = _testContext.Factory.Services.GetRequiredService(); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); Scholarship scholarship = _fakers.Scholarship.Generate(); scholarship.PrimaryContact = _fakers.Student.Generate(); @@ -218,8 +232,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.SingleData.Attributes["socialSecurityNumber"]); socialSecurityNumber.Should().Be(scholarship.PrimaryContact.SocialSecurityNumber); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(1); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize) + }, options => options.WithStrictOrdering()); } [Fact] @@ -227,7 +243,7 @@ public async Task Encrypts_on_get_secondary_resource_with_ToOne_include() { // Arrange var encryptionService = _testContext.Factory.Services.GetRequiredService(); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); Scholarship scholarship = _fakers.Scholarship.Generate(); scholarship.PrimaryContact = _fakers.Student.Generate(); @@ -253,8 +269,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.Included[0].Attributes["socialSecurityNumber"]); socialSecurityNumber.Should().Be(scholarship.PrimaryContact.SocialSecurityNumber); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(1); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize) + }, options => options.WithStrictOrdering()); } [Fact] @@ -262,7 +280,7 @@ public async Task Decrypts_on_create_resource() { // Arrange var encryptionService = _testContext.Factory.Services.GetRequiredService(); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); string newName = _fakers.Student.Generate().Name; string newSocialSecurityNumber = _fakers.Student.Generate().SocialSecurityNumber; @@ -302,8 +320,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => studentInDatabase.SocialSecurityNumber.Should().Be(newSocialSecurityNumber); }); - hitCounter.DeserializeCount.Should().Be(1); - hitCounter.SerializeCount.Should().Be(1); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnDeserialize), + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize) + }, options => options.WithStrictOrdering()); } [Fact] @@ -311,7 +332,7 @@ public async Task Encrypts_on_create_resource_with_included_ToOne_relationship() { // Arrange var encryptionService = _testContext.Factory.Services.GetRequiredService(); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); Student existingStudent = _fakers.Student.Generate(); @@ -363,8 +384,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.Included[0].Attributes["socialSecurityNumber"]); socialSecurityNumber.Should().Be(existingStudent.SocialSecurityNumber); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(1); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize) + }, options => options.WithStrictOrdering()); } [Fact] @@ -372,7 +395,7 @@ public async Task Decrypts_on_update_resource() { // Arrange var encryptionService = _testContext.Factory.Services.GetRequiredService(); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); Student existingStudent = _fakers.Student.Generate(); @@ -417,8 +440,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => studentInDatabase.SocialSecurityNumber.Should().Be(newSocialSecurityNumber); }); - hitCounter.DeserializeCount.Should().Be(1); - hitCounter.SerializeCount.Should().Be(1); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnDeserialize), + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize) + }, options => options.WithStrictOrdering()); } [Fact] @@ -426,7 +452,7 @@ public async Task Encrypts_on_update_resource_with_included_ToMany_relationship( { // Arrange var encryptionService = _testContext.Factory.Services.GetRequiredService(); - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); Scholarship existingScholarship = _fakers.Scholarship.Generate(); existingScholarship.Participants = _fakers.Student.Generate(3); @@ -489,15 +515,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string socialSecurityNumber2 = encryptionService.Decrypt((string)responseDocument.Included[1].Attributes["socialSecurityNumber"]); socialSecurityNumber2.Should().Be(existingScholarship.Participants[2].SocialSecurityNumber); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(2); + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize), + (typeof(Student), ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize) + }, options => options.WithStrictOrdering()); } [Fact] public async Task Skips_on_get_ToOne_relationship() { // Arrange - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); Scholarship scholarship = _fakers.Scholarship.Generate(); scholarship.PrimaryContact = _fakers.Student.Generate(); @@ -519,15 +548,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.SingleData.Should().NotBeNull(); responseDocument.SingleData.Id.Should().Be(scholarship.PrimaryContact.StringId); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(0); + hitCounter.HitExtensibilityPoints.Should().BeEmpty(); } [Fact] public async Task Skips_on_get_ToMany_relationship() { // Arrange - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); Scholarship scholarship = _fakers.Scholarship.Generate(); scholarship.Participants = _fakers.Student.Generate(2); @@ -550,15 +578,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.ManyData[0].Id.Should().Be(scholarship.Participants[0].StringId); responseDocument.ManyData[1].Id.Should().Be(scholarship.Participants[1].StringId); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(0); + hitCounter.HitExtensibilityPoints.Should().BeEmpty(); } [Fact] public async Task Skips_on_update_ToOne_relationship() { // Arrange - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); Scholarship existingScholarship = _fakers.Scholarship.Generate(); Student existingStudent = _fakers.Student.Generate(); @@ -588,15 +615,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(0); + hitCounter.HitExtensibilityPoints.Should().BeEmpty(); } [Fact] public async Task Skips_on_set_ToMany_relationship() { // Arrange - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); Scholarship existingScholarship = _fakers.Scholarship.Generate(); List existingStudents = _fakers.Student.Generate(2); @@ -635,15 +661,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(0); + hitCounter.HitExtensibilityPoints.Should().BeEmpty(); } [Fact] public async Task Skips_on_add_to_ToMany_relationship() { // Arrange - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); Scholarship existingScholarship = _fakers.Scholarship.Generate(); List existingStudents = _fakers.Student.Generate(2); @@ -682,15 +707,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(0); + hitCounter.HitExtensibilityPoints.Should().BeEmpty(); } [Fact] public async Task Skips_on_remove_from_ToMany_relationship() { // Arrange - var hitCounter = _testContext.Factory.Services.GetRequiredService(); + var hitCounter = _testContext.Factory.Services.GetRequiredService(); Scholarship existingScholarship = _fakers.Scholarship.Generate(); existingScholarship.Participants = _fakers.Student.Generate(2); @@ -728,8 +752,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Should().BeEmpty(); - hitCounter.DeserializeCount.Should().Be(0); - hitCounter.SerializeCount.Should().Be(0); + hitCounter.HitExtensibilityPoints.Should().BeEmpty(); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Serialization/SerializationHitCounter.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Serialization/SerializationHitCounter.cs deleted file mode 100644 index 27e3018c11..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Serialization/SerializationHitCounter.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Serialization -{ - public sealed class SerializationHitCounter - { - internal int DeserializeCount { get; private set; } - internal int SerializeCount { get; private set; } - - internal void Reset() - { - DeserializeCount = 0; - SerializeCount = 0; - } - - internal void IncrementDeserializeCount() - { - DeserializeCount++; - } - - internal void IncrementSerializeCount() - { - SerializeCount++; - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Serialization/StudentDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Serialization/StudentDefinition.cs index 2552c51031..6ce5d5656f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Serialization/StudentDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Serialization/StudentDefinition.cs @@ -8,18 +8,21 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Ser public sealed class StudentDefinition : JsonApiResourceDefinition { private readonly IEncryptionService _encryptionService; - private readonly SerializationHitCounter _hitCounter; + private readonly ResourceDefinitionHitCounter _hitCounter; - public StudentDefinition(IResourceGraph resourceGraph, IEncryptionService encryptionService, SerializationHitCounter hitCounter) + public StudentDefinition(IResourceGraph resourceGraph, IEncryptionService encryptionService, ResourceDefinitionHitCounter hitCounter) : base(resourceGraph) { + // This constructor will be resolved from the container, which means + // you can take on any dependency that is also defined in the container. + _encryptionService = encryptionService; _hitCounter = hitCounter; } public override void OnDeserialize(Student resource) { - _hitCounter.IncrementDeserializeCount(); + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnDeserialize); if (!string.IsNullOrEmpty(resource.SocialSecurityNumber)) { @@ -29,7 +32,7 @@ public override void OnDeserialize(Student resource) public override void OnSerialize(Student resource) { - _hitCounter.IncrementSerializeCount(); + _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnSerialize); if (!string.IsNullOrEmpty(resource.SocialSecurityNumber)) { diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs index ada0a8d7f0..24a833bac5 100644 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Queries.Internal; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization; @@ -50,18 +51,19 @@ protected ResponseSerializer GetResponseSerializer(IEnumerable[] inclusionChainArray = inclusionChains?.ToArray(); + IMetaBuilder meta = GetMetaBuilder(metaDict); ILinkBuilder link = GetLinkBuilder(topLinks, resourceLinks, relationshipLinks); - IEnumerable includeConstraints = GetIncludeConstraints(inclusionChains); + IEnumerable includeConstraints = GetIncludeConstraints(inclusionChainArray); IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder(); IFieldsToSerialize fieldsToSerialize = GetSerializableFields(); - IResourceDefinitionAccessor resourceDefinitionAccessor = GetResourceDefinitionAccessor(); - IResourceObjectBuilderSettingsProvider settingsProvider = GetSerializerSettingsProvider(); + IEvaluatedIncludeCache evaluatedIncludeCache = GetEvaluatedIncludeCache(inclusionChainArray); var resourceObjectBuilder = new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, resourceDefinitionAccessor, - settingsProvider); + settingsProvider, evaluatedIncludeCache); var jsonApiOptions = new JsonApiOptions(); @@ -71,12 +73,15 @@ protected ResponseSerializer GetResponseSerializer(IEnumerable> inclusionChains = null, ResourceLinks resourceLinks = null, RelationshipLinks relationshipLinks = null) { + IEnumerable[] inclusionChainArray = inclusionChains?.ToArray(); + ILinkBuilder link = GetLinkBuilder(null, resourceLinks, relationshipLinks); - IEnumerable includeConstraints = GetIncludeConstraints(inclusionChains); + IEnumerable includeConstraints = GetIncludeConstraints(inclusionChainArray); IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder(); + IEvaluatedIncludeCache evaluatedIncludeCache = GetEvaluatedIncludeCache(inclusionChainArray); return new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, GetResourceDefinitionAccessor(), - GetSerializerSettingsProvider()); + GetSerializerSettingsProvider(), evaluatedIncludeCache); } private IIncludedResourceObjectBuilder GetIncludedBuilder() @@ -142,6 +147,23 @@ private IEnumerable GetIncludeConstraints(IEnumerable< return includeConstraintProvider.AsEnumerable(); } + private IEvaluatedIncludeCache GetEvaluatedIncludeCache(IEnumerable> inclusionChains = null) + { + if (inclusionChains == null) + { + return new EvaluatedIncludeCache(); + } + + List chains = inclusionChains.Select(relationships => new ResourceFieldChainExpression(relationships.ToArray())) + .ToList(); + + IncludeExpression includeExpression = IncludeChainConverter.FromRelationshipChains(chains); + + var evaluatedIncludeCache = new EvaluatedIncludeCache(); + evaluatedIncludeCache.Set(includeExpression); + return evaluatedIncludeCache; + } + /// /// Minimal implementation of abstract JsonApiSerializer base class, with the purpose of testing the business logic for building the document structure. ///