Skip to content

Commit 04fd87d

Browse files
authored
Merge branch 'master' into new-docs
2 parents 062b208 + cbc37d1 commit 04fd87d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1666
-216
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.DS_Store
2+
13
## Ignore Visual Studio temporary files, build results, and
24
## files generated by popular Visual Studio add-ons.
35

Build.ps1

+10-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ $revision = "{0:D4}" -f [convert]::ToInt32($revision, 10)
2323

2424
dotnet restore
2525

26+
dotnet build ./src/Examples/GettingStarted/GettingStarted.csproj
27+
CheckLastExitCode
28+
2629
dotnet test ./test/UnitTests/UnitTests.csproj
2730
CheckLastExitCode
2831

@@ -35,7 +38,13 @@ CheckLastExitCode
3538
dotnet test ./test/OperationsExampleTests/OperationsExampleTests.csproj
3639
CheckLastExitCode
3740

38-
dotnet build .\src\JsonApiDotNetCore -c Release
41+
dotnet test ./test/ResourceEntitySeparationExampleTests/ResourceEntitySeparationExampleTests.csproj
42+
CheckLastExitCode
43+
44+
dotnet test ./test/DiscoveryTests/DiscoveryTests.csproj
45+
CheckLastExitCode
46+
47+
dotnet build ./src/JsonApiDotNetCore/JsonApiDotNetCore.csproj -c Release
3948
CheckLastExitCode
4049

4150
Write-Output "APPVEYOR_REPO_TAG: $env:APPVEYOR_REPO_TAG"

JsonApiDotnetCore.sln

