如何使Entity Framework数据上下文只读

140

我需要向第三方插件暴露一个Entity Framework数据上下文。目的是允许这些插件仅获取数据,而不允许它们执行插入、更新或删除等任何其他数据库修改命令。因此,如何使数据上下文或实体只读。


6
为他们提供一个场景,其中用户没有对数据库的写入访问权限。 - vcsjones
1
谢谢。我正在使用SQLite数据库。刚刚发现可以通过连接字符串选项以只读模式打开它。 - Harindaka
3
不要给他们一个DbContext,给他们一个或多个IQueryable - ta.speot.is
9个回答

215

除了与只读用户连接外,您还可以对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>();
    }
}

1
很明显你是个“内部人” :) - 这比一个“只读”连接有趣得多。 - NSGaga-mostly-inactive
12
请注意,使用 AsNoTracking() 将使惰性加载变得不可能。 - Tom Pažourek
9
不要忘记也要覆盖public override Task<int> SaveChangesAsync()方法。 - Pete
9
不要依赖此方法,因为(context as IObjectContextAdapter).ObjectContext.SaveChanges()仍将起作用。最佳选择是使用DbContext(string nameOrConnectionString);构造函数,使用读/写连接字符串进行数据库创建,并在之后使用只读连接字符串。 - Jürgen Steinblock
6
在EntityFrameworkCore中,应该这样写:public IQueryable<Customer> Customers => Set<Customer>().AsNoTracking();,意思是获取一个不跟踪变化的客户查询集合。 - DiPix
显示剩余10条评论

60
与接受的答案相反,我认为优先选择组合而不是继承会更好。这样就不需要保留像SaveChanges这样的方法抛出异常。此外,为什么你需要首先拥有这样的方法呢?你应该设计一个类的方式,使其使用者在查看其方法列表时不会被愚弄。公共接口应该与类的实际意图和目标保持一致,而在接受的答案中,拥有SaveChanges并不意味着Context是只读的。
在需要具有只读上下文的地方,例如在CQRS模式的读取侧,我使用以下实现。它除了为其使用者提供查询功能外,不提供任何其他东西。
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();

3
让它继承自 IDisposable 接口。 - hkarask
我建议使用Query<>而不是Set<>。 public IQueryable<TEntity> Get<TEntity>() where TEntity : class { return _dbContext.Query<TEntity>().AsNoTracking(); } - Allan Nielsen
1
@hkarask - 我不确定我会这样做。由于此调用未创建DbContext,因此不应将其处理。这可能会导致一些难以跟踪的错误。 - Allan Nielsen
优秀的“优先使用组合而非继承”的回答!我在回答中添加了一小部分内容,如果您(Ehsan)认为它没有增加回答的价值,请拒绝或编辑掉我的小改动。 - granadaCoder
是的,我正在按照您提到的方式实施,将其应用于CQSR模式的“查询”方面。另一个链接:https://learn.microsoft.com/en-us/archive/msdn-magazine/2015/june/cutting-edge-cqrs-for-the-common-application - granadaCoder
显示剩余2条评论

6
在我的EF Core/.NET 5.0方案中,我希望SaveChanges具有编译时安全性。这只能使用"new"而不是"override"来实现。
我同时使用读/写和只读上下文,并且一个继承自另一个,因为有许多关联表。这就是我使用的方式,“ContextData”是我的原始读/写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.");
    }
}

请注意:
  • I had to use "new" instead of "override" when overwriting inherited SaveChanges*() in order to have warnings/errors. With "override", there where no compile time errors/warnings at all.

  • With "override" you get CS0809 [1], but not with "new"

  • Using "new" will only work for the class itself, but not in context of the parent:

    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
    
  • Proper choice of (optional) arguments is required for the variants of SaveChanges and SaveChangesAsync. (This is for .NET 5.0, I have not checked whether it varies for other versions of EF Core/EF)

结论

  1. "override" 可以提供完整的继承,但在我的环境中无法使用。
  2. "new" 提供所需的功能,但在某些多态情况下会返回意外结果。
  3. 如果不使用继承,处理许多表格将很麻烦。

==> 没有万能的解决方案,选择取决于个人喜好和具体情况...

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


你可以将 SaveChanges() 方法放置于 #pragma warning disable CS0809#pragma warning restore CS0809 之间,以禁用警告并仍然使用 override - The Thirsty Ape

3
我有一个解决方案,我认为它是最好的。它基于其他答案,但使用接口来很好地限制了ContextReadOnlyContext的接口(duh)。
注意:我在这里使用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数据库的只读副本,则需要这个功能,点击此处了解更多


3
public sealed class MyDbContext : DbContext
{
    public MyDbContext(DbContextOptions<MyDbContext> options, IHttpContextAccessor httpContextAccessor)
        : base(options)
    {
        ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
    }
}

并重写 SaveChanges 方法以抛出异常。


1

由于在Entity Framework Core中不再可用DbQuery<T>,因此您需要略微修改@bricelam的答案,并直接使用IQueryable<T>代替:

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

    // [...]
}

0
你可以在SQL/MySQL等数据库中创建只读用户。
CREATE USER 'readonly-user'@'localhost' IDENTIFIED BY 'highly-secure-password';

GRANT SELECT ON special-db.* TO 'readonly-user'@'localhost';

FLUSH PRIVILEGES;

然后您可以相应地更新连接字符串。
 "ConnectionStrings": {
   "readonlyDb": "Server=localhost; Port=3306; Database=special-db; Uid=readonly-user; Pwd=highly-secure-password;"
 }

如果你需要使用代码的方式,你可以随时创建带有私有设置器的受保护构造函数。
例如:
public class Koala
 {
     protected Koala() { }

     [Key]
     [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
     public int Id { get; private set; }
     public string Name { get; private set; }
   
 }

-1
你们觉得这个怎么样?我还没有测试过,但我认为它应该可以工作。
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.");
    }
}

我还没有测试过它,请不要发布!Stack Overflow是一个知识库,而不是一个讨论想法或提案的论坛。 - Gert Arnold

-1
情况:我需要引用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(); }
    }

..... }


1
好的。下降标记了,但为什么? - Scooter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接