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 8142b2ea26..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); } @@ -68,21 +76,20 @@ public Documents Build(IEnumerable entities) private Dictionary GetMeta(IIdentifiable entity) { - if (entity == null) return null; - var builder = _jsonApiContext.MetaBuilder; - - if (entity is IHasMeta metaEntity) - builder.Add(metaEntity.GetMeta(_jsonApiContext)); - - if (_jsonApiContext.Options.IncludeTotalRecordCount) + if (_jsonApiContext.Options.IncludeTotalRecordCount && _jsonApiContext.PageManager.TotalRecords != null) builder.Add("total-records", _jsonApiContext.PageManager.TotalRecords); if (_requestMeta != null) builder.Add(_requestMeta.GetMeta()); + if (entity != null && entity is IHasMeta metaEntity) + builder.Add(metaEntity.GetMeta(_jsonApiContext)); + var meta = builder.Build(); - if (meta.Count > 0) return meta; + if (meta.Count > 0) + return meta; + return null; } @@ -99,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 { @@ -112,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)) @@ -220,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/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 502d678f0d..54fcf5afaf 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -125,6 +125,16 @@ public class JsonApiOptions /// public bool EnableOperations { get; set; } + /// + /// Whether or not to validate model state. + /// + /// + /// + /// options.ValidateModelState = true; + /// + /// + public bool ValidateModelState { get; set; } + [Obsolete("JsonContract resolver can now be set on SerializerSettings.")] public IContractResolver JsonContractResolver { diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index 760f8f8d56..fd6ec8947a 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; @@ -145,7 +146,8 @@ public virtual async Task GetRelationshipAsync(TId id, string rel public virtual async Task PostAsync([FromBody] T entity) { - if (_create == null) throw Exceptions.UnSupportedRequestMethod; + if (_create == null) + throw Exceptions.UnSupportedRequestMethod; if (entity == null) return UnprocessableEntity(); @@ -153,6 +155,9 @@ public virtual async Task PostAsync([FromBody] T entity) if (!_jsonApiContext.Options.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId)) return Forbidden(); + if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid) + return BadRequest(ModelState.ConvertToErrorCollection()); + entity = await _create.CreateAsync(entity); return Created($"{HttpContext.Request.Path}/{entity.Id}", entity); @@ -164,6 +169,8 @@ public virtual async Task PatchAsync(TId id, [FromBody] T entity) if (entity == null) return UnprocessableEntity(); + if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid) + return BadRequest(ModelState.ConvertToErrorCollection()); var updatedEntity = await _update.UpdateAsync(id, entity); 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/Extensions/ModelStateExtensions.cs b/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs new file mode 100644 index 0000000000..d67f7e66c4 --- /dev/null +++ b/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs @@ -0,0 +1,29 @@ +using JsonApiDotNetCore.Internal; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.EntityFrameworkCore.Internal; + +namespace JsonApiDotNetCore.Extensions +{ + public static class ModelStateExtensions + { + public static ErrorCollection ConvertToErrorCollection(this ModelStateDictionary modelState) + { + ErrorCollection collection = new ErrorCollection(); + foreach (var entry in modelState) + { + if (entry.Value.Errors.Any() == false) + continue; + + foreach (var modelError in entry.Value.Errors) + { + if (modelError.Exception is JsonApiException jex) + collection.Errors.AddRange(jex.GetError().Errors); + else + collection.Errors.Add(new Error(400, entry.Key, modelError.ErrorMessage, modelError.Exception != null ? ErrorMeta.FromException(modelError.Exception) : null)); + } + } + + return collection; + } + } +} diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs index 204f3e0491..e10a3f31c2 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs @@ -52,11 +52,9 @@ public Task ReadAsync(InputFormatterContext context) context.ModelState.AddModelError(context.ModelName, ex, context.Metadata); return InputFormatterResult.FailureAsync(); } - catch (JsonApiException jex) + catch (JsonApiException) { - _logger?.LogError(new EventId(), jex, "An error occurred while de-serializing the payload"); - context.ModelState.AddModelError(context.ModelName, jex, context.Metadata); - return InputFormatterResult.FailureAsync(); + throw; } } 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 PageSize > 0; - public int TotalPages => (TotalRecords == 0) ? -1 : (int)Math.Ceiling(decimal.Divide(TotalRecords, PageSize)); + public int TotalPages => (TotalRecords == null) ? -1 : (int)Math.Ceiling(decimal.Divide(TotalRecords.Value, PageSize)); public RootLinks GetPageLinks(LinkBuilder linkBuilder) { diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs index 59bb3f0f83..a914fea3a7 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index d567de200a..9b117c0913 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -1,6 +1,4 @@ -using System; using System.Linq; -using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 75fc955402..a212eecd70 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@ - + - 2.3.2 + 2.3.4 $(NetStandardVersion) JsonApiDotNetCore JsonApiDotNetCore diff --git a/src/JsonApiDotNetCore/Models/AttrAttribute.cs b/src/JsonApiDotNetCore/Models/AttrAttribute.cs index f63d48ab70..017cf72118 100644 --- a/src/JsonApiDotNetCore/Models/AttrAttribute.cs +++ b/src/JsonApiDotNetCore/Models/AttrAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using JsonApiDotNetCore.Internal; namespace JsonApiDotNetCore.Models @@ -69,34 +70,25 @@ public AttrAttribute(string publicName, string internalName, bool isImmutable = /// 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 a1e3c76214..0c3b7217fe 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -57,6 +57,10 @@ public object Deserialize(string requestBody) var entity = DocumentToObject(document.Data, document.Included); return entity; } + catch (JsonApiException) + { + throw; + } catch (Exception e) { throw new JsonApiException(400, "Failed to deserialize request body", e); @@ -109,13 +113,18 @@ public List DeserializeList(string requestBody) public object DocumentToObject(DocumentData data, List included = null) { - if (data == null) throw new JsonApiException(422, "Failed to deserialize document as json:api."); + if (data == null) + throw new JsonApiException(422, "Failed to deserialize document as json:api."); var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(data.Type?.ToString()); - _jsonApiContext.RequestEntity = contextEntity; + _jsonApiContext.RequestEntity = contextEntity ?? throw new JsonApiException(400, + message: $"This API does not contain a json:api resource named '{data.Type}'.", + detail: "This resource is not registered on the ContextGraph. " + + "If you are using Entity Framework, make sure the DbSet matches the expected resource name. " + + "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); @@ -132,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/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index f579d8c9a2..64fcf8e998 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -87,20 +87,20 @@ internal static bool PathIsRelationship(string requestPath) const char pathSegmentDelimiter = '/'; var span = requestPath.AsSpan(); - + // we need to iterate over the string, from the end, // checking whether or not the 2nd to last path segment // is "relationships" // -2 is chosen in case the path ends with '/' - for(var i = requestPath.Length - 2; i >= 0; i--) + for (var i = requestPath.Length - 2; i >= 0; i--) { // if there are not enough characters left in the path to // contain "relationships" - if(i < relationships.Length) + if (i < relationships.Length) return false; // we have found the first instance of '/' - if(span[i] == pathSegmentDelimiter) + if (span[i] == pathSegmentDelimiter) { // in the case of a "relationships" route, the next // path segment will be "relationships" @@ -113,7 +113,7 @@ internal static bool PathIsRelationship(string requestPath) return false; } - + private PageManager GetPageManager() { if (Options.DefaultPageSize == 0 && (QuerySet == null || QuerySet.PageQuery.PageSize == 0)) 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/DocumentTests/Meta.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs index bb055e9935..2b6b1e251b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample; @@ -47,13 +48,123 @@ public async Task Total_Record_Count_Included() var response = await client.SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); var documents = JsonConvert.DeserializeObject(responseBody); - + // assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(documents.Meta); Assert.Equal((long)expectedCount, (long)documents.Meta["total-records"]); } + [Fact] + public async Task Total_Record_Count_Included_When_None() + { + // arrange + _context.TodoItems.RemoveRange(_context.TodoItems); + _context.SaveChanges(); + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items"; + + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + var responseBody = await response.Content.ReadAsStringAsync(); + var documents = JsonConvert.DeserializeObject(responseBody); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(documents.Meta); + Assert.Equal(0, (long)documents.Meta["total-records"]); + } + + [Fact] + public async Task Total_Record_Count_Not_Included_In_POST_Response() + { + // arrange + _context.TodoItems.RemoveRange(_context.TodoItems); + _context.SaveChanges(); + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("POST"); + var route = $"/api/v1/todo-items"; + + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + var content = new + { + data = new + { + type = "todo-items", + attributes = new + { + description = "New Description", + } + } + }; + + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + + // act + var response = await client.SendAsync(request); + var responseBody = await response.Content.ReadAsStringAsync(); + var documents = JsonConvert.DeserializeObject(responseBody); + + // assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.False(documents.Meta.ContainsKey("total-records")); + } + + [Fact] + public async Task Total_Record_Count_Not_Included_In_PATCH_Response() + { + // arrange + _context.TodoItems.RemoveRange(_context.TodoItems); + TodoItem todoItem = new TodoItem(); + _context.TodoItems.Add(todoItem); + _context.SaveChanges(); + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("PATCH"); + var route = $"/api/v1/todo-items/{todoItem.Id}"; + + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + var content = new + { + data = new + { + type = "todo-items", + id = todoItem.Id, + attributes = new + { + description = "New Description", + } + } + }; + + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + + // act + var response = await client.SendAsync(request); + var responseBody = await response.Content.ReadAsStringAsync(); + var documents = JsonConvert.DeserializeObject(responseBody); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.False(documents.Meta.ContainsKey("total-records")); + } + [Fact] public async Task EntityThatImplements_IHasMeta_Contains_MetaData() { @@ -73,26 +184,26 @@ public async Task EntityThatImplements_IHasMeta_Contains_MetaData() // act var response = await client.SendAsync(request); var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - + // assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(documents.Meta); Assert.NotNull(expectedMeta); Assert.NotEmpty(expectedMeta); - - foreach(var hash in expectedMeta) + + foreach (var hash in expectedMeta) { - if(hash.Value is IList) + if (hash.Value is IList) { var listValue = (IList)hash.Value; - for(var i=0; i < listValue.Count; i++) + for (var i = 0; i < listValue.Count; i++) Assert.Equal(listValue[i].ToString(), ((IList)documents.Meta[hash.Key])[i].ToString()); } else { Assert.Equal(hash.Value, documents.Meta[hash.Key]); } - } + } } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs index 8573d0b560..9c8d5f8214 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs @@ -53,8 +53,10 @@ public async Task Request_ForEmptyCollection_Returns_EmptyDataCollection() var server = new TestServer(builder); var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - var expectedBody = JsonConvert.SerializeObject(new { - data = new List() + var expectedBody = JsonConvert.SerializeObject(new + { + data = new List(), + meta = new Dictionary { { "total-records", 0 } } }); // act 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 9c59372846..873b3f50d2 100644 --- a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs +++ b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs @@ -1,11 +1,13 @@ -using System; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Moq; using Xunit; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; namespace UnitTests { @@ -143,6 +145,8 @@ public async Task PatchAsync_Calls_Service() const int id = 0; var resource = new Resource(); var serviceMock = new Mock>(); + _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); + _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions()); var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); // act @@ -153,6 +157,47 @@ public async Task PatchAsync_Calls_Service() VerifyApplyContext(); } + [Fact] + public async Task PatchAsync_ModelStateInvalid_ValidateModelStateDisbled() + { + // arrange + const int id = 0; + var resource = new Resource(); + var serviceMock = new Mock>(); + _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); + _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false }); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); + + // act + var response = await controller.PatchAsync(id, resource); + + // assert + serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Once); + VerifyApplyContext(); + Assert.IsNotType(response); + } + + [Fact] + public async Task PatchAsync_ModelStateInvalid_ValidateModelStateEnabled() + { + // arrange + const int id = 0; + var resource = new Resource(); + var serviceMock = new Mock>(); + _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); + _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions{ValidateModelState = true}); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); + controller.ModelState.AddModelError("Id", "Failed Validation"); + + // act + var response = await controller.PatchAsync(id, resource); + + // assert + serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Never); + Assert.IsType(response); + Assert.IsType(((BadRequestObjectResult) response).Value); + } + [Fact] public async Task PatchAsync_Throws_405_If_No_Service() { @@ -168,6 +213,67 @@ public async Task PatchAsync_Throws_405_If_No_Service() Assert.Equal(405, exception.GetStatusCode()); } + [Fact] + public async Task PostAsync_Calls_Service() + { + // arrange + var resource = new Resource(); + var serviceMock = new Mock>(); + _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); + _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions()); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); + serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); + controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext {HttpContext = new DefaultHttpContext()}; + + // act + await controller.PostAsync(resource); + + // assert + serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once); + VerifyApplyContext(); + } + + [Fact] + public async Task PostAsync_ModelStateInvalid_ValidateModelStateDisabled() + { + // arrange + var resource = new Resource(); + var serviceMock = new Mock>(); + _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); + _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false }); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); + serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); + controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext { HttpContext = new DefaultHttpContext() }; + + // act + var response = await controller.PostAsync(resource); + + // assert + serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once); + VerifyApplyContext(); + Assert.IsNotType(response); + } + + [Fact] + public async Task PostAsync_ModelStateInvalid_ValidateModelStateEnabled() + { + // arrange + var resource = new Resource(); + var serviceMock = new Mock>(); + _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); + _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = true }); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); + controller.ModelState.AddModelError("Id", "Failed Validation"); + + // act + var response = await controller.PostAsync(resource); + + // assert + serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Never); + Assert.IsType(response); + Assert.IsType(((BadRequestObjectResult)response).Value); + } + [Fact] public async Task PatchRelationshipsAsync_Calls_Service() { 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/IdentifiableTests.cs b/test/UnitTests/Models/IdentifiableTests.cs new file mode 100644 index 0000000000..778b1b485f --- /dev/null +++ b/test/UnitTests/Models/IdentifiableTests.cs @@ -0,0 +1,26 @@ +using JsonApiDotNetCore.Models; +using Xunit; + +namespace UnitTests.Models +{ + public class IdentifiableTests + { + [Fact] + public void Can_Set_StringId_To_Value_Type() + { + var resource = new IntId(); + resource.StringId = "1"; + Assert.Equal(1, resource.Id); + } + + [Fact] + public void Setting_StringId_To_Null_Sets_Id_As_Default() + { + var resource = new IntId(); + resource.StringId = null; + Assert.Equal(0, resource.Id); + } + + private class IntId : Identifiable { } + } +} 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;