diff --git a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs index a9a74cf9a7..3b38531918 100644 --- a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; @@ -13,7 +14,6 @@ public interface IResourceGraphBuilder /// Construct the /// IResourceGraph Build(); - /// /// Add a json:api resource /// @@ -24,8 +24,6 @@ public interface IResourceGraphBuilder /// See . /// IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; - - /// /// Add a json:api resource /// @@ -37,7 +35,6 @@ public interface IResourceGraphBuilder /// See . /// IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; - /// /// Add a Json:Api resource /// @@ -49,12 +46,5 @@ public interface IResourceGraphBuilder /// See . /// IResourceGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName = null); - - /// - /// Add all the models that are part of the provided - /// that also implement - /// - /// The implementation type. - IResourceGraphBuilder AddDbContext() where T : DbContext; } } diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index 8c00ce8fc1..0d5bf08e12 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -20,6 +20,7 @@ using JsonApiDotNetCore.Serialization.Server.Builders; using JsonApiDotNetCore.Serialization.Server; using Microsoft.Extensions.DependencyInjection.Extensions; +using JsonApiDotNetCore.Extensions; namespace JsonApiDotNetCore.Builders { @@ -30,10 +31,10 @@ namespace JsonApiDotNetCore.Builders public class JsonApiApplicationBuilder { public readonly JsonApiOptions JsonApiOptions = new JsonApiOptions(); - private IResourceGraphBuilder _resourceGraphBuilder; + internal IResourceGraphBuilder _resourceGraphBuilder; + internal bool _usesDbContext; + internal readonly IServiceCollection _services; private IServiceDiscoveryFacade _serviceDiscoveryFacade; - private bool _usesDbContext; - private readonly IServiceCollection _services; private readonly IMvcCoreBuilder _mvcBuilder; public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mvcBuilder) @@ -42,11 +43,6 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv _mvcBuilder = mvcBuilder; } - internal void ConfigureLogging() - { - _services.AddLogging(); - } - /// /// Executes the action provided by the user to configure /// @@ -98,18 +94,6 @@ public void ConfigureResources(Action resourceGraphBuilde resourceGraphBuilder(_resourceGraphBuilder); } - /// - /// Executes the action provided by the user to configure the resources using . - /// Additionally, inspects the EF core database context for models that implement IIdentifiable. - /// - public void ConfigureResources(Action resourceGraphBuilder) where TContext : DbContext - { - _resourceGraphBuilder.AddDbContext(); - _usesDbContext = true; - _services.AddScoped>(); - resourceGraphBuilder?.Invoke(_resourceGraphBuilder); - } - /// /// Registers the remaining internals. /// diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index e025d4f7de..05d4a6a8c5 100644 --- a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -10,29 +10,28 @@ using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Links; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Builders { public class ResourceGraphBuilder : IResourceGraphBuilder { - private readonly List _entities = new List(); - private readonly List _validationResults = new List(); - private readonly IResourceNameFormatter _resourceNameFormatter = new KebabCaseFormatter(); + private List _resources { get; set; } = new List(); + private List _validationResults { get; set; } = new List(); + private IResourceNameFormatter _formatter { get; set; } = new KebabCaseFormatter(); public ResourceGraphBuilder() { } public ResourceGraphBuilder(IResourceNameFormatter formatter) { - _resourceNameFormatter = formatter; + _formatter = formatter; } /// public IResourceGraph Build() { - _entities.ForEach(SetResourceLinksOptions); - var resourceGraph = new ResourceGraph(_entities, _validationResults); + _resources.ForEach(SetResourceLinksOptions); + var resourceGraph = new ResourceGraph(_resources, _validationResults); return resourceGraph; } @@ -56,13 +55,19 @@ public IResourceGraphBuilder AddResource(string pluralizedTypeNa => AddResource(typeof(TResource), typeof(TId), pluralizedTypeName); /// - public IResourceGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName = null) + public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string pluralizedTypeName = null) { - AssertEntityIsNotAlreadyDefined(entityType); - - pluralizedTypeName = pluralizedTypeName ?? _resourceNameFormatter.FormatResourceName(entityType); - - _entities.Add(GetEntity(pluralizedTypeName, entityType, idType)); + AssertEntityIsNotAlreadyDefined(resourceType); + if (resourceType.Implements()) + { + pluralizedTypeName ??= _formatter.FormatResourceName(resourceType); + idType ??= TypeLocator.GetIdType(resourceType); + _resources.Add(GetEntity(pluralizedTypeName, resourceType, idType)); + } + else + { + _validationResults.Add(new ValidationResult(LogLevel.Warning, $"{resourceType} does not implement 'IIdentifiable<>'. ")); + } return this; } @@ -93,7 +98,7 @@ protected virtual List GetAttributes(Type entityType) { var idAttr = new AttrAttribute() { - PublicAttributeName = _resourceNameFormatter.FormatPropertyName(prop), + PublicAttributeName = _formatter.FormatPropertyName(prop), PropertyInfo = prop, InternalAttributeName = prop.Name }; @@ -105,7 +110,7 @@ protected virtual List GetAttributes(Type entityType) if (attribute == null) continue; - attribute.PublicAttributeName = attribute.PublicAttributeName ?? _resourceNameFormatter.FormatPropertyName(prop); + attribute.PublicAttributeName = attribute.PublicAttributeName ?? _formatter.FormatPropertyName(prop); attribute.InternalAttributeName = prop.Name; attribute.PropertyInfo = prop; @@ -123,7 +128,7 @@ protected virtual List GetRelationships(Type entityType) var attribute = (RelationshipAttribute)prop.GetCustomAttribute(typeof(RelationshipAttribute)); if (attribute == null) continue; - attribute.PublicRelationshipName = attribute.PublicRelationshipName ?? _resourceNameFormatter.FormatPropertyName(prop); + attribute.PublicRelationshipName = attribute.PublicRelationshipName ?? _formatter.FormatPropertyName(prop); attribute.InternalRelationshipName = prop.Name; attribute.RightType = GetRelationshipType(attribute, prop); attribute.LeftType = entityType; @@ -178,63 +183,9 @@ protected virtual Type GetRelationshipType(RelationshipAttribute relation, Prope private Type GetResourceDefinitionType(Type entityType) => typeof(ResourceDefinition<>).MakeGenericType(entityType); - /// - public IResourceGraphBuilder AddDbContext() where T : DbContext - { - var contextType = typeof(T); - var contextProperties = contextType.GetProperties(); - foreach (var property in contextProperties) - { - var dbSetType = property.PropertyType; - if (dbSetType.GetTypeInfo().IsGenericType - && dbSetType.GetGenericTypeDefinition() == typeof(DbSet<>)) - { - var entityType = dbSetType.GetGenericArguments()[0]; - AssertEntityIsNotAlreadyDefined(entityType); - var (isJsonApiResource, idType) = GetIdType(entityType); - if (isJsonApiResource) - _entities.Add(GetEntity(GetResourceNameFromDbSetProperty(property, entityType), entityType, idType)); - } - } - - return this; - } - - 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) - 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 the established convention using the DbSet Property.Name - // e.g DbSet FooBars { get; set; } => "foo-bars" - return _resourceNameFormatter.FormatResourceName(resourceType); - } - - private (bool isJsonApiResource, Type idType) GetIdType(Type resourceType) - { - var possible = TypeLocator.GetIdType(resourceType); - if (possible.isJsonApiResource) - return possible; - - _validationResults.Add(new ValidationResult(LogLevel.Warning, $"{resourceType} does not implement 'IIdentifiable<>'. ")); - - return (false, null); - } - private void AssertEntityIsNotAlreadyDefined(Type entityType) { - if (_entities.Any(e => e.ResourceType == entityType)) + if (_resources.Any(e => e.ResourceType == entityType)) throw new InvalidOperationException($"Cannot add entity type {entityType} to context resourceGraph, there is already an entity of that type configured."); } } diff --git a/src/JsonApiDotNetCore/Extensions/EntityFrameworkCoreExtension.cs b/src/JsonApiDotNetCore/Extensions/EntityFrameworkCoreExtension.cs new file mode 100644 index 0000000000..af9272f48c --- /dev/null +++ b/src/JsonApiDotNetCore/Extensions/EntityFrameworkCoreExtension.cs @@ -0,0 +1,111 @@ +using System; +using System.Reflection; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Graph; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Data; + +namespace JsonApiDotNetCore.Extensions.EntityFrameworkCore +{ + + /// + /// Extensions for configuring JsonApiDotNetCore with EF Core + /// + public static class IResourceGraphBuilderExtensions + { + /// + /// Add all the models that are part of the provided + /// that also implement + /// + /// The implementation type. + public static IResourceGraphBuilder AddDbContext(this IResourceGraphBuilder resourceGraphBuilder) where TDbContext : DbContext + { + var builder = (ResourceGraphBuilder)resourceGraphBuilder; + var contextType = typeof(TDbContext); + var contextProperties = contextType.GetProperties(); + foreach (var property in contextProperties) + { + var dbSetType = property.PropertyType; + if (dbSetType.GetTypeInfo().IsGenericType + && dbSetType.GetGenericTypeDefinition() == typeof(DbSet<>)) + { + var resourceType = dbSetType.GetGenericArguments()[0]; + builder.AddResource(resourceType, pluralizedTypeName: GetResourceNameFromDbSetProperty(property, resourceType)); + } + } + return resourceGraphBuilder; + } + + private static 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) + 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; + + return null; + } + } + + /// + /// Extensions for configuring JsonApiDotNetCore with EF Core + /// + public static class IServiceCollectionExtensions + { + /// + /// Enabling JsonApiDotNetCore using the EF Core DbContext to build the ResourceGraph. + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddJsonApi(this IServiceCollection services, + Action options = null, + Action discovery = null, + Action resources = null, + IMvcCoreBuilder mvcBuilder = null) + where TDbContext : DbContext + { + var application = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); + if (options != null) + application.ConfigureJsonApiOptions(options); + application.ConfigureMvc(); + if (discovery != null) + application.AutoDiscover(discovery); + application.ConfigureResources(resources); + application.ConfigureServices(); + return services; + } + } + + /// + /// Extensions for configuring JsonApiDotNetCore with EF Core + /// + public static class JsonApiApplicationBuildExtensions + { + /// + /// Executes the action provided by the user to configure the resources using . + /// Additionally, inspects the EF core database context for models that implement IIdentifiable. + /// + public static void ConfigureResources(this JsonApiApplicationBuilder builder, Action resourceGraphBuilder) where TContext : DbContext + { + builder._resourceGraphBuilder.AddDbContext(); + builder._usesDbContext = true; + builder._services.AddScoped>(); + resourceGraphBuilder?.Invoke(builder._resourceGraphBuilder); + } + } +} diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index aab18dec6b..eb30283428 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -18,33 +18,9 @@ namespace JsonApiDotNetCore.Extensions // ReSharper disable once InconsistentNaming public static class IServiceCollectionExtensions { - /// - /// Enabling JsonApiDotNetCore using the EF Core DbContext to build the ResourceGraph. - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddJsonApi(this IServiceCollection services, - Action options = null, - Action resources = null, - IMvcCoreBuilder mvcBuilder = null) - where TEfCoreDbContext : DbContext - { - var application = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); - if (options != null) - application.ConfigureJsonApiOptions(options); - application.ConfigureLogging(); - application.ConfigureMvc(); - application.ConfigureResources(resources); - application.ConfigureServices(); - return services; - } - /// /// Enabling JsonApiDotNetCore using manual declaration to build the ResourceGraph. - /// z + /// /// /// /// diff --git a/src/JsonApiDotNetCore/Graph/TypeLocator.cs b/src/JsonApiDotNetCore/Graph/TypeLocator.cs index 1e82e438c3..de5a2cd82f 100644 --- a/src/JsonApiDotNetCore/Graph/TypeLocator.cs +++ b/src/JsonApiDotNetCore/Graph/TypeLocator.cs @@ -1,3 +1,4 @@ +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Models; using System; using System.Collections.Generic; @@ -19,30 +20,10 @@ static class TypeLocator /// 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) + public static Type 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; + var identifiableInterface = resourceType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IIdentifiable<>)); + return identifiableInterface?.GetGenericArguments()[0]; } /// @@ -76,13 +57,11 @@ private static IEnumerable FindIdentifableTypes(Assembly ass /// internal static bool TryGetResourceDescriptor(Type type, out ResourceDescriptor descriptor) { - var possible = GetIdType(type); - if (possible.isJsonApiResource) + if (type.Implements()) { - descriptor = new ResourceDescriptor(type, possible.idType); + descriptor = new ResourceDescriptor(type, GetIdType(type)); return true; } - descriptor = ResourceDescriptor.Empty; return false; } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs index 0f0d045bdc..ee5451c542 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs @@ -52,6 +52,7 @@ public AppDbContext GetDbContext() request.Content = new StringContent(content); request.Content.Headers.ContentType = JsonApiContentType; var response = await _client.SendAsync(request); + var body = await response.Content?.ReadAsStringAsync(); return (body, response); } diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index fae27307d2..058c8f05eb 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -5,6 +5,7 @@ using Humanizer; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Extensions.EntityFrameworkCore; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index 20fa848b35..bd81705deb 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -19,6 +19,7 @@ using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Serialization.Server.Builders; using JsonApiDotNetCore.Serialization.Server; +using JsonApiDotNetCore.Extensions.EntityFrameworkCore; namespace UnitTests.Extensions { @@ -29,7 +30,7 @@ public void AddJsonApiInternals_Adds_All_Required_Services() { // Arrange var services = new ServiceCollection(); - + services.AddLogging(); services.AddDbContext(options => options.UseInMemoryDatabase("UnitTestDb"), ServiceLifetime.Transient); services.AddJsonApi(); diff --git a/test/UnitTests/Graph/TypeLocator_Tests.cs b/test/UnitTests/Graph/TypeLocator_Tests.cs index 26381ea1fb..502cfa1139 100644 --- a/test/UnitTests/Graph/TypeLocator_Tests.cs +++ b/test/UnitTests/Graph/TypeLocator_Tests.cs @@ -59,14 +59,13 @@ public void GetIdType_Correctly_Identifies_JsonApiResource() { // Arrange var type = typeof(Model); - var exextedIdType = typeof(int); + var expectedIdType = typeof(int); // Act - var (isJsonApiResource, idType) = TypeLocator.GetIdType(type); + var idType = TypeLocator.GetIdType(type); // Assert - Assert.True(isJsonApiResource); - Assert.Equal(exextedIdType, idType); + Assert.Equal(expectedIdType, idType); } [Fact] @@ -74,14 +73,13 @@ public void GetIdType_Correctly_Identifies_NonJsonApiResource() { // Arrange var type = typeof(DerivedType); - Type exextedIdType = null; + Type expectedIdType = null; // Act - var (isJsonApiResource, idType) = TypeLocator.GetIdType(type); + var idType = TypeLocator.GetIdType(type); // Assert - Assert.False(isJsonApiResource); - Assert.Equal(exextedIdType, idType); + Assert.Equal(expectedIdType, idType); } [Fact] diff --git a/test/UnitTests/Internal/ContextGraphBuilder_Tests.cs b/test/UnitTests/Internal/ResourceGraphBuilder_Tests.cs similarity index 96% rename from test/UnitTests/Internal/ContextGraphBuilder_Tests.cs rename to test/UnitTests/Internal/ResourceGraphBuilder_Tests.cs index cf61c512d7..e765004ac5 100644 --- a/test/UnitTests/Internal/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Internal/ResourceGraphBuilder_Tests.cs @@ -1,4 +1,5 @@ using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Extensions.EntityFrameworkCore; using JsonApiDotNetCore.Internal; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging;