Browse Source

Throw `AbpRepositoryIsReadOnlyException` when the read-only repository writes method calls.

pull/17421/head
maliming 3 years ago
parent
commit
2c24f0c706
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 22
      framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs
  2. 88
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs
  3. 75
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs

22
framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs

@ -0,0 +1,22 @@
namespace Volo.Abp.Data;
public class AbpRepositoryIsReadOnlyException : AbpException
{
/// <summary>
/// Creates a new <see cref="AbpRepositoryIsReadOnlyException"/> object.
/// </summary>
public AbpRepositoryIsReadOnlyException()
{
}
/// <summary>
/// Creates a new <see cref="AbpRepositoryIsReadOnlyException"/> object.
/// </summary>
/// <param name="message">Exception message</param>
public AbpRepositoryIsReadOnlyException(string message)
: base(message)
{
}
}

88
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs

@ -1,4 +1,3 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -6,16 +5,15 @@ using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Storage;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.DependencyInjection;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore;
@ -37,30 +35,50 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
[Obsolete("Use GetDbContextAsync() method.")]
private TDbContext GetDbContext()
{
TDbContext dbContext;
// Multi-tenancy unaware entities should always use the host connection string
if (!EntityHelper.IsMultiTenant<TEntity>())
{
using (CurrentTenant.Change(null))
{
return _dbContextProvider.GetDbContext();
dbContext = _dbContextProvider.GetDbContext();
}
}
else
{
dbContext = _dbContextProvider.GetDbContext();
}
if (IsReadOnly)
{
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
return _dbContextProvider.GetDbContext();
return dbContext;
}
protected virtual Task<TDbContext> GetDbContextAsync()
protected virtual async Task<TDbContext> GetDbContextAsync()
{
TDbContext dbContext;
// Multi-tenancy unaware entities should always use the host connection string
if (!EntityHelper.IsMultiTenant<TEntity>())
{
using (CurrentTenant.Change(null))
{
return _dbContextProvider.GetDbContextAsync();
dbContext = await _dbContextProvider.GetDbContextAsync();
}
}
else
{
dbContext = await _dbContextProvider.GetDbContextAsync();
}
if (IsReadOnly)
{
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
return _dbContextProvider.GetDbContextAsync();
return dbContext;
}
[Obsolete("Use GetDbSetAsync() method.")]
@ -75,7 +93,7 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
{
return (await GetDbContextAsync()).Set<TEntity>();
}
protected async Task<IDbConnection> GetDbConnectionAsync()
{
return (await GetDbContextAsync()).Database.GetDbConnection();
@ -107,8 +125,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
);
}
public override async Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
public async override Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
CheckAndSetId(entity);
var dbContext = await GetDbContextAsync();
@ -123,8 +142,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
return savedEntity;
}
public override async Task InsertManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
public async override Task InsertManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
var entityArray = entities.ToArray();
var dbContext = await GetDbContextAsync();
cancellationToken = GetCancellationToken(cancellationToken);
@ -153,8 +173,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
}
}
public override async Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
public async override Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
var dbContext = await GetDbContextAsync();
dbContext.Attach(entity);
@ -169,8 +190,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
return updatedEntity;
}
public override async Task UpdateManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
public async override Task UpdateManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
cancellationToken = GetCancellationToken(cancellationToken);
if (BulkOperationProvider != null)
@ -195,8 +217,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
}
}
public override async Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
public async override Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
var dbContext = await GetDbContextAsync();
dbContext.Set<TEntity>().Remove(entity);
@ -207,8 +230,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
}
}
public override async Task DeleteManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
public async override Task DeleteManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
cancellationToken = GetCancellationToken(cancellationToken);
if (BulkOperationProvider != null)
@ -233,26 +257,26 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
}
}
public override async Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default)
public async override Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default)
{
return includeDetails
? await (await WithDetailsAsync()).ToListAsync(GetCancellationToken(cancellationToken))
: await (await GetDbSetAsync()).ToListAsync(GetCancellationToken(cancellationToken));
}
public override async Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate, bool includeDetails = false, CancellationToken cancellationToken = default)
public async override Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return includeDetails
? await (await WithDetailsAsync()).Where(predicate).ToListAsync(GetCancellationToken(cancellationToken))
: await (await GetDbSetAsync()).Where(predicate).ToListAsync(GetCancellationToken(cancellationToken));
}
public override async Task<long> GetCountAsync(CancellationToken cancellationToken = default)
public async override Task<long> GetCountAsync(CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync()).LongCountAsync(GetCancellationToken(cancellationToken));
}
public override async Task<List<TEntity>> GetPagedListAsync(
public async override Task<List<TEntity>> GetPagedListAsync(
int skipCount,
int maxResultCount,
string sorting,
@ -275,17 +299,17 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
return DbSet.AsQueryable();
}
public override async Task<IQueryable<TEntity>> GetQueryableAsync()
public async override Task<IQueryable<TEntity>> GetQueryableAsync()
{
return (await GetDbSetAsync()).AsQueryable();
}
protected override async Task SaveChangesAsync(CancellationToken cancellationToken)
protected async override Task SaveChangesAsync(CancellationToken cancellationToken)
{
await (await GetDbContextAsync()).SaveChangesAsync(cancellationToken);
}
public override async Task<TEntity> FindAsync(
public async override Task<TEntity> FindAsync(
Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default)
@ -299,8 +323,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
.SingleOrDefaultAsync(GetCancellationToken(cancellationToken));
}
public override async Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
public async override Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
var dbContext = await GetDbContextAsync();
var dbSet = dbContext.Set<TEntity>();
@ -316,8 +341,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
}
}
public override async Task DeleteDirectAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
public async override Task DeleteDirectAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
{
CheckReadOnly();
var dbContext = await GetDbContextAsync();
var dbSet = dbContext.Set<TEntity>();
await dbSet.Where(predicate).ExecuteDeleteAsync(GetCancellationToken(cancellationToken));
@ -358,7 +384,7 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
return AbpEntityOptions.DefaultWithDetailsFunc(GetQueryable());
}
public override async Task<IQueryable<TEntity>> WithDetailsAsync()
public async override Task<IQueryable<TEntity>> WithDetailsAsync()
{
if (AbpEntityOptions.DefaultWithDetailsFunc == null)
{
@ -377,7 +403,7 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
);
}
public override async Task<IQueryable<TEntity>> WithDetailsAsync(params Expression<Func<TEntity, object>>[] propertySelectors)
public async override Task<IQueryable<TEntity>> WithDetailsAsync(params Expression<Func<TEntity, object>>[] propertySelectors)
{
return IncludeDetails(
await GetQueryableAsync(),
@ -421,6 +447,14 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
true
);
}
protected virtual void CheckReadOnly()
{
if (IsReadOnly)
{
throw new AbpRepositoryIsReadOnlyException($"Can not call {nameof(InsertAsync)}, {nameof(UpdateAsync)}, {nameof(DeleteAsync)}, {nameof(DeleteManyAsync)} methods on a read-only repository!");
}
}
}
public class EfCoreRepository<TDbContext, TEntity, TKey> : EfCoreRepository<TDbContext, TEntity>,
@ -457,6 +491,7 @@ public class EfCoreRepository<TDbContext, TEntity, TKey> : EfCoreRepository<TDbC
public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
var entity = await FindAsync(id, cancellationToken: cancellationToken);
if (entity == null)
{
@ -468,6 +503,7 @@ public class EfCoreRepository<TDbContext, TEntity, TKey> : EfCoreRepository<TDbC
public virtual async Task DeleteManyAsync(IEnumerable<TKey> ids, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
cancellationToken = GetCancellationToken(cancellationToken);
var entities = await (await GetDbSetAsync()).Where(x => ids.Contains(x.Id)).ToListAsync(cancellationToken);

75
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs

@ -0,0 +1,75 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Shouldly;
using Volo.Abp.Data;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.TestApp.Domain;
using Volo.Abp.TestApp.EntityFrameworkCore;
using Volo.Abp.TestApp.Testing;
using Xunit;
namespace Volo.Abp.EntityFrameworkCore.Repositories;
public class ReadOnlyRepository_Tests : TestAppTestBase<AbpEntityFrameworkCoreTestModule>
{
[Fact]
public async Task ReadOnlyRepository_Should_NoTracking()
{
// Non-read-only repository tracking default
await WithUnitOfWorkAsync(async () =>
{
var repository = GetRequiredService<IRepository<Person, Guid>>();
var db = await repository.GetDbContextAsync();
db.ChangeTracker.Entries().Count().ShouldBe(0);
var list = await repository.GetListAsync();
list.Count.ShouldBeGreaterThan(0);
db.ChangeTracker.Entries().Count().ShouldBe(list.Count);
});
// Read-only repository no tracking default
await WithUnitOfWorkAsync(async () =>
{
var readonlyRepository = GetRequiredService<IReadOnlyRepository<Person, Guid>>();
var db = await readonlyRepository.GetDbContextAsync();
db.ChangeTracker.Entries().Count().ShouldBe(0);
var list = await readonlyRepository.GetListAsync();
list.Count.ShouldBeGreaterThan(0);
db.ChangeTracker.Entries().Count().ShouldBe(0);
});
// Read-only repository can tracking manually by AsTracking
await WithUnitOfWorkAsync(async () =>
{
var readonlyRepository = GetRequiredService<IReadOnlyRepository<Person, Guid>>();
var db = await readonlyRepository.GetDbContextAsync();
db.ChangeTracker.Entries().Count().ShouldBe(0);
var list = await (await readonlyRepository.ToEfCoreRepository().GetQueryableAsync()).AsTracking().ToListAsync();
list.Count.ShouldBeGreaterThan(0);
db.ChangeTracker.Entries().Count().ShouldBe(list.Count);
});
}
[Fact]
public async Task ReadOnlyRepository_Should_Throw_AbpRepositoryIsReadOnlyException_When_Write_Method_Call()
{
await WithUnitOfWorkAsync(async () =>
{
var repository = GetRequiredService<IRepository<Person, Guid>>();
await repository.ToEfCoreRepository().InsertAsync(new Person(Guid.NewGuid(), "test", 18));
var person = await repository.ToEfCoreRepository().FirstOrDefaultAsync();
person.ShouldNotBeNull();
});
await WithUnitOfWorkAsync(async () =>
{
await Assert.ThrowsAsync<AbpRepositoryIsReadOnlyException>(async () =>
{
var readonlyRepository = GetRequiredService<IReadOnlyRepository<Person, Guid>>();
await readonlyRepository.ToEfCoreRepository().As<EfCoreRepository<TestAppDbContext, Person, Guid>>().InsertAsync(new Person(Guid.NewGuid(), "test readonly", 18));
});
});
}
}
Loading…
Cancel
Save