diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 088a2bf092..0beb0516c1 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -107,7 +107,7 @@ public DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity) Id = entity.StringId }; - if (_jsonApiContext.IsRelationshipData) + if (_jsonApiContext.IsRelationshipPath) return data; data.Attributes = new Dictionary<string, object>(); diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index b6bcda29b3..032fef13c4 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -84,11 +84,29 @@ public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshi public virtual async Task<TEntity> CreateAsync(TEntity entity) { + AttachHasManyPointers(); _dbSet.Add(entity); + await _context.SaveChangesAsync(); return entity; } + /// <summary> + /// This is used to allow creation of HasMany relationships when the + /// dependent side of the relationship already exists. + /// </summary> + private void AttachHasManyPointers() + { + var relationships = _jsonApiContext.HasManyRelationshipPointers.Get(); + foreach(var relationship in relationships) + { + foreach(var pointer in relationship.Value) + { + _context.Entry(pointer).State = EntityState.Unchanged; + } + } + } + public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity) { var oldEntity = await GetAsync(id); diff --git a/src/JsonApiDotNetCore/Data/IEntityRepository.cs b/src/JsonApiDotNetCore/Data/IEntityRepository.cs index 4c35d6ea3f..e8bb68ef90 100644 --- a/src/JsonApiDotNetCore/Data/IEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityRepository.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Data diff --git a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs b/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs index 2606342e29..3cb5ccc359 100644 --- a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs @@ -1,20 +1,12 @@ -using Microsoft.EntityFrameworkCore; using System; +using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCore.Extensions { public static class DbContextExtensions { - public static DbSet<T> GetDbSet<T>(this DbContext context) where T : class - { - var contextProperties = context.GetType().GetProperties(); - foreach(var property in contextProperties) - { - if (property.PropertyType == typeof(DbSet<T>)) - return (DbSet<T>)property.GetValue(context); - } - - throw new ArgumentException($"DbSet of type {typeof(T).FullName} not found on the DbContext", nameof(T)); - } + [Obsolete("This is no longer required since the introduction of context.Set<T>", error: false)] + public static DbSet<T> GetDbSet<T>(this DbContext context) where T : class + => context.Set<T>(); } } diff --git a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs index ccc4619966..8cc7c0dffe 100644 --- a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs @@ -31,5 +31,28 @@ public static Type GetElementType(this IEnumerable enumerable) return elementType; } + + /// <summary> + /// Creates a List{TInterface} where TInterface is the generic for type specified by t + /// </summary> + public static IEnumerable GetEmptyCollection(this Type t) + { + if (t == null) throw new ArgumentNullException(nameof(t)); + + var listType = typeof(List<>).MakeGenericType(t); + var list = (IEnumerable)Activator.CreateInstance(listType); + return list; + } + + /// <summary> + /// Creates a new instance of type t, casting it to the specified TInterface + /// </summary> + public static TInterface New<TInterface>(this Type t) + { + if (t == null) throw new ArgumentNullException(nameof(t)); + + var instance = (TInterface)Activator.CreateInstance(t); + return instance; + } } } diff --git a/src/JsonApiDotNetCore/Internal/TypeHelper.cs b/src/JsonApiDotNetCore/Internal/TypeHelper.cs index 5135473cdb..0a3e01d0d1 100644 --- a/src/JsonApiDotNetCore/Internal/TypeHelper.cs +++ b/src/JsonApiDotNetCore/Internal/TypeHelper.cs @@ -7,6 +7,14 @@ namespace JsonApiDotNetCore.Internal { public static class TypeHelper { + public static IList ConvertCollection(IEnumerable<object> collection, Type targetType) + { + var list = Activator.CreateInstance(typeof(List<>).MakeGenericType(targetType)) as IList; + foreach(var item in collection) + list.Add(ConvertType(item, targetType)); + return list; + } + public static object ConvertType(object value, Type type) { if (value == null) diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index eb649ed5dc..4f52a23002 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <VersionPrefix>2.2.4</VersionPrefix> + <VersionPrefix>2.2.5</VersionPrefix> <TargetFrameworks>$(NetStandardVersion)</TargetFrameworks> <AssemblyName>JsonApiDotNetCore</AssemblyName> <PackageId>JsonApiDotNetCore</PackageId> diff --git a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs index 4519dc8cb6..c2d7594400 100644 --- a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs @@ -12,7 +12,7 @@ public override void SetValue(object entity, object newValue) .GetType() .GetProperty(InternalRelationshipName); - propertyInfo.SetValue(entity, newValue); + propertyInfo.SetValue(entity, newValue); } } } diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index 03fdb200fc..77422027a7 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -39,9 +39,14 @@ public HasOneAttribute(string publicName, Link documentLinks = Link.All, bool ca ? $"{InternalRelationshipName}Id" : _explicitIdentifiablePropertyName; + /// <summary> + /// Sets the value of the property identified by this attribute + /// </summary> + /// <param name="entity">The target object</param> + /// <param name="newValue">The new property value</param> public override void SetValue(object entity, object newValue) { - var propertyName = (newValue.GetType() == Type) + var propertyName = (newValue?.GetType() == Type) ? InternalRelationshipName : IdentifiablePropertyName; diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index 2781ecfb53..3e66bdc8aa 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -19,6 +19,28 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI public Link DocumentLinks { get; } = Link.All; public bool CanInclude { get; } + public bool TryGetHasOne(out HasOneAttribute result) + { + if (IsHasOne) + { + result = (HasOneAttribute)this; + return true; + } + result = null; + return false; + } + + public bool TryGetHasMany(out HasManyAttribute result) + { + if (IsHasMany) + { + result = (HasManyAttribute)this; + return true; + } + result = null; + return false; + } + public abstract void SetValue(object entity, object newValue); public override string ToString() diff --git a/src/JsonApiDotNetCore/Request/HasManyRelationshipPointers.cs b/src/JsonApiDotNetCore/Request/HasManyRelationshipPointers.cs new file mode 100644 index 0000000000..721274e3d6 --- /dev/null +++ b/src/JsonApiDotNetCore/Request/HasManyRelationshipPointers.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace JsonApiDotNetCore.Request +{ + /// <summary> + /// Stores information to set relationships for the request resource. + /// These relationships must already exist and should not be re-created. + /// + /// The expected use case is POST-ing or PATCH-ing + /// an entity with HasMany relaitonships: + /// <code> + /// { + /// "data": { + /// "type": "photos", + /// "attributes": { + /// "title": "Ember Hamster", + /// "src": "http://example.com/images/productivity.png" + /// }, + /// "relationships": { + /// "tags": { + /// "data": [ + /// { "type": "tags", "id": "2" }, + /// { "type": "tags", "id": "3" } + /// ] + /// } + /// } + /// } + /// } + /// </code> + /// </summary> + public class HasManyRelationshipPointers + { + private Dictionary<Type, IList> _hasManyRelationships = new Dictionary<Type, IList>(); + + /// <summary> + /// Add the relationship to the list of relationships that should be + /// set in the repository layer. + /// </summary> + public void Add(Type dependentType, IList entities) + => _hasManyRelationships[dependentType] = entities; + + /// <summary> + /// Get all the models that should be associated + /// </summary> + public Dictionary<Type, IList> Get() => _hasManyRelationships; + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 6fb7af55e1..45d77c0f77 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Models; @@ -15,14 +16,20 @@ namespace JsonApiDotNetCore.Serialization public class JsonApiDeSerializer : IJsonApiDeSerializer { private readonly IJsonApiContext _jsonApiContext; - private readonly IGenericProcessorFactory _genericProcessorFactory; + [Obsolete( + "The deserializer no longer depends on the IGenericProcessorFactory", + error: false)] public JsonApiDeSerializer( IJsonApiContext jsonApiContext, IGenericProcessorFactory genericProcessorFactory) { _jsonApiContext = jsonApiContext; - _genericProcessorFactory = genericProcessorFactory; + } + + public JsonApiDeSerializer(IJsonApiContext jsonApiContext) + { + _jsonApiContext = jsonApiContext; } public object Deserialize(string requestBody) @@ -200,20 +207,25 @@ private object SetHasOneRelationship(object entity, var rio = (ResourceIdentifierObject)relationshipData.ExposedData; - if (rio == null) return entity; - - var newValue = rio.Id; - var foreignKey = attr.IdentifiablePropertyName; var entityProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey); - if (entityProperty == null) + if (entityProperty == null && rio != null) throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a foreign key property '{foreignKey}' for has one relationship '{attr.InternalRelationshipName}'"); - var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType); + if (entityProperty != null) + { + // e.g. PATCH /articles + // {... { "relationships":{ "Owner": { "data" :null } } } } + if (rio == null && Nullable.GetUnderlyingType(entityProperty.PropertyType) == null) + throw new JsonApiException(400, $"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null."); - _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + var newValue = rio?.Id ?? null; + var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType); - entityProperty.SetValue(entity, convertedValue); + _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + + entityProperty.SetValue(entity, convertedValue); + } } return entity; @@ -225,11 +237,6 @@ private object SetHasManyRelationship(object entity, ContextEntity contextEntity, Dictionary<string, RelationshipData> relationships) { - var entityProperty = entityProperties.FirstOrDefault(p => p.Name == attr.InternalRelationshipName); - - if (entityProperty == null) - throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain an relationsip named {attr.InternalRelationshipName}"); - var relationshipName = attr.PublicRelationshipName; if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData)) @@ -238,11 +245,18 @@ private object SetHasManyRelationship(object entity, if (data == null) return entity; - var genericProcessor = _genericProcessorFactory.GetProcessor<IGenericProcessor>(typeof(GenericProcessor<>), attr.Type); + var relationshipShells = relationshipData.ManyData.Select(r => + { + var instance = attr.Type.New<IIdentifiable>(); + instance.StringId = r.Id; + return instance; + }); + + var convertedCollection = TypeHelper.ConvertCollection(relationshipShells, attr.Type); - var ids = relationshipData.ManyData.Select(r => r.Id); + attr.SetValue(entity, convertedCollection); - genericProcessor.SetRelationships(entity, attr, ids); + _jsonApiContext.HasManyRelationshipPointers.Add(attr.Type, convertedCollection); } return entity; diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index bc7a2adb52..642ee00a57 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -80,10 +80,7 @@ private async Task<T> GetWithRelationshipsAsync(TId id) } public virtual async Task<object> GetRelationshipsAsync(TId id, string relationshipName) - { - _jsonApiContext.IsRelationshipData = true; - return await GetRelationshipAsync(id, relationshipName); - } + => await GetRelationshipAsync(id, relationshipName); public virtual async Task<object> GetRelationshipAsync(TId id, string relationshipName) { @@ -136,7 +133,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa .Relationships .FirstOrDefault(r => r.InternalRelationshipName == relationshipName); - var relationshipIds = relationships.Select(r => r.Id?.ToString()); + var relationshipIds = relationships.Select(r => r?.Id?.ToString()); await _entities.UpdateRelationshipsAsync(entity, relationship, relationshipIds); } diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index a73f0eb53a..52036f21af 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -6,29 +6,126 @@ using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Request; namespace JsonApiDotNetCore.Services { - public interface IJsonApiContext + public interface IJsonApiApplication { JsonApiOptions Options { get; set; } - IJsonApiContext ApplyContext<T>(object controller); IContextGraph ContextGraph { get; set; } - ContextEntity RequestEntity { get; set; } - string BasePath { get; set; } - QuerySet QuerySet { get; set; } - bool IsRelationshipData { get; set; } + } + + public interface IQueryRequest + { List<string> IncludedRelationships { get; set; } - bool IsRelationshipPath { get; } + QuerySet QuerySet { get; set; } PageManager PageManager { get; set; } - IMetaBuilder MetaBuilder { get; set; } - IGenericProcessorFactory GenericProcessorFactory { get; set; } + } + + public interface IUpdateRequest + { + /// <summary> + /// The attributes that were included in a PATCH request. + /// Only the attributes in this dictionary should be updated. + /// </summary> Dictionary<AttrAttribute, object> AttributesToUpdate { get; set; } + + /// <summary> + /// Any relationships that were included in a PATCH request. + /// Only the relationships in this dictionary should be updated. + /// </summary> Dictionary<RelationshipAttribute, object> RelationshipsToUpdate { get; set; } + } + + public interface IJsonApiRequest : IJsonApiApplication, IUpdateRequest, IQueryRequest + { + /// <summary> + /// The request namespace. This may be an absolute or relative path + /// depending upon the configuration. + /// </summary> + /// <example> + /// Absolute: https://example.com/api/v1 + /// + /// Relative: /api/v1 + /// </example> + string BasePath { get; set; } + + /// <summary> + /// Stores information to set relationships for the request resource. + /// These relationships must already exist and should not be re-created. + /// By default, it is the responsibility of the repository to use the + /// relationship pointers to persist the relationship. + /// + /// The expected use case is POST-ing or PATCH-ing an entity with HasMany + /// relaitonships: + /// <code> + /// { + /// "data": { + /// "type": "photos", + /// "attributes": { + /// "title": "Ember Hamster", + /// "src": "http://example.com/images/productivity.png" + /// }, + /// "relationships": { + /// "tags": { + /// "data": [ + /// { "type": "tags", "id": "2" }, + /// { "type": "tags", "id": "3" } + /// ] + /// } + /// } + /// } + /// } + /// </code> + /// </summary> + HasManyRelationshipPointers HasManyRelationshipPointers { get; } + + /// <summary> + /// If the request is a bulk json:api v1.1 operations request. + /// This is determined by the ` + /// <see cref="JsonApiDotNetCore.Serialization.JsonApiDeSerializer" />` class. + /// + /// See [json-api/1254](https://github.com/json-api/json-api/pull/1254) for details. + /// </summary> + bool IsBulkOperationRequest { get; set; } + + /// <summary> + /// The `<see cref="ContextEntity" />`for the target route. + /// </summary> + /// + /// <example> + /// For a `GET /articles` request, `RequestEntity` will be set + /// to the `Article` resource representation on the `JsonApiContext`. + /// </example> + ContextEntity RequestEntity { get; set; } + + /// <summary> + /// The concrete type of the controller that was activated by the MVC routing middleware + /// </summary> Type ControllerType { get; set; } + + /// <summary> + /// The json:api meta data at the document level + /// </summary> Dictionary<string, object> DocumentMeta { get; set; } - bool IsBulkOperationRequest { get; set; } + /// <summary> + /// If the request is on the `{id}/relationships/{relationshipName}` route + /// </summary> + bool IsRelationshipPath { get; } + + [Obsolete("Use `IsRelationshipPath` instead.")] + bool IsRelationshipData { get; set; } + } + + public interface IJsonApiContext : IJsonApiRequest + { + IJsonApiContext ApplyContext<T>(object controller); + IMetaBuilder MetaBuilder { get; set; } + IGenericProcessorFactory GenericProcessorFactory { get; set; } + + [Obsolete("Use the proxied method IControllerContext.GetControllerAttribute instead.")] TAttribute GetControllerAttribute<TAttribute>() where TAttribute : Attribute; } } diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index 2665217fef..0643d494d6 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -6,6 +6,7 @@ using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Request; using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Services @@ -51,6 +52,7 @@ public JsonApiContext( public Type ControllerType { get; set; } public Dictionary<string, object> DocumentMeta { get; set; } public bool IsBulkOperationRequest { get; set; } + public HasManyRelationshipPointers HasManyRelationshipPointers { get; } = new HasManyRelationshipPointers(); public IJsonApiContext ApplyContext<T>(object controller) { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs index 1dffa6ce87..067483a1b3 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs @@ -127,5 +127,100 @@ public async Task Can_Update_ToOne_Relationship_ThroughLink() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(todoItemsOwner); } + + [Fact] + public async Task Can_Delete_Relationship_By_Patching_Resource() + { + // arrange + var person = _personFaker.Generate(); + var todoItem = _todoItemFaker.Generate(); + todoItem.Owner = person; + + _context.People.Add(person); + _context.TodoItems.Add(todoItem); + _context.SaveChanges(); + + var builder = new WebHostBuilder() + .UseStartup<Startup>(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var content = new + { + data = new + { + type = "todo-items", + relationships = new + { + owner = new + { + data = (object)null + } + } + } + }; + + var httpMethod = new HttpMethod("PATCH"); + var route = $"/api/v1/todo-items/{todoItem.Id}"; + var request = new HttpRequestMessage(httpMethod, route); + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + + // Act + var response = await client.SendAsync(request); + + // Assert + var todoItemResult = _context.TodoItems + .AsNoTracking() + .Include(t => t.Owner) + .Single(t => t.Id == todoItem.Id); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Null(todoItemResult.Owner); + } + + [Fact] + public async Task Can_Delete_Relationship_By_Patching_Relationship() + { + // arrange + var person = _personFaker.Generate(); + var todoItem = _todoItemFaker.Generate(); + todoItem.Owner = person; + + _context.People.Add(person); + _context.TodoItems.Add(todoItem); + _context.SaveChanges(); + + var builder = new WebHostBuilder() + .UseStartup<Startup>(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var content = new + { + data = (object)null + }; + + var httpMethod = new HttpMethod("PATCH"); + var route = $"/api/v1/todo-items/{todoItem.Id}/relationships/owner"; + var request = new HttpRequestMessage(httpMethod, route); + + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + + // Act + var response = await client.SendAsync(request); + + // Assert + var todoItemResult = _context.TodoItems + .AsNoTracking() + .Include(t => t.Owner) + .Single(t => t.Id == todoItem.Id); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Null(todoItemResult.Owner); + } } } diff --git a/test/UnitTests/Builders/DocumentBuilder_Tests.cs b/test/UnitTests/Builders/DocumentBuilder_Tests.cs index dbca1bebb4..fc816765eb 100644 --- a/test/UnitTests/Builders/DocumentBuilder_Tests.cs +++ b/test/UnitTests/Builders/DocumentBuilder_Tests.cs @@ -1,268 +1,267 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using Moq; -using Xunit; - -namespace UnitTests -{ - public class DocumentBuilder_Tests - { - private readonly Mock<IJsonApiContext> _jsonApiContextMock; - private readonly PageManager _pageManager; - private readonly JsonApiOptions _options; - private readonly Mock<IRequestMeta> _requestMetaMock; - - public DocumentBuilder_Tests() - { - _jsonApiContextMock = new Mock<IJsonApiContext>(); - _requestMetaMock = new Mock<IRequestMeta>(); - - _options = new JsonApiOptions(); - - _options.BuildContextGraph(builder => - { - builder.AddResource<Model>("models"); - builder.AddResource<RelatedModel>("related-models"); - }); - - _jsonApiContextMock - .Setup(m => m.Options) - .Returns(_options); - - _jsonApiContextMock - .Setup(m => m.ContextGraph) - .Returns(_options.ContextGraph); - - _jsonApiContextMock - .Setup(m => m.MetaBuilder) - .Returns(new MetaBuilder()); - - _pageManager = new PageManager(); - _jsonApiContextMock - .Setup(m => m.PageManager) - .Returns(_pageManager); - - _jsonApiContextMock - .Setup(m => m.BasePath) - .Returns("localhost"); - - _jsonApiContextMock - .Setup(m => m.RequestEntity) - .Returns(_options.ContextGraph.GetContextEntity(typeof(Model))); - } - - [Fact] - public void Includes_Paging_Links_By_Default() - { - // arrange - _pageManager.PageSize = 1; - _pageManager.TotalRecords = 1; - _pageManager.CurrentPage = 1; - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model(); - - // act - var document = documentBuilder.Build(entity); - - // assert - Assert.NotNull(document.Links); - Assert.NotNull(document.Links.Last); - } - - [Fact] - public void Page_Links_Can_Be_Disabled_Globally() - { - // arrange - _pageManager.PageSize = 1; - _pageManager.TotalRecords = 1; - _pageManager.CurrentPage = 1; - - _options.BuildContextGraph(builder => builder.DocumentLinks = Link.None); - - _jsonApiContextMock - .Setup(m => m.ContextGraph) - .Returns(_options.ContextGraph); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model(); - - // act - var document = documentBuilder.Build(entity); - - // assert - Assert.Null(document.Links); - } - - [Fact] - public void Related_Links_Can_Be_Disabled() - { - // arrange - _pageManager.PageSize = 1; - _pageManager.TotalRecords = 1; - _pageManager.CurrentPage = 1; - - _jsonApiContextMock - .Setup(m => m.ContextGraph) - .Returns(_options.ContextGraph); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model(); - - // act - var document = documentBuilder.Build(entity); - - // assert - Assert.Null(document.Data.Relationships["related-model"].Links); - } - - [Fact] - public void Related_Data_Included_In_Relationships_By_Default() - { - // arrange - const string relatedTypeName = "related-models"; - const string relationshipName = "related-model"; - const int relatedId = 1; - _jsonApiContextMock - .Setup(m => m.ContextGraph) - .Returns(_options.ContextGraph); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model - { - RelatedModel = new RelatedModel - { - Id = relatedId - } - }; - - // act - var document = documentBuilder.Build(entity); - - // assert - var relationshipData = document.Data.Relationships[relationshipName]; - Assert.NotNull(relationshipData); - Assert.NotNull(relationshipData.SingleData); - Assert.NotNull(relationshipData.SingleData); - Assert.Equal(relatedId.ToString(), relationshipData.SingleData.Id); - Assert.Equal(relatedTypeName, relationshipData.SingleData.Type); +using System.Collections; +using System.Collections.Generic; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Moq; +using Xunit; + +namespace UnitTests +{ + public class DocumentBuilder_Tests + { + private readonly Mock<IJsonApiContext> _jsonApiContextMock; + private readonly PageManager _pageManager; + private readonly JsonApiOptions _options; + private readonly Mock<IRequestMeta> _requestMetaMock; + + public DocumentBuilder_Tests() + { + _jsonApiContextMock = new Mock<IJsonApiContext>(); + _requestMetaMock = new Mock<IRequestMeta>(); + + _options = new JsonApiOptions(); + + _options.BuildContextGraph(builder => + { + builder.AddResource<Model>("models"); + builder.AddResource<RelatedModel>("related-models"); + }); + + _jsonApiContextMock + .Setup(m => m.Options) + .Returns(_options); + + _jsonApiContextMock + .Setup(m => m.ContextGraph) + .Returns(_options.ContextGraph); + + _jsonApiContextMock + .Setup(m => m.MetaBuilder) + .Returns(new MetaBuilder()); + + _pageManager = new PageManager(); + _jsonApiContextMock + .Setup(m => m.PageManager) + .Returns(_pageManager); + + _jsonApiContextMock + .Setup(m => m.BasePath) + .Returns("localhost"); + + _jsonApiContextMock + .Setup(m => m.RequestEntity) + .Returns(_options.ContextGraph.GetContextEntity(typeof(Model))); + } + + [Fact] + public void Includes_Paging_Links_By_Default() + { + // arrange + _pageManager.PageSize = 1; + _pageManager.TotalRecords = 1; + _pageManager.CurrentPage = 1; + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var entity = new Model(); + + // act + var document = documentBuilder.Build(entity); + + // assert + Assert.NotNull(document.Links); + Assert.NotNull(document.Links.Last); } - [Fact] - public void IndependentIdentifier__Included_In_HasOne_Relationships_By_Default() - { - // arrange - const string relatedTypeName = "related-models"; - const string relationshipName = "related-model"; - const int relatedId = 1; - _jsonApiContextMock - .Setup(m => m.ContextGraph) - .Returns(_options.ContextGraph); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model + [Fact] + public void Page_Links_Can_Be_Disabled_Globally() + { + // arrange + _pageManager.PageSize = 1; + _pageManager.TotalRecords = 1; + _pageManager.CurrentPage = 1; + + _options.BuildContextGraph(builder => builder.DocumentLinks = Link.None); + + _jsonApiContextMock + .Setup(m => m.ContextGraph) + .Returns(_options.ContextGraph); + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var entity = new Model(); + + // act + var document = documentBuilder.Build(entity); + + // assert + Assert.Null(document.Links); + } + + [Fact] + public void Related_Links_Can_Be_Disabled() + { + // arrange + _pageManager.PageSize = 1; + _pageManager.TotalRecords = 1; + _pageManager.CurrentPage = 1; + + _jsonApiContextMock + .Setup(m => m.ContextGraph) + .Returns(_options.ContextGraph); + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var entity = new Model(); + + // act + var document = documentBuilder.Build(entity); + + // assert + Assert.Null(document.Data.Relationships["related-model"].Links); + } + + [Fact] + public void Related_Data_Included_In_Relationships_By_Default() + { + // arrange + const string relatedTypeName = "related-models"; + const string relationshipName = "related-model"; + const int relatedId = 1; + _jsonApiContextMock + .Setup(m => m.ContextGraph) + .Returns(_options.ContextGraph); + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var entity = new Model { - RelatedModelId = relatedId - }; - - // act - var document = documentBuilder.Build(entity); - - // assert - var relationshipData = document.Data.Relationships[relationshipName]; - Assert.NotNull(relationshipData); - Assert.NotNull(relationshipData.SingleData); - Assert.NotNull(relationshipData.SingleData); - Assert.Equal(relatedId.ToString(), relationshipData.SingleData.Id); - Assert.Equal(relatedTypeName, relationshipData.SingleData.Type); - } - - [Fact] - public void Build_Can_Build_Arrays() - { - var entities = new[] { new Model() }; - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - - var documents = documentBuilder.Build(entities); - - Assert.Equal(1, documents.Data.Count); - } - - [Fact] - public void Build_Can_Build_CustomIEnumerables() - { - var entities = new Models(new[] { new Model() }); - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - - var documents = documentBuilder.Build(entities); - - Assert.Equal(1, documents.Data.Count); - } - - - [Theory] - [InlineData(null, null, true)] - [InlineData(false, null, true)] - [InlineData(true, null, false)] - [InlineData(null, "foo", true)] - [InlineData(false, "foo", true)] - [InlineData(true, "foo", true)] - public void DocumentBuilderOptions(bool? omitNullValuedAttributes, - string attributeValue, - bool resultContainsAttribute) - { - var documentBuilderBehaviourMock = new Mock<IDocumentBuilderOptionsProvider>(); - if (omitNullValuedAttributes.HasValue) - { - documentBuilderBehaviourMock.Setup(m => m.GetDocumentBuilderOptions()) - .Returns(new DocumentBuilderOptions(omitNullValuedAttributes.Value)); - } - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, null, omitNullValuedAttributes.HasValue ? documentBuilderBehaviourMock.Object : null); - var document = documentBuilder.Build(new Model() { StringProperty = attributeValue }); - - Assert.Equal(resultContainsAttribute, document.Data.Attributes.ContainsKey("StringProperty")); - } - - private class Model : Identifiable - { - [HasOne("related-model", Link.None)] - public RelatedModel RelatedModel { get; set; } - public int RelatedModelId { get; set; } - [Attr("StringProperty")] - public string StringProperty { get; set; } - - } - - private class RelatedModel : Identifiable - { - [HasMany("models")] - public List<Model> Models { get; set; } - } - - private class Models : IEnumerable<Model> - { - private readonly IEnumerable<Model> models; - - public Models(IEnumerable<Model> models) - { - this.models = models; - } - - public IEnumerator<Model> GetEnumerator() - { - return models.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return models.GetEnumerator(); - } - } - } -} + RelatedModel = new RelatedModel + { + Id = relatedId + } + }; + + // act + var document = documentBuilder.Build(entity); + + // assert + var relationshipData = document.Data.Relationships[relationshipName]; + Assert.NotNull(relationshipData); + Assert.NotNull(relationshipData.SingleData); + Assert.NotNull(relationshipData.SingleData); + Assert.Equal(relatedId.ToString(), relationshipData.SingleData.Id); + Assert.Equal(relatedTypeName, relationshipData.SingleData.Type); + } + + [Fact] + public void IndependentIdentifier__Included_In_HasOne_Relationships_By_Default() + { + // arrange + const string relatedTypeName = "related-models"; + const string relationshipName = "related-model"; + const int relatedId = 1; + _jsonApiContextMock + .Setup(m => m.ContextGraph) + .Returns(_options.ContextGraph); + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var entity = new Model + { + RelatedModelId = relatedId + }; + + // act + var document = documentBuilder.Build(entity); + + // assert + var relationshipData = document.Data.Relationships[relationshipName]; + Assert.NotNull(relationshipData); + Assert.NotNull(relationshipData.SingleData); + Assert.NotNull(relationshipData.SingleData); + Assert.Equal(relatedId.ToString(), relationshipData.SingleData.Id); + Assert.Equal(relatedTypeName, relationshipData.SingleData.Type); + } + + [Fact] + public void Build_Can_Build_Arrays() + { + var entities = new[] { new Model() }; + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + + var documents = documentBuilder.Build(entities); + + Assert.Equal(1, documents.Data.Count); + } + + [Fact] + public void Build_Can_Build_CustomIEnumerables() + { + var entities = new Models(new[] { new Model() }); + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + + var documents = documentBuilder.Build(entities); + + Assert.Equal(1, documents.Data.Count); + } + + + [Theory] + [InlineData(null, null, true)] + [InlineData(false, null, true)] + [InlineData(true, null, false)] + [InlineData(null, "foo", true)] + [InlineData(false, "foo", true)] + [InlineData(true, "foo", true)] + public void DocumentBuilderOptions(bool? omitNullValuedAttributes, + string attributeValue, + bool resultContainsAttribute) + { + var documentBuilderBehaviourMock = new Mock<IDocumentBuilderOptionsProvider>(); + if (omitNullValuedAttributes.HasValue) + { + documentBuilderBehaviourMock.Setup(m => m.GetDocumentBuilderOptions()) + .Returns(new DocumentBuilderOptions(omitNullValuedAttributes.Value)); + } + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, null, omitNullValuedAttributes.HasValue ? documentBuilderBehaviourMock.Object : null); + var document = documentBuilder.Build(new Model() { StringProperty = attributeValue }); + + Assert.Equal(resultContainsAttribute, document.Data.Attributes.ContainsKey("StringProperty")); + } + + private class Model : Identifiable + { + [HasOne("related-model", Link.None)] + public RelatedModel RelatedModel { get; set; } + public int RelatedModelId { get; set; } + [Attr("StringProperty")] + public string StringProperty { get; set; } + + } + + private class RelatedModel : Identifiable + { + [HasMany("models")] + public List<Model> Models { get; set; } + } + + private class Models : IEnumerable<Model> + { + private readonly IEnumerable<Model> models; + + public Models(IEnumerable<Model> models) + { + this.models = models; + } + + public IEnumerator<Model> GetEnumerator() + { + return models.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return models.GetEnumerator(); + } + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Unit/Builders/MetaBuilderTests.cs b/test/UnitTests/Builders/MetaBuilderTests.cs similarity index 97% rename from test/JsonApiDotNetCoreExampleTests/Unit/Builders/MetaBuilderTests.cs rename to test/UnitTests/Builders/MetaBuilderTests.cs index 5cd0b765de..0b784ef5b7 100644 --- a/test/JsonApiDotNetCoreExampleTests/Unit/Builders/MetaBuilderTests.cs +++ b/test/UnitTests/Builders/MetaBuilderTests.cs @@ -1,8 +1,8 @@ -using Xunit; -using JsonApiDotNetCore.Builders; using System.Collections.Generic; +using JsonApiDotNetCore.Builders; +using Xunit; -namespace JsonApiDotNetCoreExampleTests.Unit.Builders +namespace UnitTests.Builders { public class MetaBuilderTests { diff --git a/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs similarity index 88% rename from test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs rename to test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index f6772fa22b..1b00c5aaa1 100644 --- a/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -10,12 +10,11 @@ using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; -using UnitTests; using Xunit; +using Microsoft.EntityFrameworkCore; -namespace JsonApiDotNetCoreExampleTests.Unit.Extensions +namespace UnitTests.Extensions { public class IServiceCollectionExtensionsTests { @@ -26,10 +25,7 @@ public void AddJsonApiInternals_Adds_All_Required_Services() var services = new ServiceCollection(); var jsonApiOptions = new JsonApiOptions(); - services.AddDbContext<AppDbContext>(options => - { - options.UseMemoryCache(new MemoryCache(new MemoryCacheOptions())); - }, ServiceLifetime.Transient); + services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase("UnitTestDb"), ServiceLifetime.Transient); // act services.AddJsonApiInternals<AppDbContext>(jsonApiOptions); diff --git a/test/UnitTests/Extensions/TypeExtensions_Tests.cs b/test/UnitTests/Extensions/TypeExtensions_Tests.cs new file mode 100644 index 0000000000..f59fa37be0 --- /dev/null +++ b/test/UnitTests/Extensions/TypeExtensions_Tests.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Models; +using Xunit; + +namespace UnitTests.Extensions +{ + public class TypeExtensions_Tests + { + [Fact] + public void GetCollection_Creates_List_If_T_Implements_Interface() + { + // arrange + var type = typeof(Model); + + // act + var collection = type.GetEmptyCollection(); + + // assert + Assert.NotNull(collection); + Assert.Empty(collection); + Assert.IsType<List<Model>>(collection); + } + + [Fact] + public void New_Creates_An_Instance_If_T_Implements_Interface() + { + // arrange + var type = typeof(Model); + + // act + var instance = type.New<IIdentifiable>(); + + // assert + Assert.NotNull(instance); + Assert.IsType<Model>(instance); + } + + private class Model : IIdentifiable + { + public string StringId { get; set; } + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Unit/Models/AttributesEqualsTests.cs b/test/UnitTests/Models/AttributesEqualsTests.cs similarity index 97% rename from test/JsonApiDotNetCoreExampleTests/Unit/Models/AttributesEqualsTests.cs rename to test/UnitTests/Models/AttributesEqualsTests.cs index 107dd1d593..0b989169ef 100644 --- a/test/JsonApiDotNetCoreExampleTests/Unit/Models/AttributesEqualsTests.cs +++ b/test/UnitTests/Models/AttributesEqualsTests.cs @@ -1,7 +1,7 @@ using JsonApiDotNetCore.Models; using Xunit; -namespace JsonApiDotNetCoreExampleTests.Unit.Models +namespace UnitTests.Models { public class AttributesEqualsTests { diff --git a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs index 1e20c0359e..ec34d87d24 100644 --- a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Request; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; using Moq; @@ -11,10 +11,13 @@ using Newtonsoft.Json.Serialization; using Xunit; -namespace UnitTests.Serialization { - public class JsonApiDeSerializerTests { +namespace UnitTests.Serialization +{ + public class JsonApiDeSerializerTests + { [Fact] - public void Can_Deserialize_Complex_Types() { + public void Can_Deserialize_Complex_Types() + { // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource<TestResource>("test-resource"); @@ -29,20 +32,18 @@ public void Can_Deserialize_Complex_Types() { jsonApiOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - var content = new Document { - Data = new DocumentData { - Type = "test-resource", - Id = "1", - Attributes = new Dictionary<string, object> { + var content = new Document + { + Data = new DocumentData { - "complex-member", - new { compoundName = "testName" } - } - } + Type = "test-resource", + Id = "1", + Attributes = new Dictionary<string, object> + { + { "complex-member", new { compoundName = "testName" } } + } } }; @@ -55,7 +56,8 @@ public void Can_Deserialize_Complex_Types() { } [Fact] - public void Can_Deserialize_Complex_List_Types() { + public void Can_Deserialize_Complex_List_Types() + { // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource<TestResourceWithList>("test-resource"); @@ -69,22 +71,18 @@ public void Can_Deserialize_Complex_List_Types() { jsonApiOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); - - var content = new Document { - Data = new DocumentData { - Type = "test-resource", - Id = "1", - Attributes = new Dictionary<string, object> { + var content = new Document + { + Data = new DocumentData { - "complex-members", - new [] { - new { compoundName = "testName" } - } - } - } + Type = "test-resource", + Id = "1", + Attributes = new Dictionary<string, object> + { + { "complex-members", new [] { new { compoundName = "testName" } } } + } } }; @@ -98,7 +96,8 @@ public void Can_Deserialize_Complex_List_Types() { } [Fact] - public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs() { + public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs() + { // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource<TestResource>("test-resource"); @@ -113,20 +112,20 @@ public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs() { jsonApiOptions.SerializerSettings.ContractResolver = new DasherizedResolver(); // <-- jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); - - var content = new Document { - Data = new DocumentData { - Type = "test-resource", - Id = "1", - Attributes = new Dictionary<string, object> { + var content = new Document + { + Data = new DocumentData { - "complex-member", - new Dictionary<string, string> { { "compound-name", "testName" } } - } - } + Type = "test-resource", + Id = "1", + Attributes = new Dictionary<string, object> + { + { + "complex-member", new Dictionary<string, string> { { "compound-name", "testName" } } + } + } } }; @@ -139,7 +138,8 @@ public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs() { } [Fact] - public void Immutable_Attrs_Are_Not_Included_In_AttributesToUpdate() { + public void Immutable_Attrs_Are_Not_Included_In_AttributesToUpdate() + { // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource<TestResource>("test-resource"); @@ -156,22 +156,21 @@ public void Immutable_Attrs_Are_Not_Included_In_AttributesToUpdate() { jsonApiOptions.SerializerSettings.ContractResolver = new DasherizedResolver(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); - - var content = new Document { - Data = new DocumentData { - Type = "test-resource", - Id = "1", - Attributes = new Dictionary<string, object> { + var content = new Document + { + Data = new DocumentData { - "complex-member", - new Dictionary<string, string> { { "compound-name", "testName" } - } - }, - { "immutable", "value" } - } + Type = "test-resource", + Id = "1", + Attributes = new Dictionary<string, object> + { + { + "complex-member", new Dictionary<string, string> { { "compound-name", "testName" } } + }, + { "immutable", "value" } + } } }; @@ -189,7 +188,8 @@ public void Immutable_Attrs_Are_Not_Included_In_AttributesToUpdate() { } [Fact] - public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship() { + public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship() + { // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource<Independent>("independents"); @@ -204,17 +204,16 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship() { var jsonApiOptions = new JsonApiOptions(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); var property = Guid.NewGuid().ToString(); - var content = new Document { - Data = new DocumentData { - Type = "independents", - Id = "1", - Attributes = new Dictionary<string, object> { { "property", property } - } + var content = new Document + { + Data = new DocumentData + { + Type = "independents", + Id = "1", + Attributes = new Dictionary<string, object> { { "property", property } } } }; @@ -229,7 +228,8 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship() { } [Fact] - public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Relationship_Body() { + public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Relationship_Body() + { // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource<Independent>("independents"); @@ -244,20 +244,18 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Rel var jsonApiOptions = new JsonApiOptions(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); var property = Guid.NewGuid().ToString(); - var content = new Document { - Data = new DocumentData { - Type = "independents", - Id = "1", - Attributes = new Dictionary<string, object> { { "property", property } - }, - // a common case for this is deserialization in unit tests - Relationships = new Dictionary<string, RelationshipData> { { "dependent", new RelationshipData { } } - } + var content = new Document + { + Data = new DocumentData + { + Type = "independents", + Id = "1", + Attributes = new Dictionary<string, object> { { "property", property } }, + // a common case for this is deserialization in unit tests + Relationships = new Dictionary<string, RelationshipData> { { "dependent", new RelationshipData { } } } } }; @@ -288,24 +286,21 @@ public void Sets_The_DocumentMeta_Property_In_JsonApiContext() var jsonApiOptions = new JsonApiOptions(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); var property = Guid.NewGuid().ToString(); - + var content = new Document - { - Meta = new Dictionary<string, object>() { {"foo", "bar"}}, + { + Meta = new Dictionary<string, object>() { { "foo", "bar" } }, Data = new DocumentData { Type = "independents", Id = "1", - Attributes = new Dictionary<string, object> { { "property", property } - }, + Attributes = new Dictionary<string, object> { { "property", property } }, // a common case for this is deserialization in unit tests - Relationships = new Dictionary<string, RelationshipData> { { "dependent", new RelationshipData { } } - } + Relationships = new Dictionary<string, RelationshipData> { { "dependent", new RelationshipData { } } } } }; @@ -318,32 +313,100 @@ public void Sets_The_DocumentMeta_Property_In_JsonApiContext() jsonApiContextMock.VerifySet(mock => mock.DocumentMeta = content.Meta); } - - private class TestResource : Identifiable { + private class TestResource : Identifiable + { [Attr("complex-member")] public ComplexType ComplexMember { get; set; } - [Attr("immutable", isImmutable : true)] + [Attr("immutable", isImmutable: true)] public string Immutable { get; set; } } - private class TestResourceWithList : Identifiable { + private class TestResourceWithList : Identifiable + { [Attr("complex-members")] public List<ComplexType> ComplexMembers { get; set; } } - private class ComplexType { + private class ComplexType + { public string CompoundName { get; set; } } - private class Independent : Identifiable { + private class Independent : Identifiable + { [Attr("property")] public string Property { get; set; } [HasOne("dependent")] public Dependent Dependent { get; set; } } - private class Dependent : Identifiable { + private class Dependent : Identifiable + { [HasOne("independent")] public Independent Independent { get; set; } public int IndependentId { get; set; } } + + [Fact] + public void Can_Deserialize_Object_With_HasManyRelationship() + { + // arrange + var contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource<OneToManyIndependent>("independents"); + contextGraphBuilder.AddResource<OneToManyDependent>("dependents"); + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock<IJsonApiContext>(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary<AttrAttribute, object>()); + jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); + + var jsonApiOptions = new JsonApiOptions(); + jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); + + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); + + var contentString = + @"{ + ""data"": { + ""type"": ""independents"", + ""id"": ""1"", + ""attributes"": { }, + ""relationships"": { + ""dependents"": { + ""data"": [ + { + ""type"": ""dependents"", + ""id"": ""2"" + } + ] + } + } + } + }"; + + // act + var result = deserializer.Deserialize<OneToManyIndependent>(contentString); + + // assert + Assert.NotNull(result); + Assert.Equal(1, result.Id); + Assert.NotNull(result.Dependents); + Assert.NotEmpty(result.Dependents); + Assert.Equal(1, result.Dependents.Count); + + var dependent = result.Dependents[0]; + Assert.Equal(2, dependent.Id); + } + + private class OneToManyDependent : Identifiable + { + [HasOne("independent")] public OneToManyIndependent Independent { get; set; } + public int IndependentId { get; set; } + } + + private class OneToManyIndependent : Identifiable + { + [HasMany("dependents")] public List<OneToManyDependent> Dependents { get; set; } + } } } diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj index 14a0d30e33..a6ed346e7d 100644 --- a/test/UnitTests/UnitTests.csproj +++ b/test/UnitTests/UnitTests.csproj @@ -12,5 +12,6 @@ <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" /> <PackageReference Include="xunit" Version="$(XUnitVersion)" /> <PackageReference Include="Moq" Version="$(MoqVersion)" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.0.2" /> </ItemGroup> </Project>