
CMS后端Web框架【免费下载链接】OrchardCoreOrchard 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 见 DataOrchardCore.Data模块参考GraphQL 的 HTTP 协议、认证与配置见 GraphQLOrchardCore.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 prefixIdTypeContentVersion42OrchardCore.ContentManagement.ContentItem, OrchardCore.ContentManagement.Abstractions{ ContentItemId: 4tavbc..., DisplayText: My blog post, ... }3Type列保存的是 .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 定义你自己的索引用IndexProviderT描述如何把一种文档类型映射为索引行using YesSql.Indexes; public class ProductIndex : MapIndex { public string Sku { get; set; } public decimal Price { get; set; } } public class ProductIndexProvider : IndexProviderContentItem { public override void Describe(DescribeContextContentItem context) { context.ForProductIndex() .When(contentItem contentItem.HasProductPart()) .Map(contentItem { var part contentItem.AsProductPart(); return new ProductIndex { Sku part.Sku, Price part.Price, }; }); } }关键点ForProductIndex()指定要维护的索引类型.When(...)是可选的条件谓词只有满足条件比如“含有某个部件”的文档才产生索引行.Map(...)从文档中提取属性构造索引行返回null表示该文档不产生索引行可用于软删除场景。在模块的Startup.ConfigureServices()中注册 Providerservices.AddIndexProviderProductIndexProvider();然后在数据迁移中创建索引表public async Taskint CreateAsync() { await SchemaBuilder.CreateMapIndexTableAsyncProductIndex(table table .Columnstring(Sku, column column.WithLength(64)) .Columndecimal(Price) ); return 1; }2.4 仓库中的真实实现ContentItemIndex 与 AliasPartIndexContentItemIndex是 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.HasAliasPart() || _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 TaskIActionResult Cheap() { // Query documents through an index. var cheapProducts await _session .QueryContentItem, ProductIndex(index index.Price 10) .ListAsync(); return View(cheapProducts); } }查询的执行路径是先用普通 SQL 在索引表上过滤再加载匹配的 JSON 文档并反序列化。会话还会缓存文档同一请求内两次加载同一个文档返回的是同一个实例。最佳实践针对内容项优先使用更高层的IContentManager或IOrchardHelper扩展如QueryContentItemsAsync它们负责加载、版本化和处理器handlers的编排只有当需要查询你自己的索引时才下沉到ISession。四、数据层配置从租户到 SQLite、YesSql 选项与表命名数据库提供程序、连接字符串和表前缀在租户安装setup时选定。YesSqlOptions命令页大小、隔离级别、自定义序列化器等与表命名预设均可配置详见 DataOrchardCore.Data模块参考。4.1 数据库提供程序数据库Provider 值连接字符串表前缀和 schemaSQLiteSqlite不使用不使用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.ConfigureYesSqlOptions(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。只有需要直接操作关系表时才使用裸 SQLIDbConnectionAccessor命名空间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 TaskIReadOnlyListCustomRow 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.QueryAsyncCustomRow(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 queriesOrchardCore.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 TokenJWT。默认情况下匿名用户不能执行 GraphQL 查询。6.3 配置内容暴露识别哪些内容类型参与 GraphQL 暴露时Orchard Core 默认排除带 stereotype 的类型以便你对带 stereotype 的内容类型保持控制。GraphQLContentOptions引入了DiscoverableSterotypes选项可以指定默认应可被发现discoverable的 stereotype。例如有多个 stereotype 为ExampleStereotype的内容类型可在启动类中加入services.ConfigureGraphQLContentOptions(options { options.DiscoverableSterotypes.Add(ExampleStereotype); });利用GraphQLContentOptions还可以自定义内容类型、部件或字段的默认可见性。例如隐藏作者名字services.ConfigureGraphQLContentOptions(options { options.IgnoreFieldContentItemType(nameof(ContentItem.Owner)); });或默认隐藏某个内容类型services.ConfigureGraphQLContentOptions(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 } } } }各项参数含义ExposeExceptionsbool默认生产环境为 false开发环境为 true设为 true 时向 GraphQL 客户端暴露堆栈跟踪。DefaultNumberOfResultsint默认100所有分页字段/类型默认返回的结果数。MaxNumberOfResultsint默认1000所有分页字段/类型返回的最大结果数。MaxNumberOfResultsValidationModeenum取值Default|Enabled|Disabled默认Default当分页参数超过最大结果数时的校验行为Default生产环境记录 info 日志并只返回最大结果数开发环境抛出 GraphQL 校验错误。Enabled抛出 GraphQL 校验错误。Disabled记录 info 日志并只返回最大结果数。MaxDepthint?默认100强制单个请求中所有查询的总最大嵌套深度。MaxComplexityint?默认null查询复杂度上限。FieldImpactdouble?默认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 : ObjectGraphTypeAutoroutePart { public AutorouteQueryObjectType() { Name AutoroutePart; // Map the fields you want to expose Field(x x.Path); } }要点继承ObjectGraphTypeGraphQL 能理解这个类型用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.AddObjectGraphTypeAutoroutePart, AutorouteQueryObjectType(); } }完成部件现在已在 GraphQL 中暴露打开查询浏览器即可查看。7.2 自定义查询过滤器Input 类型 GraphQLFilter适用场景给 Content-Type 查询添加新过滤器或需要自定义过滤逻辑如从服务取数据、比较复杂对象。步骤1. 实现 Input 类型所有 Input 类型必须继承InputObjectGraphTypepublic class AutorouteInputObjectType : InputObjectGraphTypeAutoroutePart { 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.AddObjectGraphTypeAutoroutePart, AutorouteQueryObjectType(); services.AddInputObjectGraphTypeAutoroutePart, AutorouteInputObjectType(); } }注册输入部件后它会作为父查询的一部分出现例如{ blog(autoroutePart: { path: somewhere }) { displayText } }3. 实现 Filter过滤器接收上一步构建的输入并执行真正的过滤。注意GraphQLFilter还提供PostQueryAsync可用于权限检查等其他场景public class AutoroutePartGraphQLFilter : GraphQLFilterContentItem { public override TaskIQueryContentItem PreQueryAsync(IQueryContentItem query, ResolveFieldContext context) { if (!context.HasArgument(autoroutePart)) { return Task.FromResult(query); } var part context.GetArgumentAutoroutePart(autoroutePart); if (part null) { return Task.FromResult(query); } var autorouteQuery query.WithAutoroutePartIndex(); 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.GetArgumentAutoroutePart(autoroutePart)——该参数在我们注册输入类型时被登记从这里反序列化后即可执行查询。7.3 默认 Content-Type 查询过滤器WhereInputObjectGraphType IIndexAliasProvider适用场景给 Content-Type 查询添加简单过滤器且已有带数据的数据库索引、可以用简单比较equals、contains、in…作用于索引值——例如AutoroutePartIndex.Path filterValue。步骤1. 实现WhereInputObjectGraphType。它给InputObjectGraphType增加了定义相等、子串、数组等过滤的方法。必须继承WhereInputObjectGraphType因为ContentItemsFieldType期望用它来做过滤逻辑不要用InputObjectGraphType它不会被默认 ContentItem 查询处理// Assuming weve 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 : WhereInputObjectGraphTypeAutoroutePart { // 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. AddScalarFilterFieldsStringGraphType(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 ValueTaskIEnumerableIndexAlias GetAliasesAsync() { return ValueTask.FromResultIEnumerableIndexAlias(_aliases); } }3. 更新 Startup 类[RequireFeatures(OrchardCore.Apis.GraphQL)] public sealed class Startup : StartupBase { // Assuming weve 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.AddInputObjectGraphTypeAutoroutePart, AutorouteInputObjectType(); // Register IIndexAliasProvider services.AddTransientIIndexAliasProvider, AutoroutePartIndexAliasProvider(); services.AddWhereInputIndexPropertyProviderAutoroutePartIndex(); } }完成后即可在 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 查询相关内容项内容项之间可以互相关联。假设有内容类型 Moviename、releaseYear 文本字段和 PersonFavoriteMovies 为指向 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 时用IDbConnectionAccessorIStore安全地引用表名与执行事务。对外暴露则交给GraphQL 模块标准的 HTTP 端点、ExecuteGraphQL权限与 OAuth 2.0/JWT 兼容、GraphQLContentOptions控制可见性以及从ObjectGraphType到WhereInputObjectGraphType/IIndexAliasProvider的一整套可扩展查询与过滤体系。数据主题入口Data 入门讲解How YesSql works数据模块参考DataOrchardCore.Data 迁移Migrations SQL 查询模块QueriesGraphQL 参考GraphQL 模块 GraphQL queries源码实例ContentItemIndex 与 Provider AliasPartIndex 与 Provider Alias 索引迁移 YesSqlOptions赞分享CMS后端Web框架【免费下载链接】OrchardCoreOrchard 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点击查看免费下载相关推荐goquery 过滤技巧详解Filter / Not / Has 如何精准缩小节点集合goquery 过滤技巧详解Filter / Not / Has 如何精准缩小节点集合 本文基于 goquery 源码与实际测试用例整理覆盖 filtCMS后端Web框架Orchard Core 数据迁移Data Migrations实战指南用 DataMigration 管理内容类型、数据库索引与 RecipeOrchard Core 数据迁移Data Migrations实战指南用 DataMigration 管理内容类型、数据库索引与 Recipe 导读 本CMS后端Web框架Orchard Core 内容定义存储Content Definition Store完全指南从文件存储到数据库的迁移实战Orchard Core 内容定义存储Content Definition Store完全指南从文件存储到数据库的迁移实战 Content DefinitCMS后端Web框架上一篇OpenEuler软件委员会章程解读开源治理制度的完整解析下一篇AionUi SkillsHub 技能中心 E2E 测试需求分析全记录从功能梳理、多角色评审到测试用例定稿的完整实践创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考