Skip to content

DefaultEntityRepository cleanup #518

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 30 commits into from
Jun 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5f3e04b
refactor: AttachHasOnePointers
maurei Jun 7, 2019
e8d1096
refactor: AssignHasMany pointers
maurei Jun 7, 2019
1420d6d
refactor assign relationships and applied to updateasync method
maurei Jun 7, 2019
24dd97c
refactor: tests passing again
maurei Jun 7, 2019
0304725
test: add implicit removal test
maurei Jun 7, 2019
f60587f
test: all passing
maurei Jun 7, 2019
4cd9465
chore: addedd notes wrt implicit remove
maurei Jun 7, 2019
d1f39e6
comments: added comment to assign method
maurei Jun 7, 2019
bcbf8c3
Merge branch 'master' into fix/reattachment
maurei Jun 7, 2019
7df5233
fix: support for entity resource split
maurei Jun 7, 2019
0296eb4
fix: minor refactor, comments
maurei Jun 8, 2019
f0d5924
fix: foreignkey set null bug
maurei Jun 8, 2019
9f7550c
feat: decoupled repository from JsonApiContext with respect to updati…
maurei Jun 11, 2019
7aea60c
feat: decoupled IJsonApiContext from repository wrt updating resources
maurei Jun 11, 2019
652d65f
fix: resource separation issue|
maurei Jun 11, 2019
9838627
chore: cherry picked inverse relationships from hooks branch
maurei Jun 11, 2019
fbe69fc
fix: tests
maurei Jun 11, 2019
35a2f54
feat: implicit remove support
maurei Jun 11, 2019
6e6f7fa
fix: test
maurei Jun 11, 2019
c1d472d
fix: bugs with inverse relationship loading
maurei Jun 11, 2019
f45972f
tests: implicit remove through create tests
maurei Jun 11, 2019
d8b4217
feat: mark obsolete UpdateAsync(TId id, TEntity entity) method, add n…
maurei Jun 11, 2019
30765c3
fix: #520
maurei Jun 11, 2019
9139852
fix: separation tests
maurei Jun 11, 2019
457e93d
chore: comments
maurei Jun 12, 2019
65f8a3a
Update DefaultEntityRepository.cs
maurei Jun 12, 2019
3ddb6a2
Update DefaultEntityRepository.cs
maurei Jun 17, 2019
2437077
Update TypeExtensions.cs
maurei Jun 17, 2019
415306e
Update DefaultEntityRepository.cs
maurei Jun 17, 2019
52a452d
Update JsonApiReader.cs
maurei Jun 17, 2019
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
11 changes: 11 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCoreExample.Models
{
public class Passport : Identifiable
{
public virtual int? SocialSecurityNumber { get; set; }
[HasOne("person")]
public virtual Person Person { get; set; }
}
}
4 changes: 4 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Models/Person.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,9 @@ public Dictionary<string, object> GetMeta(IJsonApiContext context)
{ "authors", new string[] { "Jared Nance" } }
};
}
public int? PassportId { get; set; }
[HasOne("passport")]
public virtual Passport Passport { get; set; }

}
}
436 changes: 271 additions & 165 deletions src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using JsonApiDotNetCore.Models;
Expand All @@ -14,6 +15,9 @@ public interface IEntityWriteRepository<TEntity, in TId>
{
Task<TEntity> CreateAsync(TEntity entity);

Task<TEntity> UpdateAsync(TEntity entity);

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

Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds);
Expand Down
3 changes: 3 additions & 0 deletions src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using JsonApiDotNetCore.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;

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


/// <summary>
/// Determines whether or not EF is already tracking an entity of the same Type and Id
/// and returns that entity.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using JsonApiDotNetCore.Middleware;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

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

using (var scope = app.ApplicationServices.CreateScope())
{
var inverseRelationshipResolver = scope.ServiceProvider.GetService<IInverseRelationships>();
inverseRelationshipResolver?.Resolve();
}

return app;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ public static void AddJsonApiInternals(
services.AddScoped<IControllerContext, Services.ControllerContext>();
services.AddScoped<IDocumentBuilderOptionsProvider, DocumentBuilderOptionsProvider>();

services.AddScoped<IInverseRelationships, InverseRelationships>();


// services.AddScoped<IActionFilter, TypeMatchFilter>();
}

