Skip to content

Commit b247d15

Browse files
authored
Merge pull request #44 from Research-Institute/relationship-attributes
Relationship attributes
2 parents a683713 + 9d6c4a1 commit b247d15

24 files changed

+131
-92
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,15 @@ public class Person : Identifiable<int>
113113
#### Relationships
114114

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

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

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

137138
public int OwnerId { get; set; }
139+
140+
[HasOne("owner")]
138141
public virtual Person Owner { get; set; }
139142
}
140143
```

src/JsonApiDotNetCore/Builders/DocumentBuilder.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -124,25 +124,25 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I
124124
{
125125
Links = new Links
126126
{
127-
Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.RelationshipName),
128-
Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.RelationshipName)
127+
Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.InternalRelationshipName),
128+
Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.InternalRelationshipName)
129129
}
130130
};
131131

132-
if (_relationshipIsIncluded(r.RelationshipName))
132+
if (_relationshipIsIncluded(r.InternalRelationshipName))
133133
{
134134
var navigationEntity = _jsonApiContext.ContextGraph
135-
.GetRelationship(entity, r.RelationshipName);
135+
.GetRelationship(entity, r.InternalRelationshipName);
136136

137137
if(navigationEntity == null)
138138
relationshipData.SingleData = null;
139139
else if (navigationEntity is IEnumerable)
140-
relationshipData.ManyData = _getRelationships((IEnumerable<object>)navigationEntity, r.RelationshipName);
140+
relationshipData.ManyData = _getRelationships((IEnumerable<object>)navigationEntity, r.InternalRelationshipName);
141141
else
142-
relationshipData.SingleData = _getRelationship(navigationEntity, r.RelationshipName);
142+
relationshipData.SingleData = _getRelationship(navigationEntity, r.InternalRelationshipName);
143143
}
144144

145-
data.Relationships.Add(r.RelationshipName.Dasherize(), relationshipData);
145+
data.Relationships.Add(r.InternalRelationshipName.Dasherize(), relationshipData);
146146
});
147147
}
148148

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

153153
contextEntity.Relationships.ForEach(r =>
154154
{
155-
if (!_relationshipIsIncluded(r.RelationshipName)) return;
155+
if (!_relationshipIsIncluded(r.InternalRelationshipName)) return;
156156

157-
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.RelationshipName);
157+
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.InternalRelationshipName);
158158

159159
if (navigationEntity is IEnumerable)
160160
foreach (var includedEntity in (IEnumerable)navigationEntity)

src/JsonApiDotNetCore/Controllers/JsonApiController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ public virtual async Task<IActionResult> PatchRelationshipsAsync(TId id, string
183183
var relationship = _jsonApiContext.ContextGraph
184184
.GetContextEntity(typeof(T))
185185
.Relationships
186-
.FirstOrDefault(r => r.RelationshipName == relationshipName);
186+
.FirstOrDefault(r => r.InternalRelationshipName == relationshipName);
187187

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

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
108108
return oldEntity;
109109
}
110110

111-
public async Task UpdateRelationshipsAsync(object parent, Relationship relationship, IEnumerable<string> relationshipIds)
111+
public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
112112
{
113-
var genericProcessor = GenericProcessorFactory.GetProcessor(relationship.BaseType, _context);
113+
var genericProcessor = GenericProcessorFactory.GetProcessor(relationship.Type, _context);
114114
await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds);
115115
}
116116

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

137137
throw new JsonApiException("400", "Invalid relationship",

src/JsonApiDotNetCore/Data/IEntityRepository.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public interface IEntityRepository<TEntity, in TId>
3434

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

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

3939
Task<bool> DeleteAsync(TId id);
4040
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using JsonApiDotNetCore.Models;
34

45
namespace JsonApiDotNetCore.Internal
56
{
@@ -8,6 +9,6 @@ public class ContextEntity
89
public string EntityName { get; set; }
910
public Type EntityType { get; set; }
1011
public List<AttrAttribute> Attributes { get; set; }
11-
public List<Relationship> Relationships { get; set; }
12+
public List<RelationshipAttribute> Relationships { get; set; }
1213
}
1314
}

src/JsonApiDotNetCore/Internal/ContextGraph.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ public string GetRelationshipName<TParent>(string relationshipName)
4646
e.EntityType == entityType)
4747
.Relationships
4848
.FirstOrDefault(r =>
49-
r.RelationshipName.ToLower() == relationshipName.ToLower())
50-
?.RelationshipName;
49+
r.InternalRelationshipName.ToLower() == relationshipName.ToLower())
50+
?.InternalRelationshipName;
5151
}
5252
}
5353
}

src/JsonApiDotNetCore/Internal/ContextGraphBuilder.cs

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Reflection;
55
using Microsoft.EntityFrameworkCore;
6+
using JsonApiDotNetCore.Models;
67

78
namespace JsonApiDotNetCore.Internal
89
{
@@ -14,7 +15,6 @@ public class ContextGraphBuilder<T> where T : DbContext
1415
public ContextGraph<T> Build()
1516
{
1617
_getFirstLevelEntities();
17-
_loadRelationships();
1818

1919
var graph = new ContextGraph<T>
2020
{
@@ -41,7 +41,8 @@ private void _getFirstLevelEntities()
4141
entities.Add(new ContextEntity {
4242
EntityName = property.Name,
4343
EntityType = entityType,
44-
Attributes = _getAttributes(entityType)
44+
Attributes = _getAttributes(entityType),
45+
Relationships = _getRelationships(entityType)
4546
});
4647
}
4748
}
@@ -65,38 +66,29 @@ private List<AttrAttribute> _getAttributes(Type entityType)
6566
return attributes;
6667
}
6768

68-
private void _loadRelationships()
69-
{
70-
_entities.ForEach(entity => {
71-
72-
var relationships = new List<Relationship>();
73-
var properties = entity.EntityType.GetProperties();
74-
75-
foreach(var entityProperty in properties)
76-
{
77-
var propertyType = entityProperty.PropertyType;
78-
79-
if(_isValidEntity(propertyType)
80-
|| (propertyType.GetTypeInfo().IsGenericType && _isValidEntity(propertyType.GetGenericArguments()[0])))
81-
relationships.Add(_getRelationshipFromPropertyInfo(entityProperty));
82-
}
83-
84-
entity.Relationships = relationships;
85-
});
86-
}
87-
88-
private bool _isValidEntity(Type type)
69+
private List<RelationshipAttribute> _getRelationships(Type entityType)
8970
{
90-
var validEntityRelationshipTypes = _entities.Select(e => e.EntityType);
91-
return validEntityRelationshipTypes.Contains(type);
71+
var attributes = new List<RelationshipAttribute>();
72+
73+
var properties = entityType.GetProperties();
74+
75+
foreach(var prop in properties)
76+
{
77+
var attribute = (RelationshipAttribute)prop.GetCustomAttribute(typeof(RelationshipAttribute));
78+
if(attribute == null) continue;
79+
attribute.InternalRelationshipName = prop.Name;
80+
attribute.Type = _getRelationshipType(attribute, prop);
81+
attributes.Add(attribute);
82+
}
83+
return attributes;
9284
}
9385

94-
private Relationship _getRelationshipFromPropertyInfo(PropertyInfo propertyInfo)
86+
private Type _getRelationshipType(RelationshipAttribute relation, PropertyInfo prop)
9587
{
96-
return new Relationship {
97-
Type = propertyInfo.PropertyType,
98-
RelationshipName = propertyInfo.Name
99-
};
88+
if(relation.IsHasMany)
89+
return prop.PropertyType.GetGenericArguments()[0];
90+
else
91+
return prop.PropertyType;
10092
}
10193
}
10294
}

src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@ public GenericProcessor(DbContext context)
1717
_context = context;
1818
}
1919

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

24-
// TODO: replace with relationship.IsMany
25-
if(relationship.Type.GetInterfaces().Contains(typeof(IEnumerable)))
24+
if(relationship.IsHasMany)
2625
{
2726
var entities = _context.GetDbSet<T>().Where(x => relationshipIds.Contains(x.Id.ToString())).ToList();
2827
relationship.SetValue(parent, entities);
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
using System.Collections.Generic;
22
using System.Threading.Tasks;
3+
using JsonApiDotNetCore.Models;
34

45
namespace JsonApiDotNetCore.Internal
56
{
67
public interface IGenericProcessor
78
{
8-
Task UpdateRelationshipsAsync(object parent, Relationship relationship, IEnumerable<string> relationshipIds);
9+
Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds);
910
}
1011
}

src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using JsonApiDotNetCore.Models;
2+
13
namespace JsonApiDotNetCore.Internal.Query
24
{
35
public class FilterQuery

src/JsonApiDotNetCore/Internal/Query/QuerySet.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using JsonApiDotNetCore.Extensions;
55
using JsonApiDotNetCore.Services;
66
using Microsoft.AspNetCore.Http;
7+
using JsonApiDotNetCore.Models;
78

89
namespace JsonApiDotNetCore.Internal.Query
910
{

src/JsonApiDotNetCore/Internal/Query/SortQuery.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using JsonApiDotNetCore.Models;
2+
13
namespace JsonApiDotNetCore.Internal.Query
24
{
35
public class SortQuery

src/JsonApiDotNetCore/Internal/Relationship.cs

Lines changed: 0 additions & 30 deletions
This file was deleted.

src/JsonApiDotNetCore/Internal/AttrAttribute.cs renamed to src/JsonApiDotNetCore/Models/AttrAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System;
22
using System.Reflection;
33

4-
namespace JsonApiDotNetCore.Internal
4+
namespace JsonApiDotNetCore.Models
55
{
66
public class AttrAttribute : Attribute
77
{
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
3+
namespace JsonApiDotNetCore.Models
4+
{
5+
public class HasManyAttribute : RelationshipAttribute
6+
{
7+
public HasManyAttribute(string publicName)
8+
: base(publicName)
9+
{
10+
PublicRelationshipName = publicName;
11+
}
12+
}
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace JsonApiDotNetCore.Models
2+
{
3+
public class HasOneAttribute : RelationshipAttribute
4+
{
5+
public HasOneAttribute(string publicName)
6+
: base(publicName)
7+
{
8+
PublicRelationshipName = publicName;
9+
}
10+
}
11+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Reflection;
3+
4+
namespace JsonApiDotNetCore.Models
5+
{
6+
public class RelationshipAttribute : Attribute
7+
{
8+
protected RelationshipAttribute(string publicName)
9+
{
10+
PublicRelationshipName = publicName;
11+
}
12+
13+
public string PublicRelationshipName { get; set; }
14+
public string InternalRelationshipName { get; set; }
15+
public Type Type { get; set; }
16+
public bool IsHasMany { get { return this.GetType() == typeof(HasManyAttribute); } }
17+
public bool IsHasOne { get { return this.GetType() == typeof(HasOneAttribute); } }
18+
19+
public void SetValue(object entity, object newValue)
20+
{
21+
var propertyInfo = entity
22+
.GetType()
23+
.GetProperty(InternalRelationshipName);
24+
25+
propertyInfo.SetValue(entity, newValue);
26+
}
27+
}
28+
}

src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,12 @@ private static object _setRelationships(
9898

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

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

106-
var relationshipName = attr.RelationshipName.Dasherize();
106+
var relationshipName = attr.InternalRelationshipName.Dasherize();
107107
RelationshipData relationshipData;
108108
if (relationships.TryGetValue(relationshipName, out relationshipData))
109109
{

src/JsonApiDotNetCore/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "1.0.0-beta1-*",
2+
"version": "1.0.0-beta2-*",
33

44
"dependencies": {
55
"Microsoft.NETCore.App": {

src/JsonApiDotNetCoreExample/Models/Person.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ public class Person : Identifiable, IHasMeta
1313
[Attr("last-name")]
1414
public string LastName { get; set; }
1515

16+
[HasMany("todo-items")]
1617
public virtual List<TodoItem> TodoItems { get; set; }
18+
19+
[HasMany("todo-item-collections")]
1720
public virtual List<TodoItemCollection> TodoItemCollections { get; set; }
1821

1922
public Dictionary<string, object> GetMeta(IJsonApiContext context)

0 commit comments

Comments
 (0)