☰
Orchard Core 数据访问实战:YesSql 文档存储、索引与会话,以及 GraphQL 数据暴露
2026/10/7 2:28:53 网站建设 项目流程
  • CMS
  • 后端
  • Web框架

【免费下载链接】OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.

项目地址:https://gitcode.com/gh_mirrors/or/OrchardCore
点击查看免费下载

Orchard Core 的持久化数据层并不使用 Entity Framework 之类的传统 ORM,而是基于 YesSql 这套“跑在关系数据库之上的 .NET 文档数据库接口”,把内容项、用户、设置、工作流等数据以 JSON 文档形式存取;同时通过 GraphQL 模块把站点内容开放给客户端查询。本文以官方 Data 主题文档(Data)为主线,完整讲解文档表与索引的底层机制、ISession会话模型、YesSql/数据库配置、裸 SQL 访问,以及如何把内容类型、部件和字段暴露为 GraphQL 并编写查询过滤器——读完即可在自己的模块中落地一套“建索引 → 注册 Provider → 建表迁移 → 查询/暴露”的完整链路。

一、数据访问总览:YesSql + GraphQL 两条主线

Orchard Core 官方把“数据”主题概括为一句话:用 YesSql 访问内部数据,并用 GraphQL 把它们暴露出去(Data 主题首页)。两条主线对应不同层次的需求:

层次技术适用场景
存储与查询YesSql内容项、用户、设置、工作流等业务数据的持久化,以及基于索引的 SQL 查询
对外暴露GraphQL 模块(OrchardCore.Apis.GraphQL)让客户端应用通过 HTTP 查询站点内容,并提供 GraphiQL Explorer 测试界面
  • 数据存储机制的入门讲解见 How YesSql works;
  • 数据库提供程序、YesSql 选项、表命名预设与裸 SQL 见 Data(OrchardCore.Data)模块参考;
  • GraphQL 的 HTTP 协议、认证与配置见 GraphQL(OrchardCore.GraphQL)模块参考;
  • GraphQL 查询类型、过滤与相关内容的定义方式见 GraphQL queries 参考。

二、YesSql 的工作原理:JSON 文档 + 关系表

Orchard Core 的大多数持久化应用数据——内容项、用户、设置、工作流——都存放在YesSql中。YesSql 是 .NET 的文档数据库接口,底层跑在关系数据库之上:你获得文档存储的灵活性(对象结构变化时无需迁移 schema),同时仍然运行在 SQL Server、SQLite、MySQL 或 PostgreSQL 上。

2.1 文档表:对象被序列化成一行 JSON

当对象被保存时,YesSql 会把它序列化为 JSON,并作为一行存入文档表。默认集合使用Document表;命名集合(named collections)则使用各自的文档表。每个租户还可以使用表前缀(table prefix):

IdTypeContentVersion
42OrchardCore.ContentManagement.ContentItem, OrchardCore.ContentManagement.Abstractions{ "ContentItemId": "4tavbc...", "DisplayText": "My blog post", ... }3

Type列保存的是 .NET 类型全名(程序集限定),Content列是 JSON 文档,Version是文档版本号。正是这种“文档即一行 JSON”的模型,决定了给内容类型添加字段或部件永远不需要数据库迁移——JSON 的形状变了即可,这也是 Orchard Core 内容模型可以任意扩展的根本原因。

显而易见的限制是:无法用 SQL 高效地查询 JSON blob 内部。这正是索引存在的意义。

2.2 索引:让查询属性进入关系表

索引是一个普通的 C# 类,持有你想要查询的属性。YesSql 为索引数据维护常规 SQL 表:

  • Map 索引行带有DocumentId列,指回其文档表;
  • Reduce 索引使用单独的表,把文档与聚合行关联起来。

当文档被创建、更新或删除时,其索引行会在同一个事务内被重新计算。索引分两类:

  • MapIndex:每个索引行映射到一个文档。一个映射可以为某个文档产生一行或多行。例如ContentItemIndex把每个内容项映射到它的ContentType、Published、Owner等属性;AliasPartIndex把每个带有AliasPart的内容项映射到它的别名。
  • ReduceIndex:每个索引行聚合一组文档,类似 SQL 的GROUP BY,用于计数或分组场景。

一条铁律:可查询的属性必须属于某个索引,其余一切都只留在 JSON 文档里。

