Skip to content

Native many to many #1037

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Aug 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions JsonApiDotNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{02
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{076E1AE4-FD25-4684-B826-CAAE37FEA0AA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{067FFD7A-C66B-473D-8471-37F5C95DF61C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCoreExampleTests", "test\JsonApiDotNetCoreExampleTests\JsonApiDotNetCoreExampleTests.csproj", "{CAF331F8-9255-4D72-A1A8-A54141E99F1E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoEntityFrameworkTests", "test\NoEntityFrameworkTests\NoEntityFrameworkTests.csproj", "{4F15A8F8-5BC6-45A1-BC51-03F921B726A4}"
Expand All @@ -36,8 +38,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReportsExample", "src\Examp
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCore", "src\JsonApiDotNetCore\JsonApiDotNetCore.csproj", "{21D27239-138D-4604-8E49-DCBE41BCE4C8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{067FFD7A-C66B-473D-8471-37F5C95DF61C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiDbContextExample", "src\Examples\MultiDbContextExample\MultiDbContextExample.csproj", "{6CAFDDBE-00AB-4784-801B-AB419C3C3A26}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiDbContextTests", "test\MultiDbContextTests\MultiDbContextTests.csproj", "{EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}"
Expand Down
27 changes: 0 additions & 27 deletions docs/usage/resources/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ public class TodoItem : Identifiable

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


## HasMany

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

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


## HasManyThrough

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.
For this reason, we have decided to fill this gap by allowing applications to declare a relationship as `HasManyThrough`.
JsonApiDotNetCore will expose this relationship to the client the same way as any other `HasMany` relationship.
However, under the covers it will use the join type and Entity Framework Core's APIs to get and set the relationship.

```c#
public class Article : Identifiable
{
// tells Entity Framework Core to ignore this property
[NotMapped]

// tells JsonApiDotNetCore to use the join table below
[HasManyThrough(nameof(ArticleTags))]
public ICollection<Tag> Tags { get; set; }

// this is the Entity Framework Core navigation to the join table
public ICollection<ArticleTag> ArticleTags { get; set; }
}
```

The left side of this relationship is of type `Article` (public name: "articles") and the right side is of type `Tag` (public name: "tags").


## Name

There are two ways the exposed relationship name is determined:
Expand Down
7 changes: 0 additions & 7 deletions src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,6 @@ public AppDbContext(DbContextOptions<AppDbContext> options)

protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<TodoItemTag>()
.HasKey(todoItemTag => new
{
todoItemTag.TodoItemId,
todoItemTag.TagId
});

// When deleting a person, un-assign him/her from existing todo items.
builder.Entity<Person>()
.HasMany(person => person.AssignedTodoItems)
Expand Down
4 changes: 4 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using JetBrains.Annotations;
using JsonApiDotNetCore.Resources;
Expand All @@ -12,5 +13,8 @@ public sealed class Tag : Identifiable
[MinLength(1)]
[Attr]
public string Name { get; set; }

[HasMany]
public ISet<TodoItem> TodoItems { get; set; }
}
}
6 changes: 1 addition & 5 deletions src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using JetBrains.Annotations;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
Expand Down Expand Up @@ -28,10 +27,7 @@ public sealed class TodoItem : Identifiable
[HasOne]
public Person Assignee { get; set; }

[NotMapped]
[HasManyThrough(nameof(TodoItemTags))]
[HasMany]
public ISet<Tag> Tags { get; set; }

public ISet<TodoItemTag> TodoItemTags { get; set; }
}
}
14 changes: 0 additions & 14 deletions src/Examples/JsonApiDotNetCoreExample/Models/TodoItemTag.cs

This file was deleted.

53 changes: 12 additions & 41 deletions src/JsonApiDotNetCore/Configuration/IResourceGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,67 +18,38 @@ public interface IResourceGraph : IResourceContextProvider
/// all exposed fields are returned.
/// </summary>
/// <typeparam name="TResource">
/// The resource for which to retrieve fields.
/// The resource type for which to retrieve fields.
/// </typeparam>
/// <param name="selector">
/// Should be of the form: (TResource e) => new { e.Field1, e.Field2 }
/// Should be of the form: (TResource r) => new { r.Field1, r.Field2 }
/// </param>
IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func<TResource, dynamic>> selector = null)
IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func<TResource, dynamic>> selector)
where TResource : class, IIdentifiable;

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

