Skip to content

#312 Deserializer not linking included relationships #315

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 17 commits into from
Jun 27, 2018
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
13 changes: 8 additions & 5 deletions src/JsonApiDotNetCore/Builders/DocumentBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,14 @@ private ResourceIdentifierObject GetRelationship(object entity)
var objType = entity.GetType();
var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(objType);

return new ResourceIdentifierObject
{
Type = contextEntity.EntityName,
Id = ((IIdentifiable)entity).StringId
};
if(entity is IIdentifiable identifiableEntity)
return new ResourceIdentifierObject
{
Type = contextEntity.EntityName,
Id = identifiableEntity.StringId
};

return null;
}

private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttribute hasOne, IIdentifiable entity)
Expand Down
34 changes: 29 additions & 5 deletions src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,19 @@ public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshi

public virtual async Task<TEntity> CreateAsync(TEntity entity)
{
AttachHasManyPointers();
AttachRelationships();
_dbSet.Add(entity);

await _context.SaveChangesAsync();
return entity;
}

protected virtual void AttachRelationships()
{
AttachHasManyPointers();
AttachHasOnePointers();
}

/// <summary>
/// This is used to allow creation of HasMany relationships when the
/// dependent side of the relationship already exists.
Expand All @@ -107,6 +113,18 @@ private void AttachHasManyPointers()
}
}

/// <summary>
/// This is used to allow creation of HasOne relationships when the
/// independent side of the relationship already exists.
/// </summary>
private void AttachHasOnePointers()
{
var relationships = _jsonApiContext.HasOneRelationshipPointers.Get();
foreach (var relationship in relationships)
if (_context.Entry(relationship.Value).State == EntityState.Detached && _context.EntityIsTracked(relationship.Value) == false)
_context.Entry(relationship.Value).State = EntityState.Unchanged;
}

public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
{
var oldEntity = await GetAsync(id);
Expand Down Expand Up @@ -185,17 +203,23 @@ public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> en

public async Task<int> CountAsync(IQueryable<TEntity> entities)
{
return await entities.CountAsync();
return (entities is IAsyncEnumerable<TEntity>)
? await entities.CountAsync()
: entities.Count();
}

public Task<TEntity> FirstOrDefaultAsync(IQueryable<TEntity> entities)
public async Task<TEntity> FirstOrDefaultAsync(IQueryable<TEntity> entities)
{
return entities.FirstOrDefaultAsync();
return (entities is IAsyncEnumerable<TEntity>)
? await entities.FirstOrDefaultAsync()
: entities.FirstOrDefault();
}

public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entities)
{
return await entities.ToListAsync();
return (entities is IAsyncEnumerable<TEntity>)
? await entities.ToListAsync()
: entities.ToList();
}
}
}
21 changes: 21 additions & 0 deletions src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
using System.Linq;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Models;
using Microsoft.EntityFrameworkCore;

namespace JsonApiDotNetCore.Extensions
Expand All @@ -8,5 +11,23 @@ public static class DbContextExtensions
[Obsolete("This is no longer required since the introduction of context.Set<T>", error: false)]
public static DbSet<T> GetDbSet<T>(this DbContext context) where T : class
=> context.Set<T>();

/// <summary>
/// Determines whether or not EF is already tracking an entity of the same Type and Id
/// </summary>
public static bool EntityIsTracked(this DbContext context, IIdentifiable entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));

var trackedEntries = context.ChangeTracker
.Entries()
.FirstOrDefault(entry =>
entry.Entity.GetType() == entity.GetType()
&& ((IIdentifiable)entry.Entity).StringId == entity.StringId
);

return trackedEntries != null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ public static void AddJsonApiInternals(
services.AddScoped<IJsonApiReader, JsonApiReader>();
services.AddScoped<IGenericProcessorFactory, GenericProcessorFactory>();
services.AddScoped(typeof(GenericProcessor<>));
services.AddScoped(typeof(GenericProcessor<,>));
services.AddScoped<IQueryAccessor, QueryAccessor>();
services.AddScoped<IQueryParser, QueryParser>();
services.AddScoped<IControllerContext, Services.ControllerContext>();
Expand Down
17 changes: 15 additions & 2 deletions src/JsonApiDotNetCore/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using JsonApiDotNetCore.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
Expand Down Expand Up @@ -51,8 +52,20 @@ public static TInterface New<TInterface>(this Type t)
{
if (t == null) throw new ArgumentNullException(nameof(t));

var instance = (TInterface)Activator.CreateInstance(t);
var instance = (TInterface)CreateNewInstance(t);
return instance;
}

private static object CreateNewInstance(Type type)
{
try
{
return Activator.CreateInstance(type);
}
catch (Exception e)
{
throw new JsonApiException(500, $"Type '{type}' cannot be instantiated using the default constructor.", e);
}
}
}
}
11 changes: 3 additions & 8 deletions src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,7 @@ public interface IGenericProcessor
void SetRelationships(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds);
}