2.3 定义你自己的索引

用IndexProvider<T>描述如何把一种文档类型映射为索引行:

using YesSql.Indexes; public class ProductIndex : MapIndex { public string Sku { get; set; } public decimal Price { get; set; } } public class ProductIndexProvider : IndexProvider<ContentItem> { public override void Describe(DescribeContext<ContentItem> context) { context.For<ProductIndex>() .When(contentItem => contentItem.Has<ProductPart>()) .Map(contentItem => { var part = contentItem.As<ProductPart>(); return new ProductIndex { Sku = part.Sku, Price = part.Price, }; }); } }

关键点:

  • For<ProductIndex>()指定要维护的索引类型;
  • .When(...)是可选的条件谓词,只有满足条件(比如“含有某个部件”)的文档才产生索引行;
  • .Map(...)从文档中提取属性构造索引行;返回null表示该文档不产生索引行(可用于软删除场景)。

在模块的Startup.ConfigureServices()中注册 Provider:

services.AddIndexProvider<ProductIndexProvider>();

然后在数据迁移中创建索引表:

public async Task<int> CreateAsync() { await SchemaBuilder.CreateMapIndexTableAsync<ProductIndex>(table => table .Column<string>("Sku", column => column.WithLength(64)) .Column<decimal>("Price") ); return 1; }

2.4 仓库中的真实实现:ContentItemIndex 与 AliasPartIndex

  • ContentItemIndex是 Orchard Core 最核心的 Map 索引,字段包括ContentItemId、ContentItemVersionId、Published、Latest、ContentType、ModifiedUtc、PublishedUtc、CreatedUtc、Owner、Author、DisplayText;配套的ContentItemIndexProvider演示了在Map中截断超长字段(例如ContentType、Owner、DisplayText限制为 255 字符,见MaxContentTypeSize等常量)的边界处理。凡是内容列表页、内容查询都要经过这张索引表。
  • AliasPartIndex演示了条件索引:Describe中通过.When(contentItem => contentItem.Has<AliasPart>() || _partRemoved.Contains(...))决定哪些文档参与索引,并在Map里对软删除(!Published && !Latest)或别名空的文档返回null以移除索引记录,同时把别名统一转为小写(part.Alias.ToLowerInvariant())。
  • 其迁移文件展示了索引表的完整生命周期:CreateAsync用CreateMapIndexTableAsync建表并创建IDX_AliasPartIndex_DocumentId复合索引;后续UpdateFrom1Async、UpdateFrom2Async、UpdateFrom3Async展示了通过AlterIndexTableAsync添加列、逐步调整索引定义的演进模式——这是多租户、多版本升级时保持索引结构一致的官方做法。

三、会话(ISession):作用域内的读写单元

YesSql 的文档读写都通过ISession进行。它是注册在依赖注入容器中的作用域(scoped)工作单元:

  • SaveAsync和Delete只是缓冲在内存里;
  • 真正执行 SQL 命令是在调用SaveChangesAsync时,且所有命令在单个事务中运行;
  • Orchard Core 会在租户 shell 作用域结束时(通常是一个请求结束时)自动提交从该作用域解析出的会话。

查询示例:

public sealed class MyController : Controller { private readonly ISession _session; public MyController(ISession session) { _session = session; } public async Task<IActionResult> Cheap() { // Query documents through an index. var cheapProducts = await _session .Query<ContentItem, ProductIndex>(index => index.Price < 10) .ListAsync(); return View(cheapProducts); } }

查询的执行路径是:先用普通 SQL 在索引表上过滤,再加载匹配的 JSON 文档并反序列化。会话还会缓存文档:同一请求内两次加载同一个文档,返回的是同一个实例。

最佳实践:针对内容项,优先使用更高层的IContentManager(或IOrchardHelper扩展如QueryContentItemsAsync),它们负责加载、版本化和处理器(handlers)的编排;只有当需要查询你自己的索引时才下沉到ISession。

四、数据层配置:从租户到 SQLite、YesSql 选项与表命名

数据库提供程序、连接字符串和表前缀在租户安装(setup)时选定。YesSqlOptions(命令页大小、隔离级别、自定义序列化器等)与表命名预设均可配置,详见 Data(OrchardCore.Data)模块参考。

4.1 数据库提供程序

数据库Provider 值连接字符串表前缀和 schema
SQLiteSqlite不使用不使用
SQL ServerSqlConnection必填支持
MySQLMySql必填支持
PostgreSQLPostgres必填支持

对 SQL Server、MySQL、PostgreSQL:需要在运行 setup之前创建好数据库,并授予配置的账户建表、改表的权限。Orchard Core 只校验连接然后创建自己的表,不会替你创建数据库。Provider、连接字符串、表前缀和 schema 都是租户设置,可以在安装界面上输入,也可以通过IShellConfiguration预配置(见 Setup 参考 与 Configuration 参考)。

4.2 SQLite 选项

SQLite 是默认 Provider,每个租户的数据库文件存放在该租户的 shell 数据目录中。

