Skip to content

Commit 3e982ec

Browse files
author
Bart Koelman
authored
Native many to many (#1037)
1 parent 99cb571 commit 3e982ec

File tree

84 files changed

+676
-1775
lines changed

Some content is hidden

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

84 files changed

+676
-1775
lines changed

JsonApiDotNetCore.sln

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{02
1818
EndProject
1919
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{076E1AE4-FD25-4684-B826-CAAE37FEA0AA}"
2020
EndProject
21+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{067FFD7A-C66B-473D-8471-37F5C95DF61C}"
22+
EndProject
2123
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCoreExampleTests", "test\JsonApiDotNetCoreExampleTests\JsonApiDotNetCoreExampleTests.csproj", "{CAF331F8-9255-4D72-A1A8-A54141E99F1E}"
2224
EndProject
2325
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoEntityFrameworkTests", "test\NoEntityFrameworkTests\NoEntityFrameworkTests.csproj", "{4F15A8F8-5BC6-45A1-BC51-03F921B726A4}"
@@ -36,8 +38,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReportsExample", "src\Examp
3638
EndProject
3739
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCore", "src\JsonApiDotNetCore\JsonApiDotNetCore.csproj", "{21D27239-138D-4604-8E49-DCBE41BCE4C8}"
3840
EndProject
39-
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{067FFD7A-C66B-473D-8471-37F5C95DF61C}"
40-
EndProject
4141
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiDbContextExample", "src\Examples\MultiDbContextExample\MultiDbContextExample.csproj", "{6CAFDDBE-00AB-4784-801B-AB419C3C3A26}"
4242
EndProject
4343
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiDbContextTests", "test\MultiDbContextTests\MultiDbContextTests.csproj", "{EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}"

docs/usage/resources/relationships.md

-27
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ public class TodoItem : Identifiable
2020

2121
The left side of this relationship is of type `TodoItem` (public name: "todoItems") and the right side is of type `Person` (public name: "persons").
2222

23-
2423
## HasMany
2524

2625
This exposes a to-many relationship.
@@ -35,32 +34,6 @@ public class Person : Identifiable
3534

3635
The left side of this relationship is of type `Person` (public name: "persons") and the right side is of type `TodoItem` (public name: "todoItems").
3736

38-
39-
## HasManyThrough
40-
41-
Earlier versions of Entity Framework Core (up to v5) [did not support](https://github.com/aspnet/EntityFrameworkCore/issues/1368) many-to-many relationships without a join entity.
42-
For this reason, we have decided to fill this gap by allowing applications to declare a relationship as `HasManyThrough`.
43-
JsonApiDotNetCore will expose this relationship to the client the same way as any other `HasMany` relationship.
44-
However, under the covers it will use the join type and Entity Framework Core's APIs to get and set the relationship.
45-
46-
```c#
47-
public class Article : Identifiable
48-
{
49-
// tells Entity Framework Core to ignore this property
50-
[NotMapped]
51-
52-
// tells JsonApiDotNetCore to use the join table below
53-
[HasManyThrough(nameof(ArticleTags))]
54-
public ICollection<Tag> Tags { get; set; }
55-
56-
// this is the Entity Framework Core navigation to the join table
57-
public ICollection<ArticleTag> ArticleTags { get; set; }
58-
}
59-
```
60-
61-
The left side of this relationship is of type `Article` (public name: "articles") and the right side is of type `Tag` (public name: "tags").
62-
63-
6437
## Name
6538

6639
There are two ways the exposed relationship name is determined:

src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs

-7
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,6 @@ public AppDbContext(DbContextOptions<AppDbContext> options)
1818

1919
protected override void OnModelCreating(ModelBuilder builder)
2020
{
21-
builder.Entity<TodoItemTag>()
22-
.HasKey(todoItemTag => new
23-
{
24-
todoItemTag.TodoItemId,
25-
todoItemTag.TagId
26-
});
27-
2821
// When deleting a person, un-assign him/her from existing todo items.
2922
builder.Entity<Person>()
3023
.HasMany(person => person.AssignedTodoItems)

src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using System.ComponentModel.DataAnnotations;
23
using JetBrains.Annotations;
34
using JsonApiDotNetCore.Resources;
@@ -12,5 +13,8 @@ public sealed class Tag : Identifiable
1213
[MinLength(1)]
1314
[Attr]
1415
public string Name { get; set; }
16+
17+
[HasMany]
18+
public ISet<TodoItem> TodoItems { get; set; }
1519
}
1620
}

src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.ComponentModel.DataAnnotations.Schema;
43
using JetBrains.Annotations;
54
using JsonApiDotNetCore.Resources;
65
using JsonApiDotNetCore.Resources.Annotations;
@@ -28,10 +27,7 @@ public sealed class TodoItem : Identifiable
2827
[HasOne]
2928
public Person Assignee { get; set; }
3029

31-
[NotMapped]
32-
[HasManyThrough(nameof(TodoItemTags))]
30+
[HasMany]
3331
public ISet<Tag> Tags { get; set; }
34-
35-
public ISet<TodoItemTag> TodoItemTags { get; set; }
3632
}
3733
}

