Skip to content

Auto Resource/Service Discovery #376

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using JsonApiDotNetCoreExample.Models;
using JsonApiDotNetCore.Models;
using Microsoft.EntityFrameworkCore;
using JsonApiDotNetCoreExample.Models.Entities;

Expand Down Expand Up @@ -43,13 +42,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)

public DbSet<TodoItem> TodoItems { get; set; }
public DbSet<Person> People { get; set; }

[Resource("todo-collections")]
public DbSet<TodoItemCollection> TodoItemCollections { get; set; }

[Resource("camelCasedModels")]
public DbSet<CamelCasedModel> CamelCasedModels { get; set; }

public DbSet<Article> Articles { get; set; }
public DbSet<Author> Authors { get; set; }
public DbSet<NonJsonApiResource> NonJsonApiResources { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace JsonApiDotNetCoreExample.Models
{
[Resource("camelCasedModels")]
public class CamelCasedModel : Identifiable
{
[Attr("compoundAttr")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace JsonApiDotNetCoreExample.Models
{
[Resource("todo-collections")]
public class TodoItemCollection : Identifiable<Guid>
{
[Attr("name")]
Expand All @@ -16,4 +17,4 @@ public class TodoItemCollection : Identifiable<Guid>
[HasOne("owner")]
public virtual Person Owner { get; set; }
}
}
}
22 changes: 8 additions & 14 deletions src/Examples/JsonApiDotNetCoreExample/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@
using Microsoft.EntityFrameworkCore;
using JsonApiDotNetCore.Extensions;
using System;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCoreExample.Resources;
using JsonApiDotNetCoreExample.Models;

namespace JsonApiDotNetCoreExample
{
Expand All @@ -33,23 +30,20 @@ public virtual IServiceProvider ConfigureServices(IServiceCollection services)
var loggerFactory = new LoggerFactory();
loggerFactory.AddConsole(LogLevel.Warning);

var mvcBuilder = services.AddMvcCore();

services
.AddSingleton<ILoggerFactory>(loggerFactory)
.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(GetDbConnectionString()), ServiceLifetime.Transient)
.AddJsonApi<AppDbContext>(options => {
.AddDbContext<AppDbContext>(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<ResourceDefinition<User>, UserResource>();
},
mvcBuilder,
discovery => discovery.AddCurrentAssemblyServices());

var provider = services.BuildServiceProvider();
var appContext = provider.GetRequiredService<AppDbContext>();
if(appContext == null)
throw new ArgumentException();

var provider = services.BuildServiceProvider();
return provider;
}

Expand Down
15 changes: 4 additions & 11 deletions src/Examples/ReportsExample/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using JsonApiDotNetCore.Extensions;
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
Expand All @@ -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<Report>("reports");
});
opt.Namespace = "api";
}, mvcBuilder);

services.AddScoped<IGetAllService<Report>, ReportService>();
services.AddJsonApi(
opt => opt.Namespace = "api",
mvcBuilder,
discovery => discovery.AddCurrentAssemblyServices());
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
Expand Down
69 changes: 53 additions & 16 deletions src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -32,13 +33,27 @@ public interface IContextGraphBuilder
/// <param name="pluralizedTypeName">The pluralized name that should be exposed by the API</param>
IContextGraphBuilder AddResource<TResource, TId>(string pluralizedTypeName) where TResource : class, IIdentifiable<TId>;

/// <summary>
/// Add a json:api resource
/// </summary>
/// <param name="entityType">The resource model type</param>
/// <param name="idType">The resource model identifier type</param>
/// <param name="pluralizedTypeName">The pluralized name that should be exposed by the API</param>
IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName);

/// <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>
IContextGraphBuilder AddDbContext<T>() where T : DbContext;

/// <summary>
/// Specify the <see cref="IResourceNameFormatter"/> used to format resource names.
/// </summary>
/// <param name="resourceNameFormatter">Formatter used to define exposed resource names by convention.</param>
IContextGraphBuilder UseNameFormatter(IResourceNameFormatter resourceNameFormatter);

/// <summary>
/// Which links to include. Defaults to <see cref="Link.All"/>.
/// </summary>
Expand All @@ -51,6 +66,8 @@ public class ContextGraphBuilder : IContextGraphBuilder
private List<ValidationResult> _validationResults = new List<ValidationResult>();

private bool _usesDbContext;
private IResourceNameFormatter _resourceNameFormatter = new DefaultResourceNameFormatter();

public Link DocumentLinks { get; set; } = Link.All;

public IContextGraph Build()
Expand All @@ -62,16 +79,20 @@ public IContextGraph Build()
return graph;
}

