Skip to content

Commit

Permalink
Merge pull request #355 from json-api-dotnet/fix/#354
Browse files Browse the repository at this point in the history
Fix/#354: Null reference exception when fetching relationships with compound name
  • Loading branch information
jaredcnance authored Jul 26, 2018
2 parents f3272b0 + 46f1e2d commit 6d2ccf5
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 47 deletions.
20 changes: 17 additions & 3 deletions src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public DefaultEntityRepository(
_genericProcessorFactory = _jsonApiContext.GenericProcessorFactory;
}

/// </ inheritdoc>
public virtual IQueryable<TEntity> Get()
{
if (_jsonApiContext.QuerySet?.Fields != null && _jsonApiContext.QuerySet.Fields.Count > 0)
Expand All @@ -56,21 +57,25 @@ public virtual IQueryable<TEntity> Get()
return _dbSet;
}

/// </ inheritdoc>
public virtual IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQuery filterQuery)
{
return entities.Filter(_jsonApiContext, filterQuery);
}

/// </ inheritdoc>
public virtual IQueryable<TEntity> Sort(IQueryable<TEntity> entities, List<SortQuery> sortQueries)
{
return entities.Sort(sortQueries);
}

/// </ inheritdoc>
public virtual async Task<TEntity> GetAsync(TId id)
{
return await Get().SingleOrDefaultAsync(e => e.Id.Equals(id));
}

/// </ inheritdoc>
public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshipName)
{
_logger.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationshipName})");
Expand All @@ -80,6 +85,7 @@ public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshi
return result;
}

/// </ inheritdoc>
public virtual async Task<TEntity> CreateAsync(TEntity entity)
{
AttachRelationships();
Expand All @@ -102,9 +108,9 @@ protected virtual void AttachRelationships()
private void AttachHasManyPointers()
{
var relationships = _jsonApiContext.HasManyRelationshipPointers.Get();
foreach(var relationship in relationships)
foreach (var relationship in relationships)
{
foreach(var pointer in relationship.Value)
foreach (var pointer in relationship.Value)
{
_context.Entry(pointer).State = EntityState.Unchanged;
}
Expand All @@ -123,6 +129,7 @@ private void AttachHasOnePointers()
_context.Entry(relationship.Value).State = EntityState.Unchanged;
}

/// </ inheritdoc>
public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
{
var oldEntity = await GetAsync(id);
Expand All @@ -141,12 +148,14 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
return oldEntity;
}

/// </ inheritdoc>
public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
{
var genericProcessor = _genericProcessorFactory.GetProcessor<IGenericProcessor>(typeof(GenericProcessor<>), relationship.Type);
await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds);
}

/// </ inheritdoc>
public virtual async Task<bool> DeleteAsync(TId id)
{
var entity = await GetAsync(id);
Expand All @@ -161,11 +170,12 @@ public virtual async Task<bool> DeleteAsync(TId id)
return true;
}

/// </ inheritdoc>
public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName)
{
var entity = _jsonApiContext.RequestEntity;
var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == relationshipName);
if (relationship == null)
if (relationship == null)
{
throw new JsonApiException(400, $"Invalid relationship {relationshipName} on {entity.EntityName}",
$"{entity.EntityName} does not have a relationship named {relationshipName}");
Expand All @@ -178,6 +188,7 @@ public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string
return entities.Include(relationship.InternalRelationshipName);
}

/// </ inheritdoc>
public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> entities, int pageSize, int pageNumber)
{
if (pageNumber >= 0)
Expand All @@ -198,20 +209,23 @@ public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> en
.ToListAsync();
}

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

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

/// </ inheritdoc>
public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entities)
{
return (entities is IAsyncEnumerable<TEntity>)
Expand Down
49 changes: 46 additions & 3 deletions src/JsonApiDotNetCore/Data/IEntityReadRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,75 @@

namespace JsonApiDotNetCore.Data
{
public interface IEntityReadRepository<TEntity>
: IEntityReadRepository<TEntity, int>
where TEntity : class, IIdentifiable<int>
public interface IEntityReadRepository<TEntity>
: IEntityReadRepository<TEntity, int>
where TEntity : class, IIdentifiable<int>
{ }

public interface IEntityReadRepository<TEntity, in TId>
where TEntity : class, IIdentifiable<TId>
{
/// <summary>
/// The base GET query. This is a good place to apply rules that should affect all reads,
/// such as authorization of resources.
/// </summary>
IQueryable<TEntity> Get();

/// <summary>
/// Include a relationship in the query
/// </summary>
/// <example>
/// <code>
/// _todoItemsRepository.GetAndIncludeAsync(1, "achieved-date");
/// </code>
/// </example>
IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName);

/// <summary>
/// Apply a filter to the provided queryable
/// </summary>
IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQuery filterQuery);

/// <summary>
/// Apply a sort to the provided queryable
/// </summary>
IQueryable<TEntity> Sort(IQueryable<TEntity> entities, List<SortQuery> sortQueries);

/// <summary>
/// Paginate the provided queryable
/// </summary>
Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> entities, int pageSize, int pageNumber);

