Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit d5ee796

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

File tree

14 files changed

+434
-58
lines changed

14 files changed

+434
-58
lines changed

‎src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs

Lines changed: 0 additions & 6 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 8 additions & 14 deletions
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

Lines changed: 4 additions & 11 deletions
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

Lines changed: 53 additions & 16 deletions
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,TyperesourceType)
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

Lines changed: 19 additions & 7 deletions
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>();
Lines changed: 51 additions & 0 deletions
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+
}
Lines changed: 16 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /