Skip to content

Commit d5ee796

Browse files
authored
Merge pull request #376 from json-api-dotnet/feat/#241-2
Auto Resource/Service Discovery
2 parents a9c7ff9 + 4e3c46e commit d5ee796

14 files changed

+434
-58
lines changed

src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs

-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using JsonApiDotNetCoreExample.Models;
2-
using JsonApiDotNetCore.Models;
32
using Microsoft.EntityFrameworkCore;
43
using JsonApiDotNetCoreExample.Models.Entities;
54

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

4443
public DbSet<TodoItem> TodoItems { get; set; }
4544
public DbSet<Person> People { get; set; }
46-
47-
[Resource("todo-collections")]
4845
public DbSet<TodoItemCollection> TodoItemCollections { get; set; }
49-
50-
[Resource("camelCasedModels")]
5146
public DbSet<CamelCasedModel> CamelCasedModels { get; set; }
52-
5347
public DbSet<Article> Articles { get; set; }
5448
public DbSet<Author> Authors { get; set; }
5549
public DbSet<NonJsonApiResource> NonJsonApiResources { get; set; }

src/Examples/JsonApiDotNetCoreExample/Models/CamelCasedModel.cs

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace JsonApiDotNetCoreExample.Models
44
{
5+
[Resource("camelCasedModels")]
56
public class CamelCasedModel : Identifiable
67
{
78
[Attr("compoundAttr")]

src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace JsonApiDotNetCoreExample.Models
66
{
7+
[Resource("todo-collections")]
78
public class TodoItemCollection : Identifiable<Guid>
89
{
910
[Attr("name")]
@@ -16,4 +17,4 @@ public class TodoItemCollection : Identifiable<Guid>
1617
[HasOne("owner")]
1718
public virtual Person Owner { get; set; }
1819
}
19-
}
20+
}

src/Examples/JsonApiDotNetCoreExample/Startup.cs

+8-14
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@
77
using Microsoft.EntityFrameworkCore;
88
using JsonApiDotNetCore.Extensions;
99
using System;
10-
using JsonApiDotNetCore.Models;
11-
using JsonApiDotNetCoreExample.Resources;
12-
using JsonApiDotNetCoreExample.Models;
1310

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

33+
var mvcBuilder = services.AddMvcCore();
34+
3635
services
3736
.AddSingleton<ILoggerFactory>(loggerFactory)
38-
.AddDbContext<AppDbContext>(options =>
39-
options.UseNpgsql(GetDbConnectionString()), ServiceLifetime.Transient)
40-
.AddJsonApi<AppDbContext>(options => {
37+
.AddDbContext<AppDbContext>(options => options.UseNpgsql(GetDbConnectionString()), ServiceLifetime.Transient)
38+
.AddJsonApi(options => {
4139
options.Namespace = "api/v1";
4240
options.DefaultPageSize = 5;
4341
options.IncludeTotalRecordCount = true;
44-
})
45-
// TODO: this should be handled via auto-discovery
46-
.AddScoped<ResourceDefinition<User>, UserResource>();
42+
},
43+
mvcBuilder,
44+
discovery => discovery.AddCurrentAssemblyServices());
4745

48-
var provider = services.BuildServiceProvider();
49-
var appContext = provider.GetRequiredService<AppDbContext>();
50-
if(appContext == null)
51-
throw new ArgumentException();
52-
46+
var provider = services.BuildServiceProvider();
5347
return provider;
5448
}
5549

src/Examples/ReportsExample/Startup.cs

+4-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using JsonApiDotNetCore.Extensions;
2-
using JsonApiDotNetCore.Services;
32
using Microsoft.AspNetCore.Builder;
43
using Microsoft.AspNetCore.Hosting;
54
using Microsoft.Extensions.Configuration;
@@ -26,16 +25,10 @@ public Startup(IHostingEnvironment env)
2625
public virtual void ConfigureServices(IServiceCollection services)
2726
{
2827
var mvcBuilder = services.AddMvcCore();
29-
services.AddJsonApi(opt =>
30-
{
31-
opt.BuildContextGraph(builder =>
32-
{
33-
builder.AddResource<Report>("reports");
34-
});
35-
opt.Namespace = "api";
36-
}, mvcBuilder);
37-
38-
services.AddScoped<IGetAllService<Report>, ReportService>();
28+
services.AddJsonApi(
29+
opt => opt.Namespace = "api",
30+
mvcBuilder,
31+
discovery => discovery.AddCurrentAssemblyServices());
3932
}
4033

4134
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)

src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs

+53-16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Reflection;
55
using JsonApiDotNetCore.Extensions;
6+
using JsonApiDotNetCore.Graph;
67
using JsonApiDotNetCore.Internal;
78
using JsonApiDotNetCore.Models;
89
using Microsoft.EntityFrameworkCore;
@@ -32,13 +33,27 @@ public interface IContextGraphBuilder
3233
/// <param name="pluralizedTypeName">The pluralized name that should be exposed by the API</param>
3334
IContextGraphBuilder AddResource<TResource, TId>(string pluralizedTypeName) where TResource : class, IIdentifiable<TId>;
3435