src/Examples/JsonApiDotNetCoreExample/Models/TodoItemTag.cs

-14
This file was deleted.

src/JsonApiDotNetCore/Configuration/IResourceGraph.cs

+12-41
Original file line numberDiff line numberDiff line change
@@ -18,67 +18,38 @@ public interface IResourceGraph : IResourceContextProvider
1818
/// all exposed fields are returned.
1919
/// </summary>
2020
/// <typeparam name="TResource">
21-
/// The resource for which to retrieve fields.
21+
/// The resource type for which to retrieve fields.
2222
/// </typeparam>
2323
/// <param name="selector">
24-
/// Should be of the form: (TResource e) => new { e.Field1, e.Field2 }
24+
/// Should be of the form: (TResource r) => new { r.Field1, r.Field2 }
2525
/// </param>
26-
IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func<TResource, dynamic>> selector = null)
26+
IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func<TResource, dynamic>> selector)
2727
where TResource : class, IIdentifiable;
2828

2929
/// <summary>
30-
/// Gets all attributes for <typeparamref name="TResource" /> that are targeted by the selector. If no selector is provided, all exposed fields are
30+
/// Gets all attributes for <typeparamref name="TResource" /> that are targeted by the selector. If no selector is provided, all exposed attributes are
3131
/// returned.
3232
/// </summary>
3333
/// <typeparam name="TResource">
34-
/// The resource for which to retrieve attributes.
34+
/// The resource type for which to retrieve attributes.
3535
/// </typeparam>
3636
/// <param name="selector">
37-
/// Should be of the form: (TResource e) => new { e.Attribute1, e.Attribute2 }
37+
/// Should be of the form: (TResource r) => new { r.Attribute1, r.Attribute2 }
3838
/// </param>
39-
IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Func<TResource, dynamic>> selector = null)
39+
IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Func<TResource, dynamic>> selector)
4040
where TResource : class, IIdentifiable;
4141

4242
/// <summary>
43-
/// Gets all relationships for <typeparamref name="TResource" /> that are targeted by the selector. If no selector is provided, all exposed fields are
44-
/// returned.
43+
/// Gets all relationships for <typeparamref name="TResource" /> that are targeted by the selector. If no selector is provided, all exposed relationships
44+
/// are returned.
4545
/// </summary>
4646
/// <typeparam name="TResource">
47-
/// The resource for which to retrieve relationships.
47+
/// The resource type for which to retrieve relationships.
4848
/// </typeparam>
4949
/// <param name="selector">
50-
/// Should be of the form: (TResource e) => new { e.Relationship1, e.Relationship2 }
50+
/// Should be of the form: (TResource r) => new { r.Relationship1, r.Relationship2 }
5151
/// </param>
52-
IReadOnlyCollection<RelationshipAttribute> GetRelationships<TResource>(Expression<Func<TResource, dynamic>> selector = null)
52+
IReadOnlyCollection<RelationshipAttribute> GetRelationships<TResource>(Expression<Func<TResource, dynamic>> selector)
5353
where TResource : class, IIdentifiable;
54-
55-
/// <summary>
56-
/// Gets all exposed fields (attributes and relationships) for the specified type.
57-
/// </summary>
58-
/// <param name="type">
59-
/// The resource type. Must implement <see cref="IIdentifiable" />.
60-
/// </param>
61-
IReadOnlyCollection<ResourceFieldAttribute> GetFields(Type type);
62-
63-
/// <summary>
64-
/// Gets all exposed attributes for the specified type.
65-
/// </summary>
66-
/// <param name="type">
67-
/// The resource type. Must implement <see cref="IIdentifiable" />.
68-
/// </param>
69-
IReadOnlyCollection<AttrAttribute> GetAttributes(Type type);
70-
71-
/// <summary>
72-
/// Gets all exposed relationships for the specified type.
73-
/// </summary>
74-
/// <param name="type">
75-
/// The resource type. Must implement <see cref="IIdentifiable" />.
76-
/// </param>
77-
IReadOnlyCollection<RelationshipAttribute> GetRelationships(Type type);
78-
79-
/// <summary>
80-
/// Traverses the resource graph, looking for the inverse relationship of the specified <paramref name="relationship" />.
81-
/// </summary>
82-
RelationshipAttribute GetInverseRelationship(RelationshipAttribute relationship);
8354
}
8455
}

