diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs deleted file mode 100644 index 789c31cb95..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs +++ /dev/null @@ -1,18 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExample.Controllers -{ - public sealed class AuthorsController : JsonApiController - { - public AuthorsController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/BlogsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/BlogsController.cs deleted file mode 100644 index 824b7a30a6..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/BlogsController.cs +++ /dev/null @@ -1,18 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExample.Controllers -{ - public sealed class BlogsController : JsonApiController - { - public BlogsController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs deleted file mode 100644 index 62fa1e96c3..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExample.Controllers -{ - public sealed class PassportsController : JsonApiController - { - public PassportsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs deleted file mode 100644 index 4b0116ece6..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs +++ /dev/null @@ -1,18 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExample.Controllers -{ - public sealed class PeopleController : JsonApiController - { - public PeopleController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs deleted file mode 100644 index 75c930126f..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs +++ /dev/null @@ -1,18 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExample.Controllers -{ - public sealed class PersonRolesController : JsonApiController - { - public PersonRolesController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/ThrowingResourcesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/ThrowingResourcesController.cs deleted file mode 100644 index 8b662cde09..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/ThrowingResourcesController.cs +++ /dev/null @@ -1,18 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExample.Controllers -{ - public sealed class ThrowingResourcesController : JsonApiController - { - public ThrowingResourcesController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs deleted file mode 100644 index 2411879bb7..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs +++ /dev/null @@ -1,28 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExample.Controllers -{ - public sealed class UsersController : JsonApiController - { - public UsersController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { } - } - - public sealed class SuperUsersController : JsonApiController - { - public SuperUsersController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooks.cs similarity index 84% rename from src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs rename to src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooks.cs index 9c91823669..5d1ac74dee 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooks.cs @@ -10,9 +10,9 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class ArticleHooksDefinition : ResourceHooksDefinition
+ public class ArticleHooks : ResourceHooksDefinition
{ - public ArticleHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + public ArticleHooks(IResourceGraph resourceGraph) : base(resourceGraph) { } public override IEnumerable
OnReturn(HashSet
resources, ResourcePipeline pipeline) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooks.cs similarity index 90% rename from src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs rename to src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooks.cs index 2e211305ca..b219e7691e 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooks.cs @@ -10,9 +10,9 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class PassportHooksDefinition : ResourceHooksDefinition + public class PassportHooks : ResourceHooksDefinition { - public PassportHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + public PassportHooks(IResourceGraph resourceGraph) : base(resourceGraph) { } diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooks.cs similarity index 83% rename from src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs rename to src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooks.cs index 42c1b405e4..f640151443 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooks.cs @@ -6,9 +6,9 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class PersonHooksDefinition : LockableHooksDefinition + public class PersonHooks : LockableHooksDefinition { - public PersonHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + public PersonHooks(IResourceGraph resourceGraph) : base(resourceGraph) { } public override IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooks.cs similarity index 81% rename from src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs rename to src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooks.cs index 4ff4508bf7..90836b4482 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooks.cs @@ -7,9 +7,9 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class TagHooksDefinition : ResourceHooksDefinition + public class TagHooks : ResourceHooksDefinition { - public TagHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + public TagHooks(IResourceGraph resourceGraph) : base(resourceGraph) { } public override IEnumerable BeforeCreate(IResourceHashSet affected, ResourcePipeline pipeline) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemHooks.cs similarity index 86% rename from src/Examples/JsonApiDotNetCoreExample/Definitions/TodoHooksDefinition.cs rename to src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemHooks.cs index c3fc7af2e5..30b9ddc2fe 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemHooks.cs @@ -9,9 +9,9 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class TodoHooksDefinition : LockableHooksDefinition + public class TodoItemHooks : LockableHooksDefinition { - public TodoHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + public TodoItemHooks(IResourceGraph resourceGraph) : base(resourceGraph) { } public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) { diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiControllerGenerator.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiControllerGenerator.cs new file mode 100644 index 0000000000..7734f37392 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiControllerGenerator.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Controllers; + +namespace JsonApiDotNetCore.Configuration +{ + /// + /// Service that allows customization of the dynamic controller generation. Register a + /// custom implementation before calling + /// in order to override the default controller generation behaviour. + /// + public interface IJsonApiControllerGenerator : IApplicationFeatureProvider { } +} diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index 6fd6a62b39..6d44ecc3ba 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -187,6 +187,12 @@ public interface IJsonApiOptions /// JsonSerializerSettings SerializerSettings { get; } + /// + /// Determines whether JsonApiDotNetCore will dynamically register controllers for every registered resource + /// in the resource graph. Defaults to true. + /// + bool AutoGenerateControllers { get; } + internal DefaultContractResolver SerializerContractResolver => (DefaultContractResolver) SerializerSettings.ContractResolver; } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 74bc6cf4a5..8dac2b59e1 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -37,7 +37,8 @@ internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder, ID private readonly ResourceGraphBuilder _resourceGraphBuilder; private readonly ServiceDiscoveryFacade _serviceDiscoveryFacade; private readonly ServiceProvider _intermediateProvider; - + private IResourceGraph _resourceGraph; + public Action ConfigureMvcOptions { get; set; } public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mvcBuilder) @@ -83,8 +84,8 @@ public void AddResourceGraph(ICollection dbContextTypes, Action @@ -101,6 +102,11 @@ public void ConfigureMvc() ConfigureMvcOptions?.Invoke(options); }); + if (_options.AutoGenerateControllers) + { + RegisterControllerFeatureProvider(); + } + if (_options.ValidateModelState) { _mvcBuilder.AddDataAnnotations(); @@ -108,6 +114,24 @@ public void ConfigureMvc() } } + private void RegisterControllerFeatureProvider() + { + IJsonApiControllerGenerator controllerFeatureProvider; + if (_services.Any(descriptor => descriptor.ServiceType == typeof(IJsonApiControllerGenerator))) + { + using (var provider = _services.BuildServiceProvider()) + { + controllerFeatureProvider = provider.GetRequiredService(); + } + } + else + { + controllerFeatureProvider = new JsonApiControllerGenerator(_resourceGraph); + } + + _mvcBuilder.ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(controllerFeatureProvider)); + } + /// /// Discovers DI registrable services in the assemblies marked for discovery. /// diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiControllerGenerator.cs b/src/JsonApiDotNetCore/Configuration/JsonApiControllerGenerator.cs new file mode 100644 index 0000000000..564c704c4f --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/JsonApiControllerGenerator.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JsonApiDotNetCore.Controllers; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Controllers; + +namespace JsonApiDotNetCore.Configuration +{ + public class JsonApiControllerGenerator : IJsonApiControllerGenerator + { + private readonly IResourceGraph _resourceGraph; + private readonly Type _controllerOpenType; + private readonly Type _baseControllerOpenType; + + public JsonApiControllerGenerator(IResourceGraph resourceGraph) + { + _resourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); + _controllerOpenType = typeof(JsonApiController<,>); + _baseControllerOpenType = typeof(BaseJsonApiController<,>); + } + + public void PopulateFeature(IEnumerable parts, ControllerFeature feature) + { + var exposedTypes = parts.SelectMany(part => ((AssemblyPart)part).Types).ToList(); + foreach (var resourceContext in _resourceGraph.GetResourceContexts()) + { + RegisterControllerForResource(feature, resourceContext, exposedTypes); + } + } + + + private void RegisterControllerForResource(ControllerFeature feature, ResourceContext resourceContext, List exposedTypes) + { + if (resourceContext != null && !resourceContext.ResourceType.IsAbstract) + { + var existingControllerType = _baseControllerOpenType.MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType).GetTypeInfo(); + if (!exposedTypes.Any(exposedType => existingControllerType.IsAssignableFrom(exposedType))) + { + var controllerType = GetControllerType(resourceContext); + feature.Controllers.Add(controllerType); + } + } + } + + protected virtual TypeInfo GetControllerType(ResourceContext resourceContext) + { + var controllerType = _controllerOpenType.MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType).GetTypeInfo(); + return controllerType; + } + } +} diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index c999508574..2404e155ce 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -76,6 +76,9 @@ public sealed class JsonApiOptions : IJsonApiOptions } }; + /// + public bool AutoGenerateControllers { get; set; } = true; + // Workaround for https://github.com/dotnet/efcore/issues/21026 internal bool DisableTopPagination { get; set; } internal bool DisableChildrenPagination { get; set; } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs index b403b72dbd..3912aefe94 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -26,24 +26,6 @@ public JsonApiController( : base(options, loggerFactory, resourceService) { } - /// - public JsonApiController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetSecondaryService getSecondary = null, - IGetRelationshipService getRelationship = null, - ICreateService create = null, - IAddToRelationshipService addToRelationship = null, - IUpdateService update = null, - ISetRelationshipService setRelationship = null, - IDeleteService delete = null, - IRemoveFromRelationshipService removeFromRelationship = null) - : base(options, loggerFactory,getAll, getById, getSecondary, getRelationship, create, addToRelationship, update, - setRelationship, delete, removeFromRelationship) - { } - /// [HttpGet] public override async Task GetAsync(CancellationToken cancellationToken) @@ -127,23 +109,5 @@ public JsonApiController( IResourceService resourceService) : base(options, loggerFactory, resourceService) { } - - /// - public JsonApiController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetSecondaryService getSecondary = null, - IGetRelationshipService getRelationship = null, - ICreateService create = null, - IAddToRelationshipService addToRelationship = null, - IUpdateService update = null, - ISetRelationshipService setRelationship = null, - IDeleteService delete = null, - IRemoveFromRelationshipService removeFromRelationship = null) - : base(options, loggerFactory, getAll, getById, getSecondary, getRelationship, create, addToRelationship, update, - setRelationship, delete, removeFromRelationship) - { } } } diff --git a/src/JsonApiDotNetCore/Middleware/IControllerResourceMapping.cs b/src/JsonApiDotNetCore/Middleware/IControllerResourceMapping.cs index de48544e79..19ac3d7e19 100644 --- a/src/JsonApiDotNetCore/Middleware/IControllerResourceMapping.cs +++ b/src/JsonApiDotNetCore/Middleware/IControllerResourceMapping.cs @@ -10,6 +10,6 @@ public interface IControllerResourceMapping /// /// Get the associated resource type for the provided controller name. /// - Type GetAssociatedResource(string controllerName); + Type GetResourceForEndpoint(string controllerName); } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index fee92a14dc..8a7d14eef5 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -12,6 +12,7 @@ using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; @@ -44,32 +45,37 @@ public async Task Invoke(HttpContext httpContext, if (request == null) throw new ArgumentNullException(nameof(request)); if (resourceContextProvider == null) throw new ArgumentNullException(nameof(resourceContextProvider)); - var routeValues = httpContext.GetRouteData().Values; - - var primaryResourceContext = CreatePrimaryResourceContext(routeValues, controllerResourceMapping, resourceContextProvider); - if (primaryResourceContext != null) + // TODO: endpoint is null when route is not found. Instead of doing half work here, we should consider just throwing a 404 here already. + var endpoint = httpContext.GetEndpoint(); + if (endpoint != null) { - if (!await ValidateContentTypeHeaderAsync(httpContext, options.SerializerSettings) || - !await ValidateAcceptHeaderAsync(httpContext, options.SerializerSettings)) + var primaryResourceContext = GetPrimaryResourceContext(endpoint, controllerResourceMapping, resourceContextProvider); + if (primaryResourceContext != null) { - return; + if (!await ValidateContentTypeHeaderAsync(httpContext, options.SerializerSettings) || + !await ValidateAcceptHeaderAsync(httpContext, options.SerializerSettings)) + { + return; + } + + var routeValues = httpContext.GetRouteData().Values; + SetupRequest((JsonApiRequest)request, primaryResourceContext, routeValues, options, resourceContextProvider, httpContext.Request); + + httpContext.RegisterJsonApiRequest(); } - - SetupRequest((JsonApiRequest)request, primaryResourceContext, routeValues, options, resourceContextProvider, httpContext.Request); - - httpContext.RegisterJsonApiRequest(); } await _next(httpContext); } - private static ResourceContext CreatePrimaryResourceContext(RouteValueDictionary routeValues, - IControllerResourceMapping controllerResourceMapping, IResourceContextProvider resourceContextProvider) + private static ResourceContext GetPrimaryResourceContext(Endpoint endpoint, IControllerResourceMapping controllerResourceMapping, IResourceContextProvider resourceContextProvider) { - var controllerName = (string) routeValues["controller"]; + var controllerDescriptor = (ControllerActionDescriptor)endpoint.Metadata.Single(meta => meta is ControllerActionDescriptor); + var controllerName = controllerDescriptor.ControllerTypeInfo.FullName; + if (controllerName != null) { - var resourceType = controllerResourceMapping.GetAssociatedResource(controllerName); + var resourceType = controllerResourceMapping.GetResourceForEndpoint(controllerName); if (resourceType != null) { return resourceContextProvider.GetResourceContext(resourceType); diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs index 81192b1c29..63c003490b 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs @@ -32,10 +32,9 @@ public class JsonApiRoutingConvention : IJsonApiRoutingConvention { private readonly IJsonApiOptions _options; private readonly IResourceGraph _resourceGraph; - private readonly HashSet _registeredTemplates = new HashSet(); + private readonly Dictionary _endpointsByRoutes = new Dictionary(); - private readonly Dictionary _registeredResources = - new Dictionary(); + private readonly Dictionary _resourcesByEndpoint = new Dictionary(); public JsonApiRoutingConvention(IJsonApiOptions options, IResourceGraph resourceGraph) { @@ -44,11 +43,11 @@ public JsonApiRoutingConvention(IJsonApiOptions options, IResourceGraph resource } /// - public Type GetAssociatedResource(string controllerName) + public Type GetResourceForEndpoint(string controllerName) { if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); - if (_registeredResources.TryGetValue(controllerName, out var resourceContext)) + if (_resourcesByEndpoint.TryGetValue(controllerName, out var resourceContext)) { return resourceContext.ResourceType; } @@ -61,36 +60,68 @@ public void Apply(ApplicationModel application) { if (application == null) throw new ArgumentNullException(nameof(application)); - foreach (var controller in application.Controllers) + RegisterResources(application.Controllers); + RegisterRoutes(application.Controllers); + } + + private void RegisterRoutes(IEnumerable controllers) + { + foreach (var model in controllers) { - var resourceType = ExtractResourceTypeFromController(controller.ControllerType); + if (!RoutingConventionDisabled(model)) + { + continue; + } + var template = GetRouteTemplateForEndpoint(model); + + model.Selectors[0].AttributeRouteModel = new AttributeRouteModel {Template = template}; + _endpointsByRoutes.Add(template, model); + } + } + + private string GetRouteTemplateForEndpoint(ControllerModel controllerModel) + { + var template = TryGetResourceBasedTemplate(controllerModel); + if (template == null || _endpointsByRoutes.ContainsKey(template)) + { + template = TryGetControllerBasedTemplate(controllerModel); + } + + if (template == null) + { + throw new InvalidConfigurationException($"Failed to create a template for {controllerModel.ControllerType.FullName} " + + $"based on the controller and resource name"); + } + + if (_endpointsByRoutes.ContainsKey(template)) + { + var overlappingEndpoint = _endpointsByRoutes[template]; + throw new InvalidConfigurationException( + $"Cannot register template {template} for {controllerModel.ControllerType.FullName} " + + $"because it is already registered for {overlappingEndpoint.ControllerType.FullName}."); + } + + return template; + } + + private void RegisterResources(IEnumerable controllers) + { + foreach (var model in controllers) + { + var resourceType = ExtractResourceTypeFromEndpoint(model.ControllerType); if (resourceType != null) { var resourceContext = _resourceGraph.GetResourceContext(resourceType); - if (resourceContext != null) { - _registeredResources.Add(controller.ControllerName, resourceContext); + _resourcesByEndpoint.Add(model.ControllerType.FullName!, resourceContext); } } - - if (!RoutingConventionDisabled(controller)) - { - continue; - } - - var template = TemplateFromResource(controller) ?? TemplateFromController(controller); - if (template == null) - { - throw new InvalidConfigurationException( - $"Controllers with overlapping route templates detected: {controller.ControllerType.FullName}"); - } - - controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel { Template = template }; } } + /// /// Verifies if routing convention should be enabled for this controller. /// @@ -101,18 +132,11 @@ private bool RoutingConventionDisabled(ControllerModel controller) return notDisabled && type.IsSubclassOf(typeof(CoreJsonApiController)); } - /// - /// Derives a template from the resource type, and checks if this template was already registered. - /// - private string TemplateFromResource(ControllerModel model) + private string TryGetResourceBasedTemplate(ControllerModel model) { - if (_registeredResources.TryGetValue(model.ControllerName, out var resourceContext)) + if (_resourcesByEndpoint.TryGetValue(model.ControllerType.FullName, out var resourceContext)) { - var template = $"{_options.Namespace}/{resourceContext.PublicName}"; - if (_registeredTemplates.Add(template)) - { - return template; - } + return $"{_options.Namespace}/{resourceContext.PublicName}"; } return null; @@ -121,25 +145,21 @@ private string TemplateFromResource(ControllerModel model) /// /// Derives a template from the controller name, and checks if this template was already registered. /// - private string TemplateFromController(ControllerModel model) + private string TryGetControllerBasedTemplate(ControllerModel model) { - string controllerName = - _options.SerializerContractResolver.NamingStrategy.GetPropertyName(model.ControllerName, false); - - var template = $"{_options.Namespace}/{controllerName}"; - - if (_registeredTemplates.Add(template)) + if (!model.ControllerType.IsGenericType || model.ControllerType.GetGenericTypeDefinition() != typeof(BaseJsonApiController<,>)) { - return template; + var controllerName = _options.SerializerContractResolver.NamingStrategy!.GetPropertyName(model.ControllerName, false); + return $"{_options.Namespace}/{controllerName}"; } - + return null; } /// /// Determines the resource associated to a controller by inspecting generic arguments in its inheritance tree. /// - private Type ExtractResourceTypeFromController(Type type) + private Type ExtractResourceTypeFromEndpoint(Type type) { var aspNetControllerType = typeof(ControllerBase); var coreControllerType = typeof(CoreJsonApiController); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs deleted file mode 100644 index d3929c6a6c..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs +++ /dev/null @@ -1,587 +0,0 @@ -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.Configuration; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreExample.Models; -using JsonApiDotNetCoreExampleTests.Acceptance.Spec; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using Xunit; -using Person = JsonApiDotNetCoreExample.Models.Person; - -namespace JsonApiDotNetCoreExampleTests.Acceptance -{ - public sealed class ResourceDefinitionTests : FunctionalTestCollection - { - private readonly Faker _userFaker; - private readonly Faker _todoItemFaker; - private readonly Faker _personFaker; - private readonly Faker
_articleFaker; - private readonly Faker _authorFaker; - private readonly Faker _tagFaker; - - public ResourceDefinitionTests(ResourceHooksApplicationFactory factory) : base(factory) - { - _authorFaker = new Faker() - .RuleFor(a => a.LastName, f => f.Random.Words(2)); - - _articleFaker = new Faker
() - .RuleFor(a => a.Caption, f => f.Random.AlphaNumeric(10)) - .RuleFor(a => a.Author, f => _authorFaker.Generate()); - - _userFaker = new Faker() - .CustomInstantiator(f => new User(_dbContext)) - .RuleFor(u => u.UserName, f => f.Internet.UserName()) - .RuleFor(u => u.Password, f => f.Internet.Password()); - - _todoItemFaker = new Faker() - .RuleFor(t => t.Description, f => f.Lorem.Sentence()) - .RuleFor(t => t.Ordinal, f => f.Random.Number()) - .RuleFor(t => t.CreatedDate, f => f.Date.Past()); - - _personFaker = new Faker() - .RuleFor(p => p.FirstName, f => f.Name.FirstName()) - .RuleFor(p => p.LastName, f => f.Name.LastName()); - - _tagFaker = new Faker() - .CustomInstantiator(f => new Tag()) - .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)); - - var options = (JsonApiOptions) _factory.Services.GetRequiredService(); - options.DisableTopPagination = false; - options.DisableChildrenPagination = false; - } - - [Fact] - public async Task Can_Create_User_With_Password() - { - // Arrange - var user = _userFaker.Generate(); - - var serializer = GetSerializer(p => new { p.Password, p.UserName }); - - var httpMethod = new HttpMethod("POST"); - var route = "/api/v1/users"; - - var request = new HttpRequestMessage(httpMethod, route) - { - Content = new StringContent(serializer.Serialize(user)) - }; - request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); - - // Act - var response = await _client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - - // response assertions - var body = await response.Content.ReadAsStringAsync(); - var returnedUser = _deserializer.DeserializeSingle(body).Data; - var document = JsonConvert.DeserializeObject(body); - Assert.False(document.SingleData.Attributes.ContainsKey("password")); - Assert.Equal(user.UserName, document.SingleData.Attributes["userName"]); - - // db assertions - var dbUser = await _dbContext.Users.FindAsync(returnedUser.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(); - _dbContext.Users.Add(user); - await _dbContext.SaveChangesAsync(); - - user.Password = _userFaker.Generate().Password; - var serializer = GetSerializer(p => new { p.Password }); - var httpMethod = new HttpMethod("PATCH"); - var route = $"/api/v1/users/{user.Id}"; - var request = new HttpRequestMessage(httpMethod, route) - { - Content = new StringContent(serializer.Serialize(user)) - }; - request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); - - // Act - var response = await _client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - // response assertions - var body = await response.Content.ReadAsStringAsync(); - var document = JsonConvert.DeserializeObject(body); - Assert.False(document.SingleData.Attributes.ContainsKey("password")); - Assert.Equal(user.UserName, document.SingleData.Attributes["userName"]); - - // db assertions - var dbUser = _dbContext.Users.AsNoTracking().Single(u => u.Id == user.Id); - Assert.Equal(user.Password, dbUser.Password); - } - - [Fact] - public async Task Unauthorized_TodoItem() - { - // Arrange - var route = "/api/v1/todoItems/1337"; - - // Act - var response = await _client.GetAsync(route); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - AssertEqualStatusCode(HttpStatusCode.Forbidden, response); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.Forbidden, errorDocument.Errors[0].StatusCode); - Assert.Equal("You are not allowed to update the author of todo items.", errorDocument.Errors[0].Title); - Assert.Null(errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Unauthorized_Passport() - { - // Arrange - var route = "/api/v1/people/1?include=passport"; - - // Act - var response = await _client.GetAsync(route); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - AssertEqualStatusCode(HttpStatusCode.Forbidden, response); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.Forbidden, errorDocument.Errors[0].StatusCode); - Assert.Equal("You are not allowed to include passports on individual persons.", errorDocument.Errors[0].Title); - Assert.Null(errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Unauthorized_Article() - { - // Arrange - var article = _articleFaker.Generate(); - article.Caption = "Classified"; - _dbContext.Articles.Add(article); - await _dbContext.SaveChangesAsync(); - - var route = $"/api/v1/articles/{article.Id}"; - - // Act - var response = await _client.GetAsync(route); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - AssertEqualStatusCode(HttpStatusCode.Forbidden, response); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.Forbidden, errorDocument.Errors[0].StatusCode); - Assert.Equal("You are not allowed to see this article.", errorDocument.Errors[0].Title); - Assert.Null(errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Article_Is_Hidden() - { - // Arrange - var articles = _articleFaker.Generate(3); - string toBeExcluded = "This should not be included"; - articles[0].Caption = toBeExcluded; - - _dbContext.Articles.AddRange(articles); - await _dbContext.SaveChangesAsync(); - - var route = "/api/v1/articles"; - - // Act - var response = await _client.GetAsync(route); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with body: {body}"); - Assert.DoesNotContain(toBeExcluded, body); - } - - [Fact] - public async Task Tag_Is_Hidden() - { - // Arrange - var article = _articleFaker.Generate(); - var tags = _tagFaker.Generate(2); - string toBeExcluded = "This should not be included"; - tags[0].Name = toBeExcluded; - - var articleTags = new[] - { - new ArticleTag - { - Article = article, - Tag = tags[0] - }, - new ArticleTag - { - Article = article, - Tag = tags[1] - } - }; - _dbContext.ArticleTags.AddRange(articleTags); - await _dbContext.SaveChangesAsync(); - - // Workaround for https://github.com/dotnet/efcore/issues/21026 - var options = (JsonApiOptions) _factory.Services.GetRequiredService(); - options.DisableTopPagination = false; - options.DisableChildrenPagination = true; - - var route = "/api/v1/articles?include=tags"; - - // Act - var response = await _client.GetAsync(route); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with body: {body}"); - Assert.DoesNotContain(toBeExcluded, body); - } - ///// - ///// In the Cascade Permission Error tests, we ensure that all the relevant - ///// resources are provided in the hook definitions. In this case, - ///// re-relating the meta object to a different article would require - ///// also a check for the lockedTodo, because we're implicitly updating - ///// its foreign key. - ///// - [Fact] - public async Task Cascade_Permission_Error_Create_ToOne_Relationship() - { - // Arrange - var lockedPerson = _personFaker.Generate(); - lockedPerson.IsLocked = true; - var passport = new Passport(_dbContext); - lockedPerson.Passport = passport; - _dbContext.People.AddRange(lockedPerson); - await _dbContext.SaveChangesAsync(); - - var content = new - { - data = new - { - type = "people", - relationships = new Dictionary - { - { "passport", new - { - data = new { type = "passports", id = lockedPerson.Passport.StringId } - } - } - } - } - }; - - var httpMethod = new HttpMethod("POST"); - var route = "/api/v1/people"; - var request = new HttpRequestMessage(httpMethod, route); - - string serializedContent = JsonConvert.SerializeObject(content); - request.Content = new StringContent(serializedContent); - request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); - - // Act - var response = await _client.SendAsync(request); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - AssertEqualStatusCode(HttpStatusCode.Forbidden, response); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.Forbidden, errorDocument.Errors[0].StatusCode); - Assert.Equal("You are not allowed to update fields or relationships of locked todo items.", errorDocument.Errors[0].Title); - Assert.Null(errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Cascade_Permission_Error_Updating_ToOne_Relationship() - { - // Arrange - var person = _personFaker.Generate(); - var passport = new Passport(_dbContext) { IsLocked = true }; - person.Passport = passport; - _dbContext.People.AddRange(person); - var newPassport = new Passport(_dbContext); - _dbContext.Passports.Add(newPassport); - await _dbContext.SaveChangesAsync(); - - var content = new - { - data = new - { - type = "people", - id = person.Id, - relationships = new Dictionary - { - { "passport", new - { - data = new { type = "passports", id = newPassport.StringId } - } - } - } - } - }; - - var httpMethod = new HttpMethod("PATCH"); - var route = $"/api/v1/people/{person.Id}"; - var request = new HttpRequestMessage(httpMethod, route); - - string serializedContent = JsonConvert.SerializeObject(content); - request.Content = new StringContent(serializedContent); - request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); - - // Act - var response = await _client.SendAsync(request); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - AssertEqualStatusCode(HttpStatusCode.Forbidden, response); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.Forbidden, errorDocument.Errors[0].StatusCode); - Assert.Equal("You are not allowed to update fields or relationships of locked persons.", errorDocument.Errors[0].Title); - Assert.Null(errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Cascade_Permission_Error_Updating_ToOne_Relationship_Deletion() - { - // Arrange - var person = _personFaker.Generate(); - var passport = new Passport(_dbContext) { IsLocked = true }; - person.Passport = passport; - _dbContext.People.AddRange(person); - var newPassport = new Passport(_dbContext); - _dbContext.Passports.Add(newPassport); - await _dbContext.SaveChangesAsync(); - - var content = new - { - data = new - { - type = "people", - id = person.Id, - relationships = new Dictionary - { - { "passport", new - { - data = (object)null - } - } - } - } - }; - - var httpMethod = new HttpMethod("PATCH"); - var route = $"/api/v1/people/{person.Id}"; - var request = new HttpRequestMessage(httpMethod, route); - - string serializedContent = JsonConvert.SerializeObject(content); - request.Content = new StringContent(serializedContent); - request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); - - // Act - var response = await _client.SendAsync(request); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - AssertEqualStatusCode(HttpStatusCode.Forbidden, response); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.Forbidden, errorDocument.Errors[0].StatusCode); - Assert.Equal("You are not allowed to update fields or relationships of locked persons.", errorDocument.Errors[0].Title); - Assert.Null(errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Cascade_Permission_Error_Delete_ToOne_Relationship() - { - // Arrange - var lockedPerson = _personFaker.Generate(); - lockedPerson.IsLocked = true; - var passport = new Passport(_dbContext); - lockedPerson.Passport = passport; - _dbContext.People.AddRange(lockedPerson); - await _dbContext.SaveChangesAsync(); - - var httpMethod = new HttpMethod("DELETE"); - var route = $"/api/v1/passports/{lockedPerson.Passport.StringId}"; - var request = new HttpRequestMessage(httpMethod, route); - - // Act - var response = await _client.SendAsync(request); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - AssertEqualStatusCode(HttpStatusCode.Forbidden, response); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.Forbidden, errorDocument.Errors[0].StatusCode); - Assert.Equal("You are not allowed to update fields or relationships of locked todo items.", errorDocument.Errors[0].Title); - Assert.Null(errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Cascade_Permission_Error_Create_ToMany_Relationship() - { - // Arrange - var persons = _personFaker.Generate(2); - var lockedTodo = _todoItemFaker.Generate(); - lockedTodo.IsLocked = true; - lockedTodo.StakeHolders = persons.ToHashSet(); - _dbContext.TodoItems.Add(lockedTodo); - await _dbContext.SaveChangesAsync(); - - var content = new - { - data = new - { - type = "todoItems", - relationships = new Dictionary - { - { "stakeHolders", new - { - data = new[] - { - new { type = "people", id = persons[0].StringId }, - new { type = "people", id = persons[1].StringId } - } - - } - } - } - } - }; - - var httpMethod = new HttpMethod("POST"); - var route = "/api/v1/todoItems"; - var request = new HttpRequestMessage(httpMethod, route); - - string serializedContent = JsonConvert.SerializeObject(content); - request.Content = new StringContent(serializedContent); - request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); - - // Act - var response = await _client.SendAsync(request); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - AssertEqualStatusCode(HttpStatusCode.Forbidden, response); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.Forbidden, errorDocument.Errors[0].StatusCode); - Assert.Equal("You are not allowed to update fields or relationships of locked todo items.", errorDocument.Errors[0].Title); - Assert.Null(errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Cascade_Permission_Error_Updating_ToMany_Relationship() - { - // Arrange - var persons = _personFaker.Generate(2); - var lockedTodo = _todoItemFaker.Generate(); - lockedTodo.IsLocked = true; - lockedTodo.StakeHolders = persons.ToHashSet(); - _dbContext.TodoItems.Add(lockedTodo); - var unlockedTodo = _todoItemFaker.Generate(); - _dbContext.TodoItems.Add(unlockedTodo); - await _dbContext.SaveChangesAsync(); - - var content = new - { - data = new - { - type = "todoItems", - id = unlockedTodo.Id, - relationships = new Dictionary - { - { "stakeHolders", new - { - data = new[] - { - new { type = "people", id = persons[0].StringId }, - new { type = "people", id = persons[1].StringId } - } - - } - } - } - } - }; - - var httpMethod = new HttpMethod("PATCH"); - var route = $"/api/v1/todoItems/{unlockedTodo.Id}"; - var request = new HttpRequestMessage(httpMethod, route); - - string serializedContent = JsonConvert.SerializeObject(content); - request.Content = new StringContent(serializedContent); - request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); - - // Act - var response = await _client.SendAsync(request); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - AssertEqualStatusCode(HttpStatusCode.Forbidden, response); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.Forbidden, errorDocument.Errors[0].StatusCode); - Assert.Equal("You are not allowed to update fields or relationships of locked todo items.", errorDocument.Errors[0].Title); - Assert.Null(errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Cascade_Permission_Error_Delete_ToMany_Relationship() - { - // Arrange - var persons = _personFaker.Generate(2); - var lockedTodo = _todoItemFaker.Generate(); - lockedTodo.IsLocked = true; - lockedTodo.StakeHolders = persons.ToHashSet(); - _dbContext.TodoItems.Add(lockedTodo); - await _dbContext.SaveChangesAsync(); - - var httpMethod = new HttpMethod("DELETE"); - var route = $"/api/v1/people/{persons[0].Id}"; - var request = new HttpRequestMessage(httpMethod, route); - - // Act - var response = await _client.SendAsync(request); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - AssertEqualStatusCode(HttpStatusCode.Forbidden, response); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.Forbidden, errorDocument.Errors[0].StatusCode); - Assert.Equal("You are not allowed to update fields or relationships of locked todo items.", errorDocument.Errors[0].Title); - Assert.Null(errorDocument.Errors[0].Detail); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceHooks/ResourceHooksStartup.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceHooks/ResourceHooksStartup.cs new file mode 100644 index 0000000000..f36175d0e5 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceHooks/ResourceHooksStartup.cs @@ -0,0 +1,42 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Definitions; +using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCoreExampleTests.IntegrationTests; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace JsonApiDotNetCoreExampleTests.Acceptance +{ + public sealed class ResourceHooksStartup : TestableStartup + { + public ResourceHooksStartup(IConfiguration configuration) : base(configuration) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddClientSerialization(); + + services.AddSingleton(); + + services.AddTransient, ArticleHooks>(); + services.AddTransient, PassportHooks>(); + services.AddTransient, TagHooks>(); + services.AddTransient, PersonHooks>(); + services.AddTransient, TodoItemHooks>(); + + base.ConfigureServices(services); + } + + protected override void SetJsonApiOptions(JsonApiOptions options) + { + base.SetJsonApiOptions(options); + + options.EnableResourceHooks = true; + options.LoadDatabaseValues = true; + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceHooks/ResourceHooksTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceHooks/ResourceHooksTests.cs new file mode 100644 index 0000000000..8cced822f5 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceHooks/ResourceHooksTests.cs @@ -0,0 +1,570 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Net; +using System.Threading.Tasks; +using Bogus; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Client.Internal; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCoreExampleTests.Helpers.Models; +using Microsoft.AspNetCore.Authentication; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json; +using Xunit; +using Person = JsonApiDotNetCoreExample.Models.Person; + +namespace JsonApiDotNetCoreExampleTests.Acceptance +{ + public sealed class ResourceDefinitionTests : IClassFixture> + { + private readonly Faker _userFaker; + private readonly Faker _todoItemFaker; + private readonly Faker _personFaker; + private readonly Faker
_articleFaker; + private readonly Faker _authorFaker; + private readonly Faker _tagFaker; + + private readonly IntegrationTestContext _testContext; + private readonly IResponseDeserializer _deserializer; + + public ResourceDefinitionTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + _authorFaker = new Faker() + .RuleFor(a => a.LastName, f => f.Random.Words(2)); + + _articleFaker = new Faker
() + .RuleFor(a => a.Caption, f => f.Random.AlphaNumeric(10)) + .RuleFor(a => a.Author, f => _authorFaker.Generate()); + + var systemClock = testContext.Factory.Services.GetRequiredService(); + var options = testContext.Factory.Services.GetRequiredService>(); + var tempDbContext = new AppDbContext(options, systemClock); + _userFaker = new Faker() + .CustomInstantiator(f => new User(tempDbContext)) + .RuleFor(u => u.UserName, f => f.Internet.UserName()) + .RuleFor(u => u.Password, f => f.Internet.Password()); + + _todoItemFaker = new Faker() + .RuleFor(t => t.Description, f => f.Lorem.Sentence()) + .RuleFor(t => t.Ordinal, f => f.Random.Number()) + .RuleFor(t => t.CreatedDate, f => f.Date.Past()); + + _personFaker = new Faker() + .RuleFor(p => p.FirstName, f => f.Name.FirstName()) + .RuleFor(p => p.LastName, f => f.Name.LastName()); + + _tagFaker = new Faker() + .CustomInstantiator(f => new Tag()) + .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)); + + _deserializer = GetDeserializer(); + // var options = (JsonApiOptions) _factory.Services.GetRequiredService(); + // options.DisableTopPagination = false; + // options.DisableChildrenPagination = false; + } + + [Fact] + public async Task Can_Create_User_With_Password() + { + // Arrange + var user = _userFaker.Generate(); + var serializer = GetSerializer(p => new { p.Password, p.UserName }); + var route = "/users"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, serializer.Serialize(user)); + + // Assert + Assert.Equal(HttpStatusCode.Created, httpResponse.StatusCode); + + var body = await httpResponse.Content.ReadAsStringAsync(); + var returnedUser = _deserializer.DeserializeSingle(body).Data; + var document = JsonConvert.DeserializeObject(body); + Assert.False(document.SingleData.Attributes.ContainsKey("password")); + Assert.Equal(user.UserName, document.SingleData.Attributes["userName"]); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + var dbUser = await dbContext.Users.FindAsync(returnedUser.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(); + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Users.Add(user); + await dbContext.SaveChangesAsync(); + }); + + user.Password = _userFaker.Generate().Password; + var serializer = GetSerializer(p => new { p.Password }); + var route = $"/users/{user.Id}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, serializer.Serialize((user))); + + // Assert + Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode); + Assert.False(responseDocument.SingleData.Attributes.ContainsKey("password")); + Assert.Equal(user.UserName, responseDocument.SingleData.Attributes["userName"]); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + var dbUser = await dbContext.Users.SingleAsync(u => u.Id == user.Id); + Assert.Equal(user.Password, dbUser.Password); + }); + } + + [Fact] + public async Task Unauthorized_TodoItem() + { + // Arrange + var route = "/todoItems/1337"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + Assert.Single(responseDocument.Errors); + Assert.Equal(HttpStatusCode.Forbidden, responseDocument.Errors[0].StatusCode); + Assert.Equal("You are not allowed to update the author of todo items.", responseDocument.Errors[0].Title); + Assert.Null(responseDocument.Errors[0].Detail); + } + + [Fact] + public async Task Unauthorized_Passport() + { + // Arrange + var route = "/people/1?include=passport"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + Assert.Single(responseDocument.Errors); + Assert.Equal(HttpStatusCode.Forbidden, responseDocument.Errors[0].StatusCode); + Assert.Equal("You are not allowed to include passports on individual persons.", responseDocument.Errors[0].Title); + Assert.Null(responseDocument.Errors[0].Detail); + } + + [Fact] + public async Task Unauthorized_Article() + { + // Arrange + var article = _articleFaker.Generate(); + article.Caption = "Classified"; + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Articles.Add(article); + await dbContext.SaveChangesAsync(); + }); + + var route = $"/articles/{article.Id}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + Assert.Single(responseDocument.Errors); + Assert.Equal(HttpStatusCode.Forbidden, responseDocument.Errors[0].StatusCode); + Assert.Equal("You are not allowed to see this article.", responseDocument.Errors[0].Title); + Assert.Null(responseDocument.Errors[0].Detail); + } + + [Fact] + public async Task Article_Is_Hidden() + { + // Arrange + var articles = _articleFaker.Generate(3); + string toBeExcluded = "This should not be included"; + articles[0].Caption = toBeExcluded; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Articles.AddRange(articles); + await dbContext.SaveChangesAsync(); + }); + + var route = "/articles"; + + // Act + var (httpResponse, responseBody) = await _testContext.ExecuteGetAsync(route); + + Assert.DoesNotContain(toBeExcluded, responseBody); + } + + [Fact] + public async Task Tag_Is_Hidden() + { + // Arrange + var article = _articleFaker.Generate(); + var tags = _tagFaker.Generate(2); + string toBeExcluded = "This should not be included"; + tags[0].Name = toBeExcluded; + + var articleTags = new[] + { + new ArticleTag + { + Article = article, + Tag = tags[0] + }, + new ArticleTag + { + Article = article, + Tag = tags[1] + } + }; + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.ArticleTags.AddRange(articleTags); + await dbContext.SaveChangesAsync(); + }); + + // Workaround for https://github.com/dotnet/efcore/issues/21026 + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.DisableTopPagination = false; + options.DisableChildrenPagination = true; + + var route = "/articles?include=tags"; + + // Act + var (httpResponse, responseBody) = await _testContext.ExecuteGetAsync(route); + + Assert.DoesNotContain(toBeExcluded, responseBody); + } + ///// + ///// In the Cascade Permission Error tests, we ensure that all the relevant + ///// resources are provided in the hook definitions. In this case, + ///// re-relating the meta object to a different article would require + ///// also a check for the lockedTodo, because we're implicitly updating + ///// its foreign key. + ///// + [Fact] + public async Task Cascade_Permission_Error_Create_ToOne_Relationship() + { + // Arrange + var lockedPerson = _personFaker.Generate(); + lockedPerson.IsLocked = true; + Passport passport; + await _testContext.RunOnDatabaseAsync(async dbContext => + { + passport = new Passport(dbContext); + lockedPerson.Passport = passport; + dbContext.People.AddRange(lockedPerson); + await dbContext.SaveChangesAsync(); + }); + + var content = new + { + data = new + { + type = "people", + relationships = new Dictionary + { + { "passport", new + { + data = new { type = "passports", id = lockedPerson.Passport.StringId } + } + } + } + } + }; + + var route = "/people"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, content); + + // Assert + Assert.Single(responseDocument.Errors); + Assert.Equal(HttpStatusCode.Forbidden, responseDocument.Errors[0].StatusCode); + Assert.Equal("You are not allowed to update fields or relationships of locked todo items.", responseDocument.Errors[0].Title); + Assert.Null(responseDocument.Errors[0].Detail); + } + + [Fact] + public async Task Cascade_Permission_Error_Updating_ToOne_Relationship() + { + // Arrange + var person = _personFaker.Generate(); + Passport passport = null; + Passport newPassport = null; + await _testContext.RunOnDatabaseAsync(async dbContext => + { + passport = new Passport(dbContext) { IsLocked = true }; + person.Passport = passport; + newPassport = new Passport(dbContext); + dbContext.People.AddRange(person); + dbContext.Passports.Add(newPassport); + await dbContext.SaveChangesAsync(); + }); + + var content = new + { + data = new + { + type = "people", + id = person.Id, + relationships = new Dictionary + { + { "passport", new + { + data = new { type = "passports", id = newPassport.StringId } + } + } + } + } + }; + + var route = $"/people/{person.Id}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, content); + + // Assert + Assert.Single(responseDocument.Errors); + Assert.Equal(HttpStatusCode.Forbidden, responseDocument.Errors[0].StatusCode); + Assert.Equal("You are not allowed to update fields or relationships of locked persons.", responseDocument.Errors[0].Title); + Assert.Null(responseDocument.Errors[0].Detail); + } + + [Fact] + public async Task Cascade_Permission_Error_Updating_ToOne_Relationship_Deletion() + { + // Arrange + var person = _personFaker.Generate(); + Passport passport = null; + Passport newPassport = null; + await _testContext.RunOnDatabaseAsync(async dbContext => + { + passport = new Passport(dbContext) { IsLocked = true }; + person.Passport = passport; + newPassport = new Passport(dbContext); + dbContext.People.AddRange(person); + dbContext.Passports.Add(newPassport); + await dbContext.SaveChangesAsync(); + }); + + var content = new + { + data = new + { + type = "people", + id = person.Id, + relationships = new Dictionary + { + { "passport", new + { + data = (object)null + } + } + } + } + }; + + var route = $"/people/{person.Id}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, content); + + // Assert + Assert.Single(responseDocument.Errors); + Assert.Equal(HttpStatusCode.Forbidden, responseDocument.Errors[0].StatusCode); + Assert.Equal("You are not allowed to update fields or relationships of locked persons.", responseDocument.Errors[0].Title); + Assert.Null(responseDocument.Errors[0].Detail); + } + + [Fact] + public async Task Cascade_Permission_Error_Delete_ToOne_Relationship() + { + // Arrange + var lockedPerson = _personFaker.Generate(); + lockedPerson.IsLocked = true; + Passport passport = null; + await _testContext.RunOnDatabaseAsync(async dbContext => + { + passport = new Passport(dbContext); + lockedPerson.Passport = passport; + dbContext.People.AddRange(lockedPerson); + await dbContext.SaveChangesAsync(); + }); + + var route = $"/passports/{lockedPerson.Passport.StringId}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); + + // Assert + Assert.Single(responseDocument.Errors); + Assert.Equal(HttpStatusCode.Forbidden, responseDocument.Errors[0].StatusCode); + Assert.Equal("You are not allowed to update fields or relationships of locked todo items.", responseDocument.Errors[0].Title); + Assert.Null(responseDocument.Errors[0].Detail); + } + + [Fact] + public async Task Cascade_Permission_Error_Create_ToMany_Relationship() + { + // Arrange + var persons = _personFaker.Generate(2); + var lockedTodo = _todoItemFaker.Generate(); + lockedTodo.IsLocked = true; + lockedTodo.StakeHolders = persons.ToHashSet(); + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.TodoItems.Add(lockedTodo); + await dbContext.SaveChangesAsync(); + }); + + var content = new + { + data = new + { + type = "todoItems", + relationships = new Dictionary + { + { "stakeHolders", new + { + data = new[] + { + new { type = "people", id = persons[0].StringId }, + new { type = "people", id = persons[1].StringId } + } + + } + } + } + } + }; + + var route = "/todoItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, content); + + // Assert + Assert.Single(responseDocument.Errors); + Assert.Equal(HttpStatusCode.Forbidden, responseDocument.Errors[0].StatusCode); + Assert.Equal("You are not allowed to update fields or relationships of locked todo items.", responseDocument.Errors[0].Title); + Assert.Null(responseDocument.Errors[0].Detail); + } + + [Fact] + public async Task Cascade_Permission_Error_Updating_ToMany_Relationship() + { + // Arrange + var persons = _personFaker.Generate(2); + var lockedTodo = _todoItemFaker.Generate(); + lockedTodo.IsLocked = true; + lockedTodo.StakeHolders = persons.ToHashSet(); + var unlockedTodo = _todoItemFaker.Generate(); + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.TodoItems.Add(lockedTodo); + dbContext.TodoItems.Add(unlockedTodo); + await dbContext.SaveChangesAsync(); + }); + + var content = new + { + data = new + { + type = "todoItems", + id = unlockedTodo.Id, + relationships = new Dictionary + { + { "stakeHolders", new + { + data = new[] + { + new { type = "people", id = persons[0].StringId }, + new { type = "people", id = persons[1].StringId } + } + + } + } + } + } + }; + + var route = $"/todoItems/{unlockedTodo.Id}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, content); + + // Assert + Assert.Single(responseDocument.Errors); + Assert.Equal(HttpStatusCode.Forbidden, responseDocument.Errors[0].StatusCode); + Assert.Equal("You are not allowed to update fields or relationships of locked todo items.", responseDocument.Errors[0].Title); + Assert.Null(responseDocument.Errors[0].Detail); + } + + [Fact] + public async Task Cascade_Permission_Error_Delete_ToMany_Relationship() + { + // Arrange + var persons = _personFaker.Generate(2); + var lockedTodo = _todoItemFaker.Generate(); + lockedTodo.IsLocked = true; + lockedTodo.StakeHolders = persons.ToHashSet(); + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.TodoItems.Add(lockedTodo); + await dbContext.SaveChangesAsync(); + }); + var route = $"/people/{persons[0].Id}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); + + // Assert + Assert.Single(responseDocument.Errors); + Assert.Equal(HttpStatusCode.Forbidden, responseDocument.Errors[0].StatusCode); + Assert.Equal("You are not allowed to update fields or relationships of locked todo items.", responseDocument.Errors[0].Title); + Assert.Null(responseDocument.Errors[0].Detail); + } + + private IRequestSerializer GetSerializer(Expression> attributes = null, Expression> relationships = null) where TResource : class, IIdentifiable + { + var serializer = _testContext.Factory.Services.GetService(); + var graph = _testContext.Factory.Services.GetService(); + serializer.AttributesToSerialize = attributes != null ? graph.GetAttributes(attributes) : null; + serializer.RelationshipsToSerialize = relationships != null ? graph.GetRelationships(relationships) : null; + return serializer; + } + + private IResponseDeserializer GetDeserializer() + { + var options = _testContext.Factory.Services.GetService(); + var formatter = new ResourceNameFormatter(options); + var resourcesContexts = _testContext.Factory.Services.GetService().GetResourceContexts(); + var builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); + foreach (var rc in resourcesContexts) + { + if (rc.ResourceType == typeof(TodoItem) || rc.ResourceType == typeof(TodoItemCollection)) + { + continue; + } + builder.Add(rc.ResourceType, rc.IdentityType, rc.PublicName); + } + builder.Add(formatter.FormatResourceName(typeof(TodoItem))); + builder.Add(formatter.FormatResourceName(typeof(TodoItemCollection))); + return new ResponseDeserializer(builder.Build(), new ResourceFactory(_testContext.Factory.Services)); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs b/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs deleted file mode 100644 index 72f879054b..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs +++ /dev/null @@ -1,33 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; - -namespace JsonApiDotNetCoreExampleTests -{ - public class ResourceHooksApplicationFactory : CustomApplicationFactoryBase - { - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - base.ConfigureWebHost(builder); - - builder.ConfigureServices(services => - { - services.AddClientSerialization(); - }); - - builder.ConfigureTestServices(services => - { - services.AddJsonApi(options => - { - options.Namespace = "api/v1"; - options.DefaultPageSize = new PageSize(5); - options.IncludeTotalResourceCount = true; - options.EnableResourceHooks = true; - options.LoadDatabaseValues = true; - options.IncludeExceptionStackTraceInErrors = true; - }, - discovery => discovery.AddAssembly(typeof(JsonApiDotNetCoreExample.Program).Assembly)); - }); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarsController.cs deleted file mode 100644 index f264c043e3..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarsController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys -{ - public sealed class CarsController : JsonApiController - { - public CarsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/DealershipsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/DealershipsController.cs deleted file mode 100644 index 53b4f281e1..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/DealershipsController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys -{ - public sealed class DealershipsController : JsonApiController - { - public DealershipsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/EnginesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/EnginesController.cs deleted file mode 100644 index 4833292cd8..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/EnginesController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys -{ - public sealed class EnginesController : JsonApiController - { - public EnginesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PoliciesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PoliciesController.cs deleted file mode 100644 index d99ab9bd6a..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PoliciesController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ContentNegotiation -{ - public sealed class PoliciesController : JsonApiController - { - public PoliciesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingsController.cs deleted file mode 100644 index 4a0b9bb366..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingsController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading -{ - public sealed class BuildingsController : JsonApiController - { - public BuildingsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StatesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StatesController.cs deleted file mode 100644 index 28c8b795b8..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StatesController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading -{ - public sealed class StatesController : JsonApiController - { - public StatesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StreetsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StreetsController.cs deleted file mode 100644 index e6b4eda7e1..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StreetsController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading -{ - public sealed class StreetsController : JsonApiController - { - public StreetsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterableResourcesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterableResourcesController.cs deleted file mode 100644 index 7314d155d0..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterableResourcesController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Filtering -{ - public sealed class FilterableResourcesController : JsonApiController - { - public FilterableResourcesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/BankAccountsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/BankAccountsController.cs deleted file mode 100644 index 91793dfc8c..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/BankAccountsController.cs +++ /dev/null @@ -1,15 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation -{ - public sealed class BankAccountsController : ObfuscatedIdentifiableController - { - public BankAccountsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/DebitCardsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/DebitCardsController.cs deleted file mode 100644 index b72cea109e..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/DebitCardsController.cs +++ /dev/null @@ -1,15 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation -{ - public sealed class DebitCardsController : ObfuscatedIdentifiableController - { - public DebitCardsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationStartup.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationStartup.cs new file mode 100644 index 0000000000..2261d7fac1 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationStartup.cs @@ -0,0 +1,26 @@ +using JsonApiDotNetCore.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation +{ + public sealed class IdObfuscationStartup : TestableStartup + { + public IdObfuscationStartup(IConfiguration configuration) : base(configuration) + { + } + + protected override void SetJsonApiOptions(JsonApiOptions options) + { + base.SetJsonApiOptions(options); + + options.ValidateModelState = true; + } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + base.ConfigureServices(services); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs index 811f58a7a3..5f3452abbc 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs @@ -7,13 +7,12 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation { - public sealed class IdObfuscationTests - : IClassFixture, ObfuscationDbContext>> + public sealed class IdObfuscationTests : IClassFixture> { - private readonly IntegrationTestContext, ObfuscationDbContext> _testContext; + private readonly IntegrationTestContext _testContext; private readonly ObfuscationFakers _fakers = new ObfuscationFakers(); - public IdObfuscationTests(IntegrationTestContext, ObfuscationDbContext> testContext) + public IdObfuscationTests(IntegrationTestContext testContext) { _testContext = testContext; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs index c762873886..849944fde1 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs @@ -10,10 +10,10 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation { - public abstract class ObfuscatedIdentifiableController : BaseJsonApiController + public sealed class ObfuscatedIdentifiableController : BaseJsonApiController where TResource : class, IIdentifiable { - protected ObfuscatedIdentifiableController(IJsonApiOptions options, ILoggerFactory loggerFactory, + public ObfuscatedIdentifiableController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedJsonApiControllerGenerator.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedJsonApiControllerGenerator.cs new file mode 100644 index 0000000000..467f82c382 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedJsonApiControllerGenerator.cs @@ -0,0 +1,28 @@ +using System; +using System.Reflection; +using JsonApiDotNetCore.Configuration; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation +{ + public sealed class ObfuscatedJsonApiControllerGenerator : JsonApiControllerGenerator + { + private readonly Type _obfuscatedIdentifiableType; + private readonly Type _obfuscatedControllerOpenType; + + public ObfuscatedJsonApiControllerGenerator(IResourceGraph resourceGraph) : base(resourceGraph) + { + _obfuscatedIdentifiableType = typeof(ObfuscatedIdentifiable); + _obfuscatedControllerOpenType = typeof(ObfuscatedIdentifiableController<>); + } + + protected override TypeInfo GetControllerType(ResourceContext resourceContext) + { + if (_obfuscatedIdentifiableType.IsAssignableFrom(resourceContext.ResourceType)) + { + return _obfuscatedControllerOpenType.MakeGenericType(resourceContext.ResourceType).GetTypeInfo(); + } + + return base.GetControllerType(resourceContext); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectoriesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectoriesController.cs deleted file mode 100644 index 5228903c5d..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectoriesController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ModelStateValidation -{ - public sealed class SystemDirectoriesController : JsonApiController - { - public SystemDirectoriesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemFilesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemFilesController.cs deleted file mode 100644 index 9278f59766..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemFilesController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ModelStateValidation -{ - public sealed class SystemFilesController : JsonApiController - { - public SystemFilesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/RgbColorsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/RgbColorsController.cs deleted file mode 100644 index 9d8e32bc25..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/RgbColorsController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite -{ - public sealed class RgbColorsController : JsonApiController - { - public RgbColorsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/UserAccountsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/UserAccountsController.cs deleted file mode 100644 index 9d409bbb57..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/UserAccountsController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite -{ - public sealed class UserAccountsController : JsonApiController - { - public UserAccountsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs deleted file mode 100644 index c6b00f25e3..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite -{ - public sealed class WorkItemGroupsController : JsonApiController - { - public WorkItemGroupsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemsController.cs deleted file mode 100644 index e3e90fe0f3..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/WorkItemsController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ReadWrite -{ - public sealed class WorkItemsController : JsonApiController - { - public WorkItemsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/CustomersController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/CustomersController.cs deleted file mode 100644 index d611659bd0..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/CustomersController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RequiredRelationships -{ - public sealed class CustomersController : JsonApiController - { - public CustomersController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/OrdersController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/OrdersController.cs deleted file mode 100644 index c6373a9c95..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/OrdersController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RequiredRelationships -{ - public sealed class OrdersController : JsonApiController - { - public OrdersController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/ShipmentsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/ShipmentsController.cs deleted file mode 100644 index 03f1e2c82f..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/ShipmentsController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RequiredRelationships -{ - public sealed class ShipmentsController : JsonApiController - { - public ShipmentsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourcesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourcesController.cs deleted file mode 100644 index fba77b6b8f..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourcesController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions -{ - public sealed class CallableResourcesController : JsonApiController - { - public CallableResourcesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Book.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Book.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Book.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Book.cs diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/CompanyHealthInsurance.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/CompanyHealthInsurance.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/CompanyHealthInsurance.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/CompanyHealthInsurance.cs diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/ContentItem.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/ContentItem.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/ContentItem.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/ContentItem.cs diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/FamilyHealthInsurance.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/FamilyHealthInsurance.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/FamilyHealthInsurance.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/FamilyHealthInsurance.cs diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/HealthInsurance.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/HealthInsurance.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/HealthInsurance.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/HealthInsurance.cs diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Human.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Human.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Human.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Human.cs diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/HumanFavoriteContentItem.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/HumanFavoriteContentItem.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/HumanFavoriteContentItem.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/HumanFavoriteContentItem.cs diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Man.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Man.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Man.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Man.cs diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/MenController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/MenController.cs deleted file mode 100644 index 43b6fd97fa..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/MenController.cs +++ /dev/null @@ -1,15 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceInheritance -{ - public sealed class MenController : JsonApiController - { - public MenController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) { } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Video.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Video.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Video.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Video.cs diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Woman.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Woman.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Models/Woman.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceInheritance/Woman.cs diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/CompaniesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/CompaniesController.cs deleted file mode 100644 index 17fe2da387..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/CompaniesController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion -{ - public sealed class CompaniesController : JsonApiController - { - public CompaniesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/DepartmentsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/DepartmentsController.cs deleted file mode 100644 index 01a77f9b08..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/DepartmentsController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion -{ - public sealed class DepartmentsController : JsonApiController - { - public DepartmentsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/GamesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/GamesController.cs deleted file mode 100644 index 2dc928de67..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/GamesController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ZeroKeys -{ - public sealed class GamesController : JsonApiController - { - public GamesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/MapsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/MapsController.cs deleted file mode 100644 index d7fb295509..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/MapsController.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ZeroKeys -{ - public sealed class MapsController : JsonApiController - { - public MapsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/PlayersController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/PlayersController.cs deleted file mode 100644 index 3313d7e4e3..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/PlayersController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ZeroKeys -{ - public sealed class PlayersController : JsonApiController - { - public PlayersController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs index 5ab2fcd1e0..34a57cccc9 100644 --- a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs +++ b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs @@ -1,12 +1,14 @@ using System; using System.IO; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources.Annotations; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Controllers; using Moq; using Xunit; @@ -81,6 +83,8 @@ private Task RunMiddlewareTask(InvokeConfiguration holder) { var controllerResourceMapping = holder.ControllerResourceMapping.Object; var context = holder.HttpContext; + var endpointMetadata = new EndpointMetadataCollection(new ControllerActionDescriptor {ControllerTypeInfo = typeof(object).GetTypeInfo()}); + context.SetEndpoint(new Endpoint(null, endpointMetadata, null)); var options = holder.Options.Object; var request = holder.Request; var resourceGraph = holder.ResourceGraph.Object; @@ -98,7 +102,7 @@ private InvokeConfiguration GetConfiguration(string path, string resourceName = }); var forcedNamespace = "api/v1"; var mockMapping = new Mock(); - mockMapping.Setup(x => x.GetAssociatedResource(It.IsAny())).Returns(typeof(string)); + mockMapping.Setup(x => x.GetResourceForEndpoint(It.IsAny())).Returns(typeof(string)); Mock mockOptions = CreateMockOptions(forcedNamespace); var mockGraph = CreateMockResourceGraph(resourceName, includeRelationship: relType != null); diff --git a/test/UnitTests/Middleware/JsonApiRequestTests.cs b/test/UnitTests/Middleware/JsonApiRequestTests.cs index f3b2a62508..aaa8e804be 100644 --- a/test/UnitTests/Middleware/JsonApiRequestTests.cs +++ b/test/UnitTests/Middleware/JsonApiRequestTests.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -7,6 +8,7 @@ using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -47,7 +49,7 @@ public async Task Sets_request_properties_correctly(string requestMethod, string var controllerResourceMappingMock = new Mock(); controllerResourceMappingMock - .Setup(x => x.GetAssociatedResource(It.IsAny())) + .Setup(x => x.GetResourceForEndpoint(It.IsAny())) .Returns(typeof(Article)); var httpContext = new DefaultHttpContext(); @@ -98,8 +100,10 @@ private static void SetupRoutes(HttpContext httpContext, string requestMethod, s feature.RouteValues["action"] = "Relationship"; } + + var endpointMetadata = new EndpointMetadataCollection(new ControllerActionDescriptor {ControllerTypeInfo = typeof(object).GetTypeInfo()}); httpContext.Features.Set(feature); - httpContext.SetEndpoint(new Endpoint(null, new EndpointMetadataCollection(), null)); + httpContext.SetEndpoint(new Endpoint(null, endpointMetadata, null)); } } }