Skip to content

Commit 5941f19

Browse files
authored
Merge pull request #518 from json-api-dotnet/fix/reattachment
DefaultEntityRepository cleanup
2 parents 027566d + 52a452d commit 5941f19

28 files changed

+806
-206
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using JsonApiDotNetCore.Models;
2+
3+
namespace JsonApiDotNetCoreExample.Models
4+
{
5+
public class Passport : Identifiable
6+
{
7+
public virtual int? SocialSecurityNumber { get; set; }
8+
[HasOne("person")]
9+
public virtual Person Person { get; set; }
10+
}
11+
}

src/Examples/JsonApiDotNetCoreExample/Models/Person.cs

+4
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,9 @@ public Dictionary<string, object> GetMeta(IJsonApiContext context)
4545
{ "authors", new string[] { "Jared Nance" } }
4646
};
4747
}
48+
public int? PassportId { get; set; }
49+
[HasOne("passport")]
50+
public virtual Passport Passport { get; set; }
51+
4852
}
4953
}

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

+271-165
Large diffs are not rendered by default.

src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Threading.Tasks;
34
using JsonApiDotNetCore.Models;
@@ -14,6 +15,9 @@ public interface IEntityWriteRepository<TEntity, in TId>
1415
{
1516
Task<TEntity> CreateAsync(TEntity entity);
1617

18+
Task<TEntity> UpdateAsync(TEntity entity);
19+
20+
[Obsolete("Use overload UpdateAsync(TEntity updatedEntity): providing parameter ID does no longer add anything relevant")]
1721
Task<TEntity> UpdateAsync(TId id, TEntity entity);
1822

1923
Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds);

src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs

+3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Threading.Tasks;
45
using JsonApiDotNetCore.Models;
56
using Microsoft.EntityFrameworkCore;
7+
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
68
using Microsoft.EntityFrameworkCore.Infrastructure;
79
using Microsoft.EntityFrameworkCore.Storage;
810

@@ -32,6 +34,7 @@ public static bool EntityIsTracked(this DbContext context, IIdentifiable entity)
3234
return GetTrackedEntity(context, entity) != null;
3335
}
3436

37+
3538
/// <summary>
3639
/// Determines whether or not EF is already tracking an entity of the same Type and Id
3740
/// and returns that entity.

src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using JsonApiDotNetCore.Middleware;
55
using Microsoft.AspNetCore.Builder;
66
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.Extensions.DependencyInjection;
78
using Microsoft.Extensions.Logging;
89

910
namespace JsonApiDotNetCore.Extensions
@@ -21,6 +22,12 @@ public static IApplicationBuilder UseJsonApi(this IApplicationBuilder app, bool
2122
if (useMvc)
2223
app.UseMvc();
2324

25+
using (var scope = app.ApplicationServices.CreateScope())
26+
{
27+
var inverseRelationshipResolver = scope.ServiceProvider.GetService<IInverseRelationships>();
28+
inverseRelationshipResolver?.Resolve();
29+
}
30+
2431
return app;
2532
}
2633

src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

+3
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ public static void AddJsonApiInternals(
155155
services.AddScoped<IControllerContext, Services.ControllerContext>();
156156
services.AddScoped<IDocumentBuilderOptionsProvider, DocumentBuilderOptionsProvider>();
157157

158+
services.AddScoped<IInverseRelationships, InverseRelationships>();
159+
160+
158161
// services.AddScoped<IActionFilter, TypeMatchFilter>();
159162
}
160163

src/JsonApiDotNetCore/Extensions/TypeExtensions.cs

