如何使用 EF Core 6 和值对象为 Web API 项目实现字段级权限

How to implement for a Web API project field level permission with EF Core 6 and value objects

我在试图找出一个可能的实施方案时感到非常沮丧。

根据 DDD 和 CQS 概念,我的应用程序的写入端 使用了各种聚合以及通过 EF Core 实现的关联存储库。

阅读方面我想使用在某些情况下link相关数据的查询。这可能只是 id 的解析(即进行最后编辑的用户的名称)或出于性能原因的子对象列表(即一个人的地址簿)。

因此 returned 对象 (GetPersonDTO) 的结构(包括决议或子对象)与写入对象 (Person) 不同。我使用值对象作为实体中所有属性的类型,以将验证保持在一个位置(始终有效的对象)。

我的问题在阅读方面。来自 GET 请求的 returned 资源表示是 JSON。与请求主题关联的权限决定字段是否包含在 JSON.

我的想法是,我使用 EF Core 来 return 一个查询对象和一个权限对象,该对象为当前主题(用户)保存该对象的字段权限。如果主题对某个字段具有读取权限,它将被映射到 DTO。 DTO 使用 Optional<T> 和自定义 JsonConverter,如图 所示。因此,所有未设置的 Optional<T> 字段将不会包含在 JSON 响应中,但它会保留设置为 NULL.

的字段

我使用原始 SQL 在 EF Core 中编写查询,因为我没能使用 LINQ 编写更复杂的查询。 EF Core 需要用于原始 SQL 查询的无键实体。我希望 EF Core 使用为写入端创建的转换器将读取字段转换回值对象。

但是无键实体不能成为关系的主体端,因此它们不能拥有实体。正如各种 GitHub issues 所示,EF Core 尚不可能从原始 SQL 查询重新创建对象图。据称

In EF each entityType maps to one database object. Same database object can be used for multiple entityTypes but you cannot specify a database object to materialize a graph of object.

如果我理解正确的话,也无法通过视图或存储过程来实现。在我看来,也不可能定义另一个使用现有 DbSet 对象的完全连接的 GetPerson 对象。

如何实现?有哪些选择?

我能想到

a) 使用原始类型的平面对象作为原始 SQL 结果,然后使用它映射到 DTO。使用原始对象图创建对象图的副作用是创建值对象会验证来自数据库的数据。所以我要么必须信任数据库,要么我需要手动调用值对象中 public 的验证方法。

b) 忘记 EF Core 并使用 ADO.NET。 return 对象就是 ADO.NET 记录。考虑到权限,记录的字段将被映射到 DTO。这很简单,开销较小,但需要框架的另一部分。

还有其他选择吗?您如何解决 return考虑字段权限的组合对象?

EF6 核心不支持持久值对象,这是 EF7 中计划的功能:https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/plan#value-objects

诸如 EF 之类的 ORM 的目的是允许程序员通过对象而不是诸如 SQL 之类的基于文本的语言来操纵 RDBMS。这个对象模型不是业务模型,而是RDBMS概念的符号(class = table, row = object, column = 属性, ...)。对于琐碎的应用程序,您可能会混淆您的模型,但您很快就会发现自己受到限制,因为业务模型具有与数据库模式不同的约束。对于较大的应用程序,您编写一个持久性模型,由与您的数据库结构相匹配的 DPO 组成,并将该模型转换为基础设施层中的其他模型。将域模型与持久性模型分离可以让您的应用程序更加灵活,并且 re-hydrating 您的域对象作为多义模型,限制独立用例的副作用。

这里的一个示例是使用 table 的规范化 RDBMS,其中包括在存储库完成的投影过程中从域模型中隐藏的代理键。这允许您的数据库解决关系映射的复杂性,而 re-hydrating 值对象在域层中没有标识。

