diff --git a/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs index 4fb672929b..e3b30c79b9 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs @@ -3,10 +3,7 @@ using JsonApiDotNetCore.Hooks; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Managers.Contracts; -using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Query; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; @@ -20,15 +17,14 @@ public CustomArticleService(ISortService sortService, IFilterService filterService, IEntityRepository repository, IJsonApiOptions options, - ICurrentRequest currentRequest, IIncludeService includeService, ISparseFieldsService sparseFieldsService, - IPageService pageManager, + IPageService pageService, IResourceGraph resourceGraph, IResourceHookExecutor hookExecutor = null, ILoggerFactory loggerFactory = null) - : base(sortService, filterService, repository, options, currentRequest, includeService, sparseFieldsService, - pageManager, resourceGraph, hookExecutor, loggerFactory) + : base(sortService, filterService, repository, options, includeService, sparseFieldsService, + pageService, resourceGraph, hookExecutor, loggerFactory) { } diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 4aa2dfeac9..866c398be7 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -4,11 +4,9 @@ using System.Linq; using System.Threading.Tasks; using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; using Microsoft.EntityFrameworkCore; @@ -20,52 +18,42 @@ namespace JsonApiDotNetCore.Data /// Provides a default repository implementation and is responsible for /// abstracting any EF Core APIs away from the service layer. /// - public class DefaultEntityRepository - : IEntityRepository, - IEntityFrameworkRepository + public class DefaultEntityRepository : IEntityRepository where TEntity : class, IIdentifiable { - private readonly ICurrentRequest _currentRequest; private readonly ITargetedFields _targetedFields; private readonly DbContext _context; private readonly DbSet _dbSet; - private readonly ILogger _logger; private readonly IResourceGraph _resourceGraph; private readonly IGenericProcessorFactory _genericProcessorFactory; - private readonly ResourceDefinition _resourceDefinition; public DefaultEntityRepository( - ICurrentRequest currentRequest, - ITargetedFields updatedFields, + ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, - IGenericProcessorFactory genericProcessorFactory, - ResourceDefinition resourceDefinition = null) - : this(currentRequest, updatedFields, contextResolver, resourceGraph, genericProcessorFactory, resourceDefinition, null) + IGenericProcessorFactory genericProcessorFactory) + : this(targetedFields, contextResolver, resourceGraph, genericProcessorFactory, null) { } public DefaultEntityRepository( - ICurrentRequest currentRequest, - ITargetedFields updatedFields, + ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, IGenericProcessorFactory genericProcessorFactory, - ResourceDefinition resourceDefinition = null, ILoggerFactory loggerFactory = null) { - _logger = loggerFactory?.CreateLogger>(); - _currentRequest = currentRequest; - _targetedFields = updatedFields; + _targetedFields = targetedFields; _resourceGraph = resourceGraph; _genericProcessorFactory = genericProcessorFactory; _context = contextResolver.GetContext(); _dbSet = _context.Set(); - _resourceDefinition = resourceDefinition; } /// public virtual IQueryable Get() => _dbSet; - + /// + public virtual IQueryable Get(TId id) => _dbSet.Where(e => e.Id.Equals(id)); + /// public virtual IQueryable Select(IQueryable entities, List fields) { @@ -79,12 +67,9 @@ public virtual IQueryable Select(IQueryable entities, List Filter(IQueryable entities, FilterQueryContext filterQueryContext) { if (filterQueryContext.IsCustom) - { // todo: consider to move this business logic to service layer - var filterQuery = filterQueryContext.Query; - var defaultQueryFilters = _resourceDefinition.GetQueryFilters(); - if (defaultQueryFilters != null && defaultQueryFilters.TryGetValue(filterQuery.Target, out var defaultQueryFilter) == true) - return defaultQueryFilter(entities, filterQuery); - + { + var query = (Func, FilterQuery, IQueryable>)filterQueryContext.CustomQuery; + return query(entities, filterQueryContext.Query); } return entities.Filter(filterQueryContext); } @@ -95,45 +80,30 @@ public virtual IQueryable Sort(IQueryable entities, SortQueryC return entities.Sort(sortQueryContext); } - /// - public virtual async Task GetAsync(TId id, List fields = null) - { - return await Select(Get(), fields).SingleOrDefaultAsync(e => e.Id.Equals(id)); - } - - /// - public virtual async Task GetAndIncludeAsync(TId id, RelationshipAttribute relationship, List fields = null) - { - _logger?.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationship.PublicRelationshipName})"); - var includedSet = Include(Select(Get(), fields), relationship); - var result = await includedSet.SingleOrDefaultAsync(e => e.Id.Equals(id)); - return result; - } - /// public virtual async Task CreateAsync(TEntity entity) { foreach (var relationshipAttr in _targetedFields.Relationships) { - var trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, entity, out bool wasAlreadyTracked); + object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, entity, out bool wasAlreadyTracked); LoadInverseRelationships(trackedRelationshipValue, relationshipAttr); if (wasAlreadyTracked) - { /// We only need to reassign the relationship value to the to-be-added /// entity when we're using a different instance (because this different one /// was already tracked) than the one assigned to the to-be-created entity. AssignRelationshipValue(entity, trackedRelationshipValue, relationshipAttr); - } else if (relationshipAttr is HasManyThroughAttribute throughAttr) - { /// even if we don't have to reassign anything because of already tracked /// entities, we still need to assign the "through" entities in the case of many-to-many. AssignHasManyThrough(entity, throughAttr, (IList)trackedRelationshipValue); - } } _dbSet.Add(entity); await _context.SaveChangesAsync(); + // this ensures relationships get reloaded from the database if they have + // been requested. See https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343 + DetachRelationships(entity); + return entity; } @@ -169,7 +139,7 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations private bool IsHasOneRelationship(string internalRelationshipName, Type type) { - var relationshipAttr = _resourceGraph.GetContextEntity(type).Relationships.SingleOrDefault(r => r.InternalRelationshipName == internalRelationshipName); + var relationshipAttr = _resourceGraph.GetContextEntity(type).Relationships.FirstOrDefault(r => r.InternalRelationshipName == internalRelationshipName); if (relationshipAttr != null) { if (relationshipAttr is HasOneAttribute) @@ -182,9 +152,7 @@ private bool IsHasOneRelationship(string internalRelationshipName, Type type) return !(type.GetProperty(internalRelationshipName).PropertyType.Inherits(typeof(IEnumerable))); } - - /// - public void DetachRelationshipPointers(TEntity entity) + private void DetachRelationships(TEntity entity) { foreach (var relationshipAttr in _targetedFields.Relationships) { @@ -212,22 +180,22 @@ public void DetachRelationshipPointers(TEntity entity) /// public virtual async Task UpdateAsync(TEntity updatedEntity) { - var databaseEntity = await GetAsync(updatedEntity.Id); + var databaseEntity = await Get(updatedEntity.Id).FirstOrDefaultAsync(); if (databaseEntity == null) return null; - foreach (var attr in _targetedFields.Attributes) - attr.SetValue(databaseEntity, attr.GetValue(updatedEntity)); + foreach (var attribute in _targetedFields.Attributes) + attribute.SetValue(databaseEntity, attribute.GetValue(updatedEntity)); foreach (var relationshipAttr in _targetedFields.Relationships) { /// loads databasePerson.todoItems LoadCurrentRelationships(databaseEntity, relationshipAttr); - /// trackedRelationshipValue is either equal to updatedPerson.todoItems - /// or replaced with the same set of todoItems from the EF Core change tracker, - /// if they were already tracked - object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out bool wasAlreadyTracked); - /// loads into the db context any persons currentlresy related + /// trackedRelationshipValue is either equal to updatedPerson.todoItems, + /// or replaced with the same set (same ids) of todoItems from the EF Core change tracker, + /// which is the case if they were already tracked + object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out _); + /// loads into the db context any persons currently related /// to the todoItems in trackedRelationshipValue LoadInverseRelationships(trackedRelationshipValue, relationshipAttr); /// assigns the updated relationship to the database entity @@ -238,7 +206,6 @@ public virtual async Task UpdateAsync(TEntity updatedEntity) return databaseEntity; } - /// /// Responsible for getting the relationship value for a given relationship /// attribute of a given entity. It ensures that the relationship value @@ -302,11 +269,10 @@ public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds); } - /// public virtual async Task DeleteAsync(TId id) { - var entity = await GetAsync(id); + var entity = await Get(id).FirstOrDefaultAsync(); if (entity == null) return false; _dbSet.Remove(entity); await _context.SaveChangesAsync(); @@ -327,33 +293,6 @@ public virtual IQueryable Include(IQueryable entities, params return entities.Include(internalRelationshipPath); } - /// - public virtual IQueryable Include(IQueryable entities, string relationshipName) - { - if (string.IsNullOrWhiteSpace(relationshipName)) throw new JsonApiException(400, "Include parameter must not be empty if provided"); - - var relationshipChain = relationshipName.Split('.'); - - // variables mutated in recursive loop - // TODO: make recursive method - string internalRelationshipPath = null; - var entity = _currentRequest.GetRequestResource(); - for (var i = 0; i < relationshipChain.Length; i++) - { - var requestedRelationship = relationshipChain[i]; - var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship); - - internalRelationshipPath = (internalRelationshipPath == null) - ? relationship.RelationshipPath - : $"{internalRelationshipPath}.{relationship.RelationshipPath}"; - - if (i < relationshipChain.Length) - entity = _resourceGraph.GetContextEntity(relationship.Type); - } - - return entities.Include(internalRelationshipPath); - } - /// public virtual async Task> PageAsync(IQueryable entities, int pageSize, int pageNumber) { @@ -493,16 +432,23 @@ private IIdentifiable AttachOrGetTracked(IIdentifiable relationshipValue) } /// - public class DefaultEntityRepository - : DefaultEntityRepository, - IEntityRepository + public class DefaultEntityRepository : DefaultEntityRepository, IEntityRepository where TEntity : class, IIdentifiable { - public DefaultEntityRepository(ICurrentRequest currentRequest, ITargetedFields updatedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, IGenericProcessorFactory genericProcessorFactory, ResourceDefinition resourceDefinition = null) : base(currentRequest, updatedFields, contextResolver, resourceGraph, genericProcessorFactory, resourceDefinition) + public DefaultEntityRepository(ITargetedFields targetedFields, + IDbContextResolver contextResolver, + IResourceGraph resourceGraph, + IGenericProcessorFactory genericProcessorFactory) + : base(targetedFields, contextResolver, resourceGraph, genericProcessorFactory) { } - public DefaultEntityRepository(ICurrentRequest currentRequest, ITargetedFields updatedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, IGenericProcessorFactory genericProcessorFactory, ResourceDefinition resourceDefinition = null, ILoggerFactory loggerFactory = null) : base(currentRequest, updatedFields, contextResolver, resourceGraph, genericProcessorFactory, resourceDefinition, loggerFactory) + public DefaultEntityRepository(ITargetedFields targetedFields, + IDbContextResolver contextResolver, + IResourceGraph resourceGraph, + IGenericProcessorFactory genericProcessorFactory, + ILoggerFactory loggerFactory = null) + : base(targetedFields, contextResolver, resourceGraph, genericProcessorFactory, loggerFactory) { } } diff --git a/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs b/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs index 5ffb59fe03..605a07257d 100644 --- a/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs @@ -20,12 +20,14 @@ public interface IEntityReadRepository /// such as authorization of resources. /// IQueryable Get(); - + /// + /// Get the entity by id + /// + IQueryable Get(TId id); /// /// Apply fields to the provided queryable /// IQueryable Select(IQueryable entities, List fields); - /// /// Include a relationship in the query /// @@ -35,51 +37,26 @@ public interface IEntityReadRepository /// /// IQueryable Include(IQueryable entities, params RelationshipAttribute[] inclusionChain); - [Obsolete] - IQueryable Include(IQueryable entities, string relationshipName); - /// /// Apply a filter to the provided queryable /// IQueryable Filter(IQueryable entities, FilterQueryContext filterQuery); - /// /// Apply a sort to the provided queryable /// IQueryable Sort(IQueryable entities, SortQueryContext sortQueries); - /// /// Paginate the provided queryable /// Task> PageAsync(IQueryable entities, int pageSize, int pageNumber); - - /// - /// Get the entity by id - /// - Task GetAsync(TId id, List fields = null); - - /// - /// Get the entity with the specified id and include the relationship. - /// - /// The entity id - /// The exposed relationship - /// - /// - /// _todoItemsRepository.GetAndIncludeAsync(1, "achieved-date"); - /// - /// - Task GetAndIncludeAsync(TId id, RelationshipAttribute relationship, List fields = null); - /// /// Count the total number of records /// Task CountAsync(IQueryable entities); - /// /// Get the first element in the collection, return the default value if collection is empty /// Task FirstOrDefaultAsync(IQueryable entities); - /// /// Convert the collection to a materialized list /// diff --git a/src/JsonApiDotNetCore/Data/IEntityRepository.cs b/src/JsonApiDotNetCore/Data/IEntityRepository.cs index 1560a7809e..2c65b0a76a 100644 --- a/src/JsonApiDotNetCore/Data/IEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityRepository.cs @@ -1,8 +1,8 @@ +using System; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Data { - public interface IEntityRepository : IEntityRepository where TEntity : class, IIdentifiable @@ -13,28 +13,6 @@ public interface IEntityRepository IEntityWriteRepository where TEntity : class, IIdentifiable { } - - /// - /// A staging interface to avoid breaking changes that - /// specifically depend on EntityFramework. - /// - internal interface IEntityFrameworkRepository - { - /// - /// Ensures that any relationship pointers created during a POST or PATCH - /// request are detached from the DbContext. - /// This allows the relationships to be fully loaded from the database. - /// - /// - /// - /// The only known case when this should be called is when a POST request is - /// sent with an ?include query. - /// - /// See https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343 - /// - void DetachRelationshipPointers(TEntity entity); - } - } diff --git a/src/JsonApiDotNetCore/Hooks/Execution/DiffableEntityHashSet.cs b/src/JsonApiDotNetCore/Hooks/Execution/DiffableEntityHashSet.cs index 9a02bb32aa..c43ae530c4 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/DiffableEntityHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/DiffableEntityHashSet.cs @@ -52,9 +52,9 @@ public DiffableEntityHashSet(HashSet requestEntities, internal DiffableEntityHashSet(IEnumerable requestEntities, IEnumerable databaseEntities, Dictionary relationships, - ITargetedFields updatedFields) + ITargetedFields targetedFields) : this((HashSet)requestEntities, (HashSet)databaseEntities, TypeHelper.ConvertRelationshipDictionary(relationships), - TypeHelper.ConvertAttributeDictionary(updatedFields.Attributes, (HashSet)requestEntities)) + TypeHelper.ConvertAttributeDictionary(targetedFields.Attributes, (HashSet)requestEntities)) { } diff --git a/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs b/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs index 99c1fef714..f0f45ab276 100644 --- a/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs +++ b/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs @@ -127,7 +127,7 @@ public interface IBeforeHooks where TResource : class, IIdentifiable /// and by this the relationship to a different Person was implicitly removed, /// this hook will be fired for the latter Person. /// - /// See for information about + /// See for information about /// when this hook is fired. /// /// diff --git a/src/JsonApiDotNetCore/Hooks/IResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/IResourceHookExecutor.cs index 2d4f1fbdb7..e642e6b9a4 100644 --- a/src/JsonApiDotNetCore/Hooks/IResourceHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/IResourceHookExecutor.cs @@ -52,7 +52,7 @@ public interface IBeforeExecutor /// Executes the Before Cycle by firing the appropiate hooks if they are implemented. /// The returned set will be used in the actual operation in . /// - /// Fires the + /// Fires the /// hook where T = for values in parameter . /// /// Fires the diff --git a/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs index 1e29dd4398..dda22e0762 100644 --- a/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs @@ -25,13 +25,13 @@ internal class ResourceHookExecutor : IResourceHookExecutor public ResourceHookExecutor( IHookExecutorHelper executorHelper, ITraversalHelper traversalHelper, - ITargetedFields updatedFields, + ITargetedFields targetedFields, IIncludeService includedRelationships, IResourceGraph resourceGraph) { _executorHelper = executorHelper; _traversalHelper = traversalHelper; - _targetedFields = updatedFields; + _targetedFields = targetedFields; _includeService = includedRelationships; _graph = resourceGraph; } diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs b/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs index 42f4cc1842..b023870066 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs @@ -39,9 +39,9 @@ internal class TraversalHelper : ITraversalHelper private readonly Dictionary RelationshipProxies = new Dictionary(); public TraversalHelper( IContextEntityProvider provider, - ITargetedFields updatedFields) + ITargetedFields targetedFields) { - _targetedFields = updatedFields; + _targetedFields = targetedFields; _provider = provider; } diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQueryContext.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQueryContext.cs index c4b883292e..e1754d4ca7 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterQueryContext.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQueryContext.cs @@ -9,7 +9,7 @@ namespace JsonApiDotNetCore.Internal.Query public class FilterQueryContext : BaseQueryContext { public FilterQueryContext(FilterQuery query) : base(query) { } - + public object CustomQuery { get; set; } public string Value => Query.Value; public FilterOperation Operation { diff --git a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs index 43b2c4f2f3..9d1c65a4c9 100644 --- a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs @@ -15,7 +15,7 @@ public interface IResourceDefinition { List GetAllowedAttributes(); List GetAllowedRelationships(); - bool HasCustomQueryFilter(string key); + object GetCustomQueryFilter(string key); List<(AttrAttribute, SortDirection)> DefaultSort(); } @@ -99,9 +99,12 @@ public void HideFields(Expression> selector) /// public virtual QueryFilters GetQueryFilters() => null; - public bool HasCustomQueryFilter(string key) + public object GetCustomQueryFilter(string key) { - return GetQueryFilters()?.Keys.Contains(key) ?? false; + var customFilters = GetQueryFilters(); + if (customFilters != null && customFilters.TryGetValue(key, out var query)) + return query; + return null; } /// diff --git a/src/JsonApiDotNetCore/QueryParameterServices/FilterService.cs b/src/JsonApiDotNetCore/QueryParameterServices/FilterService.cs index 8e723ce001..4c23ee3820 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/FilterService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/FilterService.cs @@ -38,10 +38,15 @@ public virtual void Parse(KeyValuePair queryParameter) private FilterQueryContext GetQueryContexts(FilterQuery query) { var queryContext = new FilterQueryContext(query); - if (_requestResourceDefinition != null && _requestResourceDefinition.HasCustomQueryFilter(query.Target)) + if (_requestResourceDefinition != null) { - queryContext.IsCustom = true; - return queryContext; + var customQuery = _requestResourceDefinition.GetCustomQueryFilter(query.Target); + if (customQuery != null) + { + queryContext.IsCustom = true; + queryContext.CustomQuery = customQuery; + return queryContext; + } } queryContext.Relationship = GetRelationship(query.Relationship); diff --git a/src/JsonApiDotNetCore/QueryParameterServices/SparseFieldsService.cs b/src/JsonApiDotNetCore/QueryParameterServices/SparseFieldsService.cs index c3a26fd983..e6518bc7e2 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/SparseFieldsService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/SparseFieldsService.cs @@ -45,13 +45,13 @@ public virtual void Parse(KeyValuePair queryParameter) { // expected: fields[TYPE]=prop1,prop2 var typeName = queryParameter.Key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; - var includedFields = new List { nameof(Identifiable.Id) }; + var fields = new List { nameof(Identifiable.Id) }; var relationship = _requestResource.Relationships.SingleOrDefault(a => a.Is(typeName)); if (relationship == null && string.Equals(typeName, _requestResource.EntityName, StringComparison.OrdinalIgnoreCase) == false) throw new JsonApiException(400, $"fields[{typeName}] is invalid"); - var fields = ((string)queryParameter.Value).Split(QueryConstants.COMMA); + fields.AddRange(((string)queryParameter.Value).Split(QueryConstants.COMMA)); foreach (var field in fields) { if (relationship != default) diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index 16fc252733..845a711146 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -113,6 +113,8 @@ private JToken LoadJToken(string body) JToken jToken; using (JsonReader jsonReader = new JsonTextReader(new StringReader(body))) { + // https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/509 + jsonReader.DateParseHandling = DateParseHandling.None; jToken = JToken.Load(jsonReader); } return jToken; diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs index 1139616417..8d271d5327 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs @@ -13,16 +13,16 @@ public class LinkBuilder : ILinkBuilder private readonly ICurrentRequest _currentRequest; private readonly ILinksConfiguration _options; private readonly IContextEntityProvider _provider; - private readonly IPageService _pageManager; + private readonly IPageService _pageService; public LinkBuilder(ILinksConfiguration options, ICurrentRequest currentRequest, - IPageService pageManager, + IPageService pageService, IContextEntityProvider provider) { _options = options; _currentRequest = currentRequest; - _pageManager = pageManager; + _pageService = pageService; _provider = provider; } @@ -54,23 +54,23 @@ private bool ShouldAddTopLevelLink(ContextEntity primaryResource, Link link) private void SetPageLinks(ContextEntity primaryResource, ref TopLevelLinks links) { - if (!_pageManager.ShouldPaginate()) + if (!_pageService.ShouldPaginate()) return; links = links ?? new TopLevelLinks(); - if (_pageManager.CurrentPage > 1) + if (_pageService.CurrentPage > 1) { - links.First = GetPageLink(primaryResource, 1, _pageManager.PageSize); - links.Prev = GetPageLink(primaryResource, _pageManager.CurrentPage - 1, _pageManager.PageSize); + links.First = GetPageLink(primaryResource, 1, _pageService.PageSize); + links.Prev = GetPageLink(primaryResource, _pageService.CurrentPage - 1, _pageService.PageSize); } - if (_pageManager.CurrentPage < _pageManager.TotalPages) - links.Next = GetPageLink(primaryResource, _pageManager.CurrentPage + 1, _pageManager.PageSize); + if (_pageService.CurrentPage < _pageService.TotalPages) + links.Next = GetPageLink(primaryResource, _pageService.CurrentPage + 1, _pageService.PageSize); - if (_pageManager.TotalPages > 0) - links.Last = GetPageLink(primaryResource, _pageManager.TotalPages, _pageManager.PageSize); + if (_pageService.TotalPages > 0) + links.Last = GetPageLink(primaryResource, _pageService.TotalPages, _pageService.PageSize); } private string GetSelfTopLevelLink(string resourceName) diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs index aeacf82987..1a495909a3 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs @@ -11,17 +11,17 @@ namespace JsonApiDotNetCore.Serialization.Server.Builders public class MetaBuilder : IMetaBuilder where T : class, IIdentifiable { private Dictionary _meta = new Dictionary(); - private readonly IPageService _pageManager; + private readonly IPageService _pageService; private readonly IJsonApiOptions _options; private readonly IRequestMeta _requestMeta; private readonly IHasMeta _resourceMeta; - public MetaBuilder(IPageService pageManager, + public MetaBuilder(IPageService pageService, IJsonApiOptions options, IRequestMeta requestMeta = null, ResourceDefinition resourceDefinition = null) { - _pageManager = pageManager; + _pageService = pageService; _options = options; _requestMeta = requestMeta; _resourceMeta = resourceDefinition as IHasMeta; @@ -43,8 +43,8 @@ public void Add(Dictionary values) /// public Dictionary GetMeta() { - if (_options.IncludeTotalRecordCount && _pageManager.TotalRecords != null) - _meta.Add("total-records", _pageManager.TotalRecords); + if (_options.IncludeTotalRecordCount && _pageService.TotalRecords != null) + _meta.Add("total-records", _pageService.TotalRecords); if (_requestMeta != null) Add(_requestMeta.GetMeta()); diff --git a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs index 86b42e6b3e..7c57556e7c 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs @@ -12,9 +12,9 @@ public class RequestDeserializer : BaseDocumentParser, IJsonApiDeserializer private readonly ITargetedFields _targetedFields; public RequestDeserializer(IResourceGraph resourceGraph, - ITargetedFields updatedFields) : base(resourceGraph) + ITargetedFields targetedFields) : base(resourceGraph) { - _targetedFields = updatedFields; + _targetedFields = targetedFields; } /// diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs index b335abe660..62867eae57 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Query; @@ -112,7 +111,6 @@ internal string SerializeMany(IEnumerable entities) return JsonConvert.SerializeObject(document); } - /// /// Gets the list of attributes to serialize for the given . /// Note that the choice omitting null-values is not handled here, @@ -164,17 +162,5 @@ private void AddTopLevelObjects(Document document) document.Meta = _metaBuilder.GetMeta(); document.Included = _includedBuilder.Build(); } - - /// - /// Inspects the included relationship chains (see - /// to see if should be included or not. - /// - private bool ShouldInclude(RelationshipAttribute relationship, out List> inclusionChain) - { - inclusionChain = _includeService.Get()?.Where(l => l.First().Equals(relationship)).ToList(); - if (inclusionChain == null || !inclusionChain.Any()) - return false; - return true; - } } } diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index cc63ae88b9..232ac7308b 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -1,7 +1,6 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Hooks; using Microsoft.Extensions.Logging; @@ -10,7 +9,6 @@ using System.Linq; using System.Threading.Tasks; using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Query; namespace JsonApiDotNetCore.Services @@ -25,7 +23,6 @@ public class EntityResourceService : where TResource : class, IIdentifiable { private readonly IPageService _pageManager; - private readonly ICurrentRequest _currentRequest; private readonly IJsonApiOptions _options; private readonly IResourceGraph _resourceGraph; private readonly IFilterService _filterService; @@ -42,7 +39,6 @@ public EntityResourceService( IFilterService filterService, IEntityRepository repository, IJsonApiOptions options, - ICurrentRequest currentRequest, IIncludeService includeService, ISparseFieldsService sparseFieldsService, IPageService pageManager, @@ -50,7 +46,6 @@ public EntityResourceService( IResourceHookExecutor hookExecutor = null, ILoggerFactory loggerFactory = null) { - _currentRequest = currentRequest; _includeService = includeService; _sparseFieldsService = sparseFieldsService; _pageManager = pageManager; @@ -69,16 +64,8 @@ public virtual async Task CreateAsync(TResource entity) entity = IsNull(_hookExecutor) ? entity : _hookExecutor.BeforeCreate(AsList(entity), ResourcePipeline.Post).SingleOrDefault(); entity = await _repository.CreateAsync(entity); - // this ensures relationships get reloaded from the database if they have - // been requested - // https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343 - if (ShouldIncludeRelationships()) - { - if (_repository is IEntityFrameworkRepository efRepository) - efRepository.DetachRelationshipPointers(entity); - + if (_includeService.Get().Any()) entity = await GetWithRelationshipsAsync(entity.Id); - } if (!IsNull(_hookExecutor, entity)) { @@ -87,6 +74,7 @@ public virtual async Task CreateAsync(TResource entity) } return entity; } + public virtual async Task DeleteAsync(TId id) { var entity = (TResource)Activator.CreateInstance(typeof(TResource)); @@ -96,33 +84,29 @@ public virtual async Task DeleteAsync(TId id) if (!IsNull(_hookExecutor, entity)) _hookExecutor.AfterDelete(AsList(entity), ResourcePipeline.Delete, succeeded); return succeeded; } + public virtual async Task> GetAsync() { _hookExecutor?.BeforeRead(ResourcePipeline.Get); - var entities = _repository.Get(); - entities = ApplySortAndFilterQuery(entities); - - if (ShouldIncludeRelationships()) - entities = IncludeRelationships(entities); - - - var fields = _sparseFieldsService.Get(); - if (fields.Any()) - entities = _repository.Select(entities, fields); + var entityQuery = _repository.Get(); + entityQuery = ApplyFilter(entityQuery); + entityQuery = ApplySort(entityQuery); + entityQuery = ApplyInclude(entityQuery); + entityQuery = ApplySelect(entityQuery); - if (!IsNull(_hookExecutor, entities)) + if (!IsNull(_hookExecutor, entityQuery)) { - var result = entities.ToList(); - _hookExecutor.AfterRead(result, ResourcePipeline.Get); - entities = _hookExecutor.OnReturn(result, ResourcePipeline.Get).AsQueryable(); + var entities = await _repository.ToListAsync(entityQuery); + _hookExecutor.AfterRead(entities, ResourcePipeline.Get); + entityQuery = _hookExecutor.OnReturn(entities, ResourcePipeline.Get).AsQueryable(); } if (_options.IncludeTotalRecordCount) - _pageManager.TotalRecords = await _repository.CountAsync(entities); + _pageManager.TotalRecords = await _repository.CountAsync(entityQuery); // pagination should be done last since it will execute the query - var pagedEntities = await ApplyPageQueryAsync(entities); + var pagedEntities = await ApplyPageQueryAsync(entityQuery); return pagedEntities; } @@ -131,11 +115,10 @@ public virtual async Task GetAsync(TId id) var pipeline = ResourcePipeline.GetSingle; _hookExecutor?.BeforeRead(pipeline, id.ToString()); - TResource entity; - if (ShouldIncludeRelationships()) - entity = await GetWithRelationshipsAsync(id); - else - entity = await _repository.GetAsync(id); + var entityQuery = _repository.Get(id); + entityQuery = ApplyInclude(entityQuery); + entityQuery = ApplySelect(entityQuery); + var entity = await _repository.FirstOrDefaultAsync(entityQuery); if (!IsNull(_hookExecutor, entity)) { @@ -155,7 +138,8 @@ public virtual async Task GetRelationshipsAsync(TId id, string relati // TODO: it would be better if we could distinguish whether or not the relationship was not found, // vs the relationship not being set on the instance of T - var entity = await _repository.GetAndIncludeAsync(id, relationship); + var entityQuery = _repository.Include(_repository.Get(id), relationship); + var entity = await _repository.FirstOrDefaultAsync(entityQuery); if (entity == null) // this does not make sense. If the parent entity is not found, this error is thrown? throw new JsonApiException(404, $"Relationship '{relationshipName}' not found."); @@ -178,7 +162,6 @@ public virtual async Task GetRelationshipAsync(TId id, string relationsh public virtual async Task UpdateAsync(TId id, TResource entity) { - entity = IsNull(_hookExecutor) ? entity : _hookExecutor.BeforeUpdate(AsList(entity), ResourcePipeline.Patch).SingleOrDefault(); entity = await _repository.UpdateAsync(entity); if (!IsNull(_hookExecutor, entity)) @@ -193,7 +176,8 @@ public virtual async Task UpdateAsync(TId id, TResource entity) public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipName, object related) { var relationship = GetRelationship(relationshipName); - var entity = await _repository.GetAndIncludeAsync(id, relationship); + var entityQuery = _repository.Include(_repository.Get(id), relationship); + var entity = await _repository.FirstOrDefaultAsync(entityQuery); if (entity == null) throw new JsonApiException(404, $"Entity with id {id} could not be found."); @@ -207,7 +191,6 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa entity = IsNull(_hookExecutor) ? entity : _hookExecutor.BeforeUpdate(AsList(entity), ResourcePipeline.PatchRelationship).SingleOrDefault(); await _repository.UpdateRelationshipsAsync(entity, relationship, relationshipIds); if (!IsNull(_hookExecutor, entity)) _hookExecutor.AfterUpdate(AsList(entity), ResourcePipeline.PatchRelationship); - } protected virtual async Task> ApplyPageQueryAsync(IQueryable entities) @@ -227,26 +210,62 @@ protected virtual async Task> ApplyPageQueryAsync(IQuerya return await _repository.PageAsync(entities, _pageManager.PageSize, _pageManager.CurrentPage); } - protected virtual IQueryable ApplySortAndFilterQuery(IQueryable entities) + /// + /// Applies sort queries + /// + /// + /// + protected virtual IQueryable ApplySort(IQueryable entities) + { + var queries = _sortService.Get(); + if (queries != null && queries.Any()) + foreach (var query in queries) + entities = _repository.Sort(entities, query); + + return entities; + } + + /// + /// Applies filter queries + /// + /// + /// + protected virtual IQueryable ApplyFilter(IQueryable entities) { - foreach (var query in _filterService.Get()) - entities = _repository.Filter(entities, query); + var queries = _filterService.Get(); + if (queries != null && queries.Any()) + foreach (var query in queries) + entities = _repository.Filter(entities, query); + + return entities; + } + - foreach (var query in _sortService.Get()) - entities = _repository.Sort(entities, query); + /// + /// Applies include queries + /// + /// + /// + protected virtual IQueryable ApplyInclude(IQueryable entities) + { + var chains = _includeService.Get(); + if (chains != null && chains.Any()) + foreach (var r in chains) + entities = _repository.Include(entities, r.ToArray()); return entities; } /// - /// Actually includes the relationships + /// Applies sparse field selection queries /// /// /// - protected virtual IQueryable IncludeRelationships(IQueryable entities) + protected virtual IQueryable ApplySelect(IQueryable entities) { - foreach (var r in _includeService.Get()) - entities = _repository.Include(entities, r.ToArray()); + var fields = _sparseFieldsService.Get(); + if (fields != null && fields.Any()) + entities = _repository.Select(entities, fields); return entities; } @@ -254,12 +273,12 @@ protected virtual IQueryable IncludeRelationships(IQueryable /// Get the specified id with relationships provided in the post request /// - /// + /// i /// private async Task GetWithRelationshipsAsync(TId id) { var sparseFieldset = _sparseFieldsService.Get(); - var query = _repository.Select(_repository.Get(), sparseFieldset).Where(e => e.Id.Equals(id)); + var query = _repository.Select(_repository.Get(id), sparseFieldset); foreach (var chain in _includeService.Get()) query = _repository.Include(query, chain.ToArray()); @@ -275,11 +294,6 @@ private async Task GetWithRelationshipsAsync(TId id) return value; } - private bool ShouldIncludeRelationships() - { - return _includeService.Get().Count() > 0; - } - private bool IsNull(params object[] values) { foreach (var val in values) @@ -312,11 +326,10 @@ public class EntityResourceService : EntityResourceService { public EntityResourceService(ISortService sortService, IFilterService filterService, IEntityRepository repository, - IJsonApiOptions options, ICurrentRequest currentRequest, - IIncludeService includeService, ISparseFieldsService sparseFieldsService, + IJsonApiOptions options,IIncludeService includeService, ISparseFieldsService sparseFieldsService, IPageService pageManager, IResourceGraph resourceGraph, IResourceHookExecutor hookExecutor = null, ILoggerFactory loggerFactory = null) - : base(sortService, filterService, repository, options, currentRequest, includeService, sparseFieldsService, pageManager, resourceGraph, hookExecutor, loggerFactory) + : base(sortService, filterService, repository, options, includeService, sparseFieldsService, pageManager, resourceGraph, hookExecutor, loggerFactory) { } } diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 1e80ce1f58..97dbc35eb8 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -102,10 +102,10 @@ public TestModelService( IEntityRepository repository, IJsonApiOptions options, IRequestContext currentRequest, - IPageQueryService pageManager, + IPageQueryService pageService, IResourceGraph resourceGraph, ILoggerFactory loggerFactory = null, - IResourceHookExecutor hookExecutor = null) : base(repository, options, currentRequest, pageManager, resourceGraph, loggerFactory, hookExecutor) + IResourceHookExecutor hookExecutor = null) : base(repository, options, currentRequest, pageService, resourceGraph, loggerFactory, hookExecutor) { } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs index c166e22a88..2ba9fbcff5 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs @@ -158,7 +158,7 @@ public async Task Can_Patch_Entity() var updatedTodoItem = _context.TodoItems.AsNoTracking() .Include(t => t.Owner) .SingleOrDefault(t => t.Id == todoItem.Id); - Assert.Equal(person.Id, updatedTodoItem.OwnerId); + Assert.Equal(person.Id, todoItem.OwnerId); Assert.Equal(newTodoItem.Description, updatedTodoItem.Description); Assert.Equal(newTodoItem.Ordinal, updatedTodoItem.Ordinal); } diff --git a/test/UnitTests/Builders/LinkBuilderTests.cs b/test/UnitTests/Builders/LinkBuilderTests.cs index 193b99f1f2..4dc2ec44a8 100644 --- a/test/UnitTests/Builders/LinkBuilderTests.cs +++ b/test/UnitTests/Builders/LinkBuilderTests.cs @@ -15,7 +15,7 @@ namespace UnitTests { public class LinkBuilderTests { - private readonly IPageService _pageManager; + private readonly IPageService _pageService; private readonly Mock _provider = new Mock(); private const string _host = "http://www.example.com"; private const string _topSelf = "http://www.example.com/articles"; @@ -25,7 +25,7 @@ public class LinkBuilderTests public LinkBuilderTests() { - _pageManager = GetPageManager(); + _pageService = GetPageManager(); } [Theory] @@ -141,7 +141,7 @@ public void BuildTopLevelLinks_GlobalAndResourceConfiguration_ExpectedLinks(Link var primaryResource = GetContextEntity
(topLevelLinks: resource); _provider.Setup(m => m.GetContextEntity
()).Returns(primaryResource); - var builder = new LinkBuilder(config, GetRequestManager(), _pageManager, _provider.Object); + var builder = new LinkBuilder(config, GetRequestManager(), _pageService, _provider.Object); // act var links = builder.GetTopLevelLinks(primaryResource); diff --git a/test/UnitTests/Data/DefaultEntityRepository_Tests.cs b/test/UnitTests/Data/DefaultEntityRepository_Tests.cs index 14fcbdb61b..4260618e26 100644 --- a/test/UnitTests/Data/DefaultEntityRepository_Tests.cs +++ b/test/UnitTests/Data/DefaultEntityRepository_Tests.cs @@ -81,7 +81,6 @@ private DefaultEntityRepository GetRepository() return new DefaultEntityRepository( - _currentRequestMock.Object, _targetedFieldsMock.Object, _contextResolverMock.Object, graph, null, null); diff --git a/test/UnitTests/QueryParameters/SparseFieldsServiceTests.cs b/test/UnitTests/QueryParameters/SparseFieldsServiceTests.cs index a22da83021..eeb0648110 100644 --- a/test/UnitTests/QueryParameters/SparseFieldsServiceTests.cs +++ b/test/UnitTests/QueryParameters/SparseFieldsServiceTests.cs @@ -36,13 +36,14 @@ public void Parse_ValidSelection_CanParse() const string attrName = "some-field"; const string internalAttrName = "SomeField"; var attribute = new AttrAttribute(attrName) { InternalAttributeName = internalAttrName }; + var idAttribute = new AttrAttribute("id") { InternalAttributeName = "Id" }; var query = new KeyValuePair($"fields[{type}]", new StringValues(attrName)); var contextEntity = new ContextEntity { EntityName = type, - Attributes = new List { attribute }, + Attributes = new List { attribute, idAttribute }, Relationships = new List() }; var service = GetService(contextEntity); @@ -53,7 +54,8 @@ public void Parse_ValidSelection_CanParse() // assert Assert.NotEmpty(result); - Assert.Equal(attribute, result.Single()); + Assert.Equal(idAttribute, result.First()); + Assert.Equal(attribute, result[1]); } [Fact] diff --git a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs index 91663dcfe9..8fe8cfe346 100644 --- a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs +++ b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs @@ -347,7 +347,7 @@ AppDbContext dbContext ) where TModel : class, IIdentifiable { IDbContextResolver resolver = CreateTestDbResolver(dbContext); - return new DefaultEntityRepository(null, null, resolver, null, null, null); + return new DefaultEntityRepository(null, resolver, null, null, null); } IDbContextResolver CreateTestDbResolver(AppDbContext dbContext) where TModel : class, IIdentifiable diff --git a/test/UnitTests/Services/EntityResourceService_Tests.cs b/test/UnitTests/Services/EntityResourceService_Tests.cs index 2a55ec8e37..2ff9f83cb6 100644 --- a/test/UnitTests/Services/EntityResourceService_Tests.cs +++ b/test/UnitTests/Services/EntityResourceService_Tests.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; @@ -45,8 +47,12 @@ public async Task GetRelationshipAsync_Passes_Public_ResourceName_To_Repository( const string relationshipName = "collection"; var relationship = new HasOneAttribute(relationshipName); - _repositoryMock.Setup(m => m.GetAndIncludeAsync(id, relationship, null)) - .ReturnsAsync(new TodoItem()); + var todoItem = new TodoItem(); + var query = new List { todoItem }.AsQueryable(); + + _repositoryMock.Setup(m => m.Get(id)).Returns(query); + _repositoryMock.Setup(m => m.Include(query, relationship)).Returns(query); + _repositoryMock.Setup(m => m.FirstOrDefaultAsync(query)).ReturnsAsync(todoItem); var service = GetService(); @@ -54,7 +60,9 @@ public async Task GetRelationshipAsync_Passes_Public_ResourceName_To_Repository( await service.GetRelationshipAsync(id, relationshipName); // assert - _repositoryMock.Verify(m => m.GetAndIncludeAsync(id, relationship, null), Times.Once); + _repositoryMock.Verify(m => m.Get(id), Times.Once); + _repositoryMock.Verify(m => m.Include(query, relationship), Times.Once); + _repositoryMock.Verify(m => m.FirstOrDefaultAsync(query), Times.Once); } [Fact] @@ -70,8 +78,11 @@ public async Task GetRelationshipAsync_Returns_Relationship_Value() Collection = new TodoItemCollection { Id = Guid.NewGuid() } }; - _repositoryMock.Setup(m => m.GetAndIncludeAsync(id, relationship, null)) - .ReturnsAsync(todoItem); + var query = new List { todoItem }.AsQueryable(); + + _repositoryMock.Setup(m => m.Get(id)).Returns(query); + _repositoryMock.Setup(m => m.Include(query, relationship)).Returns(query); + _repositoryMock.Setup(m => m.FirstOrDefaultAsync(query)).ReturnsAsync(todoItem); var repository = GetService(); @@ -86,7 +97,7 @@ public async Task GetRelationshipAsync_Returns_Relationship_Value() private EntityResourceService GetService() { - return new EntityResourceService(null, null, _repositoryMock.Object, new JsonApiOptions(), _crMock.Object, null, null, _pgsMock.Object, _resourceGraph); + return new EntityResourceService(null, null, _repositoryMock.Object, new JsonApiOptions(), null, null, _pgsMock.Object, _resourceGraph); } } }