/// <summary>
/// Get the entity by id
/// </summary>
Task<TEntity> GetAsync(TId id);

/// <summary>
/// Get the entity with the specified id and include the relationship.
/// </summary>
/// <param name="id">The entity id</param>
/// <param name="relationshipName">The exposed relationship name</param>
/// <example>
/// <code>
/// _todoItemsRepository.GetAndIncludeAsync(1, "achieved-date");
/// </code>
/// </example>
Task<TEntity> GetAndIncludeAsync(TId id, string relationshipName);

/// <summary>
/// Count the total number of records
/// </summary>
Task<int> CountAsync(IQueryable<TEntity> entities);

/// <summary>
/// Get the first element in the collection, return the default value if collection is empty
/// </summary>
Task<TEntity> FirstOrDefaultAsync(IQueryable<TEntity> entities);

/// <summary>
/// Convert the collection to a materialized list
/// </summary>
Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entities);
}
}
42 changes: 41 additions & 1 deletion src/JsonApiDotNetCore/Internal/ContextGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,45 @@ namespace JsonApiDotNetCore.Internal
{
public interface IContextGraph
{
object GetRelationship<TParent>(TParent entity, string relationshipName);
/// <summary>
/// Gets the value of the navigation property, defined by the relationshipName,
/// on the provided instance.
/// </summary>
/// <param name="resource">The resource instance</param>
/// <param name="propertyName">The navigation property name.</param>
/// <example>
/// <code>
/// _graph.GetRelationship(todoItem, nameof(TodoItem.Owner));
/// </code>
/// </example>
object GetRelationship<TParent>(TParent resource, string propertyName);

/// <summary>
/// Get the internal navigation property name for the specified public
/// relationship name.
/// </summary>
/// <param name="relationshipName">The public relationship name specified by a <see cref="HasOneAttribute" /> or <see cref="HasManyAttribute" /></param>
/// <example>
/// <code>
/// _graph.GetRelationshipName&lt;TodoItem&gt;("achieved-date");
/// // returns "AchievedDate"
/// </code>
/// </example>
string GetRelationshipName<TParent>(string relationshipName);

/// <summary>
/// Get the resource metadata by the DbSet property name
/// </summary>
ContextEntity GetContextEntity(string dbSetName);

/// <summary>
/// Get the resource metadata by the resource type
/// </summary>
ContextEntity GetContextEntity(Type entityType);

/// <summary>
/// Was built against an EntityFrameworkCore DbContext ?
/// </summary>
bool UsesDbContext { get; }
}

Expand Down Expand Up @@ -40,14 +75,18 @@ internal ContextGraph(List<ContextEntity> entities, bool usesDbContext, List<Val
Instance = this;
}

/// </ inheritdoc>
public bool UsesDbContext { get; }

/// </ inheritdoc>
public ContextEntity GetContextEntity(string entityName)
=> Entities.SingleOrDefault(e => string.Equals(e.EntityName, entityName, StringComparison.OrdinalIgnoreCase));

/// </ inheritdoc>
public ContextEntity GetContextEntity(Type entityType)
=> Entities.SingleOrDefault(e => e.EntityType == entityType);

/// </ inheritdoc>
public object GetRelationship<TParent>(TParent entity, string relationshipName)
{
var parentEntityType = entity.GetType();
Expand All @@ -62,6 +101,7 @@ public object GetRelationship<TParent>(TParent entity, string relationshipName)
return navigationProperty.GetValue(entity);
}

/// </ inheritdoc>
public string GetRelationshipName<TParent>(string relationshipName)
{
var entityType = typeof(TParent);
Expand Down
Loading

0 comments on commit 6d2ccf5

Please sign in to comment.