对于查询,您不应使用 GetPerson 模型发布您的实体。域层的目的是保护您的应用程序不违反任何业务规则。在查询你的应用状态时,你不会修改应用的状态,也不能违反任何规则。域模型仅对状态更改用例有用。因此,在处理查询时,您应该直接从 DPO 映射您的 DTO。您将节省性能并允许您的 DTO 通过 AutoMapper 等库直接将 sort/filter/paging 特征投影到数据库,只要投影在该库的翻译容量内。此外,您的业务规则实施不会影响/受大型复杂查询模型的影响,这是 CQS 架构的最初目的。

无论您是通过 EF 等 ORM 操作数据库,还是直接在 ADO.NET 级别操作原始 SQL 查询,都是基础架构层的实现细节。选择取决于您是否认为您可以编写比 ORM“更好”的查询,“更好”是一个主观问题,具体取决于您的项目限制。


更新 1:关于使用 Optional<T> 映射到您的 DTO,EF 核心将关系数据映射到不简单表示数据库模式的模型的能力有限.这是设计使然,您不应强制 EF 尝试将数据直接还原到 DTO。使用单个模型可以增加 API 界面和数据库持久性方案之间的一致性。每次更新持久性模式和 vice-versa 时,您都会冒接口中断更改的风险。您应该 有两种不同的模型来将表示与持久性分离。

使用EF core还是ADO.NET读取数据库在概念上没有太大变化。在这两种情况下,您都将数据库信息读入内存模型,然后将该模型转换为 DTO。不同之处在于此 in-memory 模型是基于 OOP(EF + DPO 模型)还是 key-value table(ADO.NET + 数据行)。

使用 EF 核心的优点是它不太容易 SQL 注入,因为会生成查询并且值总是为您转义。此外,持久性模型可以通过映射库(例如 AutoMapper)转换为 DTO。 AutoMapper 使翻译的编写和维护更容易、成本更低。此外,它还可以将一些翻译投射到关系模型中。

如果您设法将您的安全性建模到地图配置文件中,您的数据库只能 select DTO 中数据公开所需的列。换句话说,如果不允许用户公开 DTO.Name,那么数据库就不会将 Table.Name 包含到 select 语句中。虽然这并不总是可行,但这样做比在 SQL.

中编写“聪明”查询要容易得多

但是,EF 的一个缺点是它比 ADO.NET 慢。

如果您确实需要将查询拆分为两个阶段的转换(映射和安全),您应该将安全层放在更靠近数据库的位置,除非转换逻辑要求相应地映射数据。

这是一个有点主观的 best-practice 问题,但我会回答我是如何解决类似问题的 - 假设我确实正确理解了你的问题。

只要您已完全使用导航属性映射数据库模型,就可以生成非常复杂的查询而无需求助于原始查询。

    var dto = await context.Persons
        .Where(p => p.Id == id)
        .Select(p => new GetPersonDTO
        {
            Id = p.Id,
            InternallyVerifiedField = !p.UsersWithAccess.Contains(currentUser) ? new Optional<int>(p.InternallyVerifiedField) : new Optional<int>(),
            ExternallyVerifiedField = permissions.Contains(nameof(GetPersonDTO.ExternallyVerifiedField)) ? new Optional<int>(p.ExternallyVerifiedField) : new Optional<int>()
        })
        .SingleOrDefaultAsync();

在此示例中,InternallyVerifiedField 将依赖于一些内联查询,而 ExternallyVerifiedField 将依赖于一些外部权限对象。 ExternallyVerifiedField 的好处是,如果用户没有权限,它甚至可以在到达 sql 服务器之前从表达式中优化出来。

如果你想从一个完全连接的对象构建 dto 对象,它仍然可以在类似于

的一个查询中完成
    var dto = await context.Persons
        .Where(p => p.Id == id)
        .Select(p => new
        {
            permissions = new GetPersonDTOPermissions
            {
                FieldA = context.Permissions.Where(...)
            },
            person = p
        })
        .SingleOrDefaultAsync();

但是使用此解决方案,您需要根据结果 permissions 从图形对象 person 手动制作 dto,只要您从 context 开始并使用添加过滤器Where 查询将被内联。