Skip to content

Relationship attributes #44

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 10 commits into from
Mar 2, 2017
Merged
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,15 @@ public class Person : Identifiable<int>
#### Relationships

In order for navigation properties to be identified in the model,
they should be labeled as virtual.
they should be labeled with the appropriate attribute (either `HasOne` or `HasMany`).

```csharp
public class Person : Identifiable<int>
{
[Attr("first-name")]
public string FirstName { get; set; }

[HasMany("todo-items")]
public virtual List<TodoItem> TodoItems { get; set; }
}
```
Expand All @@ -135,6 +136,8 @@ public class TodoItem : Identifiable<int>
public string Description { get; set; }

public int OwnerId { get; set; }

[HasOne("owner")]
public virtual Person Owner { get; set; }
}
```
Expand Down
18 changes: 9 additions & 9 deletions src/JsonApiDotNetCore/Builders/DocumentBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,25 +124,25 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I
{
Links = new Links
{
Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.RelationshipName),
Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.RelationshipName)
Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.InternalRelationshipName),
Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.InternalRelationshipName)
}
};

if (_relationshipIsIncluded(r.RelationshipName))
if (_relationshipIsIncluded(r.InternalRelationshipName))
{
var navigationEntity = _jsonApiContext.ContextGraph
.GetRelationship(entity, r.RelationshipName);
.GetRelationship(entity, r.InternalRelationshipName);

if(navigationEntity == null)
relationshipData.SingleData = null;
else if (navigationEntity is IEnumerable)
relationshipData.ManyData = _getRelationships((IEnumerable<object>)navigationEntity, r.RelationshipName);
relationshipData.ManyData = _getRelationships((IEnumerable<object>)navigationEntity, r.InternalRelationshipName);
else
relationshipData.SingleData = _getRelationship(navigationEntity, r.RelationshipName);
relationshipData.SingleData = _getRelationship(navigationEntity, r.InternalRelationshipName);
}

data.Relationships.Add(r.RelationshipName.Dasherize(), relationshipData);
data.Relationships.Add(r.InternalRelationshipName.Dasherize(), relationshipData);
});
}

Expand All @@ -152,9 +152,9 @@ private List<DocumentData> _getIncludedEntities(ContextEntity contextEntity, IId

contextEntity.Relationships.ForEach(r =>
{
if (!_relationshipIsIncluded(r.RelationshipName)) return;
if (!_relationshipIsIncluded(r.InternalRelationshipName)) return;

var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.RelationshipName);
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.InternalRelationshipName);

if (navigationEntity is IEnumerable)
foreach (var includedEntity in (IEnumerable)navigationEntity)
Expand Down
2 changes: 1 addition & 1 deletion src/JsonApiDotNetCore/Controllers/JsonApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ public virtual async Task<IActionResult> PatchRelationshipsAsync(TId id, string
var relationship = _jsonApiContext.ContextGraph
.GetContextEntity(typeof(T))
.Relationships
.FirstOrDefault(r => r.RelationshipName == relationshipName);
.FirstOrDefault(r => r.InternalRelationshipName == relationshipName);

var relationshipIds = relationships.Select(r=>r.Id);

Expand Down
6 changes: 3 additions & 3 deletions src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
return oldEntity;
}

public async Task UpdateRelationshipsAsync(object parent, Relationship relationship, IEnumerable<string> relationshipIds)
public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
{
var genericProcessor = GenericProcessorFactory.GetProcessor(relationship.BaseType, _context);
var genericProcessor = GenericProcessorFactory.GetProcessor(relationship.Type, _context);
await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds);
}

Expand All @@ -131,7 +131,7 @@ public virtual async Task<bool> DeleteAsync(TId id)
public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName)
{
var entity = _jsonApiContext.RequestEntity;
if(entity.Relationships.Any(r => r.RelationshipName == relationshipName))
if(entity.Relationships.Any(r => r.InternalRelationshipName == relationshipName))
return entities.Include(relationshipName);

throw new JsonApiException("400", "Invalid relationship",
Expand Down
2 changes: 1 addition & 1 deletion src/JsonApiDotNetCore/Data/IEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public interface IEntityRepository<TEntity, in TId>

Task<TEntity> UpdateAsync(TId id, TEntity entity);

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

Task<bool> DeleteAsync(TId id);
}
Expand Down
3 changes: 2 additions & 1 deletion src/JsonApiDotNetCore/Internal/ContextEntity.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCore.Internal
{
Expand All @@ -8,6 +9,6 @@ public class ContextEntity
public string EntityName { get; set; }
public Type EntityType { get; set; }
public List<AttrAttribute> Attributes { get; set; }
public List<Relationship> Relationships { get; set; }
public List<RelationshipAttribute> Relationships { get; set; }
}
}
4 changes: 2 additions & 2 deletions src/JsonApiDotNetCore/Internal/ContextGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ public string GetRelationshipName<TParent>(string relationshipName)
e.EntityType == entityType)
.Relationships
.FirstOrDefault(r =>
r.RelationshipName.ToLower() == relationshipName.ToLower())
?.RelationshipName;
r.InternalRelationshipName.ToLower() == relationshipName.ToLower())
?.InternalRelationshipName;
}
}
}
52 changes: 22 additions & 30 deletions src/JsonApiDotNetCore/Internal/ContextGraphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCore.Internal
{
Expand All @@ -14,7 +15,6 @@ public class ContextGraphBuilder<T> where T : DbContext
public ContextGraph<T> Build()
{
_getFirstLevelEntities();
_loadRelationships();

var graph = new ContextGraph<T>
{
Expand All @@ -41,7 +41,8 @@ private void _getFirstLevelEntities()
entities.Add(new ContextEntity {
EntityName = property.Name,
EntityType = entityType,
Attributes = _getAttributes(entityType)
Attributes = _getAttributes(entityType),
Relationships = _getRelationships(entityType)
});
}
}
Expand All @@ -65,38 +66,29 @@ private List<AttrAttribute> _getAttributes(Type entityType)
return attributes;
}

private void _loadRelationships()
{
_entities.ForEach(entity => {

var relationships = new List<Relationship>();
var properties = entity.EntityType.GetProperties();

foreach(var entityProperty in properties)
{
var propertyType = entityProperty.PropertyType;

if(_isValidEntity(propertyType)
|| (propertyType.GetTypeInfo().IsGenericType && _isValidEntity(propertyType.GetGenericArguments()[0])))
relationships.Add(_getRelationshipFromPropertyInfo(entityProperty));
}

entity.Relationships = relationships;
});
}

private bool _isValidEntity(Type type)
private List<RelationshipAttribute> _getRelationships(Type entityType)
{
var validEntityRelationshipTypes = _entities.Select(e => e.EntityType);
return validEntityRelationshipTypes.Contains(type);
var attributes = new List<RelationshipAttribute>();

var properties = entityType.GetProperties();

foreach(var prop in properties)
{
var attribute = (RelationshipAttribute)prop.GetCustomAttribute(typeof(RelationshipAttribute));
if(attribute == null) continue;
attribute.InternalRelationshipName = prop.Name;
attribute.Type = _getRelationshipType(attribute, prop);
attributes.Add(attribute);
}
return attributes;
}

private Relationship _getRelationshipFromPropertyInfo(PropertyInfo propertyInfo)
private Type _getRelationshipType(RelationshipAttribute relation, PropertyInfo prop)
{
return new Relationship {
Type = propertyInfo.PropertyType,
RelationshipName = propertyInfo.Name
};
if(relation.IsHasMany)
return prop.PropertyType.GetGenericArguments()[0];
else
return prop.PropertyType;
}
}
}
7 changes: 3 additions & 4 deletions src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ public GenericProcessor(DbContext context)
_context = context;
}

public async Task UpdateRelationshipsAsync(object parent, Relationship relationship, IEnumerable<string> relationshipIds)
public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
{
var relationshipType = relationship.BaseType;
var relationshipType = relationship.Type;

// TODO: replace with relationship.IsMany
if(relationship.Type.GetInterfaces().Contains(typeof(IEnumerable)))
if(relationship.IsHasMany)
{
var entities = _context.GetDbSet<T>().Where(x => relationshipIds.Contains(x.Id.ToString())).ToList();
relationship.SetValue(parent, entities);
Expand Down
3 changes: 2 additions & 1 deletion src/JsonApiDotNetCore/Internal/Generics/IGenericProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCore.Internal
{
public interface IGenericProcessor
{
Task UpdateRelationshipsAsync(object parent, Relationship relationship, IEnumerable<string> relationshipIds);
Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds);
}
}
2 changes: 2 additions & 0 deletions src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCore.Internal.Query
{
public class FilterQuery
Expand Down
1 change: 1 addition & 0 deletions src/JsonApiDotNetCore/Internal/Query/QuerySet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using JsonApiDotNetCore.Extensions;
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Http;
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCore.Internal.Query
{
Expand Down
2 changes: 2 additions & 0 deletions src/JsonApiDotNetCore/Internal/Query/SortQuery.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCore.Internal.Query
{
public class SortQuery
Expand Down
30 changes: 0 additions & 30 deletions src/JsonApiDotNetCore/Internal/Relationship.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.Reflection;

namespace JsonApiDotNetCore.Internal
namespace JsonApiDotNetCore.Models
{
public class AttrAttribute : Attribute
{
Expand Down
13 changes: 13 additions & 0 deletions src/JsonApiDotNetCore/Models/HasManyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace JsonApiDotNetCore.Models
{
public class HasManyAttribute : RelationshipAttribute
{
public HasManyAttribute(string publicName)
: base(publicName)
{
PublicRelationshipName = publicName;
}
}
}
11 changes: 11 additions & 0 deletions src/JsonApiDotNetCore/Models/HasOneAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace JsonApiDotNetCore.Models
{
public class HasOneAttribute : RelationshipAttribute
{
public HasOneAttribute(string publicName)
: base(publicName)
{
PublicRelationshipName = publicName;
}
}
}
28 changes: 28 additions & 0 deletions src/JsonApiDotNetCore/Models/RelationshipAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Reflection;

namespace JsonApiDotNetCore.Models
{
public class RelationshipAttribute : Attribute
{
protected RelationshipAttribute(string publicName)
{
PublicRelationshipName = publicName;
}

public string PublicRelationshipName { get; set; }
public string InternalRelationshipName { get; set; }
public Type Type { get; set; }
public bool IsHasMany { get { return this.GetType() == typeof(HasManyAttribute); } }
public bool IsHasOne { get { return this.GetType() == typeof(HasOneAttribute); } }

public void SetValue(object entity, object newValue)
{
var propertyInfo = entity
.GetType()
.GetProperty(InternalRelationshipName);

propertyInfo.SetValue(entity, newValue);
}
}
}
6 changes: 3 additions & 3 deletions src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,12 @@ private static object _setRelationships(

foreach (var attr in contextEntity.Relationships)
{
var entityProperty = entityProperties.FirstOrDefault(p => p.Name == $"{attr.RelationshipName}Id");
var entityProperty = entityProperties.FirstOrDefault(p => p.Name == $"{attr.InternalRelationshipName}Id");

if (entityProperty == null)
throw new JsonApiException("400", $"{contextEntity.EntityType.Name} does not contain an relationsip named {attr.RelationshipName}");
throw new JsonApiException("400", $"{contextEntity.EntityType.Name} does not contain an relationsip named {attr.InternalRelationshipName}");

var relationshipName = attr.RelationshipName.Dasherize();
var relationshipName = attr.InternalRelationshipName.Dasherize();
RelationshipData relationshipData;
if (relationships.TryGetValue(relationshipName, out relationshipData))
{
Expand Down
2 changes: 1 addition & 1 deletion src/JsonApiDotNetCore/project.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.0.0-beta1-*",
"version": "1.0.0-beta2-*",

"dependencies": {
"Microsoft.NETCore.App": {
Expand Down
3 changes: 3 additions & 0 deletions src/JsonApiDotNetCoreExample/Models/Person.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ public class Person : Identifiable, IHasMeta
[Attr("last-name")]
public string LastName { get; set; }

[HasMany("todo-items")]
public virtual List<TodoItem> TodoItems { get; set; }

[HasMany("todo-item-collections")]
public virtual List<TodoItemCollection> TodoItemCollections { get; set; }

public Dictionary<string, object> GetMeta(IJsonApiContext context)
Expand Down
Loading