  • 数据库文件名:由DatabaseNameshell 设置控制;未提供时安装程序使用OrchardCore.db。因为该值属于租户的ShellSettings,每个租户可以使用不同的文件名。
  • 连接池:Microsoft.Data.Sqlite连接池默认开启。池化连接可能一直占用数据库文件,妨碍复制或替换文件等备份操作。需要释放文件时,把UseConnectionPooling设为false(禁用连接池可能降低性能)。

在根 Web 应用的appsettings.json中,把选项配置在OrchardCore节下:

{ "OrchardCore": { "Data": { "Sqlite": { "UseConnectionPooling": false } } } }

4.3 YesSql 选项

Orchard Core 把OrchardCore:YesSql配置节绑定到YesSqlOptions(源码见 YesSqlOptions.cs):

设置默认值说明
CommandsPageSize500YesSql 命令页的最大命令数,更大的集合会被拆分成多页
QueryGatingEnabledtrue合并相同的并发查询工作,让 YesSql 只执行一次并共享结果
EnableThreadSafetyChecksfalse启用用于诊断 YesSql 会话被并发使用的检查
IsolationLevelReadCommitted传给配置的 Provider 的默认事务隔离级别

可通过任意受支持的租户配置源配置,例如:

{ "OrchardCore": { "YesSql": { "CommandsPageSize": 1000, "QueryGatingEnabled": true, "EnableThreadSafetyChecks": false, "IsolationLevel": "ReadCommitted" } } }

YesSqlOptions还暴露了IdGenerator、IdentifierAccessorFactory、VersionAccessorFactory、ContentSerializer——这些是服务实现,无法由配置绑定创建,需要在代码中配置:

using OrchardCore.Data.YesSql; services.Configure<YesSqlOptions>(options => { options.CommandsPageSize = 1000; });

4.4 表命名预设

OrchardCore:Data:TableOptions节定义的是预设值,Orchard Core 会在首次安装租户时把这些值复制进该租户的 shell 设置:

设置新租户默认值说明
DefaultDocumentTableDocument默认 YesSql 文档表的名称
DefaultTableNameSeparator_表前缀或集合名与表名之间的分隔符。可使用一个或多个下划线,或用NULL表示无分隔符
DefaultIdentityColumnSizeInt64把标识列设为Int32或Int64
{ "OrchardCore": { "Data": { "TableOptions": { "DefaultDocumentTable": "Document", "DefaultTableNameSeparator": "_", "DefaultIdentityColumnSize": "Int64" } } } }

⚠️警告:这些预设必须在安装租户之前配置。事后修改不会重命名已有表,也不会改变已有标识列。

以上示例展示的是根 Web 应用appsettings.json的形状;在租户本地的App_Data/Sites/{tenant}/appsettings.json中,要省略外层的OrchardCore节。完整的配置源层级见 Configuration 参考。

五、裸 SQL:用 IDbConnectionAccessor 操作关系表

对于内容项,优先用IContentManager;对于 YesSql 文档和索引,优先用ISession。只有需要直接操作关系表时才使用裸 SQL:

