Skip to content
12 changes: 1 addition & 11 deletions src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using JsonApiDotNetCore.Graph;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Internal.Contracts;
Expand All @@ -13,7 +14,6 @@ public interface IResourceGraphBuilder
/// Construct the <see cref="ResourceGraph"/>
/// </summary>
IResourceGraph Build();

/// <summary>
/// Add a json:api resource
/// </summary>
Expand All @@ -24,8 +24,6 @@ public interface IResourceGraphBuilder
/// See <see cref="IResourceNameFormatter" />.
/// </param>
IResourceGraphBuilder AddResource<TResource>(string pluralizedTypeName = null) where TResource : class, IIdentifiable<int>;


/// <summary>
/// Add a json:api resource
/// </summary>
Expand All @@ -37,7 +35,6 @@ public interface IResourceGraphBuilder
/// See <see cref="IResourceNameFormatter" />.
/// </param>
IResourceGraphBuilder AddResource<TResource, TId>(string pluralizedTypeName = null) where TResource : class, IIdentifiable<TId>;

/// <summary>
/// Add a Json:Api resource
/// </summary>
Expand All @@ -49,12 +46,5 @@ public interface IResourceGraphBuilder
/// See <see cref="IResourceNameFormatter" />.
/// </param>
IResourceGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName = null);

/// <summary>
/// Add all the models that are part of the provided <see cref="DbContext" />
/// that also implement <see cref="IIdentifiable"/>
/// </summary>
/// <typeparam name="T">The <see cref="DbContext"/> implementation type.</typeparam>
IResourceGraphBuilder AddDbContext<T>() where T : DbContext;
}
}
24 changes: 4 additions & 20 deletions src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using JsonApiDotNetCore.Serialization.Server.Builders;
using JsonApiDotNetCore.Serialization.Server;
using Microsoft.Extensions.DependencyInjection.Extensions;
using JsonApiDotNetCore.Extensions;

namespace JsonApiDotNetCore.Builders
{
Expand All @@ -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)
Expand All @@ -42,11 +43,6 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv
_mvcBuilder = mvcBuilder;
}

internal void ConfigureLogging()
{
_services.AddLogging();
}

/// <summary>
/// Executes the action provided by the user to configure <see cref="JsonApiOptions"/>
/// </summary>
Expand Down Expand Up @@ -98,18 +94,6 @@ public void ConfigureResources(Action<IResourceGraphBuilder> resourceGraphBuilde
resourceGraphBuilder(_resourceGraphBuilder);
}

/// <summary>
/// Executes the action provided by the user to configure the resources using <see cref="IResourceGraphBuilder"/>.
/// Additionally, inspects the EF core database context for models that implement IIdentifiable.
/// </summary>
public void ConfigureResources<TContext>(Action<IResourceGraphBuilder> resourceGraphBuilder) where TContext : DbContext
{
_resourceGraphBuilder.AddDbContext<TContext>();
_usesDbContext = true;
_services.AddScoped<IDbContextResolver, DbContextResolver<TContext>>();
resourceGraphBuilder?.Invoke(_resourceGraphBuilder);
}

/// <summary>
/// Registers the remaining internals.
/// </summary>
Expand Down
93 changes: 22 additions & 71 deletions src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResourceContext> _entities = new List<ResourceContext>();
private readonly List<ValidationResult> _validationResults = new List<ValidationResult>();
private readonly IResourceNameFormatter _resourceNameFormatter = new KebabCaseFormatter();
private List<ResourceContext> _resources { get; set; } = new List<ResourceContext>();
private List<ValidationResult> _validationResults { get; set; } = new List<ValidationResult>();
private IResourceNameFormatter _formatter { get; set; } = new KebabCaseFormatter();

public ResourceGraphBuilder() { }

public ResourceGraphBuilder(IResourceNameFormatter formatter)
{
_resourceNameFormatter = formatter;
_formatter = formatter;
}

/// <inheritdoc />
public IResourceGraph Build()
{
_entities.ForEach(SetResourceLinksOptions);
var resourceGraph = new ResourceGraph(_entities, _validationResults);
_resources.ForEach(SetResourceLinksOptions);
var resourceGraph = new ResourceGraph(_resources, _validationResults);
return resourceGraph;
}