36+
/// <summary>
37+
/// Add a json:api resource
38+
/// </summary>
39+
/// <param name="entityType">The resource model type</param>
40+
/// <param name="idType">The resource model identifier type</param>
41+
/// <param name="pluralizedTypeName">The pluralized name that should be exposed by the API</param>
42+
IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName);
43+
3544
/// <summary>
3645
/// Add all the models that are part of the provided <see cref="DbContext" />
3746
/// that also implement <see cref="IIdentifiable"/>
3847
/// </summary>
3948
/// <typeparam name="T">The <see cref="DbContext"/> implementation type.</typeparam>
4049
IContextGraphBuilder AddDbContext<T>() where T : DbContext;
4150

51+
/// <summary>
52+
/// Specify the <see cref="IResourceNameFormatter"/> used to format resource names.
53+
/// </summary>
54+
/// <param name="resourceNameFormatter">Formatter used to define exposed resource names by convention.</param>
55+
IContextGraphBuilder UseNameFormatter(IResourceNameFormatter resourceNameFormatter);
56+
4257
/// <summary>
4358
/// Which links to include. Defaults to <see cref="Link.All"/>.
4459
/// </summary>
@@ -51,6 +66,8 @@ public class ContextGraphBuilder : IContextGraphBuilder
5166
private List<ValidationResult> _validationResults = new List<ValidationResult>();
5267

5368
private bool _usesDbContext;
69+
private IResourceNameFormatter _resourceNameFormatter = new DefaultResourceNameFormatter();
70+
5471
public Link DocumentLinks { get; set; } = Link.All;
5572

5673
public IContextGraph Build()
@@ -62,16 +79,20 @@ public IContextGraph Build()
6279
return graph;
6380
}
6481

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

86+
/// <inheritdoc />
6887
public IContextGraphBuilder AddResource<TResource, TId>(string pluralizedTypeName) where TResource : class, IIdentifiable<TId>
69-
{
70-
var entityType = typeof(TResource);
88+
=> AddResource(typeof(TResource), typeof(TId), pluralizedTypeName);
7189

90+
/// <inheritdoc />
91+
public IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName)
92+
{
7293
AssertEntityIsNotAlreadyDefined(entityType);
7394

74-
_entities.Add(GetEntity(pluralizedTypeName, entityType, typeof(TId)));
95+
_entities.Add(GetEntity(pluralizedTypeName, entityType, idType));
7596

7697
return this;
7798
}
@@ -142,6 +163,7 @@ protected virtual Type GetRelationshipType(RelationshipAttribute relation, Prope
142163

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

166+
/// <inheritdoc />
145167
public IContextGraphBuilder AddDbContext<T>() where T : DbContext
146168
{
147169
_usesDbContext = true;
@@ -164,30 +186,38 @@ public IContextGraphBuilder AddDbContext<T>() where T : DbContext
164186
var (isJsonApiResource, idType) = GetIdType(entityType);
165187

166188
if (isJsonApiResource)
167-
_entities.Add(GetEntity(GetResourceName(property), entityType, idType));
189+
_entities.Add(GetEntity(GetResourceNameFromDbSetProperty(property, entityType), entityType, idType));
168190
}
169191
}
170192

171193
return this;
172194
}
173195

174-
private string GetResourceName(PropertyInfo property)
196+
private string GetResourceNameFromDbSetProperty(PropertyInfo property, Type resourceType)
175197
{
176-
var resourceAttribute = property.GetCustomAttribute(typeof(ResourceAttribute));
177-
if (resourceAttribute == null)
178-
return property.Name.Dasherize();
179-
180-
return ((ResourceAttribute)resourceAttribute).ResourceName;
198+
// this check is actually duplicated in the DefaultResourceNameFormatter
199+
// however, we perform it here so that we allow class attributes to be prioritized over
200+
// the DbSet attribute. Eventually, the DbSet attribute should be deprecated.
201+
//
202+
// check the class definition first
203+
// [Resource("models"] public class Model : Identifiable { /* ... */ }
204+
if (resourceType.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute classResourceAttribute)
205+
return classResourceAttribute.ResourceName;
206+
207+
// check the DbContext member next
208+
// [Resource("models")] public DbSet<Model> Models { get; set; }
209+
if (property.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute resourceAttribute)
210+
return resourceAttribute.ResourceName;
211+
212+
// fallback to dsherized...this should actually check for a custom IResourceNameFormatter
213+
return _resourceNameFormatter.FormatResourceName(resourceType);
181214
}
182215

183216
private (bool isJsonApiResource, Type idType) GetIdType(Type resourceType)
184217
{
185-
var interfaces = resourceType.GetInterfaces();
186-
foreach (var type in interfaces)
187-
{
188-
if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(IIdentifiable<>))
189-
return (true, type.GetGenericArguments()[0]);
190-
}
218+
var possible = TypeLocator.GetIdType(resourceType);
219+
if (possible.isJsonApiResource)
220+
return possible;
191221