  • IDbConnectionAccessor(命名空间OrchardCore.Data,接口由OrchardCore.Data.Abstractions包提供)为当前租户创建DbConnection;
  • 从YesSql命名空间解析IStore,可拿到配置好的 SQL 方言、schema 和表前缀;
  • 自定义关系表应通过数据迁移创建,让 Orchard Core 一致地应用 schema。

5.1 引用表名

不同数据库 Provider 的标识符语法不同。应根据 YesSql store 配置构建表名,并用ISqlDialect引用:

using Dapper; using OrchardCore.Data; using YesSql; public sealed class CustomTableReader { private readonly IDbConnectionAccessor _dbConnectionAccessor; private readonly IStore _store; public CustomTableReader(IDbConnectionAccessor dbConnectionAccessor, IStore store) { _dbConnectionAccessor = dbConnectionAccessor; _store = store; } public async Task<IReadOnlyList<CustomRow>> ListAsync( CancellationToken cancellationToken) { await using var connection = _dbConnectionAccessor.CreateConnection(); await connection.OpenAsync(cancellationToken); var configuration = _store.Configuration; var tableName = configuration.SqlDialect.QuoteForTableName( $"{configuration.TablePrefix}CustomTable", configuration.Schema); var command = new CommandDefinition( $"SELECT * FROM {tableName};", cancellationToken: cancellationToken); return (await connection.QueryAsync<CustomRow>(command)).AsList(); } }

注意:IStore.Configuration.TablePrefix已经包含了配置的表名分隔符;引用表时还要传入IStore.Configuration.Schema,这样对使用非默认 schema 的租户也能正常工作。表名不能作为 SQL 参数传入,因此只应从受信任的应用与租户配置中组合标识符;数据值则务必以参数形式传给 Dapper,不要插值进 SQL。

5.2 相关写入使用事务

先打开连接再开始事务,把事务传给每个 Dapper 命令,回滚后要重新抛出异常:

await using var connection = _dbConnectionAccessor.CreateConnection(); await connection.OpenAsync(cancellationToken); await using var transaction = await connection.BeginTransactionAsync(cancellationToken); try { await connection.ExecuteAsync(new CommandDefinition( firstCommand, transaction: transaction, cancellationToken: cancellationToken)); await connection.ExecuteAsync(new CommandDefinition( secondCommand, transaction: transaction, cancellationToken: cancellationToken)); await transaction.CommitAsync(cancellationToken); } catch { await transaction.RollbackAsync(cancellationToken); throw; }

需要针对同一数据库执行 SQL 查询的场景,还可以直接使用 SQL queries(OrchardCore.Queries)模块。

六、用 GraphQL 暴露数据:HTTP 协议、认证与配置

GraphQL 模块允许客户端应用查询 Orchard 站点处理的内容:它提供 GraphiQL Explorer 视图用于测试查询,并提供 HTTP 端点供客户端发送查询。

6.1 HTTP 方法、请求头与请求体

GET 请求:GraphQL 查询放在query查询字符串参数中。例如执行:

{ me { name } }

对应的 GET 请求是:

https://localhost:44300/api/graphql?query={me{name}}

查询变量可以作为一个 JSON 编码字符串放在额外的variables参数里;如果查询包含多个命名操作,可用operationName参数控制执行哪一个。

POST 请求 ——application/json:标准 GraphQL POST 应使用application/json内容类型头,请求体为如下 JSON:

{ "query": "...", "operationName": "...", "variables": { "myVariable": "someValue", ... } }

operationName和variables都是可选字段;仅当查询中存在多个操作时才需要operationName。

POST 请求 ——application/graphql:使用application/graphql内容类型头,POST 请求体内容直接被当作 GraphQL 查询字符串。

查询字符串:此外,如果query查询字符串参数存在(如同 GET 示例),它也会被解析并按 GET 方式处理。

响应:无论查询和变量以何种方式发送,响应都以 JSON 格式返回在响应体中。一次查询可能同时产生数据和错误,返回的 JSON 对象形如:

{ "data": { ... }, "errors": [ ... ] }
  • 没有错误时,响应中不出现errors字段;
  • 没有数据时,仅当错误发生在执行阶段,data字段才会被包含。

6.2 认证与权限

执行 GraphQL 查询要求发起者拥有ExecuteGraphQL权限。和 Orchard Core 的其他 API 一样,GraphQL API 支持 cookie 和 OAuth 2.0 认证,因此兼容 OpenId 模块并支持 JSON Web Token(JWT)。默认情况下匿名用户不能执行 GraphQL 查询。

6.3 配置内容暴露

识别哪些内容类型参与 GraphQL 暴露时,Orchard Core 默认排除带 stereotype 的类型,以便你对带 stereotype 的内容类型保持控制。GraphQLContentOptions引入了DiscoverableSterotypes选项,可以指定默认应可被发现(discoverable)的 stereotype。例如有多个 stereotype 为ExampleStereotype的内容类型,可在启动类中加入:

services.Configure<GraphQLContentOptions>(options => { options.DiscoverableSterotypes.Add("ExampleStereotype"); });

利用GraphQLContentOptions还可以自定义内容类型、部件或字段的默认可见性。例如隐藏作者名字:

services.Configure<GraphQLContentOptions>(options => { options.IgnoreField<ContentItemType>(nameof(ContentItem.Owner)); });

或默认隐藏某个内容类型:

services.Configure<GraphQLContentOptions>(options => { options.ConfigureContentType("SiteLayers", x => { x.Hidden = true; }); });

6.4 GraphQL 运行时配置

可以通过标准 shell 配置设置异常暴露、最大深度、最大复杂度与字段影响(field impact):

{ "OrchardCore": { "Apis": { "GraphQL": { "ExposeExceptions": true, "MaxDepth": 50, "MaxComplexity": 100, "FieldImpact": 2.0, "DefaultNumberOfResults": 100, "MaxNumberOfResults": 1000, "MaxNumberOfResultsValidationMode": "Default" } } } }

各项参数含义:

