From 9e72a387b9e8ec7c6167a33d7b2eba67073e3a58 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Mon, 2 Jul 2018 10:12:16 -0700 Subject: [PATCH] feat(DocumentBuilder): allow for output attribute filtering --- .../Controllers/UsersController.cs | 17 ++ .../Data/AppDbContext.cs | 2 +- .../Migrations/20180327120810_initial.cs | 2 - .../JsonApiDotNetCoreExample/Models/User.cs | 10 ++ .../Resources/UserResource.cs | 12 ++ .../JsonApiDotNetCoreExample/Startup.cs | 7 +- .../Controllers/ReportsController.cs | 5 +- .../ReportsExample/Services/ReportService.cs | 1 - .../Builders/ContextGraphBuilder.cs | 12 +- .../Builders/DocumentBuilder.cs | 28 +++- .../Builders/IDocumentBuilder.cs | 6 +- .../Controllers/JsonApiCmdController.cs | 1 - .../Controllers/JsonApiQueryController.cs | 2 - .../DependencyInjection/ServiceLocator.cs | 18 +++ .../Extensions/DbContextExtensions.cs | 1 - .../Internal/ContextEntity.cs | 29 ++++ .../Internal/ContextGraph.cs | 4 +- .../Internal/Generics/GenericProcessor.cs | 1 - .../Generics/GenericProcessorFactory.cs | 1 - .../Internal/JsonApiRouteHandler.cs | 1 - .../Internal/Query/AttrFilterQuery.cs | 1 - .../Internal/Query/RelatedAttrFilterQuery.cs | 2 - src/JsonApiDotNetCore/Models/AttrAttribute.cs | 26 ++- .../Models/ResourceDefinition.cs | 107 +++++++++++++ .../Serialization/JsonApiDeSerializer.cs | 15 +- .../Serialization/JsonApiSerializer.cs | 2 +- .../Operations/Processors/GetOpProcessor.cs | 1 - .../NullValuedAttributeHandlingTests.cs | 1 - .../Extensibility/RepositoryOverrideTests.cs | 1 - .../Extensibility/RequestMetaTests.cs | 1 - .../ResourceDefinitions/OutputAttrs_Tests.cs | 148 ++++++++++++++++++ .../Acceptance/Spec/AttributeFilterTests.cs | 1 - .../Acceptance/Spec/AttributeSortTests.cs | 1 - .../Acceptance/Spec/DocumentTests/Included.cs | 1 - .../Spec/FetchingRelationshipsTests.cs | 3 +- .../Acceptance/Spec/PagingTests.cs | 1 - .../Acceptance/TodoItemsControllerTests.cs | 1 - .../Extensions/IQueryableExtensions.cs | 1 - .../AuthorizedTodoItemsRepository.cs | 1 - .../WebHostCollection.cs | 1 - .../Builders/ContextGraphBuilder_Tests.cs | 11 +- .../DocumentBuilderBehaviour_Tests.cs | 3 - .../Builders/DocumentBuilder_Tests.cs | 120 +++++++++++++- .../BaseJsonApiController_Tests.cs | 1 - .../JsonApiControllerMixin_Tests.cs | 1 - .../Data/DefaultEntityRepository_Tests.cs | 9 +- .../Models/ResourceDefinitionTests.cs | 129 +++++++++++++++ test/UnitTests/Services/QueryComposerTests.cs | 1 - 48 files changed, 655 insertions(+), 96 deletions(-) create mode 100644 src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs create mode 100644 src/Examples/JsonApiDotNetCoreExample/Models/User.cs create mode 100644 src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs create mode 100644 src/JsonApiDotNetCore/DependencyInjection/ServiceLocator.cs create mode 100644 src/JsonApiDotNetCore/Models/ResourceDefinition.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/OutputAttrs_Tests.cs create mode 100644 test/UnitTests/Models/ResourceDefinitionTests.cs diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs new file mode 100644 index 0000000000..dbd144caa4 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs @@ -0,0 +1,17 @@ +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample.Models; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCoreExample.Controllers +{ + public class UsersController : JsonApiController + { + public UsersController( + IJsonApiContext jsonApiContext, + IResourceService resourceService, + ILoggerFactory loggerFactory) + : base(jsonApiContext, resourceService, loggerFactory) + { } + } +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs index 4b9a40f7fd..6f50f9aa5b 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs @@ -37,7 +37,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public DbSet
Articles { get; set; } public DbSet Authors { get; set; } - public DbSet NonJsonApiResources { get; set; } + public DbSet Users { get; set; } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Migrations/20180327120810_initial.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/20180327120810_initial.cs index cc696f54bf..ffbf105255 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Migrations/20180327120810_initial.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Migrations/20180327120810_initial.cs @@ -1,7 +1,5 @@ -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using System; -using System.Collections.Generic; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace JsonApiDotNetCoreExample.Migrations diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/User.cs b/src/Examples/JsonApiDotNetCoreExample/Models/User.cs new file mode 100644 index 0000000000..3b66f0dbb2 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Models/User.cs @@ -0,0 +1,10 @@ +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCoreExample.Models +{ + public class User : Identifiable + { + [Attr("username")] public string Username { get; set; } + [Attr("password")] public string Password { get; set; } + } +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs new file mode 100644 index 0000000000..030bc4eaa4 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCoreExample.Models; + +namespace JsonApiDotNetCoreExample.Resources +{ + public class UserResource : ResourceDefinition + { + protected override List OutputAttrs() + => Remove(user => user.Password); + } +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startup.cs index 2c7574e1a2..ec1bdc544c 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startup.cs @@ -7,6 +7,9 @@ using Microsoft.EntityFrameworkCore; using JsonApiDotNetCore.Extensions; using System; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCoreExample.Resources; +using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExample { @@ -38,7 +41,9 @@ public virtual IServiceProvider ConfigureServices(IServiceCollection services) options.Namespace = "api/v1"; options.DefaultPageSize = 5; options.IncludeTotalRecordCount = true; - }); + }) + // TODO: this should be handled via auto-discovery + .AddScoped, UserResource>(); var provider = services.BuildServiceProvider(); var appContext = provider.GetRequiredService(); diff --git a/src/Examples/ReportsExample/Controllers/ReportsController.cs b/src/Examples/ReportsExample/Controllers/ReportsController.cs index 523ad417bd..6f431d9291 100644 --- a/src/Examples/ReportsExample/Controllers/ReportsController.cs +++ b/src/Examples/ReportsExample/Controllers/ReportsController.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; diff --git a/src/Examples/ReportsExample/Services/ReportService.cs b/src/Examples/ReportsExample/Services/ReportService.cs index 7baffc6174..9e5348a612 100644 --- a/src/Examples/ReportsExample/Services/ReportService.cs +++ b/src/Examples/ReportsExample/Services/ReportService.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Threading.Tasks; using JsonApiDotNetCore.Services; diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs index 73e355b2de..080c0a6bb7 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs @@ -59,7 +59,6 @@ public IContextGraph Build() _entities.ForEach(e => e.Links = GetLinkFlags(e.EntityType)); var graph = new ContextGraph(_entities, _usesDbContext, _validationResults); - return graph; } @@ -83,7 +82,8 @@ public IContextGraphBuilder AddResource(string pluralizedTypeNam EntityType = entityType, IdentityType = idType, Attributes = GetAttributes(entityType), - Relationships = GetRelationships(entityType) + Relationships = GetRelationships(entityType), + ResourceType = GetResourceDefinitionType(entityType) }; private Link GetLinkFlags(Type entityType) @@ -104,8 +104,12 @@ protected virtual List GetAttributes(Type entityType) foreach (var prop in properties) { var attribute = (AttrAttribute)prop.GetCustomAttribute(typeof(AttrAttribute)); - if (attribute == null) continue; + if (attribute == null) + continue; + attribute.InternalAttributeName = prop.Name; + attribute.PropertyInfo = prop; + attributes.Add(attribute); } return attributes; @@ -136,6 +140,8 @@ protected virtual Type GetRelationshipType(RelationshipAttribute relation, Prope return prop.PropertyType; } + private Type GetResourceDefinitionType(Type entityType) => typeof(ResourceDefinition<>).MakeGenericType(entityType); + public IContextGraphBuilder AddDbContext() where T : DbContext { _usesDbContext = true; diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 9f86a57945..c6f5f999b4 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -14,22 +15,29 @@ public class DocumentBuilder : IDocumentBuilder private readonly IContextGraph _contextGraph; private readonly IRequestMeta _requestMeta; private readonly DocumentBuilderOptions _documentBuilderOptions; + private readonly IScopedServiceProvider _scopedServiceProvider; - public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta = null, IDocumentBuilderOptionsProvider documentBuilderOptionsProvider = null) + public DocumentBuilder( + IJsonApiContext jsonApiContext, + IRequestMeta requestMeta = null, + IDocumentBuilderOptionsProvider documentBuilderOptionsProvider = null, + IScopedServiceProvider scopedServiceProvider = null) { _jsonApiContext = jsonApiContext; _contextGraph = jsonApiContext.ContextGraph; _requestMeta = requestMeta; - _documentBuilderOptions = documentBuilderOptionsProvider?.GetDocumentBuilderOptions() ?? new DocumentBuilderOptions(); ; + _documentBuilderOptions = documentBuilderOptionsProvider?.GetDocumentBuilderOptions() ?? new DocumentBuilderOptions(); + _scopedServiceProvider = scopedServiceProvider; } public Document Build(IIdentifiable entity) { var contextEntity = _contextGraph.GetContextEntity(entity.GetType()); + var resourceDefinition = _scopedServiceProvider?.GetService(contextEntity.ResourceType) as IResourceDefinition; var document = new Document { - Data = GetData(contextEntity, entity), + Data = GetData(contextEntity, entity, resourceDefinition), Meta = GetMeta(entity) }; @@ -44,8 +52,8 @@ public Document Build(IIdentifiable entity) public Documents Build(IEnumerable entities) { var entityType = entities.GetElementType(); - var contextEntity = _contextGraph.GetContextEntity(entityType); + var resourceDefinition = _scopedServiceProvider?.GetService(contextEntity.ResourceType) as IResourceDefinition; var enumeratedEntities = entities as IList ?? entities.ToList(); var documents = new Documents @@ -59,7 +67,7 @@ public Documents Build(IEnumerable entities) foreach (var entity in enumeratedEntities) { - documents.Data.Add(GetData(contextEntity, entity)); + documents.Data.Add(GetData(contextEntity, entity, resourceDefinition)); documents.Included = AppendIncludedObject(documents.Included, contextEntity, entity); } @@ -98,7 +106,11 @@ private List AppendIncludedObject(List includedObjec return includedObject; } + [Obsolete("You should specify an IResourceDefinition implementation using the GetData/3 overload.")] public DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity) + => GetData(contextEntity, entity, resourceDefinition: null); + + public DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity, IResourceDefinition resourceDefinition = null) { var data = new DocumentData { @@ -111,7 +123,8 @@ public DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity) data.Attributes = new Dictionary(); - contextEntity.Attributes.ForEach(attr => + var resourceAttributes = resourceDefinition?.GetOutputAttrs(entity) ?? contextEntity.Attributes; + resourceAttributes.ForEach(attr => { var attributeValue = attr.GetValue(entity); if (ShouldIncludeAttribute(attr, attributeValue)) @@ -219,8 +232,9 @@ private DocumentData GetIncludedEntity(IIdentifiable entity) if (entity == null) return null; var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(entity.GetType()); + var resourceDefinition = _scopedServiceProvider.GetService(contextEntity.ResourceType) as IResourceDefinition; - var data = GetData(contextEntity, entity); + var data = GetData(contextEntity, entity, resourceDefinition); data.Attributes = new Dictionary(); diff --git a/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs index 4fbc8df01b..dccd6f753a 100644 --- a/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; @@ -8,6 +9,9 @@ public interface IDocumentBuilder { Document Build(IIdentifiable entity); Documents Build(IEnumerable entities); + + [Obsolete("You should specify an IResourceDefinition implementation using the GetData/3 overload.")] DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity); + DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity, IResourceDefinition resourceDefinition = null); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs index 20e5445ebb..82c0fe40c4 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs @@ -3,7 +3,6 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Controllers { diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs index 3e78bc4f8f..5211e5fa3b 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs @@ -1,9 +1,7 @@ -using System.Collections.Generic; using System.Threading.Tasks; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Controllers { diff --git a/src/JsonApiDotNetCore/DependencyInjection/ServiceLocator.cs b/src/JsonApiDotNetCore/DependencyInjection/ServiceLocator.cs new file mode 100644 index 0000000000..31164ee3b9 --- /dev/null +++ b/src/JsonApiDotNetCore/DependencyInjection/ServiceLocator.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading; + +namespace JsonApiDotNetCore.DependencyInjection +{ + internal class ServiceLocator + { + public static AsyncLocal _scopedProvider = new AsyncLocal(); + public static void Initialize(IServiceProvider serviceProvider) => _scopedProvider.Value = serviceProvider; + + public static object GetService(Type type) + => _scopedProvider.Value != null + ? _scopedProvider.Value.GetService(type) + : throw new InvalidOperationException( + $"Service locator has not been initialized for the current asynchronous flow. Call {nameof(Initialize)} first." + ); + } +} diff --git a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs b/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs index 2756524dce..9176474548 100644 --- a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; diff --git a/src/JsonApiDotNetCore/Internal/ContextEntity.cs b/src/JsonApiDotNetCore/Internal/ContextEntity.cs index ff539b79ea..1e15a9c6bc 100644 --- a/src/JsonApiDotNetCore/Internal/ContextEntity.cs +++ b/src/JsonApiDotNetCore/Internal/ContextEntity.cs @@ -6,11 +6,40 @@ namespace JsonApiDotNetCore.Internal { public class ContextEntity { + /// + /// The exposed resource name + /// public string EntityName { get; set; } + + /// + /// The data model type + /// public Type EntityType { get; set; } + + /// + /// The identity member type + /// public Type IdentityType { get; set; } + + /// + /// The concrete type. + /// We store this so that we don't need to re-compute the generic type. + /// + public Type ResourceType { get; set; } + + /// + /// Exposed resource attributes + /// public List Attributes { get; set; } + + /// + /// Exposed resource relationships + /// public List Relationships { get; set; } + + /// + /// Links to include in resource responses + /// public Link Links { get; set; } = Link.All; } } diff --git a/src/JsonApiDotNetCore/Internal/ContextGraph.cs b/src/JsonApiDotNetCore/Internal/ContextGraph.cs index c27a01b7d8..9c62ae4d94 100644 --- a/src/JsonApiDotNetCore/Internal/ContextGraph.cs +++ b/src/JsonApiDotNetCore/Internal/ContextGraph.cs @@ -17,14 +17,15 @@ public class ContextGraph : IContextGraph { internal List Entities { get; } internal List ValidationResults { get; } + internal static IContextGraph Instance { get; set; } public ContextGraph() { } - public ContextGraph(List entities, bool usesDbContext) { Entities = entities; UsesDbContext = usesDbContext; ValidationResults = new List(); + Instance = this; } // eventually, this is the planned public constructor @@ -36,6 +37,7 @@ internal ContextGraph(List entities, bool usesDbContext, List public bool IsSortable { get; } + /// + /// The member property info + /// + internal PropertyInfo PropertyInfo { get; set; } + /// /// Get the value of the attribute for the given object. /// Returns null if the attribute does not belong to the /// provided object. /// - public object GetValue(object entity) - { - return entity - .GetType() - .GetProperty(InternalAttributeName) - ?.GetValue(entity); - } + public object GetValue(object entity) => PropertyInfo.GetValue(entity); /// /// Sets the value of the attribute on the given object. /// public void SetValue(object entity, object newValue) { - var propertyInfo = entity - .GetType() - .GetProperty(InternalAttributeName); - - if (propertyInfo != null) - { - var convertedValue = TypeHelper.ConvertType(newValue, propertyInfo.PropertyType); - - propertyInfo.SetValue(entity, convertedValue); - } + var convertedValue = TypeHelper.ConvertType(newValue, PropertyInfo.PropertyType); + PropertyInfo.SetValue(entity, convertedValue); } /// diff --git a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs new file mode 100644 index 0000000000..03ac62390b --- /dev/null +++ b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs @@ -0,0 +1,107 @@ +using JsonApiDotNetCore.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace JsonApiDotNetCore.Models +{ + public interface IResourceDefinition + { + List GetOutputAttrs(object instance); + } + + /// + /// A scoped service used to... + /// + /// The resource type + public class ResourceDefinition : IResourceDefinition where T : class, IIdentifiable + { + private readonly IContextGraph _graph; + private readonly ContextEntity _contextEntity; + internal readonly bool _instanceAttrsAreSpecified; + + private bool _requestCachedAttrsHaveBeenLoaded = false; + private List _requestCachedAttrs; + + public ResourceDefinition() + { + _graph = ContextGraph.Instance; + _contextEntity = ContextGraph.Instance.GetContextEntity(typeof(T)); + _instanceAttrsAreSpecified = InstanceOutputAttrsAreSpecified(); + } + + private bool InstanceOutputAttrsAreSpecified() + { + var derivedType = GetType(); + var methods = derivedType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance); + var instanceMethod = methods + .Where(m => + m.Name == nameof(OutputAttrs) + && m.GetParameters() + .FirstOrDefault() + ?.ParameterType == typeof(T)) + .FirstOrDefault(); + var declaringType = instanceMethod?.DeclaringType; + return declaringType == derivedType; + } + + // TODO: need to investigate options for caching these + protected List Remove(Expression> filter, List from = null) + { + from = from ?? _contextEntity.Attributes; + + // model => model.Attribute + if (filter.Body is MemberExpression memberExpression) + return _contextEntity.Attributes + .Where(a => a.PropertyInfo.Name != memberExpression.Member.Name) + .ToList(); + + // model => new { model.Attribute1, model.Attribute2 } + if (filter.Body is NewExpression newExpression) + { + var attributes = new List(); + foreach (var attr in _contextEntity.Attributes) + if (newExpression.Members.Any(m => m.Name == attr.PropertyInfo.Name) == false) + attributes.Add(attr); + + return attributes; + } + + throw new JsonApiException(500, + message: $"The expression returned by '{filter}' for '{GetType()}' is of type {filter.Body.GetType()}" + + " and cannot be used to select resource attributes. ", + detail: "The type must be a NewExpression. Example: article => new { article.Author }; "); + } + + /// + /// Called once per filtered resource in request. + /// + protected virtual List OutputAttrs() => _contextEntity.Attributes; + + /// + /// Called for every instance of a resource + /// + protected virtual List OutputAttrs(T instance) => _contextEntity.Attributes; + + public List GetOutputAttrs(object instance) + => _instanceAttrsAreSpecified == false + ? GetOutputAttrs() + : OutputAttrs(instance as T); + + private List GetOutputAttrs() + { + if (_requestCachedAttrsHaveBeenLoaded == false) + { + _requestCachedAttrs = OutputAttrs(); + // the reason we don't just check for null is because we + // guarantee that OutputAttrs will be called once per + // request and null is a valid return value + _requestCachedAttrsHaveBeenLoaded = true; + } + + return _requestCachedAttrs; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 285923995d..0c3b7217fe 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -124,7 +124,7 @@ public object DocumentToObject(DocumentData data, List included = + "If you have manually registered the resource, check that the call to AddResource correctly sets the public name."); ; var entity = Activator.CreateInstance(contextEntity.EntityType); - + entity = SetEntityAttributes(entity, contextEntity, data.Attributes); entity = SetRelationships(entity, contextEntity, data.Relationships, included); @@ -141,20 +141,13 @@ private object SetEntityAttributes( { if (attributeValues == null || attributeValues.Count == 0) return entity; - - var entityProperties = entity.GetType().GetProperties(); - + foreach (var attr in contextEntity.Attributes) { - var entityProperty = entityProperties.FirstOrDefault(p => p.Name == attr.InternalAttributeName); - - if (entityProperty == null) - throw new ArgumentException($"{contextEntity.EntityType.Name} does not contain an attribute named {attr.InternalAttributeName}", nameof(entity)); - if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue)) { - var convertedValue = ConvertAttrValue(newValue, entityProperty.PropertyType); - entityProperty.SetValue(entity, convertedValue); + var convertedValue = ConvertAttrValue(newValue, attr.PropertyInfo.PropertyType); + attr.PropertyInfo.SetValue(entity, convertedValue); if (attr.IsImmutable == false) _jsonApiContext.AttributesToUpdate[attr] = convertedValue; diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs index 500101cc62..a784554f58 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs @@ -13,7 +13,7 @@ public class JsonApiSerializer : IJsonApiSerializer private readonly IDocumentBuilder _documentBuilder; private readonly ILogger _logger; private readonly IJsonApiContext _jsonApiContext; - + public JsonApiSerializer( IJsonApiContext jsonApiContext, IDocumentBuilder documentBuilder) diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs index d8793d016c..a3737dd57d 100644 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs index 7942ffd919..05c348a553 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models; -using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Newtonsoft.Json; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RepositoryOverrideTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RepositoryOverrideTests.cs index 09a2791048..95ea814a5d 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RepositoryOverrideTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RepositoryOverrideTests.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Services; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs index 2397bb5529..4f9198619a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.TestHost; using Xunit; using JsonApiDotNetCoreExample.Models; -using JsonApiDotNetCoreExample; using Newtonsoft.Json; using JsonApiDotNetCore.Models; using System.Collections; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/OutputAttrs_Tests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/OutputAttrs_Tests.cs new file mode 100644 index 0000000000..c5a6c054a4 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/OutputAttrs_Tests.cs @@ -0,0 +1,148 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Bogus; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance +{ + [Collection("WebHostCollection")] + public class OutputAttrs_Tests + { + private TestFixture _fixture; + private AppDbContext _context; + private Faker _userFaker; + + public OutputAttrs_Tests(TestFixture fixture) + { + _fixture = fixture; + _context = fixture.GetService(); + _userFaker = new Faker() + .RuleFor(u => u.Username, f => f.Internet.UserName()) + .RuleFor(u => u.Password, f => f.Internet.Password()); + } + + [Fact] + public async Task Password_Is_Not_Included_In_Response_Payload() + { + // Arrange + var user = _userFaker.Generate(); + _context.Users.Add(user); + _context.SaveChanges(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/users/{user.Id}"; + var request = new HttpRequestMessage(httpMethod, route); + + // Act + var response = await _fixture.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var document = JsonConvert.DeserializeObject(body); + Assert.False(document.Data.Attributes.ContainsKey("password")); + } + + [Fact] + public async Task Can_Create_User_With_Password() + { + // Arrange + var user = _userFaker.Generate(); + var content = new + { + data = new + { + type = "users", + attributes = new Dictionary() + { + { "username", user.Username }, + { "password", user.Password }, + } + } + }; + + var httpMethod = new HttpMethod("POST"); + var route = $"/api/v1/users"; + + var request = new HttpRequestMessage(httpMethod, route); + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + + // Act + var response = await _fixture.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + // response assertions + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = (User)_fixture.GetService().Deserialize(body); + var document = JsonConvert.DeserializeObject(body); + Assert.False(document.Data.Attributes.ContainsKey("password")); + Assert.Equal(user.Username, document.Data.Attributes["username"]); + + // db assertions + var dbUser = await _context.Users.FindAsync(deserializedBody.Id); + Assert.Equal(user.Username, dbUser.Username); + Assert.Equal(user.Password, dbUser.Password); + } + + [Fact] + public async Task Can_Update_User_Password() + { + // Arrange + var user = _userFaker.Generate(); + _context.Users.Add(user); + _context.SaveChanges(); + + var newPassword = _userFaker.Generate().Password; + + var content = new + { + data = new + { + type = "users", + id = user.Id, + attributes = new Dictionary() + { + { "password", newPassword }, + } + } + }; + + var httpMethod = new HttpMethod("PATCH"); + var route = $"/api/v1/users/{user.Id}"; + + var request = new HttpRequestMessage(httpMethod, route); + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + + // Act + var response = await _fixture.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // response assertions + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = (User)_fixture.GetService().Deserialize(body); + var document = JsonConvert.DeserializeObject(body); + Assert.False(document.Data.Attributes.ContainsKey("password")); + Assert.Equal(user.Username, document.Data.Attributes["username"]); + + // db assertions + var dbUser = _context.Users.AsNoTracking().Single(u => u.Id == user.Id); + Assert.Equal(newPassword, dbUser.Password); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs index b928df4ada..9e154f9b47 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs @@ -9,7 +9,6 @@ using JsonApiDotNetCore.Serialization; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; -using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Xunit; using Person = JsonApiDotNetCoreExample.Models.Person; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeSortTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeSortTests.cs index 6de3293596..4aef3817fe 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeSortTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeSortTests.cs @@ -1,7 +1,6 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; -using JsonApiDotNetCoreExample; using Xunit; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs index 25f81fb66c..cfe842616b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Net; using System.Net.Http; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs index 621cb8e349..9c9ea29ccb 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs @@ -1,5 +1,4 @@ -using System.Linq; -using System.Net; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using Bogus; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs index 2d77982e62..0667b51756 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs @@ -5,7 +5,6 @@ using Bogus; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Models; using Xunit; using Person = JsonApiDotNetCoreExample.Models.Person; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index 672492df16..cf1ea6de96 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -7,7 +7,6 @@ using Bogus; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Newtonsoft.Json; diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs index a7bfb95b9d..9298d93a05 100644 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs @@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Storage; -using Remotion.Linq.Parsing.Structure; using Database = Microsoft.EntityFrameworkCore.Storage.Database; namespace JsonApiDotNetCoreExampleTests.Helpers.Extensions diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs index 32cb24cdcf..4ce5da35d8 100644 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs @@ -1,7 +1,6 @@ using System.Linq; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Services; using Microsoft.Extensions.Logging; diff --git a/test/JsonApiDotNetCoreExampleTests/WebHostCollection.cs b/test/JsonApiDotNetCoreExampleTests/WebHostCollection.cs index 960b7c1e46..561d86bbb0 100644 --- a/test/JsonApiDotNetCoreExampleTests/WebHostCollection.cs +++ b/test/JsonApiDotNetCoreExampleTests/WebHostCollection.cs @@ -1,4 +1,3 @@ -using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExampleTests.Acceptance; using Xunit; diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index 8bcef5e4dd..d5207fb6ef 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -1,10 +1,8 @@ -using System; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Moq; using Xunit; namespace UnitTests @@ -33,10 +31,11 @@ public void Can_Build_ContextGraph_Using_Builder() // assert var contextGraph = container.GetRequiredService(); - var dbResource = contextGraph.GetContextEntity("db-resources").EntityType; - var nonDbResource = contextGraph.GetContextEntity("non-db-resources").EntityType; - Assert.Equal(typeof(DbResource), dbResource); - Assert.Equal(typeof(NonDbResource), nonDbResource); + var dbResource = contextGraph.GetContextEntity("db-resources"); + var nonDbResource = contextGraph.GetContextEntity("non-db-resources"); + Assert.Equal(typeof(DbResource), dbResource.EntityType); + Assert.Equal(typeof(NonDbResource), nonDbResource.EntityType); + Assert.Equal(typeof(ResourceDefinition), nonDbResource.ResourceType); } } } diff --git a/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs b/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs index 333950f95f..3c5e2e5147 100644 --- a/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs +++ b/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Text; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Services; diff --git a/test/UnitTests/Builders/DocumentBuilder_Tests.cs b/test/UnitTests/Builders/DocumentBuilder_Tests.cs index 84c1f3f5c7..868ef698ee 100644 --- a/test/UnitTests/Builders/DocumentBuilder_Tests.cs +++ b/test/UnitTests/Builders/DocumentBuilder_Tests.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; +using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; @@ -154,7 +155,7 @@ public void Related_Data_Included_In_Relationships_By_Default() } [Fact] - public void IndependentIdentifier__Included_In_HasOne_Relationships_By_Default() + public void IndependentIdentifier_Included_In_HasOne_Relationships_By_Default() { // arrange const string relatedTypeName = "related-models"; @@ -204,7 +205,6 @@ public void Build_Can_Build_CustomIEnumerables() Assert.Single(documents.Data); } - [Theory] [InlineData(null, null, true)] [InlineData(false, null, true)] @@ -212,7 +212,8 @@ public void Build_Can_Build_CustomIEnumerables() [InlineData(null, "foo", true)] [InlineData(false, "foo", true)] [InlineData(true, "foo", true)] - public void DocumentBuilderOptions(bool? omitNullValuedAttributes, + public void DocumentBuilderOptions( + bool? omitNullValuedAttributes, string attributeValue, bool resultContainsAttribute) { @@ -230,12 +231,11 @@ public void DocumentBuilderOptions(bool? omitNullValuedAttributes, private class Model : Identifiable { + [Attr("StringProperty")] public string StringProperty { get; set; } + [HasOne("related-model", Link.None)] public RelatedModel RelatedModel { get; set; } public int RelatedModelId { get; set; } - [Attr("StringProperty")] - public string StringProperty { get; set; } - } private class RelatedModel : Identifiable @@ -263,5 +263,113 @@ IEnumerator IEnumerable.GetEnumerator() return models.GetEnumerator(); } } + + [Fact] + public void Build_Will_Use_Resource_If_Defined_For_Multiple_Documents() + { + var entities = new[] { new User() }; + var contextGraph = new ContextGraphBuilder() + .AddResource("user") + .Build(); + _jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + + var scopedServiceProvider = new TestScopedServiceProvider( + new ServiceCollection() + .AddScoped, UserResource>() + .BuildServiceProvider()); + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, scopedServiceProvider: scopedServiceProvider); + + var documents = documentBuilder.Build(entities); + + Assert.Single(documents.Data); + Assert.False(documents.Data[0].Attributes.ContainsKey("password")); + Assert.True(documents.Data[0].Attributes.ContainsKey("username")); + } + + [Fact] + public void Build_Will_Use_Resource_If_Defined_For_Single_Document() + { + var entity = new User(); + var contextGraph = new ContextGraphBuilder() + .AddResource("user") + .Build(); + _jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + + var scopedServiceProvider = new TestScopedServiceProvider( + new ServiceCollection() + .AddScoped, UserResource>() + .BuildServiceProvider()); + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, scopedServiceProvider: scopedServiceProvider); + + var documents = documentBuilder.Build(entity); + + Assert.False(documents.Data.Attributes.ContainsKey("password")); + Assert.True(documents.Data.Attributes.ContainsKey("username")); + } + + [Fact] + public void Build_Will_Use_Instance_Specific_Resource_If_Defined_For_Multiple_Documents() + { + var entities = new[] { new User() }; + var contextGraph = new ContextGraphBuilder() + .AddResource("user") + .Build(); + _jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + + var scopedServiceProvider = new TestScopedServiceProvider( + new ServiceCollection() + .AddScoped, InstanceSpecificUserResource>() + .BuildServiceProvider()); + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, scopedServiceProvider: scopedServiceProvider); + + var documents = documentBuilder.Build(entities); + + Assert.Single(documents.Data); + Assert.False(documents.Data[0].Attributes.ContainsKey("password")); + Assert.True(documents.Data[0].Attributes.ContainsKey("username")); + } + + [Fact] + public void Build_Will_Use_Instance_Specific_Resource_If_Defined_For_Single_Document() + { + var entity = new User(); + var contextGraph = new ContextGraphBuilder() + .AddResource("user") + .Build(); + _jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + + var scopedServiceProvider = new TestScopedServiceProvider( + new ServiceCollection() + .AddScoped, InstanceSpecificUserResource>() + .BuildServiceProvider()); + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, scopedServiceProvider: scopedServiceProvider); + + var documents = documentBuilder.Build(entity); + + Assert.False(documents.Data.Attributes.ContainsKey("password")); + Assert.True(documents.Data.Attributes.ContainsKey("username")); + } + + public class User : Identifiable + { + [Attr("username")] public string Username { get; set; } + [Attr("password")] public string Password { get; set; } + } + + public class InstanceSpecificUserResource : ResourceDefinition + { + protected override List OutputAttrs(User instance) + => Remove(user => user.Password); + } + + public class UserResource : ResourceDefinition + { + protected override List OutputAttrs() + => Remove(user => user.Password); + } } } diff --git a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs index d2d3ac8319..873b3f50d2 100644 --- a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs +++ b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs @@ -1,4 +1,3 @@ -using System; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; diff --git a/test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs b/test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs index c7a4a6cb4d..850c459e32 100644 --- a/test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs +++ b/test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Internal; diff --git a/test/UnitTests/Data/DefaultEntityRepository_Tests.cs b/test/UnitTests/Data/DefaultEntityRepository_Tests.cs index a8ec56fe9c..5b50fa4cbc 100644 --- a/test/UnitTests/Data/DefaultEntityRepository_Tests.cs +++ b/test/UnitTests/Data/DefaultEntityRepository_Tests.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Internal; -using Microsoft.AspNetCore.Mvc; using Xunit; using Moq; using Microsoft.EntityFrameworkCore; @@ -51,12 +49,15 @@ public async Task UpdateAsync_Updates_Attributes_In_AttributesToUpdate() { Id = _todoItem.Id, Description = Guid.NewGuid().ToString() - }; + }; + + var descAttr = new AttrAttribute("description", "Description"); + descAttr.PropertyInfo = typeof(TodoItem).GetProperty(nameof(TodoItem.Description)); _attrsToUpdate = new Dictionary { { - new AttrAttribute("description", "Description"), + descAttr, todoItemUpdates.Description } }; diff --git a/test/UnitTests/Models/ResourceDefinitionTests.cs b/test/UnitTests/Models/ResourceDefinitionTests.cs new file mode 100644 index 0000000000..2112a49447 --- /dev/null +++ b/test/UnitTests/Models/ResourceDefinitionTests.cs @@ -0,0 +1,129 @@ +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using System.Collections.Generic; +using Xunit; + +namespace UnitTests.Models +{ + public class ResourceDefinition_Scenario_Tests + { + private readonly IContextGraph _graph; + + public ResourceDefinition_Scenario_Tests() + { + _graph = new ContextGraphBuilder() + .AddResource("models") + .Build(); + } + + [Fact] + public void Request_Filter_Uses_Member_Expression() + { + // arrange + var resource = new RequestFilteredResource(isAdmin: true); + + // act + var attrs = resource.GetOutputAttrs(null); + + // assert + Assert.Single(attrs); + Assert.Equal(nameof(Model.Password), attrs[0].InternalAttributeName); + } + + [Fact] + public void Request_Filter_Uses_NewExpression() + { + // arrange + var resource = new RequestFilteredResource(isAdmin: false); + + // act + var attrs = resource.GetOutputAttrs(null); + + // assert + Assert.Empty(attrs); + } + + [Fact] + public void Instance_Filter_Uses_Member_Expression() + { + // arrange + var model = new Model { AlwaysExcluded = "Admin" }; + var resource = new InstanceFilteredResource(); + + // act + var attrs = resource.GetOutputAttrs(model); + + // assert + Assert.Single(attrs); + Assert.Equal(nameof(Model.Password), attrs[0].InternalAttributeName); + } + + [Fact] + public void Instance_Filter_Uses_NewExpression() + { + // arrange + var model = new Model { AlwaysExcluded = "Joe" }; + var resource = new InstanceFilteredResource(); + + // act + var attrs = resource.GetOutputAttrs(model); + + // assert + Assert.Empty(attrs); + } + + [Fact] + public void InstanceOutputAttrsAreSpecified_Returns_True_If_Instance_Method_Is_Overriden() + { + // act + var resource = new InstanceFilteredResource(); + + // assert + Assert.True(resource._instanceAttrsAreSpecified); + } + + [Fact] + public void InstanceOutputAttrsAreSpecified_Returns_False_If_Instance_Method_Is_Not_Overriden() + { + // act + var resource = new RequestFilteredResource(isAdmin: false); + + // assert + Assert.False(resource._instanceAttrsAreSpecified); + } + } + + public class Model : Identifiable + { + [Attr("name")] public string AlwaysExcluded { get; set; } + [Attr("password")] public string Password { get; set; } + } + + public class RequestFilteredResource : ResourceDefinition + { + private readonly bool _isAdmin; + + // this constructor will be resolved from the container + // that means you can take on any dependency that is also defined in the container + public RequestFilteredResource(bool isAdmin) + { + _isAdmin = isAdmin; + } + + // Called once per filtered resource in request. + protected override List OutputAttrs() + => _isAdmin + ? Remove(m => m.AlwaysExcluded) + : Remove(m => new { m.AlwaysExcluded, m.Password }, from: base.OutputAttrs()); + } + + public class InstanceFilteredResource : ResourceDefinition + { + // Called once per resource instance + protected override List OutputAttrs(Model model) + => model.AlwaysExcluded == "Admin" + ? Remove(m => m.AlwaysExcluded, base.OutputAttrs()) + : Remove(m => new { m.AlwaysExcluded, m.Password }, from: base.OutputAttrs()); + } +} diff --git a/test/UnitTests/Services/QueryComposerTests.cs b/test/UnitTests/Services/QueryComposerTests.cs index 91df486212..330083820c 100644 --- a/test/UnitTests/Services/QueryComposerTests.cs +++ b/test/UnitTests/Services/QueryComposerTests.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Services; -using Microsoft.AspNetCore.Http; using Moq; using Xunit;