Skip to content

Commit e7c7706

Browse files
committed
first pass at auto-discovery
1 parent 9e72a38 commit e7c7706

13 files changed

+357
-56
lines changed

Diff for: 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

54
namespace JsonApiDotNetCoreExample.Data
@@ -28,13 +27,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
2827

2928
public DbSet<TodoItem> TodoItems { get; set; }
3029
public DbSet<Person> People { get; set; }
31-
32-
[Resource("todo-collections")]
3330
public DbSet<TodoItemCollection> TodoItemCollections { get; set; }
34-
35-
[Resource("camelCasedModels")]
3631
public DbSet<CamelCasedModel> CamelCasedModels { get; set; }
37-
3832
public DbSet<Article> Articles { get; set; }
3933
public DbSet<Author> Authors { get; set; }
4034
public DbSet<NonJsonApiResource> NonJsonApiResources { get; set; }

Diff for: 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")]

Diff for: 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+
}

Diff for: 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

Diff for: 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)

Diff for: src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs

+16-9
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,6 +33,14 @@ 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"/>
@@ -66,12 +75,13 @@ public IContextGraphBuilder AddResource<TResource>(string pluralizedTypeName) wh
6675
=> AddResource<TResource, int>(pluralizedTypeName);
6776

6877
public IContextGraphBuilder AddResource<TResource, TId>(string pluralizedTypeName) where TResource : class, IIdentifiable<TId>
69-
{
70-
var entityType = typeof(TResource);
78+
=> AddResource(typeof(TResource), typeof(TId), pluralizedTypeName);
7179

80+
public IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName)
81+
{
7282
AssertEntityIsNotAlreadyDefined(entityType);
7383

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

7686
return this;
7787
}
@@ -182,12 +192,9 @@ private string GetResourceName(PropertyInfo property)
182192

183193
private (bool isJsonApiResource, Type idType) GetIdType(Type resourceType)
184194
{
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-
}
195+
var possible = TypeLocator.GetIdType(resourceType);
196+
if (possible.isJsonApiResource)
197+
return possible;
191198

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

Diff for: src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

+24-14
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;
@@ -34,18 +35,18 @@ public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection se
3435
return AddJsonApi<TContext>(services, options, mvcBuilder);
3536
}
3637

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

4345
options(config);
4446

4547
config.BuildContextGraph(builder => builder.AddDbContext<TContext>());
4648

47-
mvcBuilder
48-
.AddMvcOptions(opt =>
49+
mvcBuilder.AddMvcOptions(opt =>
4950
{
5051
opt.Filters.Add(typeof(JsonApiExceptionFilter));
5152
opt.SerializeAsJsonApi(config);
@@ -55,22 +56,28 @@ public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection se
5556
return services;
5657
}
5758

58-
public static IServiceCollection AddJsonApi(this IServiceCollection services,
59-
Action<JsonApiOptions> options,
60-
IMvcCoreBuilder mvcBuilder)
59+
public static IServiceCollection AddJsonApi(
60+
this IServiceCollection services,
61+
Action<JsonApiOptions> configureOptions,
62+
IMvcCoreBuilder mvcBuilder,
63+
Action<ServiceDiscoveryFacade> autoDiscover = null)
6164
{
62-
var config = new JsonApiOptions();
65+
var options = new JsonApiOptions();
66+
configureOptions(options);
6367

64-
options(config);
68+
if(autoDiscover != null)
69+
{
70+
var facade = new ServiceDiscoveryFacade(services, options.ContextGraphBuilder);
71+
autoDiscover(facade);
72+
}
6573

66-
mvcBuilder
67-
.AddMvcOptions(opt =>
74+
mvcBuilder.AddMvcOptions(opt =>
6875
{
6976
opt.Filters.Add(typeof(JsonApiExceptionFilter));
70-
opt.SerializeAsJsonApi(config);
77+
opt.SerializeAsJsonApi(options);
7178
});
7279

73-
AddJsonApiInternals(services, config);
80+
AddJsonApiInternals(services, options);
7481
return services;
7582
}
7683

@@ -90,6 +97,9 @@ public static void AddJsonApiInternals(
9097
this IServiceCollection services,
9198
JsonApiOptions jsonApiOptions)
9299
{
100+
if (jsonApiOptions.ContextGraph == null)
101+
jsonApiOptions.ContextGraph = jsonApiOptions.ContextGraphBuilder.Build();
102+
93103
if (jsonApiOptions.ContextGraph.UsesDbContext == false)
94104
{
95105
services.AddScoped<DbContext>();
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
var attribute = type.GetCustomAttributes(typeof(ResourceAttribute)).SingleOrDefault() as ResourceAttribute;
39+
if (attribute != null)
40+
return attribute.ResourceName;
41+
42+
return str.Dasherize(type.Name.Pluralize());
43+
}
44+
catch (InvalidOperationException e)
45+
{
46+
throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", e);
47+
}
48+
}
49+
}
50+
}

Diff for: src/JsonApiDotNetCore/Graph/ResourceDescriptor.cs

+16
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)