diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs b/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs
new file mode 100644
index 0000000000..6c648fd022
--- /dev/null
+++ b/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs
@@ -0,0 +1,22 @@
+namespace Volo.Abp.Data;
+
+public class AbpRepositoryIsReadOnlyException : AbpException
+{
+ ///
+ /// Creates a new object.
+ ///
+ public AbpRepositoryIsReadOnlyException()
+ {
+
+ }
+
+ ///
+ /// Creates a new object.
+ ///
+ /// Exception message
+ public AbpRepositoryIsReadOnlyException(string message)
+ : base(message)
+ {
+
+ }
+}
diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs
index 96d7b4111e..e2b40d6886 100644
--- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs
+++ b/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 : RepositoryBase, IE
[Obsolete("Use GetDbContextAsync() method.")]
private TDbContext GetDbContext()
{
+ TDbContext dbContext;
// Multi-tenancy unaware entities should always use the host connection string
if (!EntityHelper.IsMultiTenant())
{
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 GetDbContextAsync()
+ protected virtual async Task GetDbContextAsync()
{
+ TDbContext dbContext;
// Multi-tenancy unaware entities should always use the host connection string
if (!EntityHelper.IsMultiTenant())
{
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 : RepositoryBase, IE
{
return (await GetDbContextAsync()).Set();
}
-
+
protected async Task GetDbConnectionAsync()
{
return (await GetDbContextAsync()).Database.GetDbConnection();
@@ -107,8 +125,9 @@ public class EfCoreRepository : RepositoryBase, IE
);
}
- public override async Task InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
+ public async override Task InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
+ CheckReadOnly();
CheckAndSetId(entity);
var dbContext = await GetDbContextAsync();
@@ -123,8 +142,9 @@ public class EfCoreRepository : RepositoryBase, IE
return savedEntity;
}
- public override async Task InsertManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default)
+ public async override Task InsertManyAsync(IEnumerable 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 : RepositoryBase, IE
}
}
- public override async Task UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
+ public async override Task UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
+ CheckReadOnly();
var dbContext = await GetDbContextAsync();
dbContext.Attach(entity);
@@ -169,8 +190,9 @@ public class EfCoreRepository : RepositoryBase, IE
return updatedEntity;
}
- public override async Task UpdateManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default)
+ public async override Task UpdateManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
+ CheckReadOnly();
cancellationToken = GetCancellationToken(cancellationToken);
if (BulkOperationProvider != null)
@@ -195,8 +217,9 @@ public class EfCoreRepository : RepositoryBase, 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().Remove(entity);
@@ -207,8 +230,9 @@ public class EfCoreRepository : RepositoryBase, IE
}
}
- public override async Task DeleteManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default)
+ public async override Task DeleteManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
+ CheckReadOnly();
cancellationToken = GetCancellationToken(cancellationToken);
if (BulkOperationProvider != null)
@@ -233,26 +257,26 @@ public class EfCoreRepository : RepositoryBase, IE
}
}
- public override async Task> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default)
+ public async override Task> 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> GetListAsync(Expression> predicate, bool includeDetails = false, CancellationToken cancellationToken = default)
+ public async override Task> GetListAsync(Expression> 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 GetCountAsync(CancellationToken cancellationToken = default)
+ public async override Task GetCountAsync(CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync()).LongCountAsync(GetCancellationToken(cancellationToken));
}
- public override async Task> GetPagedListAsync(
+ public async override Task> GetPagedListAsync(
int skipCount,
int maxResultCount,
string sorting,
@@ -275,17 +299,17 @@ public class EfCoreRepository : RepositoryBase, IE
return DbSet.AsQueryable();
}
- public override async Task> GetQueryableAsync()
+ public async override Task> 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 FindAsync(
+ public async override Task FindAsync(
Expression> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default)
@@ -299,8 +323,9 @@ public class EfCoreRepository : RepositoryBase, IE
.SingleOrDefaultAsync(GetCancellationToken(cancellationToken));
}
- public override async Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
+ public async override Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
{
+ CheckReadOnly();
var dbContext = await GetDbContextAsync();
var dbSet = dbContext.Set();
@@ -316,8 +341,9 @@ public class EfCoreRepository : RepositoryBase, IE
}
}
- public override async Task DeleteDirectAsync(Expression> predicate, CancellationToken cancellationToken = default)
+ public async override Task DeleteDirectAsync(Expression> predicate, CancellationToken cancellationToken = default)
{
+ CheckReadOnly();
var dbContext = await GetDbContextAsync();
var dbSet = dbContext.Set();
await dbSet.Where(predicate).ExecuteDeleteAsync(GetCancellationToken(cancellationToken));
@@ -358,7 +384,7 @@ public class EfCoreRepository : RepositoryBase, IE
return AbpEntityOptions.DefaultWithDetailsFunc(GetQueryable());
}
- public override async Task> WithDetailsAsync()
+ public async override Task> WithDetailsAsync()
{
if (AbpEntityOptions.DefaultWithDetailsFunc == null)
{
@@ -377,7 +403,7 @@ public class EfCoreRepository : RepositoryBase, IE
);
}
- public override async Task> WithDetailsAsync(params Expression>[] propertySelectors)
+ public async override Task> WithDetailsAsync(params Expression>[] propertySelectors)
{
return IncludeDetails(
await GetQueryableAsync(),
@@ -421,6 +447,14 @@ public class EfCoreRepository : RepositoryBase, 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 : EfCoreRepository,
@@ -457,6 +491,7 @@ public class EfCoreRepository : EfCoreRepository : EfCoreRepository ids, bool autoSave = false, CancellationToken cancellationToken = default)
{
+ CheckReadOnly();
cancellationToken = GetCancellationToken(cancellationToken);
var entities = await (await GetDbSetAsync()).Where(x => ids.Contains(x.Id)).ToListAsync(cancellationToken);
diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs
new file mode 100644
index 0000000000..bd073ce15a
--- /dev/null
+++ b/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
+{
+ [Fact]
+ public async Task ReadOnlyRepository_Should_NoTracking()
+ {
+ // Non-read-only repository tracking default
+ await WithUnitOfWorkAsync(async () =>
+ {
+ var repository = GetRequiredService>();
+ 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>();
+ 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>();
+ 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>();
+ await repository.ToEfCoreRepository().InsertAsync(new Person(Guid.NewGuid(), "test", 18));
+ var person = await repository.ToEfCoreRepository().FirstOrDefaultAsync();
+ person.ShouldNotBeNull();
+ });
+
+ await WithUnitOfWorkAsync(async () =>
+ {
+ await Assert.ThrowsAsync(async () =>
+ {
+ var readonlyRepository = GetRequiredService>();
+ await readonlyRepository.ToEfCoreRepository().As>().InsertAsync(new Person(Guid.NewGuid(), "test readonly", 18));
+ });
+ });
+ }
+}