src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs

+20-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using System.Linq;
23
using JetBrains.Annotations;
34
using JsonApiDotNetCore.Repositories;
45
using JsonApiDotNetCore.Resources.Annotations;
@@ -9,7 +10,7 @@ namespace JsonApiDotNetCore.Configuration
910
{
1011
/// <inheritdoc />
1112
[PublicAPI]
12-
public class InverseNavigationResolver : IInverseNavigationResolver
13+
public sealed class InverseNavigationResolver : IInverseNavigationResolver
1314
{
1415
private readonly IResourceContextProvider _resourceContextProvider;
1516
private readonly IEnumerable<IDbContextResolver> _dbContextResolvers;
@@ -35,25 +36,37 @@ public void Resolve()
3536

3637
private void Resolve(DbContext dbContext)
3738
{
38-
foreach (ResourceContext resourceContext in _resourceContextProvider.GetResourceContexts())
39+
foreach (ResourceContext resourceContext in _resourceContextProvider.GetResourceContexts().Where(context => context.Relationships.Any()))
3940
{
4041
IEntityType entityType = dbContext.Model.FindEntityType(resourceContext.ResourceType);
4142

4243
if (entityType != null)
4344
{
44-
ResolveRelationships(resourceContext.Relationships, entityType);
45+
IDictionary<string, INavigationBase> navigationMap = GetNavigations(entityType);
46+
ResolveRelationships(resourceContext.Relationships, navigationMap);
4547
}
4648
}
4749
}
4850

49-
private void ResolveRelationships(IReadOnlyCollection<RelationshipAttribute> relationships, IEntityType entityType)
51+
private static IDictionary<string, INavigationBase> GetNavigations(IEntityType entityType)
52+
{
53+
// @formatter:wrap_chained_method_calls chop_always
54+
55+
return entityType.GetNavigations()
56+
.Cast<INavigationBase>()
57+
.Concat(entityType.GetSkipNavigations())
58+
.ToDictionary(navigation => navigation.Name);
59+
60+
// @formatter:wrap_chained_method_calls restore
61+
}
62+
63+
private void ResolveRelationships(IReadOnlyCollection<RelationshipAttribute> relationships, IDictionary<string, INavigationBase> navigationMap)
5064
{
5165
foreach (RelationshipAttribute relationship in relationships)
5266
{
53-
if (!(relationship is HasManyThroughAttribute))
67+
if (navigationMap.TryGetValue(relationship.Property.Name, out INavigationBase navigation))
5468
{
55-
INavigation inverseNavigation = entityType.FindNavigation(relationship.Property.Name)?.Inverse;
56-
relationship.InverseNavigationProperty = inverseNavigation?.PropertyInfo;
69+
relationship.InverseNavigationProperty = navigation.Inverse?.PropertyInfo;
5770
}
5871
}
5972
}

src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

+8-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using Microsoft.AspNetCore.Mvc.ModelBinding;
1919
using Microsoft.EntityFrameworkCore;
2020
using Microsoft.EntityFrameworkCore.Metadata;
21+
using Microsoft.EntityFrameworkCore.Metadata.Internal;
2122
using Microsoft.Extensions.DependencyInjection;
2223
using Microsoft.Extensions.DependencyInjection.Extensions;
2324
using Microsoft.Extensions.Logging;
@@ -51,7 +52,7 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv
5152
var loggerFactory = _intermediateProvider.GetRequiredService<ILoggerFactory>();
5253

5354
_resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory);
54-
_serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, loggerFactory);
55+
_serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, loggerFactory);
5556
}
5657

5758
/// <summary>
@@ -283,7 +284,12 @@ private void AddResourcesFromDbContext(DbContext dbContext, ResourceGraphBuilder
283284
{
284285
foreach (IEntityType entityType in dbContext.Model.GetEntityTypes())
285286
{
286-
builder.Add(entityType.ClrType);
287+
#pragma warning disable EF1001 // Internal EF Core API usage.
288+
if (entityType is not EntityType { IsImplicitlyCreatedJoinEntityType: true })
289+
#pragma warning restore EF1001 // Internal EF Core API usage.
290+
{
291+
builder.Add(entityType.ClrType);
292+
}
287293
}
288294
}
289295

0 commit comments

Comments
 (0)