From d0a2c42558f90b322f418dc3c13663b40a623d2d Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sat, 22 Sep 2018 20:06:07 -0700 Subject: [PATCH 01/12] fixing auto-discovery --- .../JsonApiDotNetCoreExample/Startup.cs | 5 +- src/Examples/ReportsExample/Startup.cs | 8 +- .../IServiceCollectionExtensions.cs | 54 ++++++++ .../Graph/ResourceDescriptor.cs | 2 + .../Graph/ServiceDiscoveryFacade.cs | 128 ++++++++++-------- src/JsonApiDotNetCore/Graph/TypeLocator.cs | 42 ++++-- .../Internal/JsonApiSetupException.cs | 13 ++ .../IServiceCollectionExtensionsTests.cs | 87 ++++++++++++ 8 files changed, 267 insertions(+), 72 deletions(-) create mode 100644 src/JsonApiDotNetCore/Internal/JsonApiSetupException.cs diff --git a/src/Examples/JsonApiDotNetCoreExample/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startup.cs index 29dfc9e4b5..68ea93a7fc 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startup.cs @@ -41,10 +41,9 @@ public virtual IServiceProvider ConfigureServices(IServiceCollection services) options.IncludeTotalRecordCount = true; }, mvcBuilder, - discovery => discovery.AddCurrentAssemblyServices()); + discovery => discovery.AddCurrentAssembly()); - var provider = services.BuildServiceProvider(); - return provider; + return services.BuildServiceProvider(); } public virtual void Configure( diff --git a/src/Examples/ReportsExample/Startup.cs b/src/Examples/ReportsExample/Startup.cs index 43a910a0e6..b71b7fa74a 100644 --- a/src/Examples/ReportsExample/Startup.cs +++ b/src/Examples/ReportsExample/Startup.cs @@ -25,10 +25,10 @@ public Startup(IHostingEnvironment env) public virtual void ConfigureServices(IServiceCollection services) { var mvcBuilder = services.AddMvcCore(); - services.AddJsonApi( - opt => opt.Namespace = "api", - mvcBuilder, - discovery => discovery.AddCurrentAssemblyServices()); + services.AddJsonApi( + opt => opt.Namespace = "api", + mvcBuilder, + discovery => discovery.AddCurrentAssembly()); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 4c2d6c2f79..9c1df0626e 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; @@ -7,6 +9,7 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; using JsonApiDotNetCore.Services.Operations; @@ -182,5 +185,56 @@ public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions js options.Conventions.Insert(0, new DasherizedRoutingConvention(jsonApiOptions.Namespace)); } + + /// + /// + public static IServiceCollection AddResourceService(this IServiceCollection services) + { + var typeImplemenetsAnExpectedInterface = false; + + var serviceImplementationType = typeof(T); + + // it is _possible_ that a single concrete type could be used for multiple resources... + var resourceDescriptors = GetResourceTypesFromServiceImplementation(serviceImplementationType); + + foreach(var resourceDescriptor in resourceDescriptors) + { + foreach(var openGenericType in ServiceDiscoveryFacade.ServiceInterfaces) + { + var concreteGenericType = openGenericType.GetGenericArguments().Length == 1 + ? openGenericType.MakeGenericType(resourceDescriptor.ResourceType) + : openGenericType.MakeGenericType(resourceDescriptor.ResourceType, resourceDescriptor.IdType); + + if(concreteGenericType.IsAssignableFrom(serviceImplementationType)) { + services.AddScoped(serviceImplementationType, serviceImplementationType); + typeImplemenetsAnExpectedInterface = true; + } + } + } + + if(typeImplemenetsAnExpectedInterface == false) + throw new JsonApiSetupException($"{typeImplemenetsAnExpectedInterface} does not implement any of the expected JsonApiDotNetCore interfaces."); + + return services; + } + + private static HashSet GetResourceTypesFromServiceImplementation(Type type) + { + var resourceDecriptors = new HashSet(); + var interfaces = type.GetInterfaces(); + foreach(var i in interfaces) + { + if(i.IsGenericType) + { + var firstGenericArgument = i.GetGenericTypeDefinition().GetGenericArguments().FirstOrDefault(); + if(TypeLocator.TryGetResourceDescriptor(firstGenericArgument, out var resourceDescriptor) == false) + { + resourceDecriptors.Add(resourceDescriptor); + } + } + } + + return resourceDecriptors; + } } } diff --git a/src/JsonApiDotNetCore/Graph/ResourceDescriptor.cs b/src/JsonApiDotNetCore/Graph/ResourceDescriptor.cs index e90bcdc22c..2cb1a8b812 100644 --- a/src/JsonApiDotNetCore/Graph/ResourceDescriptor.cs +++ b/src/JsonApiDotNetCore/Graph/ResourceDescriptor.cs @@ -12,5 +12,7 @@ public ResourceDescriptor(Type resourceType, Type idType) public Type ResourceType { get; set; } public Type IdType { get; set; } + + internal static ResourceDescriptor Empty => new ResourceDescriptor(null, null); } } diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index b29df8bca1..f403f69124 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -13,10 +14,39 @@ namespace JsonApiDotNetCore.Graph { public class ServiceDiscoveryFacade { + internal static HashSet ServiceInterfaces = new HashSet { + typeof(IResourceService<>), + typeof(IResourceService<,>), + typeof(ICreateService<>), + typeof(ICreateService<,>), + typeof(IGetAllService<>), + typeof(IGetAllService<,>), + typeof(IGetByIdService<>), + typeof(IGetByIdService<,>), + typeof(IGetRelationshipService<>), + typeof(IGetRelationshipService<,>), + typeof(IUpdateService<>), + typeof(IUpdateService<,>), + typeof(IDeleteService<>), + typeof(IDeleteService<,>) + }; + + internal static HashSet RepositoryInterfaces = new HashSet { + typeof(IEntityRepository<>), + typeof(IEntityRepository<,>), + typeof(IEntityWriteRepository<>), + typeof(IEntityWriteRepository<,>), + typeof(IEntityReadRepository<>), + typeof(IEntityReadRepository<,>) + }; + private readonly IServiceCollection _services; private readonly IContextGraphBuilder _graphBuilder; + private readonly List _identifiables = new List(); - public ServiceDiscoveryFacade(IServiceCollection services, IContextGraphBuilder graphBuilder) + public ServiceDiscoveryFacade( + IServiceCollection services, + IContextGraphBuilder graphBuilder) { _services = services; _graphBuilder = graphBuilder; @@ -26,20 +56,25 @@ public ServiceDiscoveryFacade(IServiceCollection services, IContextGraphBuilder /// Add resources, services and repository implementations to the container. /// /// The type name formatter used to get the string representation of resource names. - public ServiceDiscoveryFacade AddCurrentAssemblyServices(IResourceNameFormatter resourceNameFormatter = null) - => AddAssemblyServices(Assembly.GetCallingAssembly(), resourceNameFormatter); + public ServiceDiscoveryFacade AddCurrentAssembly(IResourceNameFormatter resourceNameFormatter = null) + => AddAssembly(Assembly.GetCallingAssembly(), resourceNameFormatter); /// /// Add resources, services and repository implementations to the container. /// /// The assembly to search for resources in. /// The type name formatter used to get the string representation of resource names. - public ServiceDiscoveryFacade AddAssemblyServices(Assembly assembly, IResourceNameFormatter resourceNameFormatter = null) + public ServiceDiscoveryFacade AddAssembly(Assembly assembly, IResourceNameFormatter resourceNameFormatter = null) { AddDbContextResolvers(assembly); - AddAssemblyResources(assembly, resourceNameFormatter); - AddAssemblyServices(assembly); - AddAssemblyRepositories(assembly); + + var resourceDescriptors = TypeLocator.GetIdentifableTypes(assembly); + foreach (var resourceDescriptor in resourceDescriptors) + { + AddResource(assembly, resourceDescriptor, resourceNameFormatter); + AddServices(assembly, resourceDescriptor); + AddRepositories(assembly, resourceDescriptor); + } return this; } @@ -59,18 +94,21 @@ private void AddDbContextResolvers(Assembly assembly) /// /// The assembly to search for resources in. /// The type name formatter used to get the string representation of resource names. - public ServiceDiscoveryFacade AddAssemblyResources(Assembly assembly, IResourceNameFormatter resourceNameFormatter = null) + public ServiceDiscoveryFacade AddResources(Assembly assembly, IResourceNameFormatter resourceNameFormatter = null) { var identifiables = TypeLocator.GetIdentifableTypes(assembly); foreach (var identifiable in identifiables) - { - RegisterResourceDefinition(assembly, identifiable); - AddResourceToGraph(identifiable, resourceNameFormatter); - } + AddResource(assembly, identifiable, resourceNameFormatter); return this; } + private void AddResource(Assembly assembly, ResourceDescriptor resourceDescriptor, IResourceNameFormatter resourceNameFormatter = null) + { + RegisterResourceDefinition(assembly, resourceDescriptor); + AddResourceToGraph(resourceDescriptor, resourceNameFormatter); + } + private void RegisterResourceDefinition(Assembly assembly, ResourceDescriptor identifiable) { try @@ -83,9 +121,7 @@ private void RegisterResourceDefinition(Assembly assembly, ResourceDescriptor id } catch (InvalidOperationException e) { - // TODO: need a better way to communicate failure since this is unlikely to occur during a web request - throw new JsonApiException(500, - $"Cannot define multiple ResourceDefinition<> implementations for '{identifiable.ResourceType}'", e); + throw new JsonApiSetupException($"Cannot define multiple ResourceDefinition<> implementations for '{identifiable.ResourceType}'", e); } } @@ -105,61 +141,45 @@ private string FormatResourceName(Type resourceType, IResourceNameFormatter reso /// Add implementations to container. /// /// The assembly to search for resources in. - public ServiceDiscoveryFacade AddAssemblyServices(Assembly assembly) + public ServiceDiscoveryFacade AddServices(Assembly assembly) { - RegisterServiceImplementations(assembly, typeof(IResourceService<>)); - RegisterServiceImplementations(assembly, typeof(IResourceService<,>)); - - RegisterServiceImplementations(assembly, typeof(ICreateService<>)); - RegisterServiceImplementations(assembly, typeof(ICreateService<,>)); - - RegisterServiceImplementations(assembly, typeof(IGetAllService<>)); - RegisterServiceImplementations(assembly, typeof(IGetAllService<,>)); - - RegisterServiceImplementations(assembly, typeof(IGetByIdService<>)); - RegisterServiceImplementations(assembly, typeof(IGetByIdService<,>)); - - RegisterServiceImplementations(assembly, typeof(IGetRelationshipService<>)); - RegisterServiceImplementations(assembly, typeof(IGetRelationshipService<,>)); - - RegisterServiceImplementations(assembly, typeof(IUpdateService<>)); - RegisterServiceImplementations(assembly, typeof(IUpdateService<,>)); - - RegisterServiceImplementations(assembly, typeof(IDeleteService<>)); - RegisterServiceImplementations(assembly, typeof(IDeleteService<,>)); + var resourceDescriptors = TypeLocator.GetIdentifableTypes(assembly); + foreach (var resourceDescriptor in resourceDescriptors) + AddServices(assembly, resourceDescriptor); return this; } + private void AddServices(Assembly assembly, ResourceDescriptor resourceDescriptor) + { + foreach(var serviceInterface in ServiceInterfaces) + RegisterServiceImplementations(assembly, serviceInterface, resourceDescriptor); + } + /// /// Add implementations to container. /// /// The assembly to search for resources in. - public ServiceDiscoveryFacade AddAssemblyRepositories(Assembly assembly) + public ServiceDiscoveryFacade AddRepositories(Assembly assembly) { - RegisterServiceImplementations(assembly, typeof(IEntityRepository<>)); - RegisterServiceImplementations(assembly, typeof(IEntityRepository<,>)); - - RegisterServiceImplementations(assembly, typeof(IEntityWriteRepository<>)); - RegisterServiceImplementations(assembly, typeof(IEntityWriteRepository<,>)); - - RegisterServiceImplementations(assembly, typeof(IEntityReadRepository<>)); - RegisterServiceImplementations(assembly, typeof(IEntityReadRepository<,>)); + var resourceDescriptors = TypeLocator.GetIdentifableTypes(assembly); + foreach (var resourceDescriptor in resourceDescriptors) + AddRepositories(assembly, resourceDescriptor); return this; } - private ServiceDiscoveryFacade RegisterServiceImplementations(Assembly assembly, Type interfaceType) + private void AddRepositories(Assembly assembly, ResourceDescriptor resourceDescriptor) { - var identifiables = TypeLocator.GetIdentifableTypes(assembly); - foreach (var identifiable in identifiables) - { - var service = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, identifiable.ResourceType, identifiable.IdType); - if (service.implementation != null) - _services.AddScoped(service.registrationInterface, service.implementation); - } + foreach(var serviceInterface in RepositoryInterfaces) + RegisterServiceImplementations(assembly, serviceInterface, resourceDescriptor); + } - return this; + private void RegisterServiceImplementations(Assembly assembly, Type interfaceType, ResourceDescriptor resourceDescriptor) + { + var service = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, resourceDescriptor.ResourceType, resourceDescriptor.IdType); + if (service.implementation != null) + _services.AddScoped(service.registrationInterface, service.implementation); } } } diff --git a/src/JsonApiDotNetCore/Graph/TypeLocator.cs b/src/JsonApiDotNetCore/Graph/TypeLocator.cs index 223845859a..f96e17ffe0 100644 --- a/src/JsonApiDotNetCore/Graph/TypeLocator.cs +++ b/src/JsonApiDotNetCore/Graph/TypeLocator.cs @@ -46,24 +46,44 @@ private static Type[] GetAssemblyTypes(Assembly assembly) } /// - /// Get all implementations of . in the assembly + /// Get all implementations of in the assembly /// - public static List GetIdentifableTypes(Assembly assembly) + public static IEnumerable GetIdentifableTypes(Assembly assembly) + => (_identifiableTypeCache.TryGetValue(assembly, out var descriptors) == false) + ? FindIdentifableTypes(assembly) + : _identifiableTypeCache[assembly]; + + private static IEnumerable FindIdentifableTypes(Assembly assembly) { - if (_identifiableTypeCache.TryGetValue(assembly, out var descriptors) == false) - { - descriptors = new List(); - _identifiableTypeCache[assembly] = descriptors; + var descriptors = new List(); + _identifiableTypeCache[assembly] = descriptors; - foreach (var type in assembly.GetTypes()) + foreach (var type in assembly.GetTypes()) + { + if (TryGetResourceDescriptor(type, out var descriptor)) { - var possible = GetIdType(type); - if (possible.isJsonApiResource) - descriptors.Add(new ResourceDescriptor(type, possible.idType)); + descriptors.Add(descriptor); + yield return descriptor; } } + } - return descriptors; + /// + /// Attempts to get a descriptor of the resource type. + /// + /// + /// True if the type is a valid json:api type (must implement ), false otherwise. + /// + internal static bool TryGetResourceDescriptor(Type type, out ResourceDescriptor descriptor) + { + var possible = GetIdType(type); + if (possible.isJsonApiResource) { + descriptor = new ResourceDescriptor(type, possible.idType); + return true; + } + + descriptor = ResourceDescriptor.Empty; + return false; } /// diff --git a/src/JsonApiDotNetCore/Internal/JsonApiSetupException.cs b/src/JsonApiDotNetCore/Internal/JsonApiSetupException.cs new file mode 100644 index 0000000000..34765066be --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/JsonApiSetupException.cs @@ -0,0 +1,13 @@ +using System; + +namespace JsonApiDotNetCore.Internal +{ + public class JsonApiSetupException : Exception + { + public JsonApiSetupException(string message) + : base(message) { } + + public JsonApiSetupException(string message, Exception innerException) + : base(message, innerException) { } + } +} \ No newline at end of file diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index 1b00c5aaa1..0dc7808b24 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -13,6 +13,10 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; using Microsoft.EntityFrameworkCore; +using JsonApiDotNetCore.Models; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; namespace UnitTests.Extensions { @@ -51,5 +55,88 @@ public void AddJsonApiInternals_Adds_All_Required_Services() Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService(typeof(GenericProcessor))); } + + [Fact] + public void AddResourceService_Registers_All_Shorthand_Service_Interfaces() + { + // arrange + var services = new ServiceCollection(); + + // act + services.AddResourceService(); + + // assert + var provider = services.BuildServiceProvider(); + Assert.IsType(typeof(IResourceService)); + Assert.IsType(typeof(IResourceCmdService)); + Assert.IsType(typeof(IResourceQueryService)); + Assert.IsType(typeof(IGetAllService)); + Assert.IsType(typeof(IGetByIdService)); + Assert.IsType(typeof(IGetRelationshipService)); + Assert.IsType(typeof(IGetRelationshipsService)); + Assert.IsType(typeof(ICreateService)); + Assert.IsType(typeof(IUpdateService)); + Assert.IsType(typeof(IDeleteService)); + } + + [Fact] + public void AddResourceService_Registers_All_LongForm_Service_Interfaces() + { + // arrange + var services = new ServiceCollection(); + + // act + services.AddResourceService(); + + // assert + var provider = services.BuildServiceProvider(); + Assert.IsType(typeof(IResourceService)); + Assert.IsType(typeof(IResourceCmdService)); + Assert.IsType(typeof(IResourceQueryService)); + Assert.IsType(typeof(IGetAllService)); + Assert.IsType(typeof(IGetByIdService)); + Assert.IsType(typeof(IGetRelationshipService)); + Assert.IsType(typeof(IGetRelationshipsService)); + Assert.IsType(typeof(ICreateService)); + Assert.IsType(typeof(IUpdateService)); + Assert.IsType(typeof(IDeleteService)); + } + + [Fact] + public void AddResourceService_Throws_If_Type_Does_Not_Implement_Any_Interfaces() + { + // arrange + var services = new ServiceCollection(); + + // act, assert + Assert.Throws(() => services.AddResourceService()); + } + + private class IntResource : Identifiable { } + private class GuidResource : Identifiable { } + + private class IntResourceService : IResourceService + { + public Task CreateAsync(IntResource entity) => throw new NotImplementedException(); + public Task DeleteAsync(int id) => throw new NotImplementedException(); + public Task> GetAsync() => throw new NotImplementedException(); + public Task GetAsync(int id) => throw new NotImplementedException(); + public Task GetRelationshipAsync(int id, string relationshipName) => throw new NotImplementedException(); + public Task GetRelationshipsAsync(int id, string relationshipName) => throw new NotImplementedException(); + public Task UpdateAsync(int id, IntResource entity) => throw new NotImplementedException(); + public Task UpdateRelationshipsAsync(int id, string relationshipName, List relationships) => throw new NotImplementedException(); + } + + private class GuidResourceService : IResourceService + { + public Task CreateAsync(GuidResource entity) => throw new NotImplementedException(); + public Task DeleteAsync(Guid id) => throw new NotImplementedException(); + public Task> GetAsync() => throw new NotImplementedException(); + public Task GetAsync(Guid id) => throw new NotImplementedException(); + public Task GetRelationshipAsync(Guid id, string relationshipName) => throw new NotImplementedException(); + public Task GetRelationshipsAsync(Guid id, string relationshipName) => throw new NotImplementedException(); + public Task UpdateAsync(Guid id, GuidResource entity) => throw new NotImplementedException(); + public Task UpdateRelationshipsAsync(Guid id, string relationshipName, List relationships) => throw new NotImplementedException(); + } } } From 550dcdd91a7ec36605f1933d91ba49e5273f1849 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sat, 22 Sep 2018 20:21:24 -0700 Subject: [PATCH 02/12] add getting started project --- JsonApiDotnetCore.sln | 17 ++++++++- src/Examples/GettingStarted/.gitignore | 1 + .../Controllers/ArticlesController.cs | 17 +++++++++ .../Controllers/PeopleController.cs | 17 +++++++++ .../GettingStarted/Data/SampleDbContext.cs | 15 ++++++++ .../GettingStarted/GettingStarted.csproj | 21 ++++++++++ src/Examples/GettingStarted/Models/Article.cs | 14 +++++++ src/Examples/GettingStarted/Models/Person.cs | 14 +++++++ src/Examples/GettingStarted/Program.cs | 26 +++++++++++++ src/Examples/GettingStarted/README.md | 14 +++++++ src/Examples/GettingStarted/Startup.cs | 38 +++++++++++++++++++ .../IServiceCollectionExtensions.cs | 3 +- 12 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 src/Examples/GettingStarted/.gitignore create mode 100644 src/Examples/GettingStarted/Controllers/ArticlesController.cs create mode 100644 src/Examples/GettingStarted/Controllers/PeopleController.cs create mode 100644 src/Examples/GettingStarted/Data/SampleDbContext.cs create mode 100644 src/Examples/GettingStarted/GettingStarted.csproj create mode 100644 src/Examples/GettingStarted/Models/Article.cs create mode 100644 src/Examples/GettingStarted/Models/Person.cs create mode 100644 src/Examples/GettingStarted/Program.cs create mode 100644 src/Examples/GettingStarted/README.md create mode 100644 src/Examples/GettingStarted/Startup.cs diff --git a/JsonApiDotnetCore.sln b/JsonApiDotnetCore.sln index ce0219b1c8..bc0c4c5ec6 100644 --- a/JsonApiDotnetCore.sln +++ b/JsonApiDotnetCore.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27130.2010 MinimumVisualStudioVersion = 10.0.40219.1 @@ -45,6 +45,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResourceEntitySeparationExa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResourceEntitySeparationExampleTests", "test\ResourceEntitySeparationExampleTests\ResourceEntitySeparationExampleTests.csproj", "{6DFA30D7-1679-4333-9779-6FB678E48EF5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -187,6 +189,18 @@ Global {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Release|x64.Build.0 = Release|Any CPU {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Release|x86.ActiveCfg = Release|Any CPU {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Release|x86.Build.0 = Release|Any CPU + {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Debug|x64.ActiveCfg = Debug|Any CPU + {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Debug|x64.Build.0 = Debug|Any CPU + {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Debug|x86.ActiveCfg = Debug|Any CPU + {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Debug|x86.Build.0 = Debug|Any CPU + {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|Any CPU.Build.0 = Release|Any CPU + {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|x64.ActiveCfg = Release|Any CPU + {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|x64.Build.0 = Release|Any CPU + {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|x86.ActiveCfg = Release|Any CPU + {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -205,6 +219,7 @@ Global {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} {F4097194-9415-418A-AB4E-315C5D5466AF} = {026FBC6C-AF76-4568-9B87-EC73457899FD} {6DFA30D7-1679-4333-9779-6FB678E48EF5} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} + {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71} = {026FBC6C-AF76-4568-9B87-EC73457899FD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4} diff --git a/src/Examples/GettingStarted/.gitignore b/src/Examples/GettingStarted/.gitignore new file mode 100644 index 0000000000..3997beadf8 --- /dev/null +++ b/src/Examples/GettingStarted/.gitignore @@ -0,0 +1 @@ +*.db \ No newline at end of file diff --git a/src/Examples/GettingStarted/Controllers/ArticlesController.cs b/src/Examples/GettingStarted/Controllers/ArticlesController.cs new file mode 100644 index 0000000000..248f5ffcb3 --- /dev/null +++ b/src/Examples/GettingStarted/Controllers/ArticlesController.cs @@ -0,0 +1,17 @@ +using GettingStarted.Models; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace GettingStarted +{ + public class ArticlesController : JsonApiController
+ { + public ArticlesController( + IJsonApiContext jsonApiContext, + IResourceService
resourceService, + ILoggerFactory loggerFactory) + : base(jsonApiContext, resourceService, loggerFactory) + { } + } +} \ No newline at end of file diff --git a/src/Examples/GettingStarted/Controllers/PeopleController.cs b/src/Examples/GettingStarted/Controllers/PeopleController.cs new file mode 100644 index 0000000000..b33f89effb --- /dev/null +++ b/src/Examples/GettingStarted/Controllers/PeopleController.cs @@ -0,0 +1,17 @@ +using GettingStarted.Models; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace GettingStarted +{ + public class PeopleController : JsonApiController + { + public PeopleController( + IJsonApiContext jsonApiContext, + IResourceService resourceService, + ILoggerFactory loggerFactory) + : base(jsonApiContext, resourceService, loggerFactory) + { } + } +} \ No newline at end of file diff --git a/src/Examples/GettingStarted/Data/SampleDbContext.cs b/src/Examples/GettingStarted/Data/SampleDbContext.cs new file mode 100644 index 0000000000..770999c16a --- /dev/null +++ b/src/Examples/GettingStarted/Data/SampleDbContext.cs @@ -0,0 +1,15 @@ +using GettingStarted.Models; +using Microsoft.EntityFrameworkCore; + +namespace GettingStarted +{ + public class SampleDbContext : DbContext + { + public SampleDbContext(DbContextOptions options) + : base(options) + { } + + public DbSet
Articles { get; set; } + public DbSet People { get; set; } + } +} \ No newline at end of file diff --git a/src/Examples/GettingStarted/GettingStarted.csproj b/src/Examples/GettingStarted/GettingStarted.csproj new file mode 100644 index 0000000000..6e00daefae --- /dev/null +++ b/src/Examples/GettingStarted/GettingStarted.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + diff --git a/src/Examples/GettingStarted/Models/Article.cs b/src/Examples/GettingStarted/Models/Article.cs new file mode 100644 index 0000000000..6835235be8 --- /dev/null +++ b/src/Examples/GettingStarted/Models/Article.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.Models; + +namespace GettingStarted.Models +{ + public class Article : Identifiable + { + [Attr("title")] + public string Title { get; set; } + + [HasOne("author")] + public Person Author { get; set; } + public int AuthorId { get; set; } + } +} \ No newline at end of file diff --git a/src/Examples/GettingStarted/Models/Person.cs b/src/Examples/GettingStarted/Models/Person.cs new file mode 100644 index 0000000000..1d5c79de8e --- /dev/null +++ b/src/Examples/GettingStarted/Models/Person.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace GettingStarted.Models +{ + public class Person : Identifiable + { + [Attr("name")] + public string Name { get; set; } + + [HasMany("articles")] + public List
Articles { get; set; } + } +} \ No newline at end of file diff --git a/src/Examples/GettingStarted/Program.cs b/src/Examples/GettingStarted/Program.cs new file mode 100644 index 0000000000..fdc5046542 --- /dev/null +++ b/src/Examples/GettingStarted/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace GettingStarted +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .UseUrls("http://localhost:5001") + .Build(); + } +} diff --git a/src/Examples/GettingStarted/README.md b/src/Examples/GettingStarted/README.md new file mode 100644 index 0000000000..d2c91c6d6a --- /dev/null +++ b/src/Examples/GettingStarted/README.md @@ -0,0 +1,14 @@ +## Sample project + +## Usage + +`dotnet run` to run the project + +You can verify the project is running by checking this endpoint: +`localhost:5001/api/sample-model` + +For further documentation and implementation of a JsonApiDotnetCore Application see the documentation or GitHub page: + +Repository: https://github.com/json-api-dotnet/JsonApiDotNetCore + +Documentation: https://json-api-dotnet.github.io/ \ No newline at end of file diff --git a/src/Examples/GettingStarted/Startup.cs b/src/Examples/GettingStarted/Startup.cs new file mode 100644 index 0000000000..5d0fa8dc91 --- /dev/null +++ b/src/Examples/GettingStarted/Startup.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore; +using JsonApiDotNetCore.Extensions; + +namespace GettingStarted +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddDbContext(options => + { + options.UseSqlite("Data Source=sample.db"); + }); + + var mvcCoreBuilder = services.AddMvcCore(); + services.AddJsonApi( + options => options.Namespace = "api", + mvcCoreBuilder, + discover => discover.AddCurrentAssembly()); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env, SampleDbContext context) + { + context.Database.EnsureDeleted(); // indicies need to be reset + context.Database.EnsureCreated(); + + app.UseJsonApi(); + } + } +} diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 9c1df0626e..33d91ffe96 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Builders; @@ -45,9 +46,7 @@ public static IServiceCollection AddJsonApi( IMvcCoreBuilder mvcBuilder) where TContext : DbContext { var config = new JsonApiOptions(); - options(config); - config.BuildContextGraph(builder => builder.AddDbContext()); mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, config)); From b0ab4dd159582f7197619de32b59241deccb0381 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sat, 22 Sep 2018 21:07:41 -0700 Subject: [PATCH 03/12] optional attribute and relationship names --- .../Controllers/ArticlesController.cs | 6 ++-- .../Controllers/PeopleController.cs | 6 ++-- src/Examples/GettingStarted/Models/Article.cs | 4 +-- src/Examples/GettingStarted/Models/Person.cs | 4 +-- .../Builders/ContextGraphBuilder.cs | 7 +++-- .../Configuration/JsonApiOptions.cs | 6 ++++ .../Graph/IResourceNameFormatter.cs | 22 ++++++++++++++ .../Graph/ServiceDiscoveryFacade.cs | 30 ++++++++----------- src/JsonApiDotNetCore/Models/AttrAttribute.cs | 6 ++-- .../Models/HasManyAttribute.cs | 2 +- .../Models/HasOneAttribute.cs | 2 +- .../Models/RelationshipAttribute.cs | 2 +- 12 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/Examples/GettingStarted/Controllers/ArticlesController.cs b/src/Examples/GettingStarted/Controllers/ArticlesController.cs index 248f5ffcb3..53517540b1 100644 --- a/src/Examples/GettingStarted/Controllers/ArticlesController.cs +++ b/src/Examples/GettingStarted/Controllers/ArticlesController.cs @@ -1,7 +1,6 @@ using GettingStarted.Models; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; namespace GettingStarted { @@ -9,9 +8,8 @@ public class ArticlesController : JsonApiController
{ public ArticlesController( IJsonApiContext jsonApiContext, - IResourceService
resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + IResourceService
resourceService) + : base(jsonApiContext, resourceService) { } } } \ No newline at end of file diff --git a/src/Examples/GettingStarted/Controllers/PeopleController.cs b/src/Examples/GettingStarted/Controllers/PeopleController.cs index b33f89effb..f3c0c4b868 100644 --- a/src/Examples/GettingStarted/Controllers/PeopleController.cs +++ b/src/Examples/GettingStarted/Controllers/PeopleController.cs @@ -1,7 +1,6 @@ using GettingStarted.Models; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; namespace GettingStarted { @@ -9,9 +8,8 @@ public class PeopleController : JsonApiController { public PeopleController( IJsonApiContext jsonApiContext, - IResourceService resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + IResourceService resourceService) + : base(jsonApiContext, resourceService) { } } } \ No newline at end of file diff --git a/src/Examples/GettingStarted/Models/Article.cs b/src/Examples/GettingStarted/Models/Article.cs index 6835235be8..68cecf060d 100644 --- a/src/Examples/GettingStarted/Models/Article.cs +++ b/src/Examples/GettingStarted/Models/Article.cs @@ -4,10 +4,10 @@ namespace GettingStarted.Models { public class Article : Identifiable { - [Attr("title")] + [Attr] public string Title { get; set; } - [HasOne("author")] + [HasOne] public Person Author { get; set; } public int AuthorId { get; set; } } diff --git a/src/Examples/GettingStarted/Models/Person.cs b/src/Examples/GettingStarted/Models/Person.cs index 1d5c79de8e..625cf26ab6 100644 --- a/src/Examples/GettingStarted/Models/Person.cs +++ b/src/Examples/GettingStarted/Models/Person.cs @@ -5,10 +5,10 @@ namespace GettingStarted.Models { public class Person : Identifiable { - [Attr("name")] + [Attr] public string Name { get; set; } - [HasMany("articles")] + [HasMany] public List
Articles { get; set; } } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs index ba71dbce06..7d2c6e0446 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; @@ -64,9 +65,8 @@ public class ContextGraphBuilder : IContextGraphBuilder { private List _entities = new List(); private List _validationResults = new List(); - private bool _usesDbContext; - private IResourceNameFormatter _resourceNameFormatter = new DefaultResourceNameFormatter(); + private IResourceNameFormatter _resourceNameFormatter = JsonApiOptions.ResourceNameFormatter; public Link DocumentLinks { get; set; } = Link.All; @@ -128,6 +128,7 @@ protected virtual List GetAttributes(Type entityType) if (attribute == null) continue; + attribute.PublicAttributeName = attribute.PublicAttributeName ?? JsonApiOptions.ResourceNameFormatter.FormatPropertyName(prop); attribute.InternalAttributeName = prop.Name; attribute.PropertyInfo = prop; @@ -146,6 +147,8 @@ protected virtual List GetRelationships(Type entityType) { var attribute = (RelationshipAttribute)prop.GetCustomAttribute(typeof(RelationshipAttribute)); if (attribute == null) continue; + + attribute.PublicRelationshipName = attribute.PublicRelationshipName ?? JsonApiOptions.ResourceNameFormatter.FormatPropertyName(prop); attribute.InternalRelationshipName = prop.Name; attribute.Type = GetRelationshipType(attribute, prop); attributes.Add(attribute); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 54fcf5afaf..66386aac19 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; @@ -16,6 +17,11 @@ namespace JsonApiDotNetCore.Configuration /// public class JsonApiOptions { + /// + /// Provides an interface for formatting resource names by convention + /// + public static IResourceNameFormatter ResourceNameFormatter { get; set; } = new DefaultResourceNameFormatter(); + /// /// Whether or not stack traces should be serialized in Error objects /// diff --git a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs index 57baca0901..8f5bdf3bb2 100644 --- a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs +++ b/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs @@ -17,6 +17,11 @@ public interface IResourceNameFormatter /// Get the publicly visible resource name from the internal type name /// string FormatResourceName(Type resourceType); + + /// + /// Get the publicly visible name for the given property + /// + string FormatPropertyName(PropertyInfo property); } public class DefaultResourceNameFormatter : IResourceNameFormatter @@ -47,5 +52,22 @@ public string FormatResourceName(Type type) throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", e); } } + + /// + /// Uses the internal PropertyInfo to determine the external resource name. + /// By default the name will be formatted to kebab-case. + /// + /// + /// Given the following property: + /// + /// public string CompoundProperty { get; set; } + /// + /// The public attribute will be formatted like so: + /// + /// _default.FormatPropertyName(compoundProperty).Dump(); + /// // > "compound-property" + /// + /// + public string FormatPropertyName(PropertyInfo property) => str.Dasherize(property.Name); } } diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index f403f69124..52040bc563 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -1,4 +1,5 @@ using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; @@ -55,23 +56,20 @@ public ServiceDiscoveryFacade( /// /// Add resources, services and repository implementations to the container. /// - /// The type name formatter used to get the string representation of resource names. - public ServiceDiscoveryFacade AddCurrentAssembly(IResourceNameFormatter resourceNameFormatter = null) - => AddAssembly(Assembly.GetCallingAssembly(), resourceNameFormatter); + public ServiceDiscoveryFacade AddCurrentAssembly() => AddAssembly(Assembly.GetCallingAssembly()); /// /// Add resources, services and repository implementations to the container. /// /// The assembly to search for resources in. - /// The type name formatter used to get the string representation of resource names. - public ServiceDiscoveryFacade AddAssembly(Assembly assembly, IResourceNameFormatter resourceNameFormatter = null) + public ServiceDiscoveryFacade AddAssembly(Assembly assembly) { AddDbContextResolvers(assembly); var resourceDescriptors = TypeLocator.GetIdentifableTypes(assembly); foreach (var resourceDescriptor in resourceDescriptors) { - AddResource(assembly, resourceDescriptor, resourceNameFormatter); + AddResource(assembly, resourceDescriptor); AddServices(assembly, resourceDescriptor); AddRepositories(assembly, resourceDescriptor); } @@ -93,20 +91,19 @@ private void AddDbContextResolvers(Assembly assembly) /// Adds resources to the graph and registers types on the container. /// /// The assembly to search for resources in. - /// The type name formatter used to get the string representation of resource names. - public ServiceDiscoveryFacade AddResources(Assembly assembly, IResourceNameFormatter resourceNameFormatter = null) + public ServiceDiscoveryFacade AddResources(Assembly assembly) { var identifiables = TypeLocator.GetIdentifableTypes(assembly); foreach (var identifiable in identifiables) - AddResource(assembly, identifiable, resourceNameFormatter); + AddResource(assembly, identifiable); return this; } - private void AddResource(Assembly assembly, ResourceDescriptor resourceDescriptor, IResourceNameFormatter resourceNameFormatter = null) + private void AddResource(Assembly assembly, ResourceDescriptor resourceDescriptor) { RegisterResourceDefinition(assembly, resourceDescriptor); - AddResourceToGraph(resourceDescriptor, resourceNameFormatter); + AddResourceToGraph(resourceDescriptor); } private void RegisterResourceDefinition(Assembly assembly, ResourceDescriptor identifiable) @@ -125,17 +122,14 @@ private void RegisterResourceDefinition(Assembly assembly, ResourceDescriptor id } } - private void AddResourceToGraph(ResourceDescriptor identifiable, IResourceNameFormatter resourceNameFormatter = null) + private void AddResourceToGraph(ResourceDescriptor identifiable) { - var resourceName = FormatResourceName(identifiable.ResourceType, resourceNameFormatter); + var resourceName = FormatResourceName(identifiable.ResourceType); _graphBuilder.AddResource(identifiable.ResourceType, identifiable.IdType, resourceName); } - private string FormatResourceName(Type resourceType, IResourceNameFormatter resourceNameFormatter) - { - resourceNameFormatter = resourceNameFormatter ?? new DefaultResourceNameFormatter(); - return resourceNameFormatter.FormatResourceName(resourceType); - } + private string FormatResourceName(Type resourceType) + => JsonApiOptions.ResourceNameFormatter.FormatResourceName(resourceType); /// /// Add implementations to container. diff --git a/src/JsonApiDotNetCore/Models/AttrAttribute.cs b/src/JsonApiDotNetCore/Models/AttrAttribute.cs index d5a30221bb..a5e594ea0c 100644 --- a/src/JsonApiDotNetCore/Models/AttrAttribute.cs +++ b/src/JsonApiDotNetCore/Models/AttrAttribute.cs @@ -26,7 +26,7 @@ public class AttrAttribute : Attribute /// /// /// - public AttrAttribute(string publicName, bool isImmutable = false, bool isFilterable = true, bool isSortable = true) + public AttrAttribute(string publicName = null, bool isImmutable = false, bool isFilterable = true, bool isSortable = true) { PublicAttributeName = publicName; IsImmutable = isImmutable; @@ -34,7 +34,7 @@ public AttrAttribute(string publicName, bool isImmutable = false, bool isFiltera IsSortable = isSortable; } - public AttrAttribute(string publicName, string internalName, bool isImmutable = false) + internal AttrAttribute(string publicName, string internalName, bool isImmutable = false) { PublicAttributeName = publicName; InternalAttributeName = internalName; @@ -44,7 +44,7 @@ public AttrAttribute(string publicName, string internalName, bool isImmutable = /// /// How this attribute is exposed through the API /// - public string PublicAttributeName { get; } + public string PublicAttributeName { get; internal set;} /// /// The internal property name this attribute belongs to. diff --git a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs index 5bbea86783..11479819f4 100644 --- a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs @@ -23,7 +23,7 @@ public class HasManyAttribute : RelationshipAttribute /// /// /// - public HasManyAttribute(string publicName, Link documentLinks = Link.All, bool canInclude = true) + public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true) : base(publicName, documentLinks, canInclude) { } diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index a80734817d..2d83c3dd69 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -26,7 +26,7 @@ public class HasOneAttribute : RelationshipAttribute /// /// /// - public HasOneAttribute(string publicName, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null) + public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null) : base(publicName, documentLinks, canInclude) { _explicitIdentifiablePropertyName = withForeignKey; diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index 7408df0998..b479d3bb12 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -11,7 +11,7 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI CanInclude = canInclude; } - public string PublicRelationshipName { get; } + public string PublicRelationshipName { get; internal set; } public string InternalRelationshipName { get; internal set; } /// From f740f25d5bb3c15820ff84cfa4d523498f2afc7c Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sat, 22 Sep 2018 21:09:43 -0700 Subject: [PATCH 04/12] build getting started in build.ps1 --- Build.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Build.ps1 b/Build.ps1 index 8ff62d6578..ee058a412d 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -23,6 +23,9 @@ $revision = "{0:D4}" -f [convert]::ToInt32($revision, 10) dotnet restore +dotnet build ./src/Examples/GettingStarted/GettingStarted.csproj +CheckLastExitCode + dotnet test ./test/UnitTests/UnitTests.csproj CheckLastExitCode @@ -35,7 +38,7 @@ CheckLastExitCode dotnet test ./test/OperationsExampleTests/OperationsExampleTests.csproj CheckLastExitCode -dotnet build .\src\JsonApiDotNetCore -c Release +dotnet build .src\JsonApiDotNetCore -c Release CheckLastExitCode Write-Output "APPVEYOR_REPO_TAG: $env:APPVEYOR_REPO_TAG" From 66470a94b4cb67a673a6d0d6edcfceb6e8b4e76d Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sat, 22 Sep 2018 21:12:30 -0700 Subject: [PATCH 05/12] fix build script --- Build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build.ps1 b/Build.ps1 index ee058a412d..94140b05c2 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -38,7 +38,7 @@ CheckLastExitCode dotnet test ./test/OperationsExampleTests/OperationsExampleTests.csproj CheckLastExitCode -dotnet build .src\JsonApiDotNetCore -c Release +dotnet build ./src/JsonApiDotNetCore/JsonApiDotNetCore.csproj -c Release CheckLastExitCode Write-Output "APPVEYOR_REPO_TAG: $env:APPVEYOR_REPO_TAG" From 529a56286e10f946c7c8d0c47f46b72b6a9c82a7 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sun, 23 Sep 2018 14:36:14 -0700 Subject: [PATCH 06/12] add DiscoveryTests --- Build.ps1 | 3 + JsonApiDotnetCore.sln | 15 ++++ .../GettingStarted/Data/SampleDbContext.cs | 2 + .../ResourceDefinitionExample/Model.cs | 10 +++ .../ModelDefinition.cs | 16 ++++ .../ModelsController.cs | 15 ++++ .../Data/DefaultEntityRepository.cs | 18 +++- .../Graph/ServiceDiscoveryFacade.cs | 6 +- .../Models/ResourceDefinition.cs | 16 +++- .../Services/EntityResourceService.cs | 8 +- test/DiscoveryTests/DiscoveryTests.csproj | 19 ++++ .../ServiceDiscoveryFacadeTests.cs | 89 +++++++++++++++++++ 12 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 src/Examples/GettingStarted/ResourceDefinitionExample/Model.cs create mode 100644 src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs create mode 100644 src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs create mode 100644 test/DiscoveryTests/DiscoveryTests.csproj create mode 100644 test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs diff --git a/Build.ps1 b/Build.ps1 index 94140b05c2..9e1f94b2de 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -38,6 +38,9 @@ CheckLastExitCode dotnet test ./test/OperationsExampleTests/OperationsExampleTests.csproj CheckLastExitCode +dotnet test ./test/DiscoveryTests/DiscoveryTests.csproj +CheckLastExitCode + dotnet build ./src/JsonApiDotNetCore/JsonApiDotNetCore.csproj -c Release CheckLastExitCode diff --git a/JsonApiDotnetCore.sln b/JsonApiDotnetCore.sln index bc0c4c5ec6..d046b23819 100644 --- a/JsonApiDotnetCore.sln +++ b/JsonApiDotnetCore.sln @@ -47,6 +47,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResourceEntitySeparationExa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscoveryTests", "test\DiscoveryTests\DiscoveryTests.csproj", "{09C0C8D8-B721-4955-8889-55CB149C3B5C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -201,6 +203,18 @@ Global {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|x64.Build.0 = Release|Any CPU {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|x86.ActiveCfg = Release|Any CPU {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|x86.Build.0 = Release|Any CPU + {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|x64.ActiveCfg = Debug|Any CPU + {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|x64.Build.0 = Debug|Any CPU + {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|x86.ActiveCfg = Debug|Any CPU + {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|x86.Build.0 = Debug|Any CPU + {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|Any CPU.Build.0 = Release|Any CPU + {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x64.ActiveCfg = Release|Any CPU + {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x64.Build.0 = Release|Any CPU + {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x86.ActiveCfg = Release|Any CPU + {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -220,6 +234,7 @@ Global {F4097194-9415-418A-AB4E-315C5D5466AF} = {026FBC6C-AF76-4568-9B87-EC73457899FD} {6DFA30D7-1679-4333-9779-6FB678E48EF5} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} {9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71} = {026FBC6C-AF76-4568-9B87-EC73457899FD} + {09C0C8D8-B721-4955-8889-55CB149C3B5C} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4} diff --git a/src/Examples/GettingStarted/Data/SampleDbContext.cs b/src/Examples/GettingStarted/Data/SampleDbContext.cs index 770999c16a..2f8fefb405 100644 --- a/src/Examples/GettingStarted/Data/SampleDbContext.cs +++ b/src/Examples/GettingStarted/Data/SampleDbContext.cs @@ -1,4 +1,5 @@ using GettingStarted.Models; +using GettingStarted.ResourceDefinitionExample; using Microsoft.EntityFrameworkCore; namespace GettingStarted @@ -11,5 +12,6 @@ public SampleDbContext(DbContextOptions options) public DbSet
Articles { get; set; } public DbSet People { get; set; } + public DbSet Models { get; set; } } } \ No newline at end of file diff --git a/src/Examples/GettingStarted/ResourceDefinitionExample/Model.cs b/src/Examples/GettingStarted/ResourceDefinitionExample/Model.cs new file mode 100644 index 0000000000..0bee86efe0 --- /dev/null +++ b/src/Examples/GettingStarted/ResourceDefinitionExample/Model.cs @@ -0,0 +1,10 @@ +using JsonApiDotNetCore.Models; + +namespace GettingStarted.ResourceDefinitionExample +{ + public class Model : Identifiable + { + [Attr] + public string DontExpose { get; set; } + } +} \ No newline at end of file diff --git a/src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs new file mode 100644 index 0000000000..fc41350664 --- /dev/null +++ b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace GettingStarted.ResourceDefinitionExample +{ + public class ModelDefinition : ResourceDefinition + { + // this allows POST / PATCH requests to set the value of a + // property, but we don't include this value in the response + // this might be used if the incoming value gets hashed or + // encrypted prior to being persisted and this value should + // never be sent back to the client + protected override List OutputAttrs() + => Remove(model => model.DontExpose); + } +} \ No newline at end of file diff --git a/src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs new file mode 100644 index 0000000000..a14394e830 --- /dev/null +++ b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs @@ -0,0 +1,15 @@ +using GettingStarted.Models; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; + +namespace GettingStarted.ResourceDefinitionExample +{ + public class ModelsController : JsonApiController + { + public ModelsController( + IJsonApiContext jsonApiContext, + IResourceService resourceService) + : base(jsonApiContext, resourceService) + { } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 512f45fe3c..68462ff4df 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -19,6 +19,12 @@ public class DefaultEntityRepository IEntityRepository where TEntity : class, IIdentifiable { + public DefaultEntityRepository( + IJsonApiContext jsonApiContext, + IDbContextResolver contextResolver) + : base(jsonApiContext, contextResolver) + { } + public DefaultEntityRepository( ILoggerFactory loggerFactory, IJsonApiContext jsonApiContext, @@ -42,6 +48,16 @@ public class DefaultEntityRepository private readonly IJsonApiContext _jsonApiContext; private readonly IGenericProcessorFactory _genericProcessorFactory; + public DefaultEntityRepository( + IJsonApiContext jsonApiContext, + IDbContextResolver contextResolver) + { + _context = contextResolver.GetContext(); + _dbSet = contextResolver.GetDbSet(); + _jsonApiContext = jsonApiContext; + _genericProcessorFactory = _jsonApiContext.GenericProcessorFactory; + } + public DefaultEntityRepository( ILoggerFactory loggerFactory, IJsonApiContext jsonApiContext, @@ -84,7 +100,7 @@ public virtual async Task GetAsync(TId id) /// public virtual async Task GetAndIncludeAsync(TId id, string relationshipName) { - _logger.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationshipName})"); + _logger?.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationshipName})"); var includedSet = Include(Get(), relationshipName); var result = await includedSet.SingleOrDefaultAsync(e => e.Id.Equals(id)); diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index 52040bc563..8c6ae19cf0 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -171,7 +171,11 @@ private void AddRepositories(Assembly assembly, ResourceDescriptor resourceDescr private void RegisterServiceImplementations(Assembly assembly, Type interfaceType, ResourceDescriptor resourceDescriptor) { - var service = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, resourceDescriptor.ResourceType, resourceDescriptor.IdType); + var genericArguments = interfaceType.GetTypeInfo().GenericTypeParameters.Length == 2 + ? new [] { resourceDescriptor.ResourceType, resourceDescriptor.IdType } + : new [] { resourceDescriptor.ResourceType }; + + var service = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, genericArguments); if (service.implementation != null) _services.AddScoped(service.registrationInterface, service.implementation); } diff --git a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs index 64ff918116..6033a7b856 100644 --- a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs @@ -64,7 +64,7 @@ protected List Remove(Expression> filter, List(); foreach (var attr in _contextEntity.Attributes) if (newExpression.Members.Any(m => m.Name == attr.InternalAttributeName) == false) - attributes.Add(attr); + attributes.Add(attr); return attributes; } @@ -76,12 +76,24 @@ protected List Remove(Expression> filter, List + /// Allows POST / PATCH requests to set the value of an + /// attribute, but exclude the attribute in the response + /// this might be used if the incoming value gets hashed or + /// encrypted prior to being persisted and this value should + /// never be sent back to the client. + /// /// Called once per filtered resource in request. ///
protected virtual List OutputAttrs() => _contextEntity.Attributes; /// - /// Called for every instance of a resource + /// Allows POST / PATCH requests to set the value of an + /// attribute, but exclude the attribute in the response + /// this might be used if the incoming value gets hashed or + /// encrypted prior to being persisted and this value should + /// never be sent back to the client. + /// + /// Called for every instance of a resource. /// protected virtual List OutputAttrs(T instance) => _contextEntity.Attributes; diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index 1c8dabb74b..1e175bfeb0 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -16,7 +16,7 @@ public class EntityResourceService : EntityResourceService entityRepository, - ILoggerFactory loggerFactory) : + ILoggerFactory loggerFactory = null) : base(jsonApiContext, entityRepository, loggerFactory) { } } @@ -28,7 +28,7 @@ public class EntityResourceService : EntityResourceService entityRepository, - ILoggerFactory loggerFactory) : + ILoggerFactory loggerFactory = null) : base(jsonApiContext, entityRepository, loggerFactory) { } } @@ -46,7 +46,7 @@ public class EntityResourceService : public EntityResourceService( IJsonApiContext jsonApiContext, IEntityRepository entityRepository, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory = null) { // no mapper provided, TResource & TEntity must be the same type if (typeof(TResource) != typeof(TEntity)) @@ -56,7 +56,7 @@ public EntityResourceService( _jsonApiContext = jsonApiContext; _entities = entityRepository; - _logger = loggerFactory.CreateLogger>(); + _logger = loggerFactory?.CreateLogger>(); } public EntityResourceService( diff --git a/test/DiscoveryTests/DiscoveryTests.csproj b/test/DiscoveryTests/DiscoveryTests.csproj new file mode 100644 index 0000000000..2846b365e5 --- /dev/null +++ b/test/DiscoveryTests/DiscoveryTests.csproj @@ -0,0 +1,19 @@ + + + + $(NetCoreAppVersion) + false + + + + + + + + + + + + + + diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs new file mode 100644 index 0000000000..433d23557b --- /dev/null +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -0,0 +1,89 @@ +using System; +using GettingStarted.Models; +using GettingStarted.ResourceDefinitionExample; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Graph; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace DiscoveryTests +{ + public class ServiceDiscoveryFacadeTests + { + private readonly IServiceCollection _services = new ServiceCollection(); + private readonly ContextGraphBuilder _graphBuilder = new ContextGraphBuilder(); + private ServiceDiscoveryFacade _facade => new ServiceDiscoveryFacade(_services, _graphBuilder); + + [Fact] + public void AddAssembly_Adds_All_Resources_To_Graph() + { + // arrange, act + _facade.AddAssembly(typeof(Person).Assembly); + + // assert + var graph = _graphBuilder.Build(); + var personResource = graph.GetContextEntity(typeof(Person)); + var articleResource = graph.GetContextEntity(typeof(Article)); + var modelResource = graph.GetContextEntity(typeof(Model)); + + Assert.NotNull(personResource); + Assert.NotNull(articleResource); + Assert.NotNull(modelResource); + } + + [Fact] + public void AddCurrentAssembly_Adds_Resources_To_Graph() + { + // arrange, act + _facade.AddCurrentAssembly(); + + // assert + var graph = _graphBuilder.Build(); + var testModelResource = graph.GetContextEntity(typeof(TestModel)); + Assert.NotNull(testModelResource); + } + + [Fact] + public void AddCurrentAssembly_Adds_Services_To_Container() + { + // arrange, act + _facade.AddCurrentAssembly(); + + // assert + var services = _services.BuildServiceProvider(); + Assert.IsType(services.GetService>()); + } + + [Fact] + public void AddCurrentAssembly_Adds_Repositories_To_Container() + { + // arrange, act + _facade.AddCurrentAssembly(); + + // assert + var services = _services.BuildServiceProvider(); + Assert.IsType(services.GetService>()); + } + + public class TestModel : Identifiable { } + + public class TestModelService : EntityResourceService + { + private static IEntityRepository _repo = new Mock>().Object; + private static IJsonApiContext _jsonApiContext = new Mock().Object; + public TestModelService() : base(_jsonApiContext, _repo) { } + } + + public class TestModelRepository : DefaultEntityRepository + { + private static IDbContextResolver _dbContextResolver = new Mock().Object; + private static IJsonApiContext _jsonApiContext = new Mock().Object; + public TestModelRepository() : base(_jsonApiContext, _dbContextResolver) { } + } + } +} From 453d4fc704b93c3843946ac7fba891cbf3a40709 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sun, 23 Sep 2018 15:25:17 -0700 Subject: [PATCH 07/12] fix tests --- .../IServiceCollectionExtensions.cs | 25 +++++++---- .../Graph/ServiceDiscoveryFacade.cs | 8 +++- .../IServiceCollectionExtensionsTests.cs | 42 +++++++++---------- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 33d91ffe96..74168c8b0d 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Reflection; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; @@ -186,10 +187,12 @@ public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions js } /// + /// Adds all required registrations for the service to the container /// + /// public static IServiceCollection AddResourceService(this IServiceCollection services) { - var typeImplemenetsAnExpectedInterface = false; + var typeImplementsAnExpectedInterface = false; var serviceImplementationType = typeof(T); @@ -200,19 +203,25 @@ public static IServiceCollection AddResourceService(this IServiceCollection s { foreach(var openGenericType in ServiceDiscoveryFacade.ServiceInterfaces) { - var concreteGenericType = openGenericType.GetGenericArguments().Length == 1 + // A shorthand interface is one where the id type is ommitted + // e.g. IResourceService is the shorthand for IResourceService + var isShorthandInterface = (openGenericType.GetTypeInfo().GenericTypeParameters.Length == 1); + if(isShorthandInterface && resourceDescriptor.IdType != typeof(int)) + continue; // we can't create a shorthand for id types other than int + + var concreteGenericType = isShorthandInterface ? openGenericType.MakeGenericType(resourceDescriptor.ResourceType) : openGenericType.MakeGenericType(resourceDescriptor.ResourceType, resourceDescriptor.IdType); if(concreteGenericType.IsAssignableFrom(serviceImplementationType)) { - services.AddScoped(serviceImplementationType, serviceImplementationType); - typeImplemenetsAnExpectedInterface = true; + services.AddScoped(concreteGenericType, serviceImplementationType); + typeImplementsAnExpectedInterface = true; } } } - if(typeImplemenetsAnExpectedInterface == false) - throw new JsonApiSetupException($"{typeImplemenetsAnExpectedInterface} does not implement any of the expected JsonApiDotNetCore interfaces."); + if(typeImplementsAnExpectedInterface == false) + throw new JsonApiSetupException($"{serviceImplementationType} does not implement any of the expected JsonApiDotNetCore interfaces."); return services; } @@ -225,8 +234,8 @@ private static HashSet GetResourceTypesFromServiceImplementa { if(i.IsGenericType) { - var firstGenericArgument = i.GetGenericTypeDefinition().GetGenericArguments().FirstOrDefault(); - if(TypeLocator.TryGetResourceDescriptor(firstGenericArgument, out var resourceDescriptor) == false) + var firstGenericArgument = i.GenericTypeArguments.FirstOrDefault(); + if(TypeLocator.TryGetResourceDescriptor(firstGenericArgument, out var resourceDescriptor) == true) { resourceDecriptors.Add(resourceDescriptor); } diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index 8c6ae19cf0..f8a1300c7f 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -17,7 +17,11 @@ public class ServiceDiscoveryFacade { internal static HashSet ServiceInterfaces = new HashSet { typeof(IResourceService<>), - typeof(IResourceService<,>), + typeof(IResourceService<,>), + typeof(IResourceCmdService<>), + typeof(IResourceCmdService<,>), + typeof(IResourceQueryService<>), + typeof(IResourceQueryService<,>), typeof(ICreateService<>), typeof(ICreateService<,>), typeof(IGetAllService<>), @@ -26,6 +30,8 @@ public class ServiceDiscoveryFacade typeof(IGetByIdService<,>), typeof(IGetRelationshipService<>), typeof(IGetRelationshipService<,>), + typeof(IGetRelationshipsService<>), + typeof(IGetRelationshipsService<,>), typeof(IUpdateService<>), typeof(IUpdateService<,>), typeof(IDeleteService<>), diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index 0dc7808b24..f48821e756 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -67,16 +67,16 @@ public void AddResourceService_Registers_All_Shorthand_Service_Interfaces() // assert var provider = services.BuildServiceProvider(); - Assert.IsType(typeof(IResourceService)); - Assert.IsType(typeof(IResourceCmdService)); - Assert.IsType(typeof(IResourceQueryService)); - Assert.IsType(typeof(IGetAllService)); - Assert.IsType(typeof(IGetByIdService)); - Assert.IsType(typeof(IGetRelationshipService)); - Assert.IsType(typeof(IGetRelationshipsService)); - Assert.IsType(typeof(ICreateService)); - Assert.IsType(typeof(IUpdateService)); - Assert.IsType(typeof(IDeleteService)); + Assert.IsType(provider.GetService(typeof(IResourceService))); + Assert.IsType(provider.GetService(typeof(IResourceCmdService))); + Assert.IsType(provider.GetService(typeof(IResourceQueryService))); + Assert.IsType(provider.GetService(typeof(IGetAllService))); + Assert.IsType(provider.GetService(typeof(IGetByIdService))); + Assert.IsType(provider.GetService(typeof(IGetRelationshipService))); + Assert.IsType(provider.GetService(typeof(IGetRelationshipsService))); + Assert.IsType(provider.GetService(typeof(ICreateService))); + Assert.IsType(provider.GetService(typeof(IUpdateService))); + Assert.IsType(provider.GetService(typeof(IDeleteService))); } [Fact] @@ -90,16 +90,16 @@ public void AddResourceService_Registers_All_LongForm_Service_Interfaces() // assert var provider = services.BuildServiceProvider(); - Assert.IsType(typeof(IResourceService)); - Assert.IsType(typeof(IResourceCmdService)); - Assert.IsType(typeof(IResourceQueryService)); - Assert.IsType(typeof(IGetAllService)); - Assert.IsType(typeof(IGetByIdService)); - Assert.IsType(typeof(IGetRelationshipService)); - Assert.IsType(typeof(IGetRelationshipsService)); - Assert.IsType(typeof(ICreateService)); - Assert.IsType(typeof(IUpdateService)); - Assert.IsType(typeof(IDeleteService)); + Assert.IsType(provider.GetService(typeof(IResourceService))); + Assert.IsType(provider.GetService(typeof(IResourceCmdService))); + Assert.IsType(provider.GetService(typeof(IResourceQueryService))); + Assert.IsType(provider.GetService(typeof(IGetAllService))); + Assert.IsType(provider.GetService(typeof(IGetByIdService))); + Assert.IsType(provider.GetService(typeof(IGetRelationshipService))); + Assert.IsType(provider.GetService(typeof(IGetRelationshipsService))); + Assert.IsType(provider.GetService(typeof(ICreateService))); + Assert.IsType(provider.GetService(typeof(IUpdateService))); + Assert.IsType(provider.GetService(typeof(IDeleteService))); } [Fact] @@ -109,7 +109,7 @@ public void AddResourceService_Throws_If_Type_Does_Not_Implement_Any_Interfaces( var services = new ServiceCollection(); // act, assert - Assert.Throws(() => services.AddResourceService()); + Assert.Throws(() => services.AddResourceService()); } private class IntResource : Identifiable { } From 02f98c7017fd5870835a485882161eb41f960ad4 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sun, 23 Sep 2018 15:45:45 -0700 Subject: [PATCH 08/12] add ContextGraphBuilder tests --- .../Builders/ContextGraphBuilder.cs | 32 +++++++++++----- .../Builders/ContextGraphBuilder_Tests.cs | 37 +++++++++++++++++++ 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs index 7d2c6e0446..b343243463 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs @@ -23,24 +23,36 @@ public interface IContextGraphBuilder /// Add a json:api resource ///
/// The resource model type - /// The pluralized name that should be exposed by the API - IContextGraphBuilder AddResource(string pluralizedTypeName) where TResource : class, IIdentifiable; + /// + /// The pluralized name that should be exposed by the API. + /// If nothing is specified, the configured name formatter will be used. + /// See . + /// + IContextGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; /// /// Add a json:api resource /// /// The resource model type /// The resource model identifier type - /// The pluralized name that should be exposed by the API - IContextGraphBuilder AddResource(string pluralizedTypeName) where TResource : class, IIdentifiable; + /// + /// The pluralized name that should be exposed by the API. + /// If nothing is specified, the configured name formatter will be used. + /// See . + /// + IContextGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; /// /// Add a json:api resource /// /// The resource model type /// The resource model identifier type - /// The pluralized name that should be exposed by the API - IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName); + /// + /// The pluralized name that should be exposed by the API. + /// If nothing is specified, the configured name formatter will be used. + /// See . + /// + IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName = null); /// /// Add all the models that are part of the provided @@ -80,18 +92,20 @@ public IContextGraph Build() } /// - public IContextGraphBuilder AddResource(string pluralizedTypeName) where TResource : class, IIdentifiable + public IContextGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable => AddResource(pluralizedTypeName); /// - public IContextGraphBuilder AddResource(string pluralizedTypeName) where TResource : class, IIdentifiable + public IContextGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable => AddResource(typeof(TResource), typeof(TId), pluralizedTypeName); /// - public IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName) + public IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName = null) { AssertEntityIsNotAlreadyDefined(entityType); + pluralizedTypeName = pluralizedTypeName ?? _resourceNameFormatter.FormatResourceName(entityType); + _entities.Add(GetEntity(pluralizedTypeName, entityType, idType)); return this; diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index d5207fb6ef..a7bacd2dd6 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -1,3 +1,5 @@ +using System.Linq; +using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; @@ -37,5 +39,40 @@ public void Can_Build_ContextGraph_Using_Builder() Assert.Equal(typeof(NonDbResource), nonDbResource.EntityType); Assert.Equal(typeof(ResourceDefinition), nonDbResource.ResourceType); } + + [Fact] + public void Resources_Without_Names_Specified_Will_Use_Default_Formatter() + { + // arrange + var builder = new ContextGraphBuilder(); + builder.AddResource(); + + // act + var graph = builder.Build(); + + // assert + var resource = graph.GetContextEntity(typeof(TestResource)); + Assert.Equal("test-resources", resource.EntityName); + } + + [Fact] + public void Attrs_Without_Names_Specified_Will_Use_Default_Formatter() + { + // arrange + var builder = new ContextGraphBuilder(); + builder.AddResource(); + + // act + var graph = builder.Build(); + + // assert + var resource = graph.GetContextEntity(typeof(TestResource)); + Assert.Equal("attribute", resource.Attributes.Single().PublicAttributeName); + } + + public class TestResource : Identifiable + { + [Attr] public string Attribute { get; set; } + } } } From be0d5109bf325f0c09df1e8d2c1181cb51105e3f Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sun, 23 Sep 2018 20:49:58 -0700 Subject: [PATCH 09/12] add tests --- .../Builders/ContextGraphBuilder_Tests.cs | 77 ++++++++++++++++++- test/UnitTests/Graph/TypeLocator_Tests.cs | 56 ++++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index a7bacd2dd6..ab4412de2c 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -1,6 +1,12 @@ +using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection; +using Humanizer; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; @@ -17,6 +23,11 @@ class TestContext : DbContext { public DbSet DbResources { get; set; } } + public ContextGraphBuilder_Tests() + { + JsonApiOptions.ResourceNameFormatter = new DefaultResourceNameFormatter(); + } + [Fact] public void Can_Build_ContextGraph_Using_Builder() { @@ -55,6 +66,22 @@ public void Resources_Without_Names_Specified_Will_Use_Default_Formatter() Assert.Equal("test-resources", resource.EntityName); } + [Fact] + public void Resources_Without_Names_Specified_Will_Use_Configured_Formatter() + { + // arrange + JsonApiOptions.ResourceNameFormatter = new CamelCaseNameFormatter(); + var builder = new ContextGraphBuilder(); + builder.AddResource(); + + // act + var graph = builder.Build(); + + // assert + var resource = graph.GetContextEntity(typeof(TestResource)); + Assert.Equal("testResources", resource.EntityName); + } + [Fact] public void Attrs_Without_Names_Specified_Will_Use_Default_Formatter() { @@ -67,12 +94,56 @@ public void Attrs_Without_Names_Specified_Will_Use_Default_Formatter() // assert var resource = graph.GetContextEntity(typeof(TestResource)); - Assert.Equal("attribute", resource.Attributes.Single().PublicAttributeName); + Assert.Equal("compound-attribute", resource.Attributes.Single().PublicAttributeName); } - public class TestResource : Identifiable + [Fact] + public void Attrs_Without_Names_Specified_Will_Use_Configured_Formatter() { - [Attr] public string Attribute { get; set; } + // arrange + JsonApiOptions.ResourceNameFormatter = new CamelCaseNameFormatter(); + var builder = new ContextGraphBuilder(); + builder.AddResource(); + + // act + var graph = builder.Build(); + + // assert + var resource = graph.GetContextEntity(typeof(TestResource)); + Assert.Equal("compoundAttribute", resource.Attributes.Single().PublicAttributeName); + } + + [Fact] + public void Relationships_Without_Names_Specified_Will_Use_Default_Formatter() + { + // arrange + var builder = new ContextGraphBuilder(); + builder.AddResource(); + + // act + var graph = builder.Build(); + + // assert + var resource = graph.GetContextEntity(typeof(TestResource)); + Assert.Equal("related-resource", resource.Relationships.Single(r => r.IsHasOne).PublicRelationshipName); + Assert.Equal("related-resources", resource.Relationships.Single(r => r.IsHasMany).PublicRelationshipName); + } + + public class TestResource : Identifiable { + [Attr] public string CompoundAttribute { get; set; } + [HasOne] public RelatedResource RelatedResource { get; set; } + [HasMany] public List RelatedResources { get; set; } + } + + public class RelatedResource : Identifiable { } + + public class CamelCaseNameFormatter : IResourceNameFormatter + { + public string FormatPropertyName(PropertyInfo property) => ToCamelCase(property.Name); + + public string FormatResourceName(Type resourceType) => ToCamelCase(resourceType.Name.Pluralize()); + + private string ToCamelCase(string str) => Char.ToLowerInvariant(str[0]) + str.Substring(1); } } } diff --git a/test/UnitTests/Graph/TypeLocator_Tests.cs b/test/UnitTests/Graph/TypeLocator_Tests.cs index f0d1ce04ad..890994c340 100644 --- a/test/UnitTests/Graph/TypeLocator_Tests.cs +++ b/test/UnitTests/Graph/TypeLocator_Tests.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Models; using Xunit; @@ -85,6 +86,61 @@ public void GetIdType_Correctly_Identifies_NonJsonApiResource() Assert.False(result.isJsonApiResource); Assert.Equal(exextedIdType, result.idType); } + + [Fact] + public void GetIdentifableTypes_Locates_Identifiable_Resource() + { + // arrange + var resourceType = typeof(Model); + + // act + var results = TypeLocator.GetIdentifableTypes(resourceType.Assembly); + + // assert + Assert.Contains(results, r => r.ResourceType == resourceType); + } + + [Fact] + public void GetIdentifableTypes__Only_Contains_IIdentifiable_Types() + { + // arrange + var resourceType = typeof(Model); + + // act + var resourceDescriptors = TypeLocator.GetIdentifableTypes(resourceType.Assembly); + + // assert + foreach(var resourceDescriptor in resourceDescriptors) + Assert.True(typeof(IIdentifiable).IsAssignableFrom(resourceDescriptor.ResourceType)); + } + + [Fact] + public void TryGetResourceDescriptor_Returns_True_If_Type_Is_IIdentfiable() + { + // arrange + var resourceType = typeof(Model); + + // act + var isJsonApiResource = TypeLocator.TryGetResourceDescriptor(resourceType, out var descriptor); + + // assert + Assert.True(isJsonApiResource); + Assert.Equal(resourceType, descriptor.ResourceType); + Assert.Equal(typeof(int), descriptor.IdType); + } + + [Fact] + public void TryGetResourceDescriptor_Returns_False_If_Type_Is_IIdentfiable() + { + // arrange + var resourceType = typeof(String); + + // act + var isJsonApiResource = TypeLocator.TryGetResourceDescriptor(resourceType, out var descriptor); + + // assert + Assert.False(isJsonApiResource); + } } From f8867e4bb255b730d4f6ed41d35e461c57fda596 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sun, 23 Sep 2018 21:56:49 -0700 Subject: [PATCH 10/12] feat(402): adds filter and sort to ResourceDefinition --- .../Models/ResourceDefinition.cs | 76 ++++++++++++++++++- .../Models/ResourceDefinitionTests.cs | 13 ++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs index 6033a7b856..4705e4a2fa 100644 --- a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs @@ -1,4 +1,5 @@ using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Query; using System; using System.Collections.Generic; using System.Linq; @@ -13,7 +14,10 @@ public interface IResourceDefinition } /// - /// A scoped service used to... + /// exposes developer friendly hooks into how their resources are exposed. + /// It is intended to improve the experience and reduce boilerplate for commonly required features. + /// The goal of this class is to reduce the frequency with which developers have to override the + /// service and repository layers. /// /// The resource type public class ResourceDefinition : IResourceDefinition where T : class, IIdentifiable @@ -47,8 +51,10 @@ private bool InstanceOutputAttrsAreSpecified() return declaringType == derivedType; } + public delegate dynamic FilterExpression(T type); + // TODO: need to investigate options for caching these - protected List Remove(Expression> filter, List from = null) + protected List Remove(Expression filter, List from = null) { from = from ?? _contextEntity.Attributes; @@ -115,5 +121,71 @@ private List GetOutputAttrs() return _requestCachedAttrs; } + + /// + /// Define a set of custom query expressions that can be applied + /// instead of the default query behavior. A common use-case for this + /// is including related resources and filtering on them. + /// + /// + /// A set of custom queries that will be applied instead of the default + /// queries for the given key. Null will be returned if default behavior + /// is desired. + /// + /// + /// + /// protected override QueryFilters GetQueryFilters() => { + /// { "facility", (t, value) => t.Include(t => t.Tenant) + /// .Where(t => t.Facility == value) } + /// } + /// + /// + /// If the logic is simply too complex for an in-line expression, you can + /// delegate to a private method: + /// + /// protected override QueryFilters GetQueryFilters() + /// => new QueryFilters { + /// { "is-active", FilterIsActive } + /// }; + /// + /// private IQueryable<Model> FilterIsActive(IQueryable<Model> query, string value) + /// { + /// // some complex logic goes here... + /// return query.Where(x => x.IsActive == computedValue); + /// } + /// + /// + protected virtual QueryFilters GetQueryFilters() => null; + + /// + /// This is an alias type intended to simplify the implementation's + /// method signature. + /// See for usage details. + /// + public class QueryFilters : Dictionary, string, IQueryable>> { } + + /// + /// Define a the default sort order if no sort key is provided. + /// + /// + /// A list of properties and the direction they should be sorted. + /// + /// + /// + /// protected override PropertySortOrder GetDefaultSortOrder() + /// => new PropertySortOrder { + /// (t => t.Prop1, SortDirection.Ascending), + /// (t => t.Prop2, SortDirection.Descending), + /// }; + /// + /// + protected virtual PropertySortOrder GetDefaultSortOrder() => null; + + /// + /// This is an alias type intended to simplify the implementation's + /// method signature. + /// See for usage details. + /// + public class PropertySortOrder : List<(Expression>, SortDirection)> { } } } diff --git a/test/UnitTests/Models/ResourceDefinitionTests.cs b/test/UnitTests/Models/ResourceDefinitionTests.cs index 2112a49447..de606c5227 100644 --- a/test/UnitTests/Models/ResourceDefinitionTests.cs +++ b/test/UnitTests/Models/ResourceDefinitionTests.cs @@ -1,7 +1,9 @@ using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; using System.Collections.Generic; +using System.Linq; using Xunit; namespace UnitTests.Models @@ -98,6 +100,7 @@ public class Model : Identifiable { [Attr("name")] public string AlwaysExcluded { get; set; } [Attr("password")] public string Password { get; set; } + [Attr("prop")] public string Prop { get; set; } } public class RequestFilteredResource : ResourceDefinition @@ -116,6 +119,16 @@ protected override List OutputAttrs() => _isAdmin ? Remove(m => m.AlwaysExcluded) : Remove(m => new { m.AlwaysExcluded, m.Password }, from: base.OutputAttrs()); + + protected override QueryFilters GetQueryFilters() + => new QueryFilters { + { "is-active", (query, value) => query.Select(x => x) } + }; + + protected override PropertySortOrder GetDefaultSortOrder() + => new PropertySortOrder { + (t => t.Prop, SortDirection.Ascending) + }; } public class InstanceFilteredResource : ResourceDefinition From 38c39a0f8d715478f92cfdc6d3af46a88698e43f Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 25 Sep 2018 22:12:56 -0700 Subject: [PATCH 11/12] fix tests --- test/UnitTests/Models/ResourceDefinitionTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/UnitTests/Models/ResourceDefinitionTests.cs b/test/UnitTests/Models/ResourceDefinitionTests.cs index de606c5227..0a82d82d07 100644 --- a/test/UnitTests/Models/ResourceDefinitionTests.cs +++ b/test/UnitTests/Models/ResourceDefinitionTests.cs @@ -29,8 +29,7 @@ public void Request_Filter_Uses_Member_Expression() var attrs = resource.GetOutputAttrs(null); // assert - Assert.Single(attrs); - Assert.Equal(nameof(Model.Password), attrs[0].InternalAttributeName); + Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.AlwaysExcluded)); } [Fact] @@ -43,7 +42,8 @@ public void Request_Filter_Uses_NewExpression() var attrs = resource.GetOutputAttrs(null); // assert - Assert.Empty(attrs); + Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.AlwaysExcluded)); + Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.Password)); } [Fact] @@ -57,8 +57,7 @@ public void Instance_Filter_Uses_Member_Expression() var attrs = resource.GetOutputAttrs(model); // assert - Assert.Single(attrs); - Assert.Equal(nameof(Model.Password), attrs[0].InternalAttributeName); + Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.AlwaysExcluded)); } [Fact] @@ -72,7 +71,8 @@ public void Instance_Filter_Uses_NewExpression() var attrs = resource.GetOutputAttrs(model); // assert - Assert.Empty(attrs); + Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.AlwaysExcluded)); + Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.Password)); } [Fact] From 61372fffbf1195d812399ac5f9e213abc36c5c44 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 25 Sep 2018 22:55:11 -0700 Subject: [PATCH 12/12] implement new resource definition featurees --- .../Data/DefaultEntityRepository.cs | 46 ++++++++++++++++--- .../Models/ResourceDefinition.cs | 30 ++++++++++-- .../Services/EntityResourceService.cs | 3 +- .../Models/ResourceDefinitionTests.cs | 2 +- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 68462ff4df..808c1929a4 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -21,15 +21,17 @@ public class DefaultEntityRepository { public DefaultEntityRepository( IJsonApiContext jsonApiContext, - IDbContextResolver contextResolver) - : base(jsonApiContext, contextResolver) + IDbContextResolver contextResolver, + ResourceDefinition resourceDefinition = null) + : base(jsonApiContext, contextResolver, resourceDefinition) { } public DefaultEntityRepository( ILoggerFactory loggerFactory, IJsonApiContext jsonApiContext, - IDbContextResolver contextResolver) - : base(loggerFactory, jsonApiContext, contextResolver) + IDbContextResolver contextResolver, + ResourceDefinition resourceDefinition = null) + : base(loggerFactory, jsonApiContext, contextResolver, resourceDefinition) { } } @@ -47,27 +49,32 @@ public class DefaultEntityRepository private readonly ILogger _logger; private readonly IJsonApiContext _jsonApiContext; private readonly IGenericProcessorFactory _genericProcessorFactory; + private readonly ResourceDefinition _resourceDefinition; public DefaultEntityRepository( IJsonApiContext jsonApiContext, - IDbContextResolver contextResolver) + IDbContextResolver contextResolver, + ResourceDefinition resourceDefinition = null) { _context = contextResolver.GetContext(); _dbSet = contextResolver.GetDbSet(); _jsonApiContext = jsonApiContext; _genericProcessorFactory = _jsonApiContext.GenericProcessorFactory; + _resourceDefinition = resourceDefinition; } public DefaultEntityRepository( ILoggerFactory loggerFactory, IJsonApiContext jsonApiContext, - IDbContextResolver contextResolver) + IDbContextResolver contextResolver, + ResourceDefinition resourceDefinition = null) { _context = contextResolver.GetContext(); _dbSet = contextResolver.GetDbSet(); _jsonApiContext = jsonApiContext; _logger = loggerFactory.CreateLogger>(); _genericProcessorFactory = _jsonApiContext.GenericProcessorFactory; + _resourceDefinition = resourceDefinition; } /// @@ -82,13 +89,38 @@ public virtual IQueryable Get() /// public virtual IQueryable Filter(IQueryable entities, FilterQuery filterQuery) { + if(_resourceDefinition != null) + { + var defaultQueryFilters = _resourceDefinition.GetQueryFilters(); + if(defaultQueryFilters != null && defaultQueryFilters.TryGetValue(filterQuery.Attribute, out var defaultQueryFilter) == true) + { + return defaultQueryFilter(entities, filterQuery.Value); + } + } + return entities.Filter(_jsonApiContext, filterQuery); } /// public virtual IQueryable Sort(IQueryable entities, List sortQueries) { - return entities.Sort(sortQueries); + if (sortQueries != null && sortQueries.Count > 0) + return entities.Sort(sortQueries); + + if(_resourceDefinition != null) + { + var defaultSortOrder = _resourceDefinition.DefaultSort(); + if(defaultSortOrder != null && defaultSortOrder.Count > 0) + { + foreach(var sortProp in defaultSortOrder) + { + // this is dumb...add an overload, don't allocate for no reason + entities.Sort(new SortQuery(sortProp.Item2, sortProp.Item1)); + } + } + } + + return entities; } /// diff --git a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs index 4705e4a2fa..c9cffb7062 100644 --- a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs @@ -49,12 +49,10 @@ private bool InstanceOutputAttrsAreSpecified() .FirstOrDefault(); var declaringType = instanceMethod?.DeclaringType; return declaringType == derivedType; - } - - public delegate dynamic FilterExpression(T type); + } // TODO: need to investigate options for caching these - protected List Remove(Expression filter, List from = null) + protected List Remove(Expression> filter, List from = null) { from = from ?? _contextEntity.Attributes; @@ -155,7 +153,7 @@ private List GetOutputAttrs() /// } /// /// - protected virtual QueryFilters GetQueryFilters() => null; + public virtual QueryFilters GetQueryFilters() => null; /// /// This is an alias type intended to simplify the implementation's @@ -181,6 +179,28 @@ public class QueryFilters : Dictionary, string, IQuer /// protected virtual PropertySortOrder GetDefaultSortOrder() => null; + internal List<(AttrAttribute, SortDirection)> DefaultSort() + { + var defaultSortOrder = GetDefaultSortOrder(); + if(defaultSortOrder != null && defaultSortOrder.Count > 0) + { + var order = new List<(AttrAttribute, SortDirection)>(); + foreach(var sortProp in defaultSortOrder) + { + // TODO: error handling, log or throw? + if (sortProp.Item1.Body is MemberExpression memberExpression) + order.Add( + (_contextEntity.Attributes.SingleOrDefault(a => a.InternalAttributeName != memberExpression.Member.Name), + sortProp.Item2) + ); + } + + return order; + } + + return null; + } + /// /// This is an alias type intended to simplify the implementation's /// method signature. diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index 1e175bfeb0..2ce3b9147d 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -223,8 +223,7 @@ protected virtual IQueryable ApplySortAndFilterQuery(IQueryable 0) - entities = _entities.Sort(entities, query.SortParameters); + entities = _entities.Sort(entities, query.SortParameters); return entities; } diff --git a/test/UnitTests/Models/ResourceDefinitionTests.cs b/test/UnitTests/Models/ResourceDefinitionTests.cs index 0a82d82d07..e7a1e75dcf 100644 --- a/test/UnitTests/Models/ResourceDefinitionTests.cs +++ b/test/UnitTests/Models/ResourceDefinitionTests.cs @@ -120,7 +120,7 @@ protected override List OutputAttrs() ? Remove(m => m.AlwaysExcluded) : Remove(m => new { m.AlwaysExcluded, m.Password }, from: base.OutputAttrs()); - protected override QueryFilters GetQueryFilters() + public override QueryFilters GetQueryFilters() => new QueryFilters { { "is-active", (query, value) => query.Select(x => x) } };