  • ExposeExceptions(bool,默认:生产环境为 false,开发环境为 true):设为 true 时向 GraphQL 客户端暴露堆栈跟踪。
  • DefaultNumberOfResults(int,默认:100):所有分页字段/类型默认返回的结果数。
  • MaxNumberOfResults(int,默认:1000):所有分页字段/类型返回的最大结果数。
  • MaxNumberOfResultsValidationMode(enum,取值Default|Enabled|Disabled,默认Default):当分页参数超过最大结果数时的校验行为:
    • Default:生产环境记录 info 日志并只返回最大结果数;开发环境抛出 GraphQL 校验错误。
    • Enabled:抛出 GraphQL 校验错误。
    • Disabled:记录 info 日志并只返回最大结果数。
  • MaxDepth(int?,默认:100):强制单个请求中所有查询的总最大嵌套深度。
  • MaxComplexity(int?,默认:null):查询复杂度上限。
  • FieldImpact(double?,默认:null):字段影响系数,与MaxComplexity配合抵御恶意查询。

七、定义 GraphQL 查询类型与过滤器

GraphQL queries 参考 介绍了查询的三个组成:type、arguments和return values。例如:

{ blog { displayText } }

这里blog是类型,displayText是返回值。可以扩展加入参数用于过滤,例如:

{ blog(where: {contentItemId: "4k5df0kadp9asy1n2ejzs1rz4r"}) { displayText } }

7.1 定义一个查询类型:三步接入 GraphQL schema

第一步,先有一个普通的 C# 部件:

public class AutoroutePart : ContentPart { public string Path { get; set; } public bool SetHomepage { get; set; } }

GraphQL 并不认识它,因此第二步,创建该类的 GraphQL 表示:

public class AutorouteQueryObjectType : ObjectGraphType<AutoroutePart> { public AutorouteQueryObjectType() { Name = "AutoroutePart"; // Map the fields you want to expose Field(x => x.Path); } }

要点:继承ObjectGraphType(GraphQL 能理解这个类型);用Field(x => x.Path)声明要公开暴露的字段。

第三步,在 Startup 类中注册,让 GraphQL 子系统从依赖树中识别新类型:

[RequireFeatures("OrchardCore.Apis.GraphQL")] public class sealed Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { // I have omitted the registering of the AutoroutePart, as we expect that to already be registered services.AddObjectGraphType<AutoroutePart, AutorouteQueryObjectType>(); } }

完成!部件现在已在 GraphQL 中暴露,打开查询浏览器即可查看。

7.2 自定义查询过滤器:Input 类型 + GraphQLFilter

适用场景:给 Content-Type 查询添加新过滤器,或需要自定义过滤逻辑(如从服务取数据、比较复杂对象)。步骤:

1. 实现 Input 类型(所有 Input 类型必须继承InputObjectGraphType):

public class AutorouteInputObjectType : InputObjectGraphType<AutoroutePart> { public AutorouteInputObjectType() { Name = "AutoroutePartInput"; Field(x => x.Path, nullable: true).Description("the path of the content item to filter"); } }

2. 在 Startup 类中注册:

[RequireFeatures("OrchardCore.Apis.GraphQL")] public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { // I have omitted the registering of the AutoroutePart, as we expect that to already be registered services.AddObjectGraphType<AutoroutePart, AutorouteQueryObjectType>(); services.AddInputObjectGraphType<AutoroutePart, AutorouteInputObjectType>(); } }

注册输入部件后,它会作为父查询的一部分出现,例如:

{ blog(autoroutePart: { path: "somewhere" }) { displayText } }

3. 实现 Filter:过滤器接收上一步构建的输入并执行真正的过滤。注意GraphQLFilter还提供PostQueryAsync,可用于权限检查等其他场景:

public class AutoroutePartGraphQLFilter : GraphQLFilter<ContentItem> { public override Task<IQuery<ContentItem>> PreQueryAsync(IQuery<ContentItem> query, ResolveFieldContext context) { if (!context.HasArgument("autoroutePart")) { return Task.FromResult(query); } var part = context.GetArgument<AutoroutePart>("autoroutePart"); if (part == null) { return Task.FromResult(query); } var autorouteQuery = query.With<AutoroutePartIndex>(); if (!string.IsNullOrWhiteSpace(part.Path)) { // Do not use commands that are terminating query, e.g. All() in here. Query needs to be editable, because ContentItemsFieldType that calls PreQueryAsync might need to work with it (e.g. insert another where conditions). return Task.FromResult(autorouteQuery.Where(index => index.Path == part.Path)); } return Task.FromResult(query); } }

核心是context.GetArgument<AutoroutePart>("autoroutePart")——该参数在我们注册输入类型时被登记,从这里反序列化后即可执行查询。

7.3 默认 Content-Type 查询过滤器:WhereInputObjectGraphType + IIndexAliasProvider

适用场景:给 Content-Type 查询添加简单过滤器,且已有带数据的数据库索引、可以用简单比较(equals、contains、in…)作用于索引值——例如AutoroutePartIndex.Path = filterValue。步骤:

1. 实现WhereInputObjectGraphType。它给InputObjectGraphType增加了定义相等、子串、数组等过滤的方法。必须继承WhereInputObjectGraphType,因为ContentItemsFieldType期望用它来做过滤逻辑(不要用InputObjectGraphType,它不会被默认 ContentItem 查询处理):

// Assuming we've added the necessary using directives. // It is essential to inherit from WhereInputObjectGraphType. // Do not use the InputObjectGraphType type as it will not be // handled by default ContentItem queries. public class AutorouteInputObjectType : WhereInputObjectGraphType<AutoroutePart> { // Binds the filter fields to the GraphQL type representing AutoroutePart public AutorouteInputObjectType() { Name = "AutoroutePartInput"; // Utilize the method for adding scalar fields from the base class. AddScalarFilterFields<StringGraphType>("path", S["Filter by the path of the content item"]); } }

AddScalarFilterFields会给所有 ContentItem 查询(包括自定义内容类型)添加标量过滤器:equals / not equals、contains / not contains、starts with / ends with / not starts with / not ends with、in / not in。这些过滤器会对照绑定到该ContentPart的索引进行检查。

2. 实现IIndexAliasProvider把ContentPart绑定到索引。过滤器对象中的字段名必须与索引中的字段名一致,这是过滤器自动匹配的前提:

public class AutoroutePartIndexAliasProvider : IIndexAliasProvider { private static readonly IndexAlias[] _aliases = [ new IndexAlias { Alias = "autoroutePart", // alias of graphql ContentPart. You may also use nameof(AutoroutPart).ToFieldName() Index = nameof(AutoroutePartIndex), // name of index bound to part - keep in mind, that fields need to correspond. E.g. 'path' has the same name in the index and part. IndexType = typeof(AutoroutePartIndex) } ]; public ValueTask<IEnumerable<IndexAlias>> GetAliasesAsync() { return ValueTask.FromResult<IEnumerable<IndexAlias>>(_aliases); } }

3. 更新 Startup 类:

[RequireFeatures("OrchardCore.Apis.GraphQL")] public sealed class Startup : StartupBase { // Assuming we've added the necessary using directives. public override void ConfigureServices(IServiceCollection services) { // Code to register the AutoroutePart and AutorouteQueryObjectType is assumed to be present. // Register WhereInputObjectGraphType services.AddInputObjectGraphType<AutoroutePart, AutorouteInputObjectType>(); // Register IIndexAliasProvider services.AddTransient<IIndexAliasProvider, AutoroutePartIndexAliasProvider>(); services.AddWhereInputIndexPropertyProvider<AutoroutePartIndex>(); } }

完成后即可在 GraphQL 界面看到所有内容类型查询上出现新的过滤器。示例查询(以person类型上的path字段为例,过滤字段嵌套在where对象内):

{ person(where: {path: {path_contains: "", path: "", path_ends_with: "", path_in: "", path_not: "", path_not_contains: "", path_not_ends_with: "", path_not_in: "", path_not_starts_with: "", path_starts_with: ""}}) { name } }

如果注册部件时使用collapse = true,字段将不再嵌套在对象内:

{ person(where: {path_contains: "", path: "", path_ends_with: "", path_in: "", path_not: "", path_not_contains: "", path_not_ends_with: "", path_not_in: "", path_not_starts_with: "", path_starts_with: ""}) { name } }

更深入的理解可查阅WhereInputObjectGraphType、ContentItemsFieldType与AutoroutePartIndex的实现。

7.4 用参数过滤与自定义输出

还可以利用查询参数进行过滤,或在Resolve方法中根据参数值定制查询输出。适用场景:为任意类型、内容部件或字段添加过滤器;使用自定义过滤逻辑;或根据参数值切换数据源/逻辑。Orchard Core 中ContentItemQuery与MediaAssetQuery是“按参数过滤查询”的实现范例,MediaFieldQueryObjectType是“在字段上应用参数”的范例。

7.5 查询相关内容项

内容项之间可以互相关联。假设有内容类型 Movie(name、releaseYear 文本字段)和 Person(FavoriteMovies 为指向 Movie 的内容选择器字段)。想查询 Person 的 Favorite Movies 时,下面的查询会报错:

{ person { name favoriteMovies { contentItems { name releaseYear } } } }

错误会提示name和releaseYear不是 Content Item 的字段。内联片段(inline fragment)就是告诉查询解析器把这些通用条目当作Movie类型而非通用Content Item处理的构造。注意下面的... on Movie片段:

{ person { name favoriteMovies { contentItems { ... on Movie { name releaseYear } } } } }

使用 List Part 获取父内容项:使用 List Part 时还可以访问父列表内容项。下面的查询演示了为某篇博客文章获取父级 blog 内容项:

{ blogPost { blog { listContentItem { ... on Blog { displayText } } } } }

八、小结与延伸

Orchard Core 的数据层是一条清晰的两段式链路:YesSql 负责把业务对象沉淀为 JSON 文档 + 关系索引表(ContentItemIndex、AliasPartIndex是官方最佳范本),ISession提供事务性工作单元,YesSqlOptions、SQLite 选项与表命名预设负责按租户定制行为;需要裸 SQL 时用IDbConnectionAccessor+IStore安全地引用表名与执行事务。对外暴露则交给GraphQL 模块:标准的 HTTP 端点、ExecuteGraphQL权限与 OAuth 2.0/JWT 兼容、GraphQLContentOptions控制可见性,以及从ObjectGraphType到WhereInputObjectGraphType/IIndexAliasProvider的一整套可扩展查询与过滤体系。

  • 数据主题入口:Data | 入门讲解:How YesSql works
  • 数据模块参考:Data(OrchardCore.Data) | 迁移:Migrations | SQL 查询模块:Queries
  • GraphQL 参考:GraphQL 模块 | GraphQL queries
  • 源码实例:ContentItemIndex 与 Provider | AliasPartIndex 与 Provider | Alias 索引迁移 | YesSqlOptions
  • CMS
  • 后端
  • Web框架

【免费下载链接】OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.

项目地址:https://gitcode.com/gh_mirrors/or/OrchardCore
点击查看免费下载

相关推荐

上一篇:OpenEuler软件委员会章程解读:开源治理制度的完整解析
下一篇:AionUi SkillsHub 技能中心 E2E 测试需求分析全记录:从功能梳理、多角色评审到测试用例定稿的完整实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询