Skip to content

Use AsNoTracking with readonly queries #225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 32 additions & 14 deletions src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,34 +48,52 @@ public DefaultEntityRepository(
_genericProcessorFactory = _jsonApiContext.GenericProcessorFactory;
}

public virtual IQueryable<TEntity> Get()
/// <summary>
/// Get resource by id. This method will be removed in the next major release
/// and replaced by the following signature: Get(bool isReadonly = false)
/// </summary>
public virtual IQueryable<TEntity> Get() => Get(isReadonly: false);

public virtual IQueryable<TEntity> Get(bool isReadonly = false)
{
if (_jsonApiContext.QuerySet?.Fields != null && _jsonApiContext.QuerySet.Fields.Any())
return _dbSet.Select(_jsonApiContext.QuerySet?.Fields);

if(isReadonly)
return _dbSet.AsNoTracking();

return _dbSet;
}

public virtual IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQuery filterQuery)
{
return entities.Filter(_jsonApiContext, filterQuery);
}
=> entities.Filter(_jsonApiContext, filterQuery);

public virtual IQueryable<TEntity> Sort(IQueryable<TEntity> entities, List<SortQuery> sortQueries)
{
return entities.Sort(sortQueries);
}

public virtual async Task<TEntity> GetAsync(TId id)
{
return await Get().SingleOrDefaultAsync(e => e.Id.Equals(id));
}

=> entities.Sort(sortQueries);

/// <summary>
/// Get resource by id. This method will be removed in the next major release
/// and replaced by the following signature: GetAsync(TId id, bool isReadonly = false)
/// </summary>
public virtual async Task<TEntity> GetAsync(TId id)
=> await Get().SingleOrDefaultAsync(e => e.Id.Equals(id));

public virtual async Task<TEntity> GetAsync(TId id, bool isReadonly = false)
=> await Get(isReadonly).SingleOrDefaultAsync(e => e.Id.Equals(id));

/// <summary>
/// Get resource by id with relationship sideloaded.
/// This method will be removed in the next major release and replaced by
/// the following signature: GetAndIncludeAsync(TId id, string relationshipName, bool isReadonly)
/// </summary>
public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshipName)
=> await GetAndIncludeAsync(id, relationshipName, isReadonly: false);

public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshipName, bool isReadonly)
{
_logger.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationshipName})");

var result = await Get()
var result = await Get(isReadonly)
.Include(relationshipName)
.SingleOrDefaultAsync(e => e.Id.Equals(id));

Expand Down
4 changes: 0 additions & 4 deletions src/JsonApiDotNetCore/Data/IEntityRepository.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
53 changes: 48 additions & 5 deletions src/JsonApiDotNetCore/Services/EntityResourceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@ public class EntityResourceService<T>
IResourceService<T>
where T : class, IIdentifiable<int>
{
private readonly IEntityRepository<T> _entities;

public EntityResourceService(
IJsonApiContext jsonApiContext,
IEntityRepository<T> entityRepository,
ILoggerFactory loggerFactory)
: base(jsonApiContext, entityRepository, loggerFactory)
{ }
{
_entities = entityRepository;
}

protected override bool IsUsingDefaultRepositoryImplementation()
=> _entities.GetType().IsAssignableFrom(typeof(DefaultEntityRepository<T>));
}