/// <inheritdoc />
public IContextGraphBuilder AddResource<TResource>(string pluralizedTypeName) where TResource : class, IIdentifiable<int>
=> AddResource<TResource, int>(pluralizedTypeName);

/// <inheritdoc />
public IContextGraphBuilder AddResource<TResource, TId>(string pluralizedTypeName) where TResource : class, IIdentifiable<TId>
{
var entityType = typeof(TResource);
=> AddResource(typeof(TResource), typeof(TId), pluralizedTypeName);

/// <inheritdoc />
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;
}
Expand Down Expand Up @@ -142,6 +163,7 @@ protected virtual Type GetRelationshipType(RelationshipAttribute relation, Prope

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

/// <inheritdoc />
public IContextGraphBuilder AddDbContext<T>() where T : DbContext
{
_usesDbContext = true;
Expand All @@ -164,30 +186,38 @@ public IContextGraphBuilder AddDbContext<T>() where T : DbContext
var (isJsonApiResource, idType) = GetIdType(entityType);

if (isJsonApiResource)
_entities.Add(GetEntity(GetResourceName(property), entityType, idType));
_entities.Add(GetEntity(GetResourceNameFromDbSetProperty(property, entityType), entityType, idType));
}
}

return this;
}

private string GetResourceName(PropertyInfo property)
private string GetResourceNameFromDbSetProperty(PropertyInfo property, Type resourceType)
{
var resourceAttribute = property.GetCustomAttribute(typeof(ResourceAttribute));
if (resourceAttribute == null)
return property.Name.Dasherize();

return ((ResourceAttribute)resourceAttribute).ResourceName;
// 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 dsherized...this should actually check for a custom IResourceNameFormatter
return _resourceNameFormatter.FormatResourceName(resourceType);
}

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<>'. "));

Expand All @@ -199,5 +229,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.");
}

/// <inheritdoc />
public IContextGraphBuilder UseNameFormatter(IResourceNameFormatter resourceNameFormatter)
{
_resourceNameFormatter = resourceNameFormatter;
return this;
}
}
}
26 changes: 19 additions & 7 deletions src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -35,9 +36,10 @@ public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection se
return AddJsonApi<TContext>(services, options, mvcBuilder);
}

public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection services,
Action<JsonApiOptions> options,
IMvcCoreBuilder mvcBuilder) where TContext : DbContext
public static IServiceCollection AddJsonApi<TContext>(
this IServiceCollection services,
Action<JsonApiOptions> options,
IMvcCoreBuilder mvcBuilder) where TContext : DbContext
{
var config = new JsonApiOptions();

Expand All @@ -51,13 +53,20 @@ public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection se
return services;
}

public static IServiceCollection AddJsonApi(this IServiceCollection services,
Action<JsonApiOptions> options,
IMvcCoreBuilder mvcBuilder)
public static IServiceCollection AddJsonApi(
this IServiceCollection services,
Action<JsonApiOptions> configureOptions,
IMvcCoreBuilder mvcBuilder,
Action<ServiceDiscoveryFacade> autoDiscover = null)
{
var config = new JsonApiOptions();
configureOptions(config);

options(config);
if(autoDiscover != null)
{
var facade = new ServiceDiscoveryFacade(services, config.ContextGraphBuilder);
autoDiscover(facade);
}

mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, config));

Expand Down Expand Up @@ -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<DbContext>();
Expand Down
51 changes: 51 additions & 0 deletions src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Linq;
using System.Reflection;
using Humanizer;
using JsonApiDotNetCore.Models;
using str = JsonApiDotNetCore.Extensions.StringExtensions;


namespace JsonApiDotNetCore.Graph
{
/// <summary>
/// Provides an interface for formatting resource names by convention
/// </summary>
public interface IResourceNameFormatter
{
/// <summary>
/// Get the publicly visible resource name from the internal type name
/// </summary>
string FormatResourceName(Type resourceType);
}

public class DefaultResourceNameFormatter : IResourceNameFormatter
{
/// <summary>
/// Uses the internal type name to determine the external resource name.
/// By default we us Humanizer for pluralization and then we dasherize the name.
/// </summary>
/// <example>
/// <code>
/// _default.FormatResourceName(typeof(TodoItem)).Dump();
/// // > "todo-items"
/// </code>
/// </example>
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;

return str.Dasherize(type.Name.Pluralize());
}
catch (InvalidOperationException e)
{
throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", e);
}
}
}
}
16 changes: 16 additions & 0 deletions src/JsonApiDotNetCore/Graph/ResourceDescriptor.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Loading