Expand Down
36 changes: 36 additions & 0 deletions src/JsonApiDotNetCore/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,42 @@ namespace JsonApiDotNetCore.Extensions
{
internal static class TypeExtensions
{
/// <summary>
/// Extension to use the LINQ AddRange method on an IList
/// </summary>
public static void AddRange<T>(this IList list, IEnumerable<T> items)
{
if (list == null) throw new ArgumentNullException(nameof(list));
if (items == null) throw new ArgumentNullException(nameof(items));

if (list is List<T>)
{
((List<T>)list).AddRange(items);
}
else
{
foreach (var item in items)
{
list.Add(item);
}
}
}


/// <summary>
/// Extension to use the LINQ cast method in a non-generic way:
/// <code>
/// Type targetType = typeof(TEntity)
/// ((IList)myList).Cast(targetType).
/// </code>
/// </summary>
public static IEnumerable Cast(this IEnumerable source, Type type)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (type == null) throw new ArgumentNullException(nameof(type));
return TypeHelper.ConvertCollection(source.Cast<object>(), type);
}

public static Type GetElementType(this IEnumerable enumerable)
{
var enumerableTypes = enumerable.GetType()
Expand Down
57 changes: 56 additions & 1 deletion src/JsonApiDotNetCore/Formatters/JsonApiReader.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections;
using System.IO;
using System.Threading.Tasks;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Serialization;
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Mvc.Formatters;
Expand Down Expand Up @@ -37,7 +39,7 @@ public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
var body = GetRequestBody(context.HttpContext.Request.Body);

object model =null;
object model = null;

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


if (model == null)
{
_logger?.LogError("An error occurred while de-serializing the payload");
}

if (context.HttpContext.Request.Method == "PATCH")
{
bool idMissing;
if (model is IList list)
{
idMissing = CheckForId(list);
}
else
{
idMissing = CheckForId(model);
}
if (idMissing)
{
_logger?.LogError("Payload must include id attribute");
throw new JsonApiException(400, "Payload must include id attribute");
}
}
return InputFormatterResult.SuccessAsync(model);
}
catch (Exception ex)
Expand All @@ -62,6 +83,40 @@ public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
}
}

/// <summary> Checks if the deserialized payload has an ID included </summary
private bool CheckForId(object model)
{
if (model == null) return false;
if (model is ResourceObject ro)
{
if (string.IsNullOrEmpty(ro.Id)) return true;
}
else if (model is IIdentifiable identifiable)
{
if (string.IsNullOrEmpty(identifiable.StringId)) return true;
}
return false;
}

/// <summary> Checks if the elements in the deserialized payload have an ID included </summary
private bool CheckForId(IList modelList)
{
foreach (var model in modelList)
{
if (model == null) continue;
if (model is ResourceObject ro)
{
if (string.IsNullOrEmpty(ro.Id)) return true;
}
else if (model is IIdentifiable identifiable)
{
if (string.IsNullOrEmpty(identifiable.StringId)) return true;
}
}
return false;

}

private string GetRequestBody(Stream body)
{
using (var reader = new StreamReader(body))
Expand Down
71 changes: 71 additions & 0 deletions src/JsonApiDotNetCore/Internal/InverseRelationships.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using JsonApiDotNetCore.Data;
using JsonApiDotNetCore.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace JsonApiDotNetCore.Internal
{
/// <summary>
/// Responsible for populating the RelationshipAttribute InverseNavigation property.
///
/// This service is instantiated in the configure phase of the application.
///
/// When using a data access layer different from EF Core, and when using ResourceHooks
/// that depend on the inverse navigation property (BeforeImplicitUpdateRelationship),
/// you will need to override this service, or pass along the inverseNavigationProperty in
/// the RelationshipAttribute.
/// </summary>
public interface IInverseRelationships
{
/// <summary>
/// This method is called upon startup by JsonApiDotNetCore. It should
/// deal with resolving the inverse relationships.
/// </summary>
void Resolve();
}

/// <inheritdoc />
public class InverseRelationships : IInverseRelationships
{
private readonly ResourceGraph _graph;
private readonly IDbContextResolver _resolver;

public InverseRelationships(IResourceGraph graph, IDbContextResolver resolver = null)
{
_graph = (ResourceGraph)graph;
_resolver = resolver;
}

/// <inheritdoc />
public void Resolve()
{
if (EntityFrameworkCoreIsEnabled())
{
DbContext context = _resolver.GetContext();

foreach (ContextEntity ce in _graph.Entities)
{
IEntityType meta = context.Model.FindEntityType(ce.EntityType);
if (meta == null) continue;
foreach (var attr in ce.Relationships)
{
if (attr is HasManyThroughAttribute) continue;
INavigation inverseNavigation = meta.FindNavigation(attr.InternalRelationshipName)?.FindInverse();
attr.InverseNavigation = inverseNavigation?.Name;
}
}
}
}

/// <summary>
/// If EF Core is not being used, we're expecting the resolver to not be registered.
/// </summary>
/// <returns><c>true</c>, if entity framework core was enabled, <c>false</c> otherwise.</returns>
/// <param name="resolver">Resolver.</param>
private bool EntityFrameworkCoreIsEnabled()
{
return _resolver != null;
}
}
}
6 changes: 4 additions & 2 deletions src/JsonApiDotNetCore/Models/HasManyAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ public class HasManyAttribute : RelationshipAttribute
/// </code>
///
/// </example>
public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string mappedBy = null)
public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string mappedBy = null, string inverseNavigationProperty = null)
: base(publicName, documentLinks, canInclude, mappedBy)
{ }
{
InverseNavigation = inverseNavigationProperty;
}