Expand All @@ -56,13 +55,19 @@ public IResourceGraphBuilder AddResource<TResource, TId>(string pluralizedTypeNa
=> AddResource(typeof(TResource), typeof(TId), pluralizedTypeName);

/// <inheritdoc />
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<IIdentifiable>())
{
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;
}
Expand Down Expand Up @@ -93,7 +98,7 @@ protected virtual List<AttrAttribute> GetAttributes(Type entityType)
{
var idAttr = new AttrAttribute()
{
PublicAttributeName = _resourceNameFormatter.FormatPropertyName(prop),
PublicAttributeName = _formatter.FormatPropertyName(prop),
PropertyInfo = prop,
InternalAttributeName = prop.Name
};
Expand All @@ -105,7 +110,7 @@ protected virtual List<AttrAttribute> 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;

Expand All @@ -123,7 +128,7 @@ protected virtual List<RelationshipAttribute> 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;
Expand Down Expand Up @@ -178,63 +183,9 @@ protected virtual Type GetRelationshipType(RelationshipAttribute relation, Prope

private Type GetResourceDefinitionType(Type entityType) => typeof(ResourceDefinition<>).MakeGenericType(entityType);

/// <inheritdoc />
public IResourceGraphBuilder AddDbContext<T>() 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<Model> 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<FooBar> 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.");
}
}
Expand Down
111 changes: 111 additions & 0 deletions src/JsonApiDotNetCore/Extensions/EntityFrameworkCoreExtension.cs
Original file line number Diff line number Diff line change
@@ -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
{

/// <summary>
/// Extensions for configuring JsonApiDotNetCore with EF Core
/// </summary>
public static class IResourceGraphBuilderExtensions
{
/// <summary>
/// Add all the models that are part of the provided <see cref="DbContext" />
/// that also implement <see cref="IIdentifiable"/>
/// </summary>
/// <typeparam name="TDbContext">The <see cref="DbContext"/> implementation type.</typeparam>
public static IResourceGraphBuilder AddDbContext<TDbContext>(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<Model> Models { get; set; }
if (property.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute resourceAttribute)
return resourceAttribute.ResourceName;

return null;
}
}

/// <summary>
/// Extensions for configuring JsonApiDotNetCore with EF Core
/// </summary>
public static class IServiceCollectionExtensions
{
/// <summary>
/// Enabling JsonApiDotNetCore using the EF Core DbContext to build the ResourceGraph.
/// </summary>
/// <typeparam name="TContext"></typeparam>
/// <param name="services"></param>
/// <param name="options"></param>
/// <param name="resources"></param>
/// <returns></returns>
public static IServiceCollection AddJsonApi<TDbContext>(this IServiceCollection services,
Action<JsonApiOptions> options = null,
Action<IServiceDiscoveryFacade> discovery = null,
Action<IResourceGraphBuilder> 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<TDbContext>(resources);
application.ConfigureServices();
return services;
}
}

/// <summary>
/// Extensions for configuring JsonApiDotNetCore with EF Core
/// </summary>
public static class JsonApiApplicationBuildExtensions
{
/// <summary>
/// Executes the action provided by the user to configure the resources using <see cref="IResourceGraphBuilder"/>.
/// Additionally, inspects the EF core database context for models that implement IIdentifiable.
/// </summary>
public static void ConfigureResources<TContext>(this JsonApiApplicationBuilder builder, Action<IResourceGraphBuilder> resourceGraphBuilder) where TContext : DbContext
{
builder._resourceGraphBuilder.AddDbContext<TContext>();
builder._usesDbContext = true;
builder._services.AddScoped<IDbContextResolver, DbContextResolver<TContext>>();
resourceGraphBuilder?.Invoke(builder._resourceGraphBuilder);
}
}
}
26 changes: 1 addition & 25 deletions src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,9 @@ namespace JsonApiDotNetCore.Extensions
// ReSharper disable once InconsistentNaming
public static class IServiceCollectionExtensions
{
/// <summary>
/// Enabling JsonApiDotNetCore using the EF Core DbContext to build the ResourceGraph.
/// </summary>
/// <typeparam name="TContext"></typeparam>
/// <param name="services"></param>
/// <param name="options"></param>
/// <param name="resources"></param>
/// <returns></returns>
public static IServiceCollection AddJsonApi<TEfCoreDbContext>(this IServiceCollection services,
Action<JsonApiOptions> options = null,
Action<IResourceGraphBuilder> 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<TEfCoreDbContext>(resources);
application.ConfigureServices();
return services;
}

/// <summary>
/// Enabling JsonApiDotNetCore using manual declaration to build the ResourceGraph.
/// </summary>z
/// </summary>
/// <param name="services"></param>
/// <param name="options"></param>
/// <param name="resources"></param>
Expand Down
Loading