+28-13
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResourceEntitySeparationExa
4747
EndProject
4848
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{DF9BFD82-D937-4907-B0B4-64670417115F}"
4949
EndProject
50+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscoveryTests", "test\DiscoveryTests\DiscoveryTests.csproj", "{09C0C8D8-B721-4955-8889-55CB149C3B5C}"
51+
EndProject
5052
Global
5153
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5254
Debug|Any CPU = Debug|Any CPU
@@ -189,18 +191,30 @@ Global
189191
{6DFA30D7-1679-4333-9779-6FB678E48EF5}.Release|x64.Build.0 = Release|Any CPU
190192
{6DFA30D7-1679-4333-9779-6FB678E48EF5}.Release|x86.ActiveCfg = Release|Any CPU
191193
{6DFA30D7-1679-4333-9779-6FB678E48EF5}.Release|x86.Build.0 = Release|Any CPU
192-
{DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
193-
{DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|Any CPU.Build.0 = Debug|Any CPU
194-
{DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|x64.ActiveCfg = Debug|Any CPU
195-
{DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|x64.Build.0 = Debug|Any CPU
196-
{DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|x86.ActiveCfg = Debug|Any CPU
197-
{DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|x86.Build.0 = Debug|Any CPU
198-
{DF9BFD82-D937-4907-B0B4-64670417115F}.Release|Any CPU.ActiveCfg = Release|Any CPU
199-
{DF9BFD82-D937-4907-B0B4-64670417115F}.Release|Any CPU.Build.0 = Release|Any CPU
200-
{DF9BFD82-D937-4907-B0B4-64670417115F}.Release|x64.ActiveCfg = Release|Any CPU
201-
{DF9BFD82-D937-4907-B0B4-64670417115F}.Release|x64.Build.0 = Release|Any CPU
202-
{DF9BFD82-D937-4907-B0B4-64670417115F}.Release|x86.ActiveCfg = Release|Any CPU
203-
{DF9BFD82-D937-4907-B0B4-64670417115F}.Release|x86.Build.0 = Release|Any CPU
194+
{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
195+
{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Debug|Any CPU.Build.0 = Debug|Any CPU
196+
{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Debug|x64.ActiveCfg = Debug|Any CPU
197+
{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Debug|x64.Build.0 = Debug|Any CPU
198+
{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Debug|x86.ActiveCfg = Debug|Any CPU
199+
{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Debug|x86.Build.0 = Debug|Any CPU
200+
{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|Any CPU.ActiveCfg = Release|Any CPU
201+
{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|Any CPU.Build.0 = Release|Any CPU
202+
{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|x64.ActiveCfg = Release|Any CPU
203+
{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|x64.Build.0 = Release|Any CPU
204+
{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|x86.ActiveCfg = Release|Any CPU
205+
{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71}.Release|x86.Build.0 = Release|Any CPU
206+
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
207+
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
208+
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|x64.ActiveCfg = Debug|Any CPU
209+
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|x64.Build.0 = Debug|Any CPU
210+
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|x86.ActiveCfg = Debug|Any CPU
211+
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|x86.Build.0 = Debug|Any CPU
212+
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
213+
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|Any CPU.Build.0 = Release|Any CPU
214+
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x64.ActiveCfg = Release|Any CPU
215+
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x64.Build.0 = Release|Any CPU
216+
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x86.ActiveCfg = Release|Any CPU
217+
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x86.Build.0 = Release|Any CPU
204218
EndGlobalSection
205219
GlobalSection(SolutionProperties) = preSolution
206220
HideSolutionNode = FALSE
@@ -219,7 +233,8 @@ Global
219233
{9CD2C116-D133-4FE4-97DA-A9FEAFF045F1} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
220234
{F4097194-9415-418A-AB4E-315C5D5466AF} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
221235
{6DFA30D7-1679-4333-9779-6FB678E48EF5} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
222-
{DF9BFD82-D937-4907-B0B4-64670417115F} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
236+
{9B2A5AD7-0BF4-472E-A1B4-8BB623FDEB71} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
237+
{09C0C8D8-B721-4955-8889-55CB149C3B5C} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
223238
EndGlobalSection
224239
GlobalSection(ExtensibilityGlobals) = postSolution
225240
SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4}

build.sh

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ dotnet test ./test/UnitTests/UnitTests.csproj
99
dotnet test ./test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj
1010
dotnet test ./test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj
1111
dotnet test ./test/OperationsExampleTests/OperationsExampleTests.csproj
12+
dotnet test ./test/ResourceEntitySeparationExampleTests/ResourceEntitySeparationExampleTests.csproj

src/Examples/GettingStarted/Data/SampleDbContext.cs

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using GettingStarted.Models;
2+
using GettingStarted.ResourceDefinitionExample;
23
using Microsoft.EntityFrameworkCore;
34

45
namespace GettingStarted
@@ -11,5 +12,6 @@ public SampleDbContext(DbContextOptions<SampleDbContext> options)
1112

1213
public DbSet<Article> Articles { get; set; }
1314
public DbSet<Person> People { get; set; }
15+
public DbSet<Model> Models { get; set; }
1416
}
1517
}

src/Examples/GettingStarted/Models/Article.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ public class Article : Identifiable
66
{
77
[Attr]
88
public string Title { get; set; }
9-
9+
1010
[HasOne]
1111
public Person Author { get; set; }
1212
public int AuthorId { get; set; }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using JsonApiDotNetCore.Models;
2+
3+
namespace GettingStarted.ResourceDefinitionExample
4+
{
5+
public class Model : Identifiable
6+
{
7+
[Attr]
8+
public string DontExpose { get; set; }
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Collections.Generic;
2+
using JsonApiDotNetCore.Models;
3+
4+
namespace GettingStarted.ResourceDefinitionExample
5+
{
6+
public class ModelDefinition : ResourceDefinition<Model>
7+
{
8+
// this allows POST / PATCH requests to set the value of a
9+
// property, but we don't include this value in the response
10+
// this might be used if the incoming value gets hashed or
11+
// encrypted prior to being persisted and this value should
12+
// never be sent back to the client
13+
protected override List<AttrAttribute> OutputAttrs()
14+
=> Remove(model => model.DontExpose);
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using GettingStarted.Models;
2+
using JsonApiDotNetCore.Controllers;
3+
using JsonApiDotNetCore.Services;
4+
5+
namespace GettingStarted.ResourceDefinitionExample
6+
{
7+
public class ModelsController : JsonApiController<Model>
8+
{
9+
public ModelsController(
10+
IJsonApiContext jsonApiContext,
11+
IResourceService<Model> resourceService)
12+
: base(jsonApiContext, resourceService)
13+
{ }
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using JsonApiDotNetCore.Controllers;
2+
using JsonApiDotNetCore.Services;
3+
using JsonApiDotNetCoreExample.Models;
4+
5+
namespace JsonApiDotNetCoreExample.Controllers
6+
{
7+
public class ArticlesController : JsonApiController<Article>
8+
{
9+
public ArticlesController(
10+
IJsonApiContext jsonApiContext,
11+
IResourceService<Article> resourceService)
12+
: base(jsonApiContext, resourceService)
13+
{ }
14+
}
15+
}

src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs

+6-3
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
3131
modelBuilder.Entity<CourseStudentEntity>()
3232
.HasOne(r => r.Course)
3333
.WithMany(c => c.Students)
34-
.HasForeignKey(r => r.CourseId)
35-
;
34+
.HasForeignKey(r => r.CourseId);
3635

3736
modelBuilder.Entity<CourseStudentEntity>()
3837
.HasOne(r => r.Student)
3938
.WithMany(s => s.Courses)
4039
.HasForeignKey(r => r.StudentId);
40+
41+
modelBuilder.Entity<ArticleTag>()
42+
.HasKey(bc => new { bc.ArticleId, bc.TagId });
4143
}
4244

4345
public DbSet<TodoItem> TodoItems { get; set; }
@@ -53,7 +55,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
5355
public DbSet<DepartmentEntity> Departments { get; set; }
5456
public DbSet<CourseStudentEntity> Registrations { get; set; }
5557
public DbSet<StudentEntity> Students { get; set; }
56-
5758
public DbSet<PersonRole> PersonRoles { get; set; }
59+
public DbSet<ArticleTag> ArticleTags { get; set; }
60+
public DbSet<Tag> Tags { get; set; }
5861
}
5962
}

src/Examples/JsonApiDotNetCoreExample/Models/Article.cs

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations.Schema;
13
using JsonApiDotNetCore.Models;
24

35
namespace JsonApiDotNetCoreExample.Models
@@ -10,5 +12,10 @@ public class Article : Identifiable
1012
[HasOne("author")]
1113
public Author Author { get; set; }
1214
public int AuthorId { get; set; }
15+
16+
[NotMapped]
17+
[HasManyThrough(nameof(ArticleTags))]
18+
public List<Tag> Tags { get; set; }
19+
public List<ArticleTag> ArticleTags { get; set; }
1320
}
1421
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace JsonApiDotNetCoreExample.Models
2+
{
3+
public class ArticleTag
4+
{
5+
public int ArticleId { get; set; }
6+
public Article Article { get; set; }
7+
8+
public int TagId { get; set; }
9+
public Tag Tag { get; set; }
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using JsonApiDotNetCore.Models;
2+
3+
namespace JsonApiDotNetCoreExample.Models
4+
{
5+
public class Tag : Identifiable
6+
{
7+
[Attr]
8+
public string Name { get; set; }
9+
}
10+
}

src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs

+67-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Linq;
45
using System.Reflection;
@@ -23,24 +24,36 @@ public interface IContextGraphBuilder
2324
/// Add a json:api resource
2425
/// </summary>
2526
/// <typeparam name="TResource">The resource model type</typeparam>
26-
/// <param name="pluralizedTypeName">The pluralized name that should be exposed by the API</param>
27-
IContextGraphBuilder AddResource<TResource>(string pluralizedTypeName) where TResource : class, IIdentifiable<int>;
27+
/// <param name="pluralizedTypeName">
28+
/// The pluralized name that should be exposed by the API.
29+
/// If nothing is specified, the configured name formatter will be used.
30+
/// See <see cref="JsonApiOptions.ResourceNameFormatter" />.
31+
/// </param>
32+
IContextGraphBuilder AddResource<TResource>(string pluralizedTypeName = null) where TResource : class, IIdentifiable<int>;
2833

2934
/// <summary>
3035
/// Add a json:api resource
3136
/// </summary>
3237
/// <typeparam name="TResource">The resource model type</typeparam>
3338
/// <typeparam name="TId">The resource model identifier type</typeparam>
34-
/// <param name="pluralizedTypeName">The pluralized name that should be exposed by the API</param>
35-
IContextGraphBuilder AddResource<TResource, TId>(string pluralizedTypeName) where TResource : class, IIdentifiable<TId>;
39+
/// <param name="pluralizedTypeName">
40+
/// The pluralized name that should be exposed by the API.
41+
/// If nothing is specified, the configured name formatter will be used.
42+
/// See <see cref="JsonApiOptions.ResourceNameFormatter" />.
43+
/// </param>
44+
IContextGraphBuilder AddResource<TResource, TId>(string pluralizedTypeName = null) where TResource : class, IIdentifiable<TId>;
3645

3746
/// <summary>
3847
/// Add a json:api resource
3948
/// </summary>
4049
/// <param name="entityType">The resource model type</param>
4150
/// <param name="idType">The resource model identifier type</param>
42-
/// <param name="pluralizedTypeName">The pluralized name that should be exposed by the API</param>
43-
IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName);
51+
/// <param name="pluralizedTypeName">
52+
/// The pluralized name that should be exposed by the API.
53+
/// If nothing is specified, the configured name formatter will be used.
54+
/// See <see cref="JsonApiOptions.ResourceNameFormatter" />.
55+
/// </param>
56+
IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName = null);
4457

4558
/// <summary>
4659
/// Add all the models that are part of the provided <see cref="DbContext" />
@@ -80,18 +93,20 @@ public IContextGraph Build()
8093
}
8194

8295
/// <inheritdoc />
83-
public IContextGraphBuilder AddResource<TResource>(string pluralizedTypeName) where TResource : class, IIdentifiable<int>
96+
public IContextGraphBuilder AddResource<TResource>(string pluralizedTypeName = null) where TResource : class, IIdentifiable<int>
8497
=> AddResource<TResource, int>(pluralizedTypeName);
8598

8699
/// <inheritdoc />
87-
public IContextGraphBuilder AddResource<TResource, TId>(string pluralizedTypeName) where TResource : class, IIdentifiable<TId>
100+
public IContextGraphBuilder AddResource<TResource, TId>(string pluralizedTypeName = null) where TResource : class, IIdentifiable<TId>
88101
=> AddResource(typeof(TResource), typeof(TId), pluralizedTypeName);
89102

90103
/// <inheritdoc />
91-
public IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName)
104+
public IContextGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName = null)
92105
{
93106
AssertEntityIsNotAlreadyDefined(entityType);
94107

108+
pluralizedTypeName = pluralizedTypeName ?? _resourceNameFormatter.FormatResourceName(entityType);
109+
95110
_entities.Add(GetEntity(pluralizedTypeName, entityType, idType));
96111

97112
return this;
@@ -152,7 +167,47 @@ protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
152167
attribute.InternalRelationshipName = prop.Name;
153168
attribute.Type = GetRelationshipType(attribute, prop);
154169
attributes.Add(attribute);
170+
171+
if(attribute is HasManyThroughAttribute hasManyThroughAttribute) {
172+
var throughProperty = properties.SingleOrDefault(p => p.Name == hasManyThroughAttribute.InternalThroughName);
173+
if(throughProperty == null)
174+
throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Type does not contain a property named '{hasManyThroughAttribute.InternalThroughName}'.");
175+
176+
if(throughProperty.PropertyType.Implements<IList>() == false)
177+
throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}.{throughProperty.Name}'. Property type does not implement IList.");
178+
179+
// assumption: the property should be a generic collection, e.g. List<ArticleTag>
180+
if(throughProperty.PropertyType.IsGenericType == false)
181+
throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Expected through entity to be a generic type, such as List<{prop.PropertyType}>.");
182+
183+
// Article → List<ArticleTag>
184+
hasManyThroughAttribute.ThroughProperty = throughProperty;
185+
186+
// ArticleTag
187+
hasManyThroughAttribute.ThroughType = throughProperty.PropertyType.GetGenericArguments()[0];
188+
189+
var throughProperties = hasManyThroughAttribute.ThroughType.GetProperties();
190+
191+
// ArticleTag.Article
192+
hasManyThroughAttribute.LeftProperty = throughProperties.SingleOrDefault(x => x.PropertyType == entityType)
193+
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {entityType}");
194+
195+
// ArticleTag.ArticleId
196+
var leftIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.LeftProperty.Name);
197+
hasManyThroughAttribute.LeftIdProperty = throughProperties.SingleOrDefault(x => x.Name == leftIdPropertyName)
198+
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {entityType} with name {leftIdPropertyName}");
199+
200+
// Article → ArticleTag.Tag
201+
hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.Type)
202+
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.Type}");
203+
204+
// ArticleTag.TagId
205+
var rightIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.RightProperty.Name);
206+
hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName)
207+
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {hasManyThroughAttribute.Type} with name {rightIdPropertyName}");
208+
}
155209
}
210+
156211
return attributes;
157212
}
158213

@@ -212,8 +267,9 @@ private string GetResourceNameFromDbSetProperty(PropertyInfo property, Type reso
212267
if (property.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute resourceAttribute)
213268
return resourceAttribute.ResourceName;
214269

215-
// fallback to dsherized...this should actually check for a custom IResourceNameFormatter
216-
return _resourceNameFormatter.FormatResourceName(resourceType);
270+
// fallback to the established convention using the DbSet Property.Name
271+
// e.g DbSet<FooBar> FooBars { get; set; } => "foo-bars"
272+
return _resourceNameFormatter.ApplyCasingConvention(property.Name);
217273
}
218274

219275
private (bool isJsonApiResource, Type idType) GetIdType(Type resourceType)

0 commit comments

Comments
 (0)