diff --git a/docs/EntityRepositories.md b/docs/EntityRepositories.md index 9dc269b622..62d156e427 100644 --- a/docs/EntityRepositories.md +++ b/docs/EntityRepositories.md @@ -24,16 +24,14 @@ A sample implementation that performs data authorization might look like: public class MyAuthorizedEntityRepository : DefaultEntityRepository { private readonly ILogger _logger; - private readonly AppDbContext _context; private readonly IAuthenticationService _authenticationService; - public MyAuthorizedEntityRepository(AppDbContext context, + public MyAuthorizedEntityRepository( ILoggerFactory loggerFactory, IJsonApiContext jsonApiContext, IAuthenticationService authenticationService) - : base(context, loggerFactory, jsonApiContext) - { - _context = context; + : base(loggerFactory, jsonApiContext) + { _logger = loggerFactory.CreateLogger(); _authenticationService = authenticationService; } diff --git a/src/JsonApiDotNetCore/Data/DbContextResolver.cs b/src/JsonApiDotNetCore/Data/DbContextResolver.cs new file mode 100644 index 0000000000..8aedee0f4e --- /dev/null +++ b/src/JsonApiDotNetCore/Data/DbContextResolver.cs @@ -0,0 +1,21 @@ +using System; +using JsonApiDotNetCore.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace JsonApiDotNetCore.Data +{ + public class DbContextResolver : IDbContextResolver + { + private readonly DbContext _context; + + public DbContextResolver(DbContext context) + { + _context = context; + } + + public DbContext GetContext() => _context; + + public DbSet GetDbSet() where TEntity : class + => _context.GetDbSet(); + } +} diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 4630cf3b49..952b6f8326 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -17,12 +18,19 @@ public class DefaultEntityRepository IEntityRepository where TEntity : class, IIdentifiable { + [Obsolete("DbContext is no longer directly injected into the ctor. Use JsonApiContext.GetDbContextResolver() instead")] public DefaultEntityRepository( DbContext context, ILoggerFactory loggerFactory, IJsonApiContext jsonApiContext) : base(context, loggerFactory, jsonApiContext) { } + + public DefaultEntityRepository( + ILoggerFactory loggerFactory, + IJsonApiContext jsonApiContext) + : base(loggerFactory, jsonApiContext) + { } } public class DefaultEntityRepository @@ -35,6 +43,7 @@ public class DefaultEntityRepository private readonly IJsonApiContext _jsonApiContext; private readonly IGenericProcessorFactory _genericProcessorFactory; + [Obsolete("DbContext is no longer directly injected into the ctor. Use JsonApiContext.GetDbContextResolver() instead")] public DefaultEntityRepository( DbContext context, ILoggerFactory loggerFactory, @@ -47,6 +56,18 @@ public DefaultEntityRepository( _genericProcessorFactory = _jsonApiContext.GenericProcessorFactory; } + public DefaultEntityRepository( + ILoggerFactory loggerFactory, + IJsonApiContext jsonApiContext) + { + var contextResolver = jsonApiContext.GetDbContextResolver(); + _context = contextResolver.GetContext(); + _dbSet = contextResolver.GetDbSet(); + _jsonApiContext = jsonApiContext; + _logger = loggerFactory.CreateLogger>(); + _genericProcessorFactory = _jsonApiContext.GenericProcessorFactory; + } + public virtual IQueryable Get() { if(_jsonApiContext.QuerySet?.Fields != null && _jsonApiContext.QuerySet.Fields.Any()) @@ -111,10 +132,8 @@ public virtual async Task UpdateAsync(TId id, TEntity entity) if (oldEntity == null) return null; - _jsonApiContext.RequestEntity.Attributes.ForEach(attr => - { - attr.SetValue(oldEntity, attr.GetValue(entity)); - }); + foreach(var attr in _jsonApiContext.AttributesToUpdate) + attr.Key.SetValue(oldEntity, attr.Value); foreach(var relationship in _jsonApiContext.RelationshipsToUpdate) relationship.Key.SetValue(oldEntity, relationship.Value); diff --git a/src/JsonApiDotNetCore/Data/IDbContextResolver.cs b/src/JsonApiDotNetCore/Data/IDbContextResolver.cs new file mode 100644 index 0000000000..5a8c5a2e12 --- /dev/null +++ b/src/JsonApiDotNetCore/Data/IDbContextResolver.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore; + +namespace JsonApiDotNetCore.Data +{ + public interface IDbContextResolver + { + DbContext GetContext(); + DbSet GetDbSet() + where TEntity : class; + } +} diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 43bc63da4a..012f47da2e 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -89,6 +89,7 @@ public static void AddJsonApiInternals( services.AddSingleton(new DbContextOptionsBuilder().Options); } + services.AddScoped(); services.AddScoped(typeof(IEntityRepository<>), typeof(DefaultEntityRepository<>)); services.AddScoped(typeof(IEntityRepository<,>), typeof(DefaultEntityRepository<,>)); services.AddScoped(typeof(IResourceService<>), typeof(EntityResourceService<>)); diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index eb6ca54017..1dbf2ce662 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@  - 2.0.6 + 2.0.7 netstandard1.6 JsonApiDotNetCore JsonApiDotNetCore diff --git a/src/JsonApiDotNetCore/Models/AttrAttribute.cs b/src/JsonApiDotNetCore/Models/AttrAttribute.cs index f9b9f89d89..65fcb3d44b 100644 --- a/src/JsonApiDotNetCore/Models/AttrAttribute.cs +++ b/src/JsonApiDotNetCore/Models/AttrAttribute.cs @@ -11,6 +11,12 @@ public AttrAttribute(string publicName) PublicAttributeName = publicName; } + public AttrAttribute(string publicName, string internalName) + { + PublicAttributeName = publicName; + InternalAttributeName = internalName; + } + public string PublicAttributeName { get; set; } public string InternalAttributeName { get; set; } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index fbbd4f69e2..d64d18366f 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -27,7 +27,7 @@ public JsonApiDeSerializer( public object Deserialize(string requestBody) { var document = JsonConvert.DeserializeObject(requestBody); - var entity = DataToObject(document.Data); + var entity = DocumentToObject(document.Data); return entity; } @@ -46,7 +46,6 @@ public object DeserializeRelationship(string requestBody) return new List { data.ToObject() }; } - public List DeserializeList(string requestBody) { var documents = JsonConvert.DeserializeObject(requestBody); @@ -54,22 +53,22 @@ public List DeserializeList(string requestBody) var deserializedList = new List(); foreach (var data in documents.Data) { - var entity = DataToObject(data); + var entity = DocumentToObject(data); deserializedList.Add((TEntity)entity); } return deserializedList; } - private object DataToObject(DocumentData data) + private object DocumentToObject(DocumentData data) { var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(data.Type); _jsonApiContext.RequestEntity = contextEntity; var entity = Activator.CreateInstance(contextEntity.EntityType); - entity = _setEntityAttributes(entity, contextEntity, data.Attributes); - entity = _setRelationships(entity, contextEntity, data.Relationships); + entity = SetEntityAttributes(entity, contextEntity, data.Attributes); + entity = SetRelationships(entity, contextEntity, data.Relationships); var identifiableEntity = (IIdentifiable)entity; @@ -79,7 +78,7 @@ private object DataToObject(DocumentData data) return identifiableEntity; } - private object _setEntityAttributes( + private object SetEntityAttributes( object entity, ContextEntity contextEntity, Dictionary attributeValues) { if (attributeValues == null || attributeValues.Count == 0) @@ -99,13 +98,14 @@ private object _setEntityAttributes( { var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType); entityProperty.SetValue(entity, convertedValue); + _jsonApiContext.AttributesToUpdate[attr] = convertedValue; } } return entity; } - private object _setRelationships( + private object SetRelationships( object entity, ContextEntity contextEntity, Dictionary relationships) @@ -118,14 +118,14 @@ private object _setRelationships( foreach (var attr in contextEntity.Relationships) { entity = attr.IsHasOne - ? _setHasOneRelationship(entity, entityProperties, attr, contextEntity, relationships) - : _setHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships); + ? SetHasOneRelationship(entity, entityProperties, attr, contextEntity, relationships) + : SetHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships); } return entity; } - private object _setHasOneRelationship(object entity, + private object SetHasOneRelationship(object entity, PropertyInfo[] entityProperties, RelationshipAttribute attr, ContextEntity contextEntity, @@ -158,7 +158,7 @@ private object _setHasOneRelationship(object entity, return entity; } - private object _setHasManyRelationship(object entity, + private object SetHasManyRelationship(object entity, PropertyInfo[] entityProperties, RelationshipAttribute attr, ContextEntity contextEntity, diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index cfc58cec61..0319f0aeb6 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; @@ -22,6 +23,8 @@ public interface IJsonApiContext PageManager PageManager { get; set; } IMetaBuilder MetaBuilder { get; set; } IGenericProcessorFactory GenericProcessorFactory { get; set; } + Dictionary AttributesToUpdate { get; set; } Dictionary RelationshipsToUpdate { get; set; } + IDbContextResolver GetDbContextResolver(); } } diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index 908a5c9cf9..f4f53c03c1 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -1,7 +1,9 @@ +using System; using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; @@ -13,19 +15,22 @@ namespace JsonApiDotNetCore.Services public class JsonApiContext : IJsonApiContext { private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IDbContextResolver _contextResolver; + public JsonApiContext( + IDbContextResolver contextResolver, IContextGraph contextGraph, IHttpContextAccessor httpContextAccessor, JsonApiOptions options, IMetaBuilder metaBuilder, IGenericProcessorFactory genericProcessorFactory) { + _contextResolver = contextResolver; ContextGraph = contextGraph; _httpContextAccessor = httpContextAccessor; Options = options; MetaBuilder = metaBuilder; GenericProcessorFactory = genericProcessorFactory; - RelationshipsToUpdate = new Dictionary(); } public JsonApiOptions Options { get; set; } @@ -39,7 +44,8 @@ public JsonApiContext( public PageManager PageManager { get; set; } public IMetaBuilder MetaBuilder { get; set; } public IGenericProcessorFactory GenericProcessorFactory { get; set; } - public Dictionary RelationshipsToUpdate { get; set; } + public Dictionary AttributesToUpdate { get; set; } = new Dictionary(); + public Dictionary RelationshipsToUpdate { get; set; } = new Dictionary(); public IJsonApiContext ApplyContext() { @@ -47,8 +53,8 @@ public IJsonApiContext ApplyContext() var path = context.Request.Path.Value.Split('/'); RequestEntity = ContextGraph.GetContextEntity(typeof(T)); - - if(context.Request.Query.Any()) + + if (context.Request.Query.Any()) { QuerySet = new QuerySet(this, context.Request.Query); IncludedRelationships = QuerySet.IncludedRelationships; @@ -61,14 +67,17 @@ public IJsonApiContext ApplyContext() return this; } + public IDbContextResolver GetDbContextResolver() => _contextResolver; + private PageManager GetPageManager() { - if(Options.DefaultPageSize == 0 && (QuerySet == null || QuerySet.PageQuery.PageSize == 0)) + if (Options.DefaultPageSize == 0 && (QuerySet == null || QuerySet.PageQuery.PageSize == 0)) return new PageManager(); - - var query = QuerySet?.PageQuery ?? new PageQuery(); - return new PageManager { + var query = QuerySet?.PageQuery ?? new PageQuery(); + + return new PageManager + { DefaultPageSize = Options.DefaultPageSize, CurrentPage = query.PageOffset > 0 ? query.PageOffset : 1, PageSize = query.PageSize > 0 ? query.PageSize : Options.DefaultPageSize diff --git a/test/UnitTests/JsonApiControllerMixin_Tests.cs b/test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs similarity index 100% rename from test/UnitTests/JsonApiControllerMixin_Tests.cs rename to test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs diff --git a/test/UnitTests/Data/DefaultEntityRepository_Tests.cs b/test/UnitTests/Data/DefaultEntityRepository_Tests.cs new file mode 100644 index 0000000000..e596b35317 --- /dev/null +++ b/test/UnitTests/Data/DefaultEntityRepository_Tests.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal; +using Microsoft.AspNetCore.Mvc; +using Xunit; +using Moq; +using Microsoft.EntityFrameworkCore; +using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Models; +using Microsoft.Extensions.Logging; +using JsonApiDotNetCore.Services; +using System.Threading.Tasks; + +namespace UnitTests.Data +{ + public class DefaultEntityRepository_Tests : JsonApiControllerMixin + { + private readonly Mock _jsonApiContextMock; + private readonly Mock _loggFactoryMock; + private readonly Mock> _dbSetMock; + private readonly Mock _contextMock; + private readonly Mock _contextResolverMock; + private readonly TodoItem _todoItem; + private Dictionary _attrsToUpdate = new Dictionary(); + private Dictionary _relationshipsToUpdate = new Dictionary(); + + public DefaultEntityRepository_Tests() + { + _todoItem = new TodoItem + { + Id = 1, + Description = Guid.NewGuid().ToString(), + Ordinal = 10 + }; + _jsonApiContextMock = new Mock(); + _loggFactoryMock = new Mock(); + _dbSetMock = DbSetMock.Create(new[] { _todoItem }); + _contextMock = new Mock(); + _contextResolverMock = new Mock(); + } + + [Fact] + public async Task UpdateAsync_Updates_Attributes_In_AttributesToUpdate() + { + // arrange + var todoItemUpdates = new TodoItem + { + Id = _todoItem.Id, + Description = Guid.NewGuid().ToString() + }; + + _attrsToUpdate = new Dictionary + { + { + new AttrAttribute("description", "Description"), + todoItemUpdates.Description + } + }; + + var repository = GetRepository(); + + // act + var updatedItem = await repository.UpdateAsync(_todoItem.Id, todoItemUpdates); + + // assert + Assert.NotNull(updatedItem); + Assert.Equal(_todoItem.Ordinal, updatedItem.Ordinal); + Assert.Equal(todoItemUpdates.Description, updatedItem.Description); + } + + private DefaultEntityRepository GetRepository() + { + _contextResolverMock + .Setup(m => m.GetContext()) + .Returns(_contextMock.Object); + + _contextResolverMock + .Setup(m => m.GetDbSet()) + .Returns(_dbSetMock.Object); + + _jsonApiContextMock + .Setup(m => m.AttributesToUpdate) + .Returns(_attrsToUpdate); + + _jsonApiContextMock + .Setup(m => m.RelationshipsToUpdate) + .Returns(_relationshipsToUpdate); + + _jsonApiContextMock + .Setup(m => m.GetDbContextResolver()) + .Returns(_contextResolverMock.Object); + + return new DefaultEntityRepository( + _loggFactoryMock.Object, + _jsonApiContextMock.Object); + } + } +} diff --git a/test/UnitTests/DbSetMock.cs b/test/UnitTests/DbSetMock.cs new file mode 100644 index 0000000000..f56a3b1f01 --- /dev/null +++ b/test/UnitTests/DbSetMock.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query.Internal; +using Moq; + +public static class DbSetMock +{ + public static Mock> Create(params T[] elements) where T : class + { + return new List(elements).AsDbSetMock(); + } +} + +public static class ListExtensions +{ + public static Mock> AsDbSetMock(this List list) where T : class + { + IQueryable queryableList = list.AsQueryable(); + Mock> dbSetMock = new Mock>(); + + dbSetMock.As>().Setup(x => x.Expression).Returns(queryableList.Expression); + dbSetMock.As>().Setup(x => x.ElementType).Returns(queryableList.ElementType); + dbSetMock.As>().Setup(x => x.GetEnumerator()).Returns(queryableList.GetEnumerator()); + + dbSetMock.As>().Setup(m => m.GetEnumerator()).Returns(new TestAsyncEnumerator(queryableList.GetEnumerator())); + dbSetMock.As>().Setup(m => m.Provider).Returns(new TestAsyncQueryProvider(queryableList.Provider)); + return dbSetMock; + } +} + +internal class TestAsyncQueryProvider : IAsyncQueryProvider +{ + private readonly IQueryProvider _inner; + + internal TestAsyncQueryProvider(IQueryProvider inner) + { + _inner = inner; + } + + public IQueryable CreateQuery(Expression expression) + { + return new TestAsyncEnumerable(expression); + } + + public IQueryable CreateQuery(Expression expression) + { + return new TestAsyncEnumerable(expression); + } + + public object Execute(Expression expression) + { + return _inner.Execute(expression); + } + + public TResult Execute(Expression expression) + { + return _inner.Execute(expression); + } + + public IAsyncEnumerable ExecuteAsync(Expression expression) + { + return new TestAsyncEnumerable(expression); + } + + public Task ExecuteAsync(Expression expression, CancellationToken cancellationToken) + { + return Task.FromResult(Execute(expression)); + } +} + +internal class TestAsyncEnumerable : EnumerableQuery, IAsyncEnumerable, IQueryable +{ + public TestAsyncEnumerable(IEnumerable enumerable) + : base(enumerable) + { } + + public TestAsyncEnumerable(Expression expression) + : base(expression) + { } + + public IAsyncEnumerator GetEnumerator() + { + return new TestAsyncEnumerator(this.AsEnumerable().GetEnumerator()); + } + + IQueryProvider IQueryable.Provider + { + get { return new TestAsyncQueryProvider(this); } + } +} + +internal class TestAsyncEnumerator : IAsyncEnumerator +{ + private readonly IEnumerator _inner; + + public TestAsyncEnumerator(IEnumerator inner) + { + _inner = inner; + } + + public void Dispose() + { + _inner.Dispose(); + } + + public T Current + { + get + { + return _inner.Current; + } + } + + public Task MoveNext(CancellationToken cancellationToken) + { + return Task.FromResult(_inner.MoveNext()); + } +} \ No newline at end of file diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj index b0626bbb85..1af9a3a29f 100644 --- a/test/UnitTests/UnitTests.csproj +++ b/test/UnitTests/UnitTests.csproj @@ -1,19 +1,16 @@ - netcoreapp1.1 - false - + - + - - + \ No newline at end of file