public class GenericProcessor<T> : GenericProcessor<T, int> where T : class, IIdentifiable<int>
{
public GenericProcessor(IDbContextResolver contextResolver) : base(contextResolver) { }
}

public class GenericProcessor<T, TId> : IGenericProcessor where T : class, IIdentifiable<TId>
public class GenericProcessor<T> : IGenericProcessor where T : class, IIdentifiable
{
private readonly DbContext _context;
public GenericProcessor(IDbContextResolver contextResolver)
Expand All @@ -38,12 +33,12 @@ public void SetRelationships(object parent, RelationshipAttribute relationship,
{
if (relationship.IsHasMany)
{
var entities = _context.GetDbSet<T>().Where(x => relationshipIds.Contains(x.StringId)).ToList();
var entities = _context.Set<T>().Where(x => relationshipIds.Contains(x.StringId)).ToList();
relationship.SetValue(parent, entities);
}
else
{
var entity = _context.GetDbSet<T>().SingleOrDefault(x => relationshipIds.First() == x.StringId);
var entity = _context.Set<T>().SingleOrDefault(x => relationshipIds.First() == x.StringId);
relationship.SetValue(parent, entity);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<VersionPrefix>2.3.1</VersionPrefix>
<VersionPrefix>2.3.2</VersionPrefix>
<TargetFrameworks>$(NetStandardVersion)</TargetFrameworks>
<AssemblyName>JsonApiDotNetCore</AssemblyName>
<PackageId>JsonApiDotNetCore</PackageId>
Expand Down
9 changes: 8 additions & 1 deletion src/JsonApiDotNetCore/Models/Identifiable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class Identifiable<T> : IIdentifiable<T>
public string StringId
{
get => GetStringId(Id);
set => Id = (T)GetConcreteId(value);
set => Id = GetTypedId(value);
}

protected virtual string GetStringId(object value)
Expand All @@ -34,6 +34,13 @@ protected virtual string GetStringId(object value)
: stringValue;
}

protected virtual T GetTypedId(string value)
{
var convertedValue = TypeHelper.ConvertType(value, typeof(T));
return convertedValue == null ? default : (T)convertedValue;
}

[Obsolete("Use GetTypedId instead")]
protected virtual object GetConcreteId(string value)
{
return TypeHelper.ConvertType(value, typeof(T));
Expand Down
11 changes: 11 additions & 0 deletions src/JsonApiDotNetCore/Models/RelationshipAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI

public string PublicRelationshipName { get; }
public string InternalRelationshipName { 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.
/// </summary>
///
/// <example>
/// <code>
/// public List&lt;Articles&gt; Articles { get; set; } // Type => Article
/// </code>
/// </example>
public Type Type { get; internal set; }
public bool IsHasMany => GetType() == typeof(HasManyAttribute);
public bool IsHasOne => GetType() == typeof(HasOneAttribute);
Expand Down
46 changes: 46 additions & 0 deletions src/JsonApiDotNetCore/Request/HasOneRelationshipPointers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using JsonApiDotNetCore.Models;
using System;
using System.Collections.Generic;

namespace JsonApiDotNetCore.Request
{
/// <summary>
/// Stores information to set relationships for the request resource.
/// These relationships must already exist and should not be re-created.
///
/// The expected use case is POST-ing or PATCH-ing
/// an entity with HasOne relationships:
/// <code>
/// {
/// "data": {
/// "type": "photos",
/// "attributes": {
/// "title": "Ember Hamster",
/// "src": "http://example.com/images/productivity.png"
/// },
/// "relationships": {
/// "photographer": {
/// "data": { "type": "people", "id": "2" }
/// }
/// }
/// }
/// }
/// </code>
/// </summary>
public class HasOneRelationshipPointers
{
private Dictionary<Type, IIdentifiable> _hasOneRelationships = new Dictionary<Type, IIdentifiable>();

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

/// <summary>
/// Get all the models that should be associated
/// </summary>
public Dictionary<Type, IIdentifiable> Get() => _hasOneRelationships;
}
}
4 changes: 2 additions & 2 deletions src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ public interface IJsonApiDeSerializer
TEntity Deserialize<TEntity>(string requestBody);
object DeserializeRelationship(string requestBody);
List<TEntity> DeserializeList<TEntity>(string requestBody);
object DocumentToObject(DocumentData data);
object DocumentToObject(DocumentData data, List<DocumentData> included = null);
}
}
}
Loading