/// <summary>
/// Gets all relationships for <typeparamref name="TResource" /> that are targeted by the selector. If no selector is provided, all exposed fields are
/// returned.
/// Gets all relationships for <typeparamref name="TResource" /> that are targeted by the selector. If no selector is provided, all exposed relationships
/// are returned.
/// </summary>
/// <typeparam name="TResource">
/// The resource for which to retrieve relationships.
/// The resource type for which to retrieve relationships.
/// </typeparam>
/// <param name="selector">
/// Should be of the form: (TResource e) => new { e.Relationship1, e.Relationship2 }
/// Should be of the form: (TResource r) => new { r.Relationship1, r.Relationship2 }
/// </param>
IReadOnlyCollection<RelationshipAttribute> GetRelationships<TResource>(Expression<Func<TResource, dynamic>> selector = null)
IReadOnlyCollection<RelationshipAttribute> GetRelationships<TResource>(Expression<Func<TResource, dynamic>> selector)
where TResource : class, IIdentifiable;

/// <summary>
/// Gets all exposed fields (attributes and relationships) for the specified type.
/// </summary>
/// <param name="type">
/// The resource type. Must implement <see cref="IIdentifiable" />.
/// </param>
IReadOnlyCollection<ResourceFieldAttribute> GetFields(Type type);

/// <summary>
/// Gets all exposed attributes for the specified type.
/// </summary>
/// <param name="type">
/// The resource type. Must implement <see cref="IIdentifiable" />.
/// </param>
IReadOnlyCollection<AttrAttribute> GetAttributes(Type type);

/// <summary>
/// Gets all exposed relationships for the specified type.
/// </summary>
/// <param name="type">
/// The resource type. Must implement <see cref="IIdentifiable" />.
/// </param>
IReadOnlyCollection<RelationshipAttribute> GetRelationships(Type type);

/// <summary>
/// Traverses the resource graph, looking for the inverse relationship of the specified <paramref name="relationship" />.
/// </summary>
RelationshipAttribute GetInverseRelationship(RelationshipAttribute relationship);
}
}
27 changes: 20 additions & 7 deletions src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using JsonApiDotNetCore.Repositories;
using JsonApiDotNetCore.Resources.Annotations;
Expand All @@ -9,7 +10,7 @@ namespace JsonApiDotNetCore.Configuration
{
/// <inheritdoc />
[PublicAPI]
public class InverseNavigationResolver : IInverseNavigationResolver
public sealed class InverseNavigationResolver : IInverseNavigationResolver
{
private readonly IResourceContextProvider _resourceContextProvider;
private readonly IEnumerable<IDbContextResolver> _dbContextResolvers;
Expand All @@ -35,25 +36,37 @@ public void Resolve()

private void Resolve(DbContext dbContext)
{
foreach (ResourceContext resourceContext in _resourceContextProvider.GetResourceContexts())
foreach (ResourceContext resourceContext in _resourceContextProvider.GetResourceContexts().Where(context => context.Relationships.Any()))
{
IEntityType entityType = dbContext.Model.FindEntityType(resourceContext.ResourceType);

if (entityType != null)
{
ResolveRelationships(resourceContext.Relationships, entityType);
IDictionary<string, INavigationBase> navigationMap = GetNavigations(entityType);
ResolveRelationships(resourceContext.Relationships, navigationMap);
}
}
}

private void ResolveRelationships(IReadOnlyCollection<RelationshipAttribute> relationships, IEntityType entityType)
private static IDictionary<string, INavigationBase> GetNavigations(IEntityType entityType)
{
// @formatter:wrap_chained_method_calls chop_always

return entityType.GetNavigations()
.Cast<INavigationBase>()
.Concat(entityType.GetSkipNavigations())
.ToDictionary(navigation => navigation.Name);

// @formatter:wrap_chained_method_calls restore
}

private void ResolveRelationships(IReadOnlyCollection<RelationshipAttribute> relationships, IDictionary<string, INavigationBase> navigationMap)
{
foreach (RelationshipAttribute relationship in relationships)
{
if (!(relationship is HasManyThroughAttribute))
if (navigationMap.TryGetValue(relationship.Property.Name, out INavigationBase navigation))
{
INavigation inverseNavigation = entityType.FindNavigation(relationship.Property.Name)?.Inverse;
relationship.InverseNavigationProperty = inverseNavigation?.PropertyInfo;
relationship.InverseNavigationProperty = navigation.Inverse?.PropertyInfo;
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -51,7 +52,7 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv
var loggerFactory = _intermediateProvider.GetRequiredService<ILoggerFactory>();

_resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory);
_serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, loggerFactory);
_serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, loggerFactory);
}

/// <summary>
Expand Down Expand Up @@ -283,7 +284,12 @@ private void AddResourcesFromDbContext(DbContext dbContext, ResourceGraphBuilder
{
foreach (IEntityType entityType in dbContext.Model.GetEntityTypes())
{
builder.Add(entityType.ClrType);
#pragma warning disable EF1001 // Internal EF Core API usage.
if (entityType is not EntityType { IsImplicitlyCreatedJoinEntityType: true })
#pragma warning restore EF1001 // Internal EF Core API usage.
{
builder.Add(entityType.ClrType);
}
}
}

Expand Down
Loading