From 71e52aa2bbc064a4b5051924bd50e4c853df052f Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sat, 11 Aug 2018 22:06:38 -0700 Subject: [PATCH 1/5] first pass at auto-discovery --- .../Data/AppDbContext.cs | 6 - .../Models/CamelCasedModel.cs | 1 + .../Models/TodoItemCollection.cs | 3 +- .../JsonApiDotNetCoreExample/Startup.cs | 22 +-- src/Examples/ReportsExample/Startup.cs | 15 +- .../Builders/ContextGraphBuilder.cs | 25 ++-- .../IServiceCollectionExtensions.cs | 30 ++-- .../Graph/IResourceNameFormatter.cs | 50 +++++++ .../Graph/ResourceDescriptor.cs | 16 ++ .../Graph/ServiceDiscoveryFacade.cs | 141 ++++++++++++++++++ src/JsonApiDotNetCore/Graph/TypeLocator.cs | 93 ++++++++++++ .../JsonApiDotNetCore.csproj | 1 + .../Serialization/JsonApiDeSerializer.cs | 2 +- 13 files changed, 354 insertions(+), 51 deletions(-) create mode 100644 src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs create mode 100644 src/JsonApiDotNetCore/Graph/ResourceDescriptor.cs create mode 100644 src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs create mode 100644 src/JsonApiDotNetCore/Graph/TypeLocator.cs diff --git a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs index 9c0ada4e4b..cee66678ab 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs @@ -1,5 +1,4 @@ using JsonApiDotNetCoreExample.Models; -using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; using JsonApiDotNetCoreExample.Models.Entities; @@ -43,13 +42,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public DbSet TodoItems { get; set; } public DbSet People { get; set; } - - [Resource("todo-collections")] public DbSet TodoItemCollections { get; set; } - - [Resource("camelCasedModels")] public DbSet CamelCasedModels { get; set; } - public DbSet
Articles { get; set; } public DbSet Authors { get; set; } public DbSet NonJsonApiResources { get; set; } diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/CamelCasedModel.cs b/src/Examples/JsonApiDotNetCoreExample/Models/CamelCasedModel.cs index 7adf628f38..43d5a43272 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/CamelCasedModel.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/CamelCasedModel.cs @@ -2,6 +2,7 @@ namespace JsonApiDotNetCoreExample.Models { + [Resource("camelCasedModels")] public class CamelCasedModel : Identifiable { [Attr("compoundAttr")] diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs index 95a523dff3..85877b3848 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs @@ -4,6 +4,7 @@ namespace JsonApiDotNetCoreExample.Models { + [Resource("todo-collections")] public class TodoItemCollection : Identifiable { [Attr("name")] @@ -16,4 +17,4 @@ public class TodoItemCollection : Identifiable [HasOne("owner")] public virtual Person Owner { get; set; } } -} \ No newline at end of file +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startup.cs index ec1bdc544c..29dfc9e4b5 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startup.cs @@ -7,9 +7,6 @@ using Microsoft.EntityFrameworkCore; using JsonApiDotNetCore.Extensions; using System; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCoreExample.Resources; -using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExample { @@ -33,23 +30,20 @@ public virtual IServiceProvider ConfigureServices(IServiceCollection services) var loggerFactory = new LoggerFactory(); loggerFactory.AddConsole(LogLevel.Warning); + var mvcBuilder = services.AddMvcCore(); + services .AddSingleton(loggerFactory) - .AddDbContext(options => - options.UseNpgsql(GetDbConnectionString()), ServiceLifetime.Transient) - .AddJsonApi(options => { + .AddDbContext(options => options.UseNpgsql(GetDbConnectionString()), ServiceLifetime.Transient) + .AddJsonApi(options => { options.Namespace = "api/v1"; options.DefaultPageSize = 5; options.IncludeTotalRecordCount = true; - }) - // TODO: this should be handled via auto-discovery - .AddScoped, UserResource>(); + }, + mvcBuilder, + discovery => discovery.AddCurrentAssemblyServices()); - var provider = services.BuildServiceProvider(); - var appContext = provider.GetRequiredService(); - if(appContext == null) - throw new ArgumentException(); - + var provider = services.BuildServiceProvider(); return provider; } diff --git a/src/Examples/ReportsExample/Startup.cs b/src/Examples/ReportsExample/Startup.cs index fe476c0406..43a910a0e6 100644 --- a/src/Examples/ReportsExample/Startup.cs +++ b/src/Examples/ReportsExample/Startup.cs @@ -1,5 +1,4 @@ using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -26,16 +25,10 @@ public Startup(IHostingEnvironment env) public virtual void ConfigureServices(IServiceCollection services) { var mvcBuilder = services.AddMvcCore(); - services.AddJsonApi(opt => - { - opt.BuildContextGraph(builder => - { - builder.AddResource("reports"); - }); - opt.Namespace = "api"; - }, mvcBuilder); - - services.AddScoped, ReportService>(); + services.AddJsonApi( + opt => opt.Namespace = "api", + mvcBuilder, + discovery => discovery.AddCurrentAssemblyServices()); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs index 080c0a6bb7..1d9ef83ecd 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; @@ -32,6 +33,14 @@ public interface IContextGraphBuilder /// The pluralized name that should be exposed by the API IContextGraphBuilder AddResource(string pluralizedTypeName) 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); + /// /// Add all the models that are part of the provided /// that also implement @@ -66,12 +75,13 @@ public IContextGraphBuilder AddResource(string pluralizedTypeName) wh => AddResource(pluralizedTypeName); public IContextGraphBuilder AddResource(string pluralizedTypeName) where TResource : class, IIdentifiable - { - var entityType = typeof(TResource); + => AddResource(typeof(TResource), typeof(TId), pluralizedTypeName); + public IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName) + { AssertEntityIsNotAlreadyDefined(entityType); - _entities.Add(GetEntity(pluralizedTypeName, entityType, typeof(TId))); + _entities.Add(GetEntity(pluralizedTypeName, entityType, idType)); return this; } @@ -182,12 +192,9 @@ private string GetResourceName(PropertyInfo property) private (bool isJsonApiResource, Type idType) GetIdType(Type resourceType) { - var interfaces = resourceType.GetInterfaces(); - foreach (var type in interfaces) - { - if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(IIdentifiable<>)) - return (true, type.GetGenericArguments()[0]); - } + var possible = TypeLocator.GetIdType(resourceType); + if (possible.isJsonApiResource) + return possible; _validationResults.Add(new ValidationResult(LogLevel.Warning, $"{resourceType} does not implement 'IIdentifiable<>'. ")); diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 4a56620b78..09dedad001 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Formatters; +using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Middleware; @@ -35,9 +36,10 @@ public static IServiceCollection AddJsonApi(this IServiceCollection se return AddJsonApi(services, options, mvcBuilder); } - public static IServiceCollection AddJsonApi(this IServiceCollection services, - Action options, - IMvcCoreBuilder mvcBuilder) where TContext : DbContext + public static IServiceCollection AddJsonApi( + this IServiceCollection services, + Action options, + IMvcCoreBuilder mvcBuilder) where TContext : DbContext { var config = new JsonApiOptions(); @@ -51,17 +53,24 @@ public static IServiceCollection AddJsonApi(this IServiceCollection se return services; } - public static IServiceCollection AddJsonApi(this IServiceCollection services, - Action options, - IMvcCoreBuilder mvcBuilder) + public static IServiceCollection AddJsonApi( + this IServiceCollection services, + Action configureOptions, + IMvcCoreBuilder mvcBuilder, + Action autoDiscover = null) { - var config = new JsonApiOptions(); + var options = new JsonApiOptions(); + configureOptions(options); - options(config); + if(autoDiscover != null) + { + var facade = new ServiceDiscoveryFacade(services, options.ContextGraphBuilder); + autoDiscover(facade); + } mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, config)); - AddJsonApiInternals(services, config); + AddJsonApiInternals(services, options); return services; } @@ -88,6 +97,9 @@ public static void AddJsonApiInternals( this IServiceCollection services, JsonApiOptions jsonApiOptions) { + if (jsonApiOptions.ContextGraph == null) + jsonApiOptions.ContextGraph = jsonApiOptions.ContextGraphBuilder.Build(); + if (jsonApiOptions.ContextGraph.UsesDbContext == false) { services.AddScoped(); diff --git a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs new file mode 100644 index 0000000000..021a3399e6 --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using System.Reflection; +using Humanizer; +using JsonApiDotNetCore.Models; +using str = JsonApiDotNetCore.Extensions.StringExtensions; + + +namespace JsonApiDotNetCore.Graph +{ + /// + /// Provides an interface for formatting resource names by convention + /// + public interface IResourceNameFormatter + { + /// + /// Get the publicly visible resource name from the internal type name + /// + string FormatResourceName(Type resourceType); + } + + public class DefaultResourceNameFormatter : IResourceNameFormatter + { + /// + /// Uses the internal type name to determine the external resource name. + /// By default we us Humanizer for pluralization and then we dasherize the name. + /// + /// + /// + /// _default.FormatResourceName(typeof(TodoItem)).Dump(); + /// // > "todo-items" + /// + /// + public string FormatResourceName(Type type) + { + try + { + var attribute = type.GetCustomAttributes(typeof(ResourceAttribute)).SingleOrDefault() as ResourceAttribute; + if (attribute != null) + return attribute.ResourceName; + + return str.Dasherize(type.Name.Pluralize()); + } + catch (InvalidOperationException e) + { + throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", e); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Graph/ResourceDescriptor.cs b/src/JsonApiDotNetCore/Graph/ResourceDescriptor.cs new file mode 100644 index 0000000000..e90bcdc22c --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/ResourceDescriptor.cs @@ -0,0 +1,16 @@ +using System; + +namespace JsonApiDotNetCore.Graph +{ + internal struct ResourceDescriptor + { + public ResourceDescriptor(Type resourceType, Type idType) + { + ResourceType = resourceType; + IdType = idType; + } + + public Type ResourceType { get; set; } + public Type IdType { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs new file mode 100644 index 0000000000..ca156718fe --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -0,0 +1,141 @@ +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; +using System.Reflection; + +namespace JsonApiDotNetCore.Graph +{ + public class ServiceDiscoveryFacade + { + private readonly IServiceCollection _services; + private readonly IContextGraphBuilder _graphBuilder; + + public ServiceDiscoveryFacade(IServiceCollection services, IContextGraphBuilder graphBuilder) + { + _services = services; + _graphBuilder = graphBuilder; + } + + /// + /// 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); + + /// + /// 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) + { + AddDbContextResolvers(assembly); + AddAssemblyResources(assembly, resourceNameFormatter); + AddAssemblyServices(assembly); + AddAssemblyRepositories(assembly); + + return this; + } + + private void AddDbContextResolvers(Assembly assembly) + { + var dbContextTypes = TypeLocator.GetDerivedTypes(assembly, typeof(DbContext)); + foreach(var dbContextType in dbContextTypes) + { + var resolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); + _services.AddScoped(typeof(IDbContextResolver), resolverType); + } + } + + /// + /// 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 AddAssemblyResources(Assembly assembly, IResourceNameFormatter resourceNameFormatter = null) + { + var identifiables = TypeLocator.GetIdentifableTypes(assembly); + foreach (var identifiable in identifiables) + { + RegisterResourceDefinition(assembly, identifiable); + AddResourceToGraph(identifiable, resourceNameFormatter); + } + + return this; + } + + private void RegisterResourceDefinition(Assembly assembly, ResourceDescriptor identifiable) + { + try + { + var resourceDefinition = TypeLocator.GetDerivedGenericTypes(assembly, typeof(ResourceDefinition<>), identifiable.ResourceType) + .SingleOrDefault(); + + if (resourceDefinition != null) + _services.AddScoped(typeof(ResourceDefinition<>).MakeGenericType(identifiable.ResourceType), resourceDefinition); + } + 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); + } + } + + private void AddResourceToGraph(ResourceDescriptor identifiable, IResourceNameFormatter resourceNameFormatter = null) + { + var resourceName = FormatResourceName(identifiable.ResourceType, resourceNameFormatter); + _graphBuilder.AddResource(identifiable.ResourceType, identifiable.IdType, resourceName); + } + + private string FormatResourceName(Type resourceType, IResourceNameFormatter resourceNameFormatter) + { + resourceNameFormatter = resourceNameFormatter ?? new DefaultResourceNameFormatter(); + return resourceNameFormatter.FormatResourceName(resourceType); + } + + /// + /// Add implementations to container. + /// + /// The assembly to search for resources in. + public ServiceDiscoveryFacade AddAssemblyServices(Assembly assembly) + { + RegisterServiceImplementations(assembly, typeof(IResourceService<,>)); + RegisterServiceImplementations(assembly, typeof(ICreateService<,>)); + RegisterServiceImplementations(assembly, typeof(IGetAllService<,>)); + RegisterServiceImplementations(assembly, typeof(IGetByIdService<,>)); + RegisterServiceImplementations(assembly, typeof(IGetRelationshipService<,>)); + RegisterServiceImplementations(assembly, typeof(IUpdateService<,>)); + RegisterServiceImplementations(assembly, typeof(IDeleteService<,>)); + + return this; + } + + /// + /// Add implementations to container. + /// + /// The assembly to search for resources in. + public ServiceDiscoveryFacade AddAssemblyRepositories(Assembly assembly) + => RegisterServiceImplementations(assembly, typeof(IEntityRepository<,>)); + + private ServiceDiscoveryFacade RegisterServiceImplementations(Assembly assembly, Type interfaceType) + { + 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); + } + + return this; + } + } +} diff --git a/src/JsonApiDotNetCore/Graph/TypeLocator.cs b/src/JsonApiDotNetCore/Graph/TypeLocator.cs new file mode 100644 index 0000000000..6cd2e89c43 --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/TypeLocator.cs @@ -0,0 +1,93 @@ +using JsonApiDotNetCore.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace JsonApiDotNetCore.Graph +{ + internal static class TypeLocator + { + private static Dictionary _typeCache = new Dictionary(); + private static Dictionary> _identifiableTypeCache = new Dictionary>(); + + public static (bool isJsonApiResource, Type idType) GetIdType(Type resourceType) + { + var identitifableType = GetIdentifiableIdType(resourceType); + return (identitifableType != null) + ? (true, identitifableType) + : (false, null); + } + + private static Type GetIdentifiableIdType(Type identifiableType) + => GetIdentifiableInterface(identifiableType)?.GetGenericArguments()[0]; + + private static Type GetIdentifiableInterface(Type type) + => type.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IIdentifiable<>)); + + // TODO: determine if this optimization is even helpful... + private static Type[] GetAssemblyTypes(Assembly assembly) + { + if (_typeCache.TryGetValue(assembly, out var types) == false) + { + types = assembly.GetTypes(); + _typeCache[assembly] = types; + } + + return types; + } + + public static List GetIdentifableTypes(Assembly assembly) + { + if (_identifiableTypeCache.TryGetValue(assembly, out var descriptors) == false) + { + descriptors = new List(); + _identifiableTypeCache[assembly] = descriptors; + + foreach (var type in assembly.GetTypes()) + { + var possible = GetIdType(type); + if (possible.isJsonApiResource) + descriptors.Add(new ResourceDescriptor(type, possible.idType)); + } + } + + return descriptors; + } + + public static (Type implementation, Type registrationInterface) GetGenericInterfaceImplementation(Assembly assembly, Type openGenericInterfaceType, params Type[] genericInterfaceArguments) + { + foreach (var type in assembly.GetTypes()) + { + var interfaces = type.GetInterfaces(); + foreach (var interfaceType in interfaces) + if (interfaceType.GetTypeInfo().IsGenericType && interfaceType.GetGenericTypeDefinition() == openGenericInterfaceType) + return ( + type, + interfaceType.MakeGenericType(genericInterfaceArguments) + ); + } + + return (null, null); + } + + public static IEnumerable GetDerivedGenericTypes(Assembly assembly, Type openGenericType, Type genericArgument) + { + var genericType = openGenericType.MakeGenericType(genericArgument); + foreach (var type in assembly.GetTypes()) + { + if (genericType.IsAssignableFrom(type)) + yield return type; + } + } + + public static IEnumerable GetDerivedTypes(Assembly assembly, Type inheritedType) + { + foreach (var type in assembly.GetTypes()) + { + if(inheritedType.IsAssignableFrom(type)) + yield return type; + } + } + } +} diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index c786cd5153..e3a2ee9298 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -21,6 +21,7 @@ + diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index fc9f11d7e0..29569f43e9 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -121,7 +121,7 @@ public object DocumentToObject(DocumentData data, List included = message: $"This API does not contain a json:api resource named '{data.Type}'.", detail: "This resource is not registered on the ContextGraph. " + "If you are using Entity Framework, make sure the DbSet matches the expected resource name. " - + "If you have manually registered the resource, check that the call to AddResource correctly sets the public name."); ; + + "If you have manually registered the resource, check that the call to AddResource correctly sets the public name."); var entity = Activator.CreateInstance(contextEntity.EntityType); From ea7f78f2c9302fefde96ffb1f5ef4d73e97463e6 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 2 Jul 2018 16:41:06 -0700 Subject: [PATCH 2/5] fix tests --- .../Builders/ContextGraphBuilder.cs | 21 ++++++++++++------- .../Graph/IResourceNameFormatter.cs | 3 +-- .../Internal/ContextEntity.cs | 4 +++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs index 1d9ef83ecd..e7de302075 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs @@ -174,20 +174,27 @@ public IContextGraphBuilder AddDbContext() where T : DbContext var (isJsonApiResource, idType) = GetIdType(entityType); if (isJsonApiResource) - _entities.Add(GetEntity(GetResourceName(property), entityType, idType)); + _entities.Add(GetEntity(GetResourceName(property, entityType), entityType, idType)); } } return this; } - private string GetResourceName(PropertyInfo property) + private string GetResourceName(PropertyInfo property, Type resourceType) { - var resourceAttribute = property.GetCustomAttribute(typeof(ResourceAttribute)); - if (resourceAttribute == null) - return property.Name.Dasherize(); - - return ((ResourceAttribute)resourceAttribute).ResourceName; + // check the class definition first + // [Resource("models"] public class Model : Identifiable { /* ... */ } + if (resourceType.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute classResourceAttribute) + return classResourceAttribute.ResourceName; + + // check the DbContext member next + // [Resource("models")] public DbSet Models { get; set; } + if (property.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute resourceAttribute) + return resourceAttribute.ResourceName; + + // fallback to dsherized...this should actually check for a custom IResourceNameFormatter + return property.Name.Dasherize(); } private (bool isJsonApiResource, Type idType) GetIdType(Type resourceType) diff --git a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs index 021a3399e6..c67852620e 100644 --- a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs +++ b/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs @@ -35,8 +35,7 @@ public string FormatResourceName(Type type) { try { - var attribute = type.GetCustomAttributes(typeof(ResourceAttribute)).SingleOrDefault() as ResourceAttribute; - if (attribute != null) + if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) return attribute.ResourceName; return str.Dasherize(type.Name.Pluralize()); diff --git a/src/JsonApiDotNetCore/Internal/ContextEntity.cs b/src/JsonApiDotNetCore/Internal/ContextEntity.cs index 1e15a9c6bc..867a04350c 100644 --- a/src/JsonApiDotNetCore/Internal/ContextEntity.cs +++ b/src/JsonApiDotNetCore/Internal/ContextEntity.cs @@ -9,7 +9,9 @@ public class ContextEntity /// /// The exposed resource name /// - public string EntityName { get; set; } + public string EntityName { + get; + set; } /// /// The data model type From 9832241591de8bfb5097e285108b3a8459cc2a6d Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sat, 11 Aug 2018 22:07:42 -0700 Subject: [PATCH 3/5] use IResourceNameFormatter in ContextGraph add documentation --- .../Builders/ContextGraphBuilder.cs | 25 +++++++-- src/JsonApiDotNetCore/Graph/TypeLocator.cs | 54 ++++++++++++++++--- .../JsonApiDotNetCore.csproj | 2 +- 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs index e7de302075..6c836cdaca 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs @@ -48,6 +48,12 @@ public interface IContextGraphBuilder /// The implementation type. IContextGraphBuilder AddDbContext() where T : DbContext; + /// + /// Specify the used to format resource names. + /// + /// Formatter used to define exposed resource names by convention. + IContextGraphBuilder UseNameFormatter(IResourceNameFormatter resourceNameFormatter); + /// /// Which links to include. Defaults to . /// @@ -60,6 +66,8 @@ public class ContextGraphBuilder : IContextGraphBuilder private List _validationResults = new List(); private bool _usesDbContext; + private IResourceNameFormatter _resourceNameFormatter = new DefaultResourceNameFormatter(); + public Link DocumentLinks { get; set; } = Link.All; public IContextGraph Build() @@ -71,12 +79,15 @@ public IContextGraph Build() return graph; } + /// public IContextGraphBuilder AddResource(string pluralizedTypeName) where TResource : class, IIdentifiable => AddResource(pluralizedTypeName); + /// public IContextGraphBuilder AddResource(string pluralizedTypeName) where TResource : class, IIdentifiable => AddResource(typeof(TResource), typeof(TId), pluralizedTypeName); + /// public IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName) { AssertEntityIsNotAlreadyDefined(entityType); @@ -152,6 +163,7 @@ protected virtual Type GetRelationshipType(RelationshipAttribute relation, Prope private Type GetResourceDefinitionType(Type entityType) => typeof(ResourceDefinition<>).MakeGenericType(entityType); + /// public IContextGraphBuilder AddDbContext() where T : DbContext { _usesDbContext = true; @@ -174,14 +186,14 @@ public IContextGraphBuilder AddDbContext() where T : DbContext var (isJsonApiResource, idType) = GetIdType(entityType); if (isJsonApiResource) - _entities.Add(GetEntity(GetResourceName(property, entityType), entityType, idType)); + _entities.Add(GetEntity(GetResourceNameFromDbSetProperty(property, entityType), entityType, idType)); } } return this; } - private string GetResourceName(PropertyInfo property, Type resourceType) + private string GetResourceNameFromDbSetProperty(PropertyInfo property, Type resourceType) { // check the class definition first // [Resource("models"] public class Model : Identifiable { /* ... */ } @@ -194,7 +206,7 @@ private string GetResourceName(PropertyInfo property, Type resourceType) return resourceAttribute.ResourceName; // fallback to dsherized...this should actually check for a custom IResourceNameFormatter - return property.Name.Dasherize(); + return _resourceNameFormatter.FormatResourceName(resourceType); } private (bool isJsonApiResource, Type idType) GetIdType(Type resourceType) @@ -213,5 +225,12 @@ private void AssertEntityIsNotAlreadyDefined(Type entityType) if (_entities.Any(e => e.EntityType == entityType)) throw new InvalidOperationException($"Cannot add entity type {entityType} to context graph, there is already an entity of that type configured."); } + + /// + public IContextGraphBuilder UseNameFormatter(IResourceNameFormatter resourceNameFormatter) + { + _resourceNameFormatter = resourceNameFormatter; + return this; + } } } diff --git a/src/JsonApiDotNetCore/Graph/TypeLocator.cs b/src/JsonApiDotNetCore/Graph/TypeLocator.cs index 6cd2e89c43..5bdb029e50 100644 --- a/src/JsonApiDotNetCore/Graph/TypeLocator.cs +++ b/src/JsonApiDotNetCore/Graph/TypeLocator.cs @@ -6,11 +6,19 @@ namespace JsonApiDotNetCore.Graph { + /// + /// Used to locate types and facilitate auto-resource discovery + /// internal static class TypeLocator { private static Dictionary _typeCache = new Dictionary(); private static Dictionary> _identifiableTypeCache = new Dictionary>(); + + /// + /// Determine whether or not this is a json:api resource by checking if it implements . + /// Returns the status and the resultant id type, either `(true, Type)` OR `(false, null)` + /// public static (bool isJsonApiResource, Type idType) GetIdType(Type resourceType) { var identitifableType = GetIdentifiableIdType(resourceType); @@ -37,6 +45,10 @@ private static Type[] GetAssemblyTypes(Assembly assembly) return types; } + + /// + /// Get all implementations of . in the assembly + /// public static List GetIdentifableTypes(Assembly assembly) { if (_identifiableTypeCache.TryGetValue(assembly, out var descriptors) == false) @@ -55,6 +67,17 @@ public static List GetIdentifableTypes(Assembly assembly) return descriptors; } + /// + /// Get all implementations of the generic interface + /// + /// The assembly to search + /// The open generic type, e.g. `typeof(IResourceService<>)` + /// Parameters to the generic type + /// + /// + /// GetGenericInterfaceImplementation(assembly, typeof(IResourceService<>), typeof(Article), typeof(Guid)); + /// + /// public static (Type implementation, Type registrationInterface) GetGenericInterfaceImplementation(Assembly assembly, Type openGenericInterfaceType, params Type[] genericInterfaceArguments) { foreach (var type in assembly.GetTypes()) @@ -71,16 +94,33 @@ public static (Type implementation, Type registrationInterface) GetGenericInterf return (null, null); } - public static IEnumerable GetDerivedGenericTypes(Assembly assembly, Type openGenericType, Type genericArgument) + /// + /// Get all derivitives of the concrete, generic type. + /// + /// The assembly to search + /// The open generic type, e.g. `typeof(ResourceDefinition<>)` + /// Parameters to the generic type + /// + /// + /// GetDerivedGenericTypes(assembly, typeof(ResourceDefinition<>), typeof(Article)) + /// + /// + public static IEnumerable GetDerivedGenericTypes(Assembly assembly, Type openGenericType, params Type[] genericArguments) { - var genericType = openGenericType.MakeGenericType(genericArgument); - foreach (var type in assembly.GetTypes()) - { - if (genericType.IsAssignableFrom(type)) - yield return type; - } + var genericType = openGenericType.MakeGenericType(genericArguments); + return GetDerivedTypes(assembly, genericType); } + /// + /// Get all derivitives of the specified type. + /// + /// The assembly to search + /// The inherited type + /// + /// + /// GetDerivedGenericTypes(assembly, typeof(DbContext)) + /// + /// public static IEnumerable GetDerivedTypes(Assembly assembly, Type inheritedType) { foreach (var type in assembly.GetTypes()) diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index e3a2ee9298..183a7c8084 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@ - 2.5.1 + 3.0.0 $(NetStandardVersion) JsonApiDotNetCore JsonApiDotNetCore From cd2f92a3b680058368bcb68a9d939f84e431e9d9 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sat, 11 Aug 2018 21:55:34 -0700 Subject: [PATCH 4/5] add documentation --- src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs | 4 ++++ src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs index 6c836cdaca..ba71dbce06 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs @@ -195,6 +195,10 @@ public IContextGraphBuilder AddDbContext() where T : DbContext private string GetResourceNameFromDbSetProperty(PropertyInfo property, Type resourceType) { + // this check is actually duplicated in the DefaultResourceNameFormatter + // however, we perform it here so that we allow class attributes to be prioritized over + // the DbSet attribute. Eventually, the DbSet attribute should be deprecated. + // // check the class definition first // [Resource("models"] public class Model : Identifiable { /* ... */ } if (resourceType.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute classResourceAttribute) diff --git a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs index c67852620e..57baca0901 100644 --- a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs +++ b/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs @@ -35,6 +35,8 @@ public string FormatResourceName(Type type) { try { + // check the class definition first + // [Resource("models"] public class Model : Identifiable { /* ... */ } if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) return attribute.ResourceName; From 4e3c46e08b1401e7a9bd5493e08ae003b6af49f2 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Sat, 11 Aug 2018 22:19:05 -0700 Subject: [PATCH 5/5] fix build --- .../Extensions/IServiceCollectionExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 09dedad001..4c2d6c2f79 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -59,18 +59,18 @@ public static IServiceCollection AddJsonApi( IMvcCoreBuilder mvcBuilder, Action autoDiscover = null) { - var options = new JsonApiOptions(); - configureOptions(options); + var config = new JsonApiOptions(); + configureOptions(config); if(autoDiscover != null) { - var facade = new ServiceDiscoveryFacade(services, options.ContextGraphBuilder); + var facade = new ServiceDiscoveryFacade(services, config.ContextGraphBuilder); autoDiscover(facade); } mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, config)); - AddJsonApiInternals(services, options); + AddJsonApiInternals(services, config); return services; }