/// <summary>
/// Sets the value of the property identified by this attribute
Expand Down
16 changes: 8 additions & 8 deletions src/JsonApiDotNetCore/Models/HasOneAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ public class HasOneAttribute : RelationshipAttribute
/// </code>
///
/// </example>
public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null, string mappedBy = null)
public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null, string mappedBy = null, string inverseNavigationProperty = null)
: base(publicName, documentLinks, canInclude, mappedBy)
{
_explicitIdentifiablePropertyName = withForeignKey;
InverseNavigation = inverseNavigationProperty;
}

private readonly string _explicitIdentifiablePropertyName;
Expand All @@ -50,14 +51,13 @@ public HasOneAttribute(string publicName = null, Link documentLinks = Link.All,
/// <param name="newValue">The new property value</param>
public override void SetValue(object resource, object newValue)
{
var propertyName = (newValue?.GetType() == Type)
? InternalRelationshipName
: IdentifiablePropertyName;

var propertyInfo = resource
.GetType()
.GetProperty(propertyName);
string propertyName = InternalRelationshipName;
// if we're deleting the relationship (setting it to null),
// we set the foreignKey to null. We could also set the actual property to null,
// but then we would first need to load the current relationship, which requires an extra query.
if (newValue == null) propertyName = IdentifiablePropertyName;

var propertyInfo = resource.GetType().GetProperty(propertyName);
propertyInfo.SetValue(resource, newValue);
}

Expand Down
5 changes: 3 additions & 2 deletions src/JsonApiDotNetCore/Models/RelationshipAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI
}

public string PublicRelationshipName { get; internal set; }
public string InternalRelationshipName { get; internal set; }

public string InternalRelationshipName { get; internal set; }
public string InverseNavigation { get; internal set; }

/// <summary>
/// The related entity type. This does not necessarily match the navigation property type.
/// In the case of a HasMany relationship, this value will be the generic argument type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ namespace JsonApiDotNetCore.Request
/// </summary>
public class HasManyRelationshipPointers
{
private Dictionary<RelationshipAttribute, IList> _hasManyRelationships = new Dictionary<RelationshipAttribute, IList>();
private readonly Dictionary<RelationshipAttribute, IList> _hasManyRelationships = new Dictionary<RelationshipAttribute, IList>();

/// <summary>
/// Add the relationship to the list of relationships that should be
Expand Down
6 changes: 3 additions & 3 deletions src/JsonApiDotNetCore/Request/HasOneRelationshipPointers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,18 @@ namespace JsonApiDotNetCore.Request
/// </summary>
public class HasOneRelationshipPointers
{
private Dictionary<RelationshipAttribute, IIdentifiable> _hasOneRelationships = new Dictionary<RelationshipAttribute, IIdentifiable>();
private readonly Dictionary<HasOneAttribute, IIdentifiable> _hasOneRelationships = new Dictionary<HasOneAttribute, IIdentifiable>();

/// <summary>
/// Add the relationship to the list of relationships that should be
/// set in the repository layer.
/// </summary>
public void Add(RelationshipAttribute relationship, IIdentifiable entity)
public void Add(HasOneAttribute relationship, IIdentifiable entity)
=> _hasOneRelationships[relationship] = entity;

/// <summary>
/// Get all the models that should be associated
/// </summary>
public Dictionary<RelationshipAttribute, IIdentifiable> Get() => _hasOneRelationships;
public Dictionary<HasOneAttribute, IIdentifiable> Get() => _hasOneRelationships;
}
}
Loading