Skip to content

Fix/#354: Null reference exception when fetching relationships with compound name #355

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 6 commits into from
Jul 26, 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
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