如何使实体框架数据上下文只读

我需要公开一个实体框架数据上下文第三方插件。其目的是允许这些插件只获取数据,而不允许它们发出插入、更新或删除或任何其他数据库修改命令。因此,如何使数据上下文或实体只读。

67144 次浏览

除了与只读用户连接之外,还可以对 DbContext 执行其他一些操作。

public class MyReadOnlyContext : DbContext
{
// Use ReadOnlyConnectionString from App/Web.config
public MyContext()
: base("Name=ReadOnlyConnectionString")
{
}


// Don't expose Add(), Remove(), etc.
public DbQuery<Customer> Customers
{
get
{
// Don't track changes to query results
return Set<Customer>().AsNoTracking();
}
}


public override int SaveChanges()
{
// Throw if they try to call this
throw new InvalidOperationException("This context is read-only.");
}


protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Need this since there is no DbSet<Customer> property
modelBuilder.Entity<Customer>();
}
}

与公认的答案相反,我认为 喜欢合成胜过继承会更好。这样就不需要保留 SaveChanges 之类的方法来抛出异常。此外,为什么首先需要这样的方法?您应该设计一个类,使它的使用者在查看它的方法列表时不会被骗。公共接口应该与类的实际意图和目标保持一致,而在可接受的答案中,SaveChanges 并不意味着 Context 是只读的。

在需要只读上下文的地方,比如在 CQRS模式的 Read 端,我使用以下实现。除了向消费者提供查询功能之外,它没有提供任何其他功能。

public class ReadOnlyDataContext
{
private readonly DbContext _dbContext;


public ReadOnlyDataContext(DbContext dbContext)
{
_dbContext = dbContext;
}


public IQueryable<TEntity> Set<TEntity>() where TEntity : class
{
return _dbContext.Set<TEntity>().AsNoTracking();
}
}

通过使用 ReadOnlyDataContext,您只能访问 DbContext 的查询功能。假设您有一个名为 Order 的实体,那么您将以如下方式使用 ReadOnlyDataContext 实例。

readOnlyDataContext.Set<Order>().Where(q=> q.Status==OrderStatus.Delivered).ToArray();

如果您希望手动选择(并限制)通过这个新上下文公开哪些实体,那么可以使用另一个选项。您可以删除上面的基于通用的方法(包含 TEntity 的完整块)并使用类似于下面的代码。

    public IQueryable<MyFirstThing> MyFirstHandPickThings => this.dbContext.Set<MyFirstThing>().AsNoTracking();


public IQueryable<MySecondThing> MySecondHandPickThings => this.dbContext.Set<MySecondThing>().AsNoTracking();
public sealed class MyDbContext : DbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options, IHttpContextAccessor httpContextAccessor)
: base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
}

并重写 SaveChanges 以引发 Exception

情境: 我需要引用 DB1在 DB2中创建记录,并希望在此过程中保护 DB1。DB1和 DB2是彼此的模式副本。

我更新了自动生成的实体上下文文件。并在实例化实体上下文时放入只读选项,并覆盖 SaveChanges () ,以便在使用 ReadOnly 选项时中止写操作。

缺点:

  1. 您必须在配置设置中创建一个单独的 EF 连接字符串
  2. 在自动更新模型时,您必须非常小心。保留代码更改的副本,并记住在模型更新后应用它。
  3. 未提供未执行保存的通知。我选择不提供通知,因为我的使用是非常有限的,我们执行了很多保存。

好处:

  1. 您不必实现 CQRS 类型的解决方案。
  2. 通过使用相同的实体模型,您不必创建第二个实体模型并对其进行维护。
  3. 数据库或其用户帐户没有更改。

只要确保在命名上下文实例化时使用 ReadOnly 或类似的命名方法即可。

public partial class db1_Entities : DbContext
{
public bool IsReadOnly { get; private set; }


public db1_Entities()
: base(ConfigurationManager.ConnectionStrings["db1_Entities"].ConnectionString)
{
}


public db1_Entities(bool readOnlyDB)
: base(ConfigurationManager.ConnectionStrings["db1_ReadOnly_Entities "].ConnectionString)
{
//  Don't use this instantiation unless you want a read-only reference.
if (useReferenceDB == false)
{
this.Dispose();
return;
}
else
{ IsReadOnly = true; }
}


public override int SaveChanges()
{
if (IsReadOnly == true)
{ return -1; }
else
{ return base.SaveChanges(); }
}


public override Task<int> SaveChangesAsync()
{
if (isReadOnly == true)
{ return null; }
else
{ return base.SaveChangesAsync(); }
}

..... }

由于 DbQuery<T>实体框架核心中不再可用,所以需要稍微修改@bricelam 的答案,直接使用 IQueryable<T>代替:

public class ReadOnlyContext : DbContext
{
public IQueryable<Customer> Customers => this.Set<Customer>().AsNoTracking();


// [...]
}

在我使用 EF Core/的场景中。NET 5.0,我希望保存更改的编译时安全性。这只对“新”有效,而不是“覆盖”。

我并排使用读/写和只读上下文,其中一个上下文从另一个上下文继承,因为附加了很多表。这就是我使用的方法,“ ContextData”是我最初的 R/W DbContext:

public class ContextDataReadOnly : ContextData
{
public ContextDataReadOnly() : base()
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}


