diff --git a/README.md b/README.md index cbd344b80f..6f8c350398 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ public class Person : Identifiable #### 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 @@ -121,6 +121,7 @@ public class Person : Identifiable [Attr("first-name")] public string FirstName { get; set; } + [HasMany("todo-items")] public virtual List TodoItems { get; set; } } ``` @@ -135,6 +136,8 @@ public class TodoItem : Identifiable public string Description { get; set; } public int OwnerId { get; set; } + + [HasOne("owner")] public virtual Person Owner { get; set; } } ``` diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 9dc64e06b4..284b2184c5 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -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)navigationEntity, r.RelationshipName); + relationshipData.ManyData = _getRelationships((IEnumerable)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); }); } @@ -152,9 +152,9 @@ private List _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) diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs index 6a4723e77a..ab6eedbf9d 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -183,7 +183,7 @@ public virtual async Task 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); diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 95d8c1a0b2..5a44bb076c 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -108,9 +108,9 @@ public virtual async Task UpdateAsync(TId id, TEntity entity) return oldEntity; } - public async Task UpdateRelationshipsAsync(object parent, Relationship relationship, IEnumerable relationshipIds) + public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds) { - var genericProcessor = GenericProcessorFactory.GetProcessor(relationship.BaseType, _context); + var genericProcessor = GenericProcessorFactory.GetProcessor(relationship.Type, _context); await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds); } @@ -131,7 +131,7 @@ public virtual async Task DeleteAsync(TId id) public virtual IQueryable Include(IQueryable 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", diff --git a/src/JsonApiDotNetCore/Data/IEntityRepository.cs b/src/JsonApiDotNetCore/Data/IEntityRepository.cs index 3eb449c8cf..8df7019f13 100644 --- a/src/JsonApiDotNetCore/Data/IEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityRepository.cs @@ -34,7 +34,7 @@ public interface IEntityRepository Task UpdateAsync(TId id, TEntity entity); - Task UpdateRelationshipsAsync(object parent, Relationship relationship, IEnumerable relationshipIds); + Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds); Task DeleteAsync(TId id); } diff --git a/src/JsonApiDotNetCore/Internal/ContextEntity.cs b/src/JsonApiDotNetCore/Internal/ContextEntity.cs index a1afeeefed..e43d85ab0f 100644 --- a/src/JsonApiDotNetCore/Internal/ContextEntity.cs +++ b/src/JsonApiDotNetCore/Internal/ContextEntity.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Internal { @@ -8,6 +9,6 @@ public class ContextEntity public string EntityName { get; set; } public Type EntityType { get; set; } public List Attributes { get; set; } - public List Relationships { get; set; } + public List Relationships { get; set; } } } diff --git a/src/JsonApiDotNetCore/Internal/ContextGraph.cs b/src/JsonApiDotNetCore/Internal/ContextGraph.cs index 7d1493f2c1..f59ce6f497 100644 --- a/src/JsonApiDotNetCore/Internal/ContextGraph.cs +++ b/src/JsonApiDotNetCore/Internal/ContextGraph.cs @@ -46,8 +46,8 @@ public string GetRelationshipName(string relationshipName) e.EntityType == entityType) .Relationships .FirstOrDefault(r => - r.RelationshipName.ToLower() == relationshipName.ToLower()) - ?.RelationshipName; + r.InternalRelationshipName.ToLower() == relationshipName.ToLower()) + ?.InternalRelationshipName; } } } diff --git a/src/JsonApiDotNetCore/Internal/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Internal/ContextGraphBuilder.cs index db01fabc6c..e856714b40 100644 --- a/src/JsonApiDotNetCore/Internal/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Internal/ContextGraphBuilder.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using Microsoft.EntityFrameworkCore; +using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Internal { @@ -14,7 +15,6 @@ public class ContextGraphBuilder where T : DbContext public ContextGraph Build() { _getFirstLevelEntities(); - _loadRelationships(); var graph = new ContextGraph { @@ -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) }); } } @@ -65,38 +66,29 @@ private List _getAttributes(Type entityType) return attributes; } - private void _loadRelationships() - { - _entities.ForEach(entity => { - - var relationships = new List(); - 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 _getRelationships(Type entityType) { - var validEntityRelationshipTypes = _entities.Select(e => e.EntityType); - return validEntityRelationshipTypes.Contains(type); + var attributes = new List(); + + 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; } } } diff --git a/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs b/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs index 669e931006..5fa1857b94 100644 --- a/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs +++ b/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs @@ -17,12 +17,11 @@ public GenericProcessor(DbContext context) _context = context; } - public async Task UpdateRelationshipsAsync(object parent, Relationship relationship, IEnumerable relationshipIds) + public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable 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().Where(x => relationshipIds.Contains(x.Id.ToString())).ToList(); relationship.SetValue(parent, entities); diff --git a/src/JsonApiDotNetCore/Internal/Generics/IGenericProcessor.cs b/src/JsonApiDotNetCore/Internal/Generics/IGenericProcessor.cs index 5b60dc0738..313db15dc1 100644 --- a/src/JsonApiDotNetCore/Internal/Generics/IGenericProcessor.cs +++ b/src/JsonApiDotNetCore/Internal/Generics/IGenericProcessor.cs @@ -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 relationshipIds); + Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds); } } diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs index 45811e88bd..364d0342c6 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs @@ -1,3 +1,5 @@ +using JsonApiDotNetCore.Models; + namespace JsonApiDotNetCore.Internal.Query { public class FilterQuery diff --git a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs index 2dd9e56b97..ed31fd7969 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs @@ -4,6 +4,7 @@ using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; +using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Internal.Query { diff --git a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs index 827ac3523b..7ef6682cc6 100644 --- a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs @@ -1,3 +1,5 @@ +using JsonApiDotNetCore.Models; + namespace JsonApiDotNetCore.Internal.Query { public class SortQuery diff --git a/src/JsonApiDotNetCore/Internal/Relationship.cs b/src/JsonApiDotNetCore/Internal/Relationship.cs deleted file mode 100644 index c477c2a248..0000000000 --- a/src/JsonApiDotNetCore/Internal/Relationship.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using JsonApiDotNetCore.Extensions; - -namespace JsonApiDotNetCore.Internal -{ - public class Relationship - { - public Type Type { get; set; } - public Type BaseType { get { - return (Type.GetInterfaces().Contains(typeof(IEnumerable))) ? - Type.GenericTypeArguments[0] : - Type; - } } - - public string RelationshipName { get; set; } - - public void SetValue(object entity, object newValue) - { - var propertyInfo = entity - .GetType() - .GetProperty(RelationshipName); - - propertyInfo.SetValue(entity, newValue); - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/AttrAttribute.cs b/src/JsonApiDotNetCore/Models/AttrAttribute.cs similarity index 96% rename from src/JsonApiDotNetCore/Internal/AttrAttribute.cs rename to src/JsonApiDotNetCore/Models/AttrAttribute.cs index 743412e3f2..3ce9a2196a 100644 --- a/src/JsonApiDotNetCore/Internal/AttrAttribute.cs +++ b/src/JsonApiDotNetCore/Models/AttrAttribute.cs @@ -1,7 +1,7 @@ using System; using System.Reflection; -namespace JsonApiDotNetCore.Internal +namespace JsonApiDotNetCore.Models { public class AttrAttribute : Attribute { diff --git a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs new file mode 100644 index 0000000000..13e4a9efad --- /dev/null +++ b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace JsonApiDotNetCore.Models +{ + public class HasManyAttribute : RelationshipAttribute + { + public HasManyAttribute(string publicName) + : base(publicName) + { + PublicRelationshipName = publicName; + } + } +} diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs new file mode 100644 index 0000000000..e5670eae29 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -0,0 +1,11 @@ +namespace JsonApiDotNetCore.Models +{ + public class HasOneAttribute : RelationshipAttribute + { + public HasOneAttribute(string publicName) + : base(publicName) + { + PublicRelationshipName = publicName; + } + } +} diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs new file mode 100644 index 0000000000..45b3565592 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -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); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index edb895a5db..ad7e7cba18 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -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)) { diff --git a/src/JsonApiDotNetCore/project.json b/src/JsonApiDotNetCore/project.json index 8785c130a7..86cf04693f 100644 --- a/src/JsonApiDotNetCore/project.json +++ b/src/JsonApiDotNetCore/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-beta1-*", + "version": "1.0.0-beta2-*", "dependencies": { "Microsoft.NETCore.App": { diff --git a/src/JsonApiDotNetCoreExample/Models/Person.cs b/src/JsonApiDotNetCoreExample/Models/Person.cs index 900971951d..3689f39537 100644 --- a/src/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/JsonApiDotNetCoreExample/Models/Person.cs @@ -13,7 +13,10 @@ public class Person : Identifiable, IHasMeta [Attr("last-name")] public string LastName { get; set; } + [HasMany("todo-items")] public virtual List TodoItems { get; set; } + + [HasMany("todo-item-collections")] public virtual List TodoItemCollections { get; set; } public Dictionary GetMeta(IJsonApiContext context) diff --git a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs index fbf4fe8ba6..5d706da5db 100644 --- a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs +++ b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs @@ -12,9 +12,12 @@ public class TodoItem : Identifiable public long Ordinal { get; set; } public int? OwnerId { get; set; } + public int? CollectionId { get; set; } + + [HasOne("owner")] public virtual Person Owner { get; set; } - public int? CollectionId { get; set; } + [HasOne("collection")] public virtual TodoItemCollection Collection { get; set; } } } diff --git a/src/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs b/src/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs index 2c4e42c3f0..c5b1dda453 100644 --- a/src/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs +++ b/src/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs @@ -5,9 +5,14 @@ namespace JsonApiDotNetCoreExample.Models { public class TodoItemCollection : Identifiable { + [Attr("name")] public string Name { get; set; } - public virtual List TodoItems { get; set; } public int OwnerId { get; set; } + + [HasMany("todo-items")] + public virtual List TodoItems { get; set; } + + [HasOne("owner")] public virtual Person Owner { get; set; } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs index 0be777a4f7..e024786252 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs @@ -14,6 +14,7 @@ using JsonApiDotNetCoreExampleTests.Startups; using JsonApiDotNetCoreExample.Models; using System.Collections; +using System; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests { @@ -32,7 +33,10 @@ public Meta(DocsFixture fixture) public async Task Total_Record_Count_Included() { // arrange - var expectedCount = _context.TodoItems.Count(); + _context.TodoItems.RemoveRange(_context.TodoItems); + _context.TodoItems.Add(new TodoItem()); + _context.SaveChanges(); + var expectedCount = 1; var builder = new WebHostBuilder() .UseStartup(); @@ -45,7 +49,8 @@ public async Task Total_Record_Count_Included() // act var response = await client.SendAsync(request); - var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var responseBody = await response.Content.ReadAsStringAsync(); + var documents = JsonConvert.DeserializeObject(responseBody); // assert Assert.Equal(HttpStatusCode.OK, response.StatusCode);