+36
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,42 @@ namespace JsonApiDotNetCore.Extensions
88
{
99
internal static class TypeExtensions
1010
{
11+
/// <summary>
12+
/// Extension to use the LINQ AddRange method on an IList
13+
/// </summary>
14+
public static void AddRange<T>(this IList list, IEnumerable<T> items)
15+
{
16+
if (list == null) throw new ArgumentNullException(nameof(list));
17+
if (items == null) throw new ArgumentNullException(nameof(items));
18+
19+
if (list is List<T>)
20+
{
21+
((List<T>)list).AddRange(items);
22+
}
23+
else
24+
{
25+
foreach (var item in items)
26+
{
27+
list.Add(item);
28+
}
29+
}
30+
}
31+
32+
33+
/// <summary>
34+
/// Extension to use the LINQ cast method in a non-generic way:
35+
/// <code>
36+
/// Type targetType = typeof(TEntity)
37+
/// ((IList)myList).Cast(targetType).
38+
/// </code>
39+
/// </summary>
40+
public static IEnumerable Cast(this IEnumerable source, Type type)
41+
{
42+
if (source == null) throw new ArgumentNullException(nameof(source));
43+
if (type == null) throw new ArgumentNullException(nameof(type));
44+
return TypeHelper.ConvertCollection(source.Cast<object>(), type);
45+
}
46+
1147
public static Type GetElementType(this IEnumerable enumerable)
1248
{
1349
var enumerableTypes = enumerable.GetType()

src/JsonApiDotNetCore/Formatters/JsonApiReader.cs

+56-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
2+
using System.Collections;
23
using System.IO;
34
using System.Threading.Tasks;
45
using JsonApiDotNetCore.Internal;
6+
using JsonApiDotNetCore.Models;
57
using JsonApiDotNetCore.Serialization;
68
using JsonApiDotNetCore.Services;
79
using Microsoft.AspNetCore.Mvc.Formatters;
@@ -37,7 +39,7 @@ public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
3739
{
3840
var body = GetRequestBody(context.HttpContext.Request.Body);
3941

40-
object model =null;
42+
object model = null;
4143

4244
if (_jsonApiContext.IsRelationshipPath)
4345
{
@@ -48,10 +50,29 @@ public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
4850
model = _deSerializer.Deserialize(body);
4951
}
5052

53+
5154
if (model == null)
5255
{
5356
_logger?.LogError("An error occurred while de-serializing the payload");
5457
}
58+
59+
if (context.HttpContext.Request.Method == "PATCH")
60+
{
61+
bool idMissing;
62+
if (model is IList list)
63+
{
64+
idMissing = CheckForId(list);
65+
}
66+
else
67+
{
68+
idMissing = CheckForId(model);
69+
}
70+
if (idMissing)
71+
{
72+
_logger?.LogError("Payload must include id attribute");
73+
throw new JsonApiException(400, "Payload must include id attribute");
74+
}
75+
}
5576
return InputFormatterResult.SuccessAsync(model);
5677
}
5778
catch (Exception ex)
@@ -62,6 +83,40 @@ public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
6283
}
6384
}
6485

86+
/// <summary> Checks if the deserialized payload has an ID included </summary
87+
private bool CheckForId(object model)
88+
{
89+
if (model == null) return false;
90+
if (model is ResourceObject ro)
91+
{
92+
if (string.IsNullOrEmpty(ro.Id)) return true;
93+
}
94+
else if (model is IIdentifiable identifiable)
95+
{
96+
if (string.IsNullOrEmpty(identifiable.StringId)) return true;
97+
}
98+
return false;
99+
}
100+
101+
/// <summary> Checks if the elements in the deserialized payload have an ID included </summary
102+
private bool CheckForId(IList modelList)
103+
{
104+
foreach (var model in modelList)
105+
{
106+
if (model == null) continue;
107+
if (model is ResourceObject ro)
108+
{
109+
if (string.IsNullOrEmpty(ro.Id)) return true;
110+
}
111+
else if (model is IIdentifiable identifiable)
112+
{
113+
if (string.IsNullOrEmpty(identifiable.StringId)) return true;
114+
}
115+
}
116+
return false;
117+
118+
}
119+
65120
private string GetRequestBody(Stream body)
66121
{
67122
using (var reader = new StreamReader(body))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using JsonApiDotNetCore.Data;
3+
using JsonApiDotNetCore.Models;
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.EntityFrameworkCore.Metadata;
6+
7+
namespace JsonApiDotNetCore.Internal
8+
{
9+
/// <summary>
10+
/// Responsible for populating the RelationshipAttribute InverseNavigation property.
11+
///
12+
/// This service is instantiated in the configure phase of the application.
13+
///
14+
/// When using a data access layer different from EF Core, and when using ResourceHooks
15+
/// that depend on the inverse navigation property (BeforeImplicitUpdateRelationship),
16+
/// you will need to override this service, or pass along the inverseNavigationProperty in
17+
/// the RelationshipAttribute.
18+
/// </summary>
19+
public interface IInverseRelationships
20+
{
21+
/// <summary>
22+
/// This method is called upon startup by JsonApiDotNetCore. It should
23+
/// deal with resolving the inverse relationships.
24+
/// </summary>
25+
void Resolve();
26+
}
27+
28+
/// <inheritdoc />
29+
public class InverseRelationships : IInverseRelationships
30+
{
31+
private readonly ResourceGraph _graph;
32+
private readonly IDbContextResolver _resolver;
33+
34+
public InverseRelationships(IResourceGraph graph, IDbContextResolver resolver = null)
35+
{
36+
_graph = (ResourceGraph)graph;
37+
_resolver = resolver;
38+
}
39+
40+
/// <inheritdoc />
41+
public void Resolve()
42+
{
43+
if (EntityFrameworkCoreIsEnabled())
44+
{
45+
DbContext context = _resolver.GetContext();
46+
47+
foreach (ContextEntity ce in _graph.Entities)
48+
{
49+
IEntityType meta = context.Model.FindEntityType(ce.EntityType);
50+
if (meta == null) continue;
51+
foreach (var attr in ce.Relationships)
52+
{
53+
if (attr is HasManyThroughAttribute) continue;
54+
INavigation inverseNavigation = meta.FindNavigation(attr.InternalRelationshipName)?.FindInverse();
55+
attr.InverseNavigation = inverseNavigation?.Name;
56+
}
57+
}
58+
}
59+
}
60+
61+
/// <summary>
62+
/// If EF Core is not being used, we're expecting the resolver to not be registered.
63+
/// </summary>
64+
/// <returns><c>true</c>, if entity framework core was enabled, <c>false</c> otherwise.</returns>
65+
/// <param name="resolver">Resolver.</param>
66+
private bool EntityFrameworkCoreIsEnabled()
67+
{
68+
return _resolver != null;
69+
}
70+
}
71+
}

src/JsonApiDotNetCore/Models/HasManyAttribute.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ public class HasManyAttribute : RelationshipAttribute
2424
/// </code>
2525
///
2626
/// </example>
27-
public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string mappedBy = null)
27+
public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string mappedBy = null, string inverseNavigationProperty = null)
2828
: base(publicName, documentLinks, canInclude, mappedBy)
29-
{ }
29+
{
30+
InverseNavigation = inverseNavigationProperty;
31+
}
3032