[Obsolete("This context is read-only", true)]
public new int SaveChanges()
{
throw new InvalidOperationException("This context is read-only.");
}


[Obsolete("This context is read-only", true)]
public new int SaveChanges(bool acceptAll)
{
throw new InvalidOperationException("This context is read-only.");
}


[Obsolete("This context is read-only", true)]
public new Task<int> SaveChangesAsync(CancellationToken token = default)
{
throw new InvalidOperationException("This context is read-only.");
}


[Obsolete("This context is read-only", true)]
public new Task<int> SaveChangesAsync(bool acceptAll, CancellationToken token = default)
{
throw new InvalidOperationException("This context is read-only.");
}
}

请注意:

  • 在覆盖继承的 SaveChanges * ()时,为了得到警告/错误,我不得不使用“ new”而不是“  重写”。使用“重写”,根本不存在编译时错误/警告。

  • 使用“覆盖”可以得到 CS0809[1] ,但使用“新”则不能

  • 使用“ new”只适用于类本身,但不适用于父类的上下文:

    Base b = new Derived();
    Derived d = new Derived();
    
    
    b.SaveChanges();     // Calls Base.SaveChanges, will compile and run without exception
    d.SaveChanges();     // Calls Derived.SaveChanges, will not compile
    
  • 对于 SaveChanges 和 SaveChangesAsync 的变体,需要正确选择(可选)参数。(这是为了。NET 5.0,我还没有检查它是否与其他版本的 EF Core/EF 有所不同)

结论

  1. “覆盖”将提供完整的继承,但在我的环境中不起作用
  2. “ new”提供了所需的函数,但是在某些多态性场景中会返回意外的结果
  3. 如果有很多表,那么根本不使用继承将是痛苦的

没有灵丹妙药,选择取决于品味和环境..。

[1] https://learn.microsoft.com/en-us/dotnet/csharp/misc/cs0809?f1url=%3FappId%3Droslyn%26k%3Dk(CS0809)

你们觉得这个怎么样,伙计们? 我还没有测试过,但我认为它应该可以工作。

public class ReadOnlyContext : DbContext
{
public ReadOnlyContext(DbContextOptions<MyDbContext> options) : base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
ChangeTracker.Tracked += ChangeTracker_Tracked;
}


private void ChangeTracker_Tracked(object sender, EntityTrackedEventArgs e)
{
throw new Exception("The dbcontext is in readonly mode.");
}
}

我有一个解决方案,我相信是最好的。它基于这里的其他答案,但是使用接口很好地限制了 ContextReadOnlyContext的接口。

注意: 我在这里使用的是 EF Core 样式,但是这个模式也可以用于旧的 EF。

对于 Context,我们遵循常规的接口模式,其中接口定义了 Context 类的那些方面,我们希望能够从应用程序中使用这些方面。在我们的应用程序中,我们将注入 IContext而不是 Context


public interface IContext : IDisposable
{
DbSet<Customer> Customers{ get; }
int SaveChanges();
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}


public class Context :DbContext, IContext
{
public DbSet<Customer> Customers { get; set; }
    

public Context(DbContextOptions options)
: base(options)
{
}
}

现在,我们通过扩展 Context来实现我们的 ReadOnlyContext,并限制它的功能,使它成为只读的,但是我们也创建了一个匹配的 IReadOnlyContext接口,通过公开 IQueryable而不是 DbSet和不公开 SaveChanges来进一步限制它。在我们的应用程序中使用它时,我们注入 IReadOnlyContext而不是 ReadOnlyContext

public interface IReadOnlyContext : IDisposable
{
IQueryable<Customer> Customers { get; }
}




public class ReadOnlyContext : Context, IReadOnlyContext
{
public new IQueryable<Customer> Customers => base.Customers.AsQueryable();


public ReadOnlyContext(DbContextOptions options)
: base(options)
{
}




[Obsolete("This context is read-only", true)]
public new int SaveChanges()
{
throw new InvalidOperationException("This context is read-only.");
}


[Obsolete("This context is read-only", true)]
public new int SaveChanges(bool acceptAll)
{
throw new InvalidOperationException("This context is read-only.");
}


[Obsolete("This context is read-only", true)]
public new Task<int> SaveChangesAsync(CancellationToken token = default)
{
throw new InvalidOperationException("This context is read-only.");
}


[Obsolete("This context is read-only", true)]
public new Task<int> SaveChangesAsync(bool acceptAll, CancellationToken token = default)
{
throw new InvalidOperationException("This context is read-only.");
}
}


这些背景的设置可能是这样的:

    public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<IReadOnlyContext, ReadOnlyContext>(
contextOptions => contextOptions
.UseSqlServer(
_configuration["ConnectionStrings:ReadOnlyConnection"] ??
_configuration["ConnectionStrings:DefaultConnection"],
sqlServerOptions => sqlServerOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
);
services.AddDbContext<IContext, Context>(
contextOptions => contextOptions
.UseSqlServer(
_configuration["ConnectionStrings:DefaultConnection"],
sqlServerOptions =>
sqlServerOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
)
);
}

正如你所看到的,它与依赖注入方法很好地结合在一起,并且它允许使用一个单独的连接字符串,如果你想连接到一个 Azure 数据库的只读复制,你需要这个字符串。