public class EntityResourceService<T, TId> : IResourceService<T, TId>
Expand All @@ -41,7 +48,7 @@ public EntityResourceService(

public virtual async Task<IEnumerable<T>> GetAsync()
{
var entities = _entities.Get();
var entities = GetReadonly();

entities = ApplySortAndFilterQuery(entities);

Expand All @@ -62,7 +69,8 @@ public virtual async Task<T> GetAsync(TId id)
if (ShouldIncludeRelationships())
entity = await GetWithRelationshipsAsync(id);
else
entity = await _entities.GetAsync(id);
entity = await GetByIdReadonlyAsync(id);

return entity;
}

Expand All @@ -71,11 +79,12 @@ private bool ShouldIncludeRelationships()

private async Task<T> GetWithRelationshipsAsync(TId id)
{
var query = _entities.Get().Where(e => e.Id.Equals(id));
var query = GetReadonly().Where(e => e.Id.Equals(id));
_jsonApiContext.QuerySet.IncludedRelationships.ForEach(r =>
{
query = _entities.Include(query, r);
});

return await _entities.FirstOrDefaultAsync(query);
}

Expand All @@ -95,7 +104,7 @@ public virtual async Task<object> GetRelationshipAsync(TId id, string relationsh

_logger.LogTrace($"Looking up '{relationshipName}'...");

var entity = await _entities.GetAndIncludeAsync(id, relationshipName);
var entity = await GetAndIncludeReadonlyAsync(id, relationshipName);
if (entity == null)
throw new JsonApiException(404, $"Relationship {relationshipName} not found.");

Expand All @@ -105,6 +114,40 @@ public virtual async Task<object> GetRelationshipAsync(TId id, string relationsh
return relationship;
}

/// <summary>
/// This is a temporary measure to maintain backwards API compatibility.
/// It is expected that this method will be removed in the next major release.
/// </summary>
private IQueryable<T> GetReadonly() =>
(IsUsingDefaultRepositoryImplementation())
? ((DefaultEntityRepository<T, TId>)_entities).Get(isReadonly: true)
: _entities.Get();

/// <summary>
/// This is a temporary measure to maintain backwards API compatibility.
/// It is expected that this method will be removed in the next major release.
/// </summary>
private async Task<T> GetByIdReadonlyAsync(TId id) =>
await (
(IsUsingDefaultRepositoryImplementation())
? ((DefaultEntityRepository<T, TId>)_entities).GetAsync(id, isReadonly: true)
: _entities.GetAsync(id)
);

/// <summary>
/// This is a temporary measure to maintain backwards API compatibility.
/// It is expected that this method will be removed in the next major release.
/// </summary>
private async Task<T> GetAndIncludeReadonlyAsync(TId id, string relationshipName) =>
await (
(IsUsingDefaultRepositoryImplementation())
? ((DefaultEntityRepository<T, TId>)_entities).GetAndIncludeAsync(id, relationshipName, isReadonly: true)
: _entities.GetAndIncludeAsync(id, relationshipName)
);

protected virtual bool IsUsingDefaultRepositoryImplementation()
=> _entities.GetType().IsAssignableFrom(typeof(DefaultEntityRepository<T, TId>));

public virtual async Task<T> CreateAsync(T entity)
{
return await _entities.CreateAsync(entity);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
Expand Down Expand Up @@ -25,11 +26,12 @@ public RepositoryOverrideTests(TestFixture<Startup> fixture)
}

[Fact]
public async Task Total_Record_Count_Included()
public async Task Can_Override_Repository()
{
// arrange
var builder = new WebHostBuilder()
.UseStartup<AuthorizedStartup>();

var server = new TestServer(builder);
var client = server.CreateClient();
var context = (AppDbContext)server.Host.Services.GetService(typeof(AppDbContext));
Expand All @@ -43,8 +45,10 @@ public async Task Total_Record_Count_Included()
context.TodoItems.Add(ownedTodoItem);
context.TodoItems.Add(unOwnedTodoItem);
context.SaveChanges();

const int expectedCount = 1;

var authService = (IAuthorizationService)server.Host.Services.GetService(typeof(IAuthorizationService));
var authService = (IAuthorizationService)server.Host.Services.GetService(typeof(IAuthorizationService));
authService.CurrentUserId = person.Id;

var httpMethod = new HttpMethod("GET");
Expand All @@ -59,6 +63,8 @@ public async Task Total_Record_Count_Included()

// assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedCount, deserializedBody.Count);

foreach(var item in deserializedBody)
Assert.Equal(person.Id, item.OwnerId);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Linq;
using JsonApiDotNetCore.Data;
using JsonApiDotNetCore.Services;
using JsonApiDotNetCoreExample.Data;
using JsonApiDotNetCoreExample.Models;
using JsonApiDotNetCoreExampleTests.Services;
using Microsoft.Extensions.Logging;
Expand Down