3133
/// <summary>
3234
/// Sets the value of the property identified by this attribute

src/JsonApiDotNetCore/Models/HasOneAttribute.cs

+8-8
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ public class HasOneAttribute : RelationshipAttribute
2828
/// </code>
2929
///
3030
/// </example>
31-
public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null, string mappedBy = null)
31+
public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null, string mappedBy = null, string inverseNavigationProperty = null)
3232
: base(publicName, documentLinks, canInclude, mappedBy)
3333
{
3434
_explicitIdentifiablePropertyName = withForeignKey;
35+
InverseNavigation = inverseNavigationProperty;
3536
}
3637

3738
private readonly string _explicitIdentifiablePropertyName;
@@ -50,14 +51,13 @@ public HasOneAttribute(string publicName = null, Link documentLinks = Link.All,
5051
/// <param name="newValue">The new property value</param>
5152
public override void SetValue(object resource, object newValue)
5253
{
53-
var propertyName = (newValue?.GetType() == Type)
54-
? InternalRelationshipName
55-
: IdentifiablePropertyName;
56-
57-
var propertyInfo = resource
58-
.GetType()
59-
.GetProperty(propertyName);
54+
string propertyName = InternalRelationshipName;
55+
// if we're deleting the relationship (setting it to null),
56+
// we set the foreignKey to null. We could also set the actual property to null,
57+
// but then we would first need to load the current relationship, which requires an extra query.
58+
if (newValue == null) propertyName = IdentifiablePropertyName;
6059

60+
var propertyInfo = resource.GetType().GetProperty(propertyName);
6161
propertyInfo.SetValue(resource, newValue);
6262
}
6363

src/JsonApiDotNetCore/Models/RelationshipAttribute.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI
1515
}
1616

1717
public string PublicRelationshipName { get; internal set; }
18-
public string InternalRelationshipName { get; internal set; }
19-
18+
public string InternalRelationshipName { get; internal set; }
19+
public string InverseNavigation { get; internal set; }
20+
2021
/// <summary>
2122
/// The related entity type. This does not necessarily match the navigation property type.
2223
/// In the case of a HasMany relationship, this value will be the generic argument type.

src/JsonApiDotNetCore/Request/HasManyRelationshipPointers.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ namespace JsonApiDotNetCore.Request
3232
/// </summary>
3333
public class HasManyRelationshipPointers
3434
{
35-
private Dictionary<RelationshipAttribute, IList> _hasManyRelationships = new Dictionary<RelationshipAttribute, IList>();
35+
private readonly Dictionary<RelationshipAttribute, IList> _hasManyRelationships = new Dictionary<RelationshipAttribute, IList>();
3636

3737
/// <summary>
3838
/// Add the relationship to the list of relationships that should be

src/JsonApiDotNetCore/Request/HasOneRelationshipPointers.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,18 @@ namespace JsonApiDotNetCore.Request
2828
/// </summary>
2929
public class HasOneRelationshipPointers
3030
{
31-
private Dictionary<RelationshipAttribute, IIdentifiable> _hasOneRelationships = new Dictionary<RelationshipAttribute, IIdentifiable>();
31+
private readonly Dictionary<HasOneAttribute, IIdentifiable> _hasOneRelationships = new Dictionary<HasOneAttribute, IIdentifiable>();
3232

3333
/// <summary>
3434
/// Add the relationship to the list of relationships that should be
3535
/// set in the repository layer.
3636
/// </summary>
37-
public void Add(RelationshipAttribute relationship, IIdentifiable entity)
37+
public void Add(HasOneAttribute relationship, IIdentifiable entity)
3838
=> _hasOneRelationships[relationship] = entity;
3939

4040
/// <summary>
4141
/// Get all the models that should be associated
4242
/// </summary>
43-
public Dictionary<RelationshipAttribute, IIdentifiable> Get() => _hasOneRelationships;
43+
public Dictionary<HasOneAttribute, IIdentifiable> Get() => _hasOneRelationships;
4444
}
4545
}

0 commit comments

Comments
 (0)