192222
_validationResults.Add(new ValidationResult(LogLevel.Warning, $"{resourceType} does not implement 'IIdentifiable<>'. "));
193223

@@ -199,5 +229,12 @@ private void AssertEntityIsNotAlreadyDefined(Type entityType)
199229
if (_entities.Any(e => e.EntityType == entityType))
200230
throw new InvalidOperationException($"Cannot add entity type {entityType} to context graph, there is already an entity of that type configured.");
201231
}
232+
233+
/// <inheritdoc />
234+
public IContextGraphBuilder UseNameFormatter(IResourceNameFormatter resourceNameFormatter)
235+
{
236+
_resourceNameFormatter = resourceNameFormatter;
237+
return this;
238+
}
202239
}
203240
}

src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

+19-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using JsonApiDotNetCore.Configuration;
44
using JsonApiDotNetCore.Data;
55
using JsonApiDotNetCore.Formatters;
6+
using JsonApiDotNetCore.Graph;
67
using JsonApiDotNetCore.Internal;
78
using JsonApiDotNetCore.Internal.Generics;
89
using JsonApiDotNetCore.Middleware;
@@ -35,9 +36,10 @@ public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection se
3536
return AddJsonApi<TContext>(services, options, mvcBuilder);
3637
}
3738

38-
public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection services,
39-
Action<JsonApiOptions> options,
40-
IMvcCoreBuilder mvcBuilder) where TContext : DbContext
39+
public static IServiceCollection AddJsonApi<TContext>(
40+
this IServiceCollection services,
41+
Action<JsonApiOptions> options,
42+
IMvcCoreBuilder mvcBuilder) where TContext : DbContext
4143
{
4244
var config = new JsonApiOptions();
4345

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

54-
public static IServiceCollection AddJsonApi(this IServiceCollection services,
55-
Action<JsonApiOptions> options,
56-
IMvcCoreBuilder mvcBuilder)
56+
public static IServiceCollection AddJsonApi(
57+
this IServiceCollection services,
58+
Action<JsonApiOptions> configureOptions,
59+
IMvcCoreBuilder mvcBuilder,
60+
Action<ServiceDiscoveryFacade> autoDiscover = null)
5761
{
5862
var config = new JsonApiOptions();
63+
configureOptions(config);
5964

60-
options(config);
65+
if(autoDiscover != null)
66+
{
67+
var facade = new ServiceDiscoveryFacade(services, config.ContextGraphBuilder);
68+
autoDiscover(facade);
69+
}
6170

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

@@ -88,6 +97,9 @@ public static void AddJsonApiInternals(
8897
this IServiceCollection services,
8998
JsonApiOptions jsonApiOptions)
9099
{
100+
if (jsonApiOptions.ContextGraph == null)
101+
jsonApiOptions.ContextGraph = jsonApiOptions.ContextGraphBuilder.Build();
102+
91103
if (jsonApiOptions.ContextGraph.UsesDbContext == false)
92104
{
93105
services.AddScoped<DbContext>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
using System.Linq;
3+
using System.Reflection;
4+
using Humanizer;
5+
using JsonApiDotNetCore.Models;
6+
using str = JsonApiDotNetCore.Extensions.StringExtensions;
7+
8+
9+
namespace JsonApiDotNetCore.Graph
10+
{
11+
/// <summary>
12+
/// Provides an interface for formatting resource names by convention
13+
/// </summary>
14+
public interface IResourceNameFormatter
15+
{
16+
/// <summary>
17+
/// Get the publicly visible resource name from the internal type name
18+
/// </summary>
19+
string FormatResourceName(Type resourceType);
20+
}
21+
22+
public class DefaultResourceNameFormatter : IResourceNameFormatter
23+
{
24+
/// <summary>
25+
/// Uses the internal type name to determine the external resource name.
26+
/// By default we us Humanizer for pluralization and then we dasherize the name.
27+
/// </summary>
28+
/// <example>
29+
/// <code>
30+
/// _default.FormatResourceName(typeof(TodoItem)).Dump();
31+
/// // > "todo-items"
32+
/// </code>
33+
/// </example>
34+
public string FormatResourceName(Type type)
35+
{
36+
try
37+
{
38+
// check the class definition first
39+
// [Resource("models"] public class Model : Identifiable { /* ... */ }
40+
if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute)
41+
return attribute.ResourceName;
42+
43+
return str.Dasherize(type.Name.Pluralize());
44+
}
45+
catch (InvalidOperationException e)
46+
{
47+
throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", e);
48+
}
49+
}
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
3+
namespace JsonApiDotNetCore.Graph
4+
{
5+
internal struct ResourceDescriptor
6+
{
7+
public ResourceDescriptor(Type resourceType, Type idType)
8+
{
9+
ResourceType = resourceType;
10+
IdType = idType;
11+
}
12+
13+
public Type ResourceType { get; set; }
14+
public Type IdType { get; set; }
15+
}
16+
}

0 commit comments

Comments
 (0)