Skip to content

Commit 9613835

Browse files
committed
fix(ContextGraphBuilder): don't throw if DbContext contains non json:api resource
Also begins work for #170
1 parent e30f911 commit 9613835

File tree

9 files changed

+180
-44
lines changed

9 files changed

+180
-44
lines changed

Diff for: src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs

+2
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
3737

3838
public DbSet<Article> Articles { get; set; }
3939
public DbSet<Author> Authors { get; set; }
40+
41+
public DbSet<NonJsonApiResource> NonJsonApiResources { get; set; }
4042
}
4143
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace JsonApiDotNetCoreExample.Models
2+
{
3+
public class NonJsonApiResource
4+
{
5+
public int Id { get; set; }
6+
}
7+
}

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

+48-5
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,50 @@
66
using JsonApiDotNetCore.Internal;
77
using JsonApiDotNetCore.Models;
88
using Microsoft.EntityFrameworkCore;
9+
using Microsoft.Extensions.Logging;
910

1011
namespace JsonApiDotNetCore.Builders
1112
{
13+
public interface IContextGraphBuilder
14+
{
15+
/// <summary>
16+
/// Construct the <see cref="ContextGraph"/>
17+
/// </summary>
18+
IContextGraph Build();
19+
20+
/// <summary>
21+
/// Add a json:api resource
22+
/// </summary>
23+
/// <typeparam name="TResource">The resource model type</typeparam>
24+
/// <param name="pluralizedTypeName">The pluralized name that should be exposed by the API</param>
25+
IContextGraphBuilder AddResource<TResource>(string pluralizedTypeName) where TResource : class, IIdentifiable<int>;
26+
27+
/// <summary>
28+
/// Add a json:api resource
29+
/// </summary>
30+
/// <typeparam name="TResource">The resource model type</typeparam>
31+
/// <typeparam name="TId">The resource model identifier type</typeparam>
32+
/// <param name="pluralizedTypeName">The pluralized name that should be exposed by the API</param>
33+
IContextGraphBuilder AddResource<TResource, TId>(string pluralizedTypeName) where TResource : class, IIdentifiable<TId>;
34+
35+
/// <summary>
36+
/// Add all the models that are part of the provided <see cref="DbContext" />
37+
/// that also implement <see cref="IIdentifiable"/>
38+
/// </summary>
39+
/// <typeparam name="T">The <see cref="DbContext"/> implementation type.</typeparam>
40+
IContextGraphBuilder AddDbContext<T>() where T : DbContext;
41+
42+
/// <summary>
43+
/// Which links to include. Defaults to <see cref="Link.All"/>.
44+
/// </summary>
45+
Link DocumentLinks { get; set; }
46+
}
47+
1248
public class ContextGraphBuilder : IContextGraphBuilder
1349
{
1450
private List<ContextEntity> _entities = new List<ContextEntity>();
51+
private List<ValidationResult> _validationResults = new List<ValidationResult>();
52+
1553
private bool _usesDbContext;
1654
public Link DocumentLinks { get; set; } = Link.All;
1755

@@ -20,7 +58,7 @@ public IContextGraph Build()
2058
// this must be done at build so that call order doesn't matter
2159
_entities.ForEach(e => e.Links = GetLinkFlags(e.EntityType));
2260

23-
var graph = new ContextGraph(_entities, _usesDbContext);
61+
var graph = new ContextGraph(_entities, _usesDbContext, _validationResults);
2462

2563
return graph;
2664
}
@@ -117,7 +155,10 @@ public IContextGraphBuilder AddDbContext<T>() where T : DbContext
117155

118156
AssertEntityIsNotAlreadyDefined(entityType);
119157

120-
_entities.Add(GetEntity(GetResourceName(property), entityType, GetIdType(entityType)));
158+
var (isJsonApiResource, idType) = GetIdType(entityType);
159+
160+
if (isJsonApiResource)
161+
_entities.Add(GetEntity(GetResourceName(property), entityType, idType));
121162
}
122163
}
123164

@@ -133,16 +174,18 @@ private string GetResourceName(PropertyInfo property)
133174
return ((ResourceAttribute)resourceAttribute).ResourceName;
134175
}
135176

136-
private Type GetIdType(Type resourceType)
177+
private (bool isJsonApiResource, Type idType) GetIdType(Type resourceType)
137178
{
138179
var interfaces = resourceType.GetInterfaces();
139180
foreach (var type in interfaces)
140181
{
141182
if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(IIdentifiable<>))
142-
return type.GetGenericArguments()[0];
183+
return (true, type.GetGenericArguments()[0]);
143184
}
144185

145-
throw new ArgumentException("Type does not implement 'IIdentifiable<TId>'", nameof(resourceType));
186+
_validationResults.Add(new ValidationResult(LogLevel.Warning, $"{resourceType} does not implement 'IIdentifiable<>'. "));
187+
188+
return (false, null);
146189
}
147190

148191
private void AssertEntityIsNotAlreadyDefined(Type entityType)

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

-15
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,55 @@
1+
using JsonApiDotNetCore.Builders;
12
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Internal;
24
using JsonApiDotNetCore.Middleware;
35
using Microsoft.AspNetCore.Builder;
46
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.Extensions.Logging;
58

69
namespace JsonApiDotNetCore.Extensions
710
{
811
// ReSharper disable once InconsistentNaming
912
public static class IApplicationBuilderExtensions
1013
{
1114
public static IApplicationBuilder UseJsonApi(this IApplicationBuilder app, bool useMvc = true)
15+
{
16+
DisableDetailedErrorsIfProduction(app);
17+
LogContextGraphValidations(app);
18+
19+
app.UseMiddleware<RequestMiddleware>();
20+
21+
if (useMvc)
22+
app.UseMvc();
23+
24+
return app;
25+
}
26+
27+
private static void DisableDetailedErrorsIfProduction(IApplicationBuilder app)
1228
{
1329
var environment = (IHostingEnvironment)app.ApplicationServices.GetService(typeof(IHostingEnvironment));
1430

15-
if(environment.IsProduction())
31+
if (environment.IsProduction())
1632
{
1733
JsonApiOptions.DisableErrorStackTraces = true;
1834
JsonApiOptions.DisableErrorSource = true;
1935
}
36+
}
2037

21-
app.UseMiddleware<RequestMiddleware>();
22-
23-
if (useMvc)
24-
app.UseMvc();
38+
private static void LogContextGraphValidations(IApplicationBuilder app)
39+
{
40+
var logger = app.ApplicationServices.GetService(typeof(ILogger<ContextGraphBuilder>)) as ILogger;
41+
var contextGraph = app.ApplicationServices.GetService(typeof(IContextGraph)) as ContextGraph;
2542

26-
return app;
43+
if (logger != null && contextGraph != null)
44+
{
45+
contextGraph.ValidationResults.ForEach((v) =>
46+
logger.Log(
47+
v.LogLevel,
48+
new EventId(),
49+
v.Message,
50+
exception: null,
51+
formatter: (m, e) => m));
52+
}
2753
}
2854
}
2955
}

Diff for: src/JsonApiDotNetCore/Internal/ContextGraph.cs

+27-5
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,47 @@
44

55
namespace JsonApiDotNetCore.Internal
66
{
7+
public interface IContextGraph
8+
{
9+
object GetRelationship<TParent>(TParent entity, string relationshipName);
10+
string GetRelationshipName<TParent>(string relationshipName);
11+
ContextEntity GetContextEntity(string dbSetName);
12+
ContextEntity GetContextEntity(Type entityType);
13+
bool UsesDbContext { get; }
14+
}
15+
716
public class ContextGraph : IContextGraph
817
{
9-
private List<ContextEntity> _entities;
18+
internal List<ContextEntity> Entities { get; }
19+
internal List<ValidationResult> ValidationResults { get; }
1020

1121
public ContextGraph() { }
1222

1323
public ContextGraph(List<ContextEntity> entities, bool usesDbContext)
1424
{
15-
_entities = entities;
25+
Entities = entities;
26+
UsesDbContext = usesDbContext;
27+
ValidationResults = new List<ValidationResult>();
28+
}
29+
30+
// eventually, this is the planned public constructor
31+
// to avoid breaking changes, we will be leaving the original constructor in place
32+
// until the context graph validation process is completed
33+
// you can track progress on this issue here: https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/170
34+
internal ContextGraph(List<ContextEntity> entities, bool usesDbContext, List<ValidationResult> validationResults)
35+
{
36+
Entities = entities;
1637
UsesDbContext = usesDbContext;
38+
ValidationResults = validationResults;
1739
}
1840

1941
public bool UsesDbContext { get; }
2042

2143
public ContextEntity GetContextEntity(string entityName)
22-
=> _entities.SingleOrDefault(e => string.Equals(e.EntityName, entityName, StringComparison.OrdinalIgnoreCase));
44+
=> Entities.SingleOrDefault(e => string.Equals(e.EntityName, entityName, StringComparison.OrdinalIgnoreCase));
2345

2446
public ContextEntity GetContextEntity(Type entityType)
25-
=> _entities.SingleOrDefault(e => e.EntityType == entityType);
47+
=> Entities.SingleOrDefault(e => e.EntityType == entityType);
2648

2749
public object GetRelationship<TParent>(TParent entity, string relationshipName)
2850
{
@@ -41,7 +63,7 @@ public object GetRelationship<TParent>(TParent entity, string relationshipName)
4163
public string GetRelationshipName<TParent>(string relationshipName)
4264
{
4365
var entityType = typeof(TParent);
44-
return _entities
66+
return Entities
4567
.SingleOrDefault(e => e.EntityType == entityType)
4668
?.Relationships
4769
.SingleOrDefault(r => r.Is(relationshipName))

Diff for: src/JsonApiDotNetCore/Internal/IContextGraph.cs

-13
This file was deleted.

Diff for: src/JsonApiDotNetCore/Internal/ValidationResults.cs

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Microsoft.Extensions.Logging;
2+
3+
namespace JsonApiDotNetCore.Internal
4+
{
5+
internal class ValidationResult
6+
{
7+
public ValidationResult(LogLevel logLevel, string message)
8+
{
9+
LogLevel = logLevel;
10+
Message = message;
11+
}
12+
13+
public LogLevel LogLevel { get; set; }
14+
public string Message { get; set; }
15+
}
16+
}

Diff for: test/UnitTests/Internal/ContextGraphBuilder_Tests.cs

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using JsonApiDotNetCore.Builders;
2+
using JsonApiDotNetCore.Internal;
3+
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.Logging;
5+
using Xunit;
6+
7+
namespace UnitTests.Internal
8+
{
9+
public class ContextGraphBuilder_Tests
10+
{
11+
[Fact]
12+
public void AddDbContext_Does_Not_Throw_If_Context_Contains_Members_That_DoNot_Implement_IIdentifiable()
13+
{
14+
// arrange
15+
var contextGraphBuilder = new ContextGraphBuilder();
16+
17+
// act
18+
contextGraphBuilder.AddDbContext<TestContext>();
19+
var contextGraph = contextGraphBuilder.Build() as ContextGraph;
20+
21+
// assert
22+
Assert.Empty(contextGraph.Entities);
23+
}
24+
25+
[Fact]
26+
public void Adding_DbContext_Members_That_DoNot_Implement_IIdentifiable_Creates_Warning()
27+
{
28+
// arrange
29+
var contextGraphBuilder = new ContextGraphBuilder();
30+
31+
// act
32+
contextGraphBuilder.AddDbContext<TestContext>();
33+
var contextGraph = contextGraphBuilder.Build() as ContextGraph;
34+
35+
// assert
36+
Assert.Equal(1, contextGraph.ValidationResults.Count);
37+
Assert.Contains(contextGraph.ValidationResults, v => v.LogLevel == LogLevel.Warning);
38+
}
39+
40+
private class Foo { }
41+
42+
private class TestContext : DbContext
43+
{
44+
public DbSet<Foo> Foos { get; set; }
45+
}
46+
}
47+
48+
}

0 commit comments

Comments
 (0)