Planetary Docs 示例旨在提供一个全面的示例,展示如何使用 EF Core Azure Cosmos DB 提供程序构建具有创建、读取和更新要求的应用。 此 Blazor Server 应用提供搜索功能,提供一个界面用于添加和更新文档,还提供一个系统用于存储已进行版本控制的快照。 有效负载是具有元数据的文档,这些元数据可能会随时间变化,因此它非常适合文档数据库。 数据库使用具有不同分区键的多个容器,并使用一种机制来提供快速、低成本的特定字段搜索。 它还可处理并发。
开始使用
下面有简单几个步骤教你如何开始使用。
克隆此存储库
使用首选工具克隆存储库。 git
命令如下所示:
git clone https://github.com/dotnet/EntityFramework.Docs
创建 Azure Cosmos DB 实例
要运行此演示,需要运行 Azure Cosmos DB 仿真器或创建 Azure Cosmos DB 帐户。 要了解操作方法,可以阅读创建 Azure Cosmos DB 帐户。 请务必查看免费帐户的选项!
选择“SQL API”。
此项目配置为使用“开箱即用”的仿真器。
初始化数据库
导航到 PlanetaryDocsLoader
项目。
如果使用仿真器,请确保仿真器正在运行。
如果使用 Azure Cosmos DB 帐户,请使用以下项更新 Program.cs
:
- Azure Cosmos DB 终结点
- Azure Cosmos DB 键
终结点是
URI
,键是你在 Azure 门户中 Azure Cosmos DB 帐户的“键”窗格上的Primary Key
。
运行应用程序(从命令行运行 dotnet run
)。 在应用程序分析文档、将文档加载到数据库,然后运行测试时,应该会显示状态。 此步骤可能需要几分钟时间。
配置并运行 Blazor 应用
如果使用仿真器,则 Blazor 应用已准备好运行。 如果使用帐户,请导航到 Blazor Server 项目 PlanetaryDocs
,更新 appsettings.json
文件中的 CosmosSettings
或在 appsettings.Development.json
中创建新节,然后添加访问键和终结点。 运行应用。 你应该已准备好继续操作!
项目详细信息
以下功能已集成到此项目中。
PlanetaryDocsLoader
分析文档存储库并将文档插入到数据库中。 它包括用于验证功能是否正常工作的测试。
PlanetaryDocs.Domain
托管用于数据访问的域类、验证逻辑和签名(接口)。
PlanetaryDocs.DataAccess
包含 EF Core DbContext
和数据访问服务的实现。
DocsContext
- 具有演示如何映射所有权的模型构建代码
- 使用具有 JSON 序列化的值转换器来支持基元集合和嵌套复杂类型
- 演示如何使用分区键,包括如何为模型定义分区键以及如何在查询中指定它们
- 提供按实体指定容器的示例
- 演示如何关闭鉴别器
- 在同一容器中存储两种实体类型(别名和标记)
- 使用“阴影属性”跟踪别名和标记上的分区键
- 挂钩到
SavingChanges
事件以自动生成审核快照
DocumentService
- 显示 C.R.U. 操作的各种策略
- 以编程方式同步相关实体
- 演示如何处理针对已断开连接的实体的并发性更新
- 使用新的
IDbContextFactory<T>
实现来管理上下文实例
PlanetaryDocs
是一个 Blazor Server 应用。
TitleService
、HistoryService
和MultiLineEditService
中的 JavaScript 互操作示例。- 使用键盘处理程序支持在编辑页上进行基于键盘的导航和输入
- 显示具有内置防抖的通用自动完成组件
HtmlPreview
使用虚拟textarea
来呈现 HTML 预览MarkDig
用于将 markdown 转换为 HTMLMultiLineEdit
组件显示了一种解决方法,它使用 JavaScript 互操作来限制具有大型输入值的字段Editor
组件支持并发。 如果在单独的选项卡中打开文档两次并在两者中进行编辑,则第二个选项卡将通知已进行更改,并提供重置或覆盖的选项
你的反馈很宝贵! 提交问题来报告缺陷或请求更改(我们也接受拉取请求。)
Planetary Docs 简介
你可能知道(或可能不知道)Microsoft 的官方文档完全在开源上运行。 它使用 Markdown 和一些元数据增强来生成 .NET 开发人员日常使用的交互式文档。 Planetary Docs 的假设场景是提供一种基于 Web 的工具来创作文档。它支持设置标题、说明、作者别名,并且支持分配标记、编辑 Markdown 和预览 HTML 输出。
它是全球的,因为 Azure Cosmos DB 是“全球规模”。 该应用提供文档搜索功能。 文档存储在别名和标记下,以便快速查找,但你也可使用全文搜索。 该应用会自动审核文档(它会在每次编辑文档时拍摄文档的快照,并提供历史记录视图)。
注意
不会实现删除和还原操作。
Document
的文档如下所示:
using System;
using System.Collections.Generic;
namespace PlanetaryDocs.Domain
{
/// <summary>
/// A document item.
/// </summary>
public class Document
{
/// <summary>
/// Gets or sets the unique identifier.
/// </summary>
public string Uid { get; set; }
/// <summary>
/// Gets or sets the title.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Gets or sets the description.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Gets or sets the published date.
/// </summary>
public DateTime PublishDate { get; set; }
/// <summary>
/// Gets or sets the markdown content.
/// </summary>
public string Markdown { get; set; }
/// <summary>
/// Gets or sets the generated html.
/// </summary>
public string Html { get; set; }
/// <summary>
/// Gets or sets the author's alias.
/// </summary>
public string AuthorAlias { get; set; }
/// <summary>
/// Gets or sets the list of related tags.
/// </summary>
public List<string> Tags { get; set; }
= new List<string>();
/// <summary>
/// Gets or sets the concurrency token.
/// </summary>
public string ETag { get; set; }
/// <summary>
/// Gets the hash code.
/// </summary>
/// <returns>The hash code of the unique identifier.</returns>
public override int GetHashCode() => Uid.GetHashCode();
/// <summary>
/// Implements equality.
/// </summary>
/// <param name="obj">The object to compare to.</param>
/// <returns>A value indicating whether the unique identifiers match.</returns>
public override bool Equals(object obj) =>
obj is Document document && document.Uid == Uid;
/// <summary>
/// Gets the string representation.
/// </summary>
/// <returns>The string representation.</returns>
public override string ToString() =>
$"Document {Uid} by {AuthorAlias} with {Tags.Count} tags: {Title}.";
}
}
为了更快地查找,DocumentSummary
类包含文档的一些基本信息。
namespace PlanetaryDocs.Domain
{
/// <summary>
/// Represents a summary of a document.
/// </summary>
public class DocumentSummary
{
/// <summary>
/// Initializes a new instance of the <see cref="DocumentSummary"/> class.
/// </summary>
public DocumentSummary()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DocumentSummary"/> class
/// and initializes it with the <see cref="Document"/>.
/// </summary>
/// <param name="doc">The <see cref="Document"/> to summarize.</param>
public DocumentSummary(Document doc)
{
Uid = doc.Uid;
Title = doc.Title;
AuthorAlias = doc.AuthorAlias;
}
/// <summary>
/// Gets or sets the unique id of the <see cref="Document"/>.
/// </summary>
public string Uid { get; set; }
/// <summary>
/// Gets or sets the title.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Gets or sets the alias of the author.
/// </summary>
public string AuthorAlias { get; set; }
/// <summary>
/// Gets the hash code.
/// </summary>
/// <returns>The hash code of the document identifier.</returns>
public override int GetHashCode() => Uid.GetHashCode();
/// <summary>
/// Implements equality.
/// </summary>
/// <param name="obj">The object to compare to.</param>
/// <returns>A value indicating whether the unique identifiers match.</returns>
public override bool Equals(object obj) =>
obj is DocumentSummary ds && ds.Uid == Uid;
/// <summary>
/// Gets the string representation.
/// </summary>
/// <returns>The string representation.</returns>
public override string ToString() => $"Summary for {Uid} by {AuthorAlias}: {Title}.";
}
}
这由 Author
和 Tag
使用。 这两者看起来很相似。 下面是 Tag
代码:
using System.Collections.Generic;
namespace PlanetaryDocs.Domain
{
/// <summary>
/// A tag.
/// </summary>
public class Tag : IDocSummaries
{
/// <summary>
/// Gets or sets the name of the tag.
/// </summary>
public string TagName { get; set; }
/// <summary>
/// Gets or sets a summary of documents with the tag.
/// </summary>
public List<DocumentSummary> Documents { get; set; }
= new List<DocumentSummary>();
/// <summary>
/// Gets or sets the concurrency token.
/// </summary>
public string ETag { get; set; }
/// <summary>
/// Gets the hash code.
/// </summary>
/// <returns>The hash code of the tag name.</returns>
public override int GetHashCode() => TagName.GetHashCode();
/// <summary>
/// Implements equality.
/// </summary>
/// <param name="obj">The object to compare to.</param>
/// <returns>A value indicating whether the tag names match.</returns>
public override bool Equals(object obj) =>
obj is Tag tag && tag.TagName == TagName;
/// <summary>
/// Gets the string representation.
/// </summary>
/// <returns>The string representation.</returns>
public override string ToString() =>
$"Tag {TagName} tagged by {Documents.Count} documents.";
}
}
ETag
属性是在模型上实现的,因此它可在应用中发送并维护值(而不是使用阴影属性)。 ETag
用于在 Azure Cosmos DB 中实现并发性。 示例应用中实现了对并发性的支持。 若要测试这一点,请尝试在两个选项卡中打开同一个文档,然后更新一个文档并保存它,最后更新另一个文档并保存它。
EF Core 中断开连接的实体常常让人们困扰。 此应用中使用这种模式来提供示例。 它在 Blazor Server 中不是必需的,但可让应用缩放变得更简单。 替代方法是使用 EF Core 的更改跟踪器来跟踪实体的状态。 通过更改跟踪器,可删除 ETag
属性并改用阴影属性。
最后,提供了 DocumentAudit
文档。
using System;
using System.Text.Json;
namespace PlanetaryDocs.Domain
{
/// <summary>
/// Represents a snapshot of the document.
/// </summary>
public class DocumentAudit
{
/// <summary>
/// Initializes a new instance of the <see cref="DocumentAudit"/> class.
/// </summary>
public DocumentAudit()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DocumentAudit"/> class
/// and configures it with the <see cref="Document"/> passed in.
/// </summary>
/// <param name="document">The document to audit.</param>
public DocumentAudit(Document document)
{
Id = Guid.NewGuid();
Uid = document.Uid;
Document = JsonSerializer.Serialize(document);
Timestamp = DateTimeOffset.UtcNow;
}
/// <summary>
/// Gets or sets a unique identifier.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the identifier of the document.
/// </summary>
public string Uid { get; set; }
/// <summary>
/// Gets or sets the timestamp of the audit.
/// </summary>
public DateTimeOffset Timestamp { get; set; }
/// <summary>
/// Gets or sets the JSON serialized snapshot.
/// </summary>
public string Document { get; set; }
/// <summary>
/// Deserializes the snapshot.
/// </summary>
/// <returns>The <see cref="Document"/> snapshot.</returns>
public Document GetDocumentSnapshot() =>
JsonSerializer.Deserialize<Document>(Document);
}
}
理想情况下,Document
快照是一个适当的属性,而不是字符串。 这是 EF Core 当前具有的 EF Core Azure Cosmos DB 提供程序限制之一。 Document
无法既充当独立实体,又充当“自有”实体。 如果希望用户能够搜索历史文档中的属性,可以将这些属性添加到 DocumentAudit
类中,以便自动索引这些属性,或者创建共享相同属性但被配置为由 DocumentAudit
父级“拥有”的 DocumentSnapshot
类。
Azure Cosmos DB 设置
数据存储的策略是使用 3 个容器。
一个名为 Documents
的容器专用于文档。 这些文档按 id
进行分区。 每个文档一个分区。 这是合理的。
审核存储在名为 Audits
的容器中。 分区键是文档 ID,因此所有历史记录都存储在同一分区中。 这样,就能对历史数据进行快速的单分区查询。
最后,有一些元数据存储在 Meta
中。 分区键是元数据类型:Author
或 Tag
。 元数据包含相关文档的摘要。 如果用户想要搜索带有标记 x
的文档,该应用无需扫描所有文档。 相反,它会在文档中读取标记 x
,其中包含标记了它的相关文档的集合。 这种方法可实现快速读取,但对于写入和更新来说,需要额外的一些工作,稍后将对此进行讲解。
Entity Framework Core
Planetary Docs 的 DbContext
在 PlanetaryDocs.DataAccess
项目中命名为 DocsContext
。 它具有一个构造函数,该构造函数采用 DbContextOptions<DocsContext>
参数,并将其传递给基类以启用运行时配置。
public DocsContext(DbContextOptions<DocsContext> options)
: base(options) =>
SavingChanges += DocsContext_SavingChanges;
DbSet<>
泛型类型用于指定应保留的类。
/// <summary>
/// Gets or sets the audits collection.
/// </summary>
public DbSet<DocumentAudit> Audits { get; set; }
/// <summary>
/// Gets or sets the documents collection.
/// </summary>
public DbSet<Document> Documents { get; set; }
/// <summary>
/// Gets or sets the tags collection.
/// </summary>
public DbSet<Tag> Tags { get; set; }
/// <summary>
/// Gets or sets the authors collection.
/// </summary>
public DbSet<Author> Authors { get; set; }
DbContext
类上的一些帮助程序方法可让你更轻松地搜索和分配元数据。 这两个元数据项都使用基于字符串的键,并将类型指定为分区键。 这样,可以通过通用策略来查找记录:
public async ValueTask<T> FindMetaAsync<T>(string key)
where T : class, IDocSummaries
{
var partitionKey = ComputePartitionKey<T>();
try
{
return await FindAsync<T>(key, partitionKey);
}
catch (CosmosException ce)
{
if (ce.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
throw;
}
}
FindAsync
是基础 DbContext
上的现有方法,作为 EF Core 的一部分提供,它不要求关闭类型来指定键。 该方法将此键作为 object
参数,并根据模型的内部表示形式应用它。
实体在 OnModelCreating
重载中配置。 下面是 DocumentAudit
的第一个配置。
modelBuilder.Entity<DocumentAudit>()
.HasNoDiscriminator()
.ToContainer(nameof(Audits))
.HasPartitionKey(da => da.Uid)
.HasKey(da => new { da.Id, da.Uid });
此配置将以下情况通知给 EF Core...
- 表中只存储一种类型,因此无需鉴别器即可区分类型。
- 文档应存储在名为
Audits
的容器中。 - 分区键是文档 ID。
- 访问键是审核的唯一标识符与分区键(文档的唯一标识符)的组合。
接下来,配置 Document
:
var docModel = modelBuilder.Entity<Document>();
docModel.ToContainer(nameof(Documents))
.HasNoDiscriminator()
.HasKey(d => d.Uid);
docModel.HasPartitionKey(d => d.Uid)
.Property(p => p.ETag)
.IsETagConcurrency();
下面我们另外指定一些详细信息,例如应如何映射 ETag
属性:
var tagModel = modelBuilder.Entity<Tag>();
tagModel.Property<string>(PartitionKey);
tagModel.HasPartitionKey(PartitionKey);
tagModel.ToContainer(Meta)
.HasKey(nameof(Tag.TagName), PartitionKey);
tagModel.Property(t => t.ETag)
.IsETagConcurrency();
分区键配置为阴影属性。 与 ETag
属性不同,分区键是固定的,因此不必存在于模型中。 请阅读此内容,详细了解 EF Core 中的模型:创建和配置模型。
SaveChanges
事件用于在每次插入或更新文档时都自动插入文档快照。 每次保存更改并触发事件时,都会查询 ChangeTracker
来查找已添加或已更新的 Document
实体。 为每一项插入一个审核条目。
private void DocsContext_SavingChanges(
object sender,
SavingChangesEventArgs e)
{
var entries = ChangeTracker.Entries<Document>()
.Where(
e => e.State == EntityState.Added ||
e.State == EntityState.Modified)
.Select(e => e.Entity)
.ToList();
foreach (var docEntry in entries)
{
Audits.Add(new DocumentAudit(docEntry));
}
}
这样做可确保即使生成共享相同 DbContext
的其他应用,也会生成审核。
数据服务
一个常见问题是开发人员是否应将存储库模式与 EF Core 结合使用,答案是“根据情况决定”。如果 DbContext
是可测试的,并且可通过接口进行模拟,那么在很多情况下,完全可以直接使用它。 无论是否专门使用存储库模式,当需要在 EF Core 功能之外执行与数据库相关的任务时,添加数据访问层通常都是有意义的。 在此示例中,存在与数据库相关的逻辑,与使 DbContext
膨胀相比,隔离它更有意义,因此在 DocumentService
中实现该逻辑。
服务构造函数会获得一个 DbContext
工厂。 它由 EF Core 提供,便于使用首选配置轻松创建新的上下文。 该应用使用“每个操作的上下文”,而不是使用长期上下文和更改跟踪。 以下配置会获取设置,并通知工厂创建连接到 Azure Cosmos DB 的上下文。 然后,该工厂会自动注入服务。
services.Configure<CosmosSettings>(
Configuration.GetSection(nameof(CosmosSettings)));
services.AddDbContextFactory<DocsContext>(
(IServiceProvider sp, DbContextOptionsBuilder opts) =>
{
var cosmosSettings = sp
.GetRequiredService<IOptions<CosmosSettings>>()
.Value;
opts.UseCosmos(
cosmosSettings.EndPoint,
cosmosSettings.AccessKey,
nameof(DocsContext));
});
services.AddScoped<IDocumentService, DocumentService>();
使用此模式既演示了断开连接的实体,也针对 Blazor SignalR 线路可能断开的情况构建了一些复原能力。
加载文档
文档加载旨在获取未跟踪更改的快照,因为这些更改是在单独的操作中发送的。 主要要求是设置分区键。
private static async Task<Document> LoadDocNoTrackingAsync(
DocsContext context, Document document) =>
await context.Documents
.WithPartitionKey(document.Uid)
.AsNoTracking()
.SingleOrDefaultAsync(d => d.Uid == document.Uid);
查询文档
通过文档查询,用户能够在文档中的任意位置搜索文本,并按作者和/或标记进一步筛选。 伪代码如下所示:
- 如果存在标记,请加载该标记,并将文档摘要列表用作结果集
- 如果还存在作者,请加载作者并筛选结果,找到标记和作者之间的结果交集
- 如果存在文本,请加载与该文本匹配的文档,然后筛选结果,找到作者和标记之间的交集
- 如果还存在文本,请加载与该文本匹配的文档,然后筛选结果,找到标记结果
- 如果还存在作者,请加载作者并筛选结果,找到标记和作者之间的结果交集
- 否则,如果存在作者,请加载作者并筛选结果,找到文档摘要列表作为结果集
- 如果存在文本,请加载与该文本匹配的文档,然后筛选结果,找到作者结果
- 否则,请加载与文本匹配的文档
在性能方面,基于标记和/或作者的搜索只要求加载一两个文档。 文本搜索始终会加载匹配的文档,然后根据现有文档进一步筛选列表,因此速度明显变慢(但速度仍然很快)。
实现如下所示。 请注意,由于 Equals
和 GetHashCode
进行如下替代,HashSet
正常工作:
public async Task<List<DocumentSummary>> QueryDocumentsAsync(
string searchText,
string authorAlias,
string tag)
{
using var context = factory.CreateDbContext();
var result = new HashSet<DocumentSummary>();
var partialResults = false;
if (!string.IsNullOrWhiteSpace(authorAlias))
{
partialResults = true;
var author = await context.FindMetaAsync<Author>(authorAlias);
foreach (var ds in author.Documents)
{
result.Add(ds);
}
}
if (!string.IsNullOrWhiteSpace(tag))
{
var tagEntity = await context.FindMetaAsync<Tag>(tag);
var resultSet =
Enumerable.Empty<DocumentSummary>();
// alias _AND_ tag
if (partialResults)
{
resultSet = result.Intersect(tagEntity.Documents);
}
else
{
resultSet = tagEntity.Documents;
}
result.Clear();
foreach (var docSummary in resultSet)
{
result.Add(docSummary);
}
partialResults = true;
}
// nothing more to do?
if (string.IsNullOrWhiteSpace(searchText))
{
return result.OrderBy(r => r.Title).ToList();
}
// no list to filter further
if (partialResults && result.Count < 1)
{
return result.ToList();
}
// find documents that match
var documents = await context.Documents.Where(
d => d.Title.Contains(searchText) ||
d.Description.Contains(searchText) ||
d.Markdown.Contains(searchText))
.ToListAsync();
// now only intersect with alias/tag constraints
if (partialResults)
{
var uids = result.Select(ds => ds.Uid).ToList();
documents = documents.Where(d => uids.Contains(d.Uid))
.ToList();
}
return documents.Select(d => new DocumentSummary(d))
.OrderBy(ds => ds.Title).ToList();
}
创建文档
通常,使用 EF Core 创建文档非常简单,如下所示:
context.Add(document);
await context.SaveChangesAsync();
但是,对于 PlanetaryDocs
,文档可关联多个标记和一个作者。 这些文档具有必须显式更新的摘要,因为没有正式关系。
注意
此示例使用代码来使文档保持同步。如果数据库由多个应用程序和服务使用,那么最好在数据库级别实现逻辑,并改为使用触发器和存储过程。
泛型方法负责使文档保持同步。无论是对于作者还是标记,伪代码都是相同的:
- 如果已插入或更新文档
- 新文档将导致“已更改作者”和“已添加标记”
- 如果已更改作者或已移除标记
- 加载旧作者或已移除的标记的元数据文档
- 从摘要列表中移除文档
- 如果作者已更改
- 加载新作者的元数据文档
- 将文档添加到摘要列表
- 加载模型的所有标记
- 在每个标记的摘要列表中更新作者
- 如果已添加标记
- 如果存在标记
- 加载标记的元数据文档
- 将文档添加到摘要列表
- Else
- 使用摘要列表中的文档创建新标记
- 如果存在标记
- 如果已更新文档且已更改标题
- 加载现有作者和/或标记的元数据
- 更新摘要列表中的标题
此算法就是 EF Core 如何大展身手的一个示例。 所有这些操作都可在单个传递中发生。 如果某个标记被多次引用,只加载该标记一次。 最后对保存更改的调用将提交所有更改(包括插入项)。
下面是用于处理标记更改的代码,在插入过程中进行调用:
private static async Task HandleTagsAsync(
DocsContext context,
Document document,
bool authorChanged)
{
var refDoc = await LoadDocNoTrackingAsync(context, document);
// did the title change?
var updatedTitle = refDoc != null && refDoc.Title != document.Title;
// tags removed need summary taken away
if (refDoc != null)
{
var removed = refDoc.Tags.Where(
t => !document.Tags.Any(dt => dt == t));
foreach (var removedTag in removed)
{
var tag = await context.FindMetaAsync<Tag>(removedTag);
if (tag != null)
{
var docSummary =
tag.Documents.Find(
d => d.Uid == document.Uid);
if (docSummary != null)
{
tag.Documents.Remove(docSummary);
context.Entry(tag).State = EntityState.Modified;
}
}
}
}
// figure out new tags
var tagsAdded = refDoc == null ?
document.Tags : document.Tags.Where(
t => !refDoc.Tags.Any(rt => rt == t));
// do existing tags need title updated?
if (updatedTitle || authorChanged)
{
// added ones will be handled later
var tagsToChange = document.Tags.Except(tagsAdded);
foreach (var tagName in tagsToChange)
{
var tag = await context.FindMetaAsync<Tag>(tagName);
var ds = tag.Documents.SingleOrDefault(ds => ds.Uid == document.Uid);
if (ds != null)
{
ds.Title = document.Title;
ds.AuthorAlias = document.AuthorAlias;
context.Entry(tag).State = EntityState.Modified;
}
}
}
// brand new tags (for the document)
foreach (var tagAdded in tagsAdded)
{
var tag = await context.FindMetaAsync<Tag>(tagAdded);
// new tag (overall)
if (tag == null)
{
tag = new Tag { TagName = tagAdded };
context.SetPartitionKey(tag);
context.Add(tag);
}
else
{
context.Entry(tag).State = EntityState.Modified;
}
// either way, add the document summary
tag.Documents.Add(new DocumentSummary(document));
}
}
实现的算法适用于插入、更新和删除操作。
更新文档
现在已经了实现元数据同步,更新代码如下所示:
public async Task UpdateDocumentAsync(Document document)
{
using var context = factory.CreateDbContext();
await HandleMetaAsync(context, document);
context.Update(document);
await context.SaveChangesAsync();
}
在这种情况中,并发是有效的,因为我们将已加载的实体版本保留在 ETag
属性中。
删除文档
delete 代码使用简化的算法来移除对标记和创作的现有引用。
public async Task DeleteDocumentAsync(string uid)
{
using var context = factory.CreateDbContext();
var docToDelete = await LoadDocumentAsync(uid);
var author = await context.FindMetaAsync<Author>(docToDelete.AuthorAlias);
var summary = author.Documents.Find(d => d.Uid == uid);
if (summary != null)
{
author.Documents.Remove(summary);
context.Update(author);
}
foreach (var tag in docToDelete.Tags)
{
var tagEntity = await context.FindMetaAsync<Tag>(tag);
var tagSummary = tagEntity.Documents.Find(d => d.Uid == uid);
if (tagSummary != null)
{
tagEntity.Documents.Remove(tagSummary);
context.Update(tagEntity);
}
}
context.Remove(docToDelete);
await context.SaveChangesAsync();
}
搜索元数据(标记或作者)
查找与文本字符串匹配的标记或作者是一个简单的查询。 键旨在将查询设置为单分区查询来提高性能并降低查询的成本。
public async Task<List<string>> SearchAuthorsAsync(string searchText)
{
using var context = factory.CreateDbContext();
var partitionKey = DocsContext.ComputePartitionKey<Author>();
return (await context.Authors
.WithPartitionKey(partitionKey)
.Select(a => a.Alias)
.ToListAsync())
.Where(
a => a.Contains(searchText, System.StringComparison.InvariantCultureIgnoreCase))
.OrderBy(a => a)
.ToList();
}
ComputePartitionKey
方法将简单类型名称作为分区返回。 作者列表并不长,因此代码先拉取别名,然后为 contains 逻辑应用内存中筛选器。
处理文档审核
最后一组 API 处理自动生成的审核。 此方法会加载文档审核,然后将其投影到摘要中。 不会在查询中完成投影,因为它需要对快照进行反序列化。 相反,会获取审核列表,然后对快照进行反序列化来提取相关数据,从而显示标题和作者等内容。
public async Task<List<DocumentAuditSummary>> LoadDocumentHistoryAsync(string uid)
{
using var context = factory.CreateDbContext();
return (await context.Audits
.WithPartitionKey(uid)
.Where(da => da.Uid == uid)
.ToListAsync())
.Select(da => new DocumentAuditSummary(da))
.OrderBy(das => das.Timestamp)
.ToList();
}
ToListAsync
将查询结果具体化,查询后的所有内容都在内存中进行操作。
通过该应用,你还可使用实时文档所用的同一查看器控件来查看审核记录。 方法会加载审核,将快照具体化,并返回 Document
实体供视图使用。
public async Task<Document> LoadDocumentSnapshotAsync(System.Guid guid, string uid)
{
using var context = factory.CreateDbContext();
try
{
var audit = await context.FindAsync<DocumentAudit>(guid, uid);
return audit.GetDocumentSnapshot();
}
catch (CosmosException ce)
{
if (ce.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
throw;
}
}
最后,尽管可以删除记录,但审核仍会保留。 Web 应用尚未实现此功能,但数据服务中实现了这一点。 这些步骤只是将所请求的版本反序列化并查询该版本。
public async Task<Document> RestoreDocumentAsync(Guid id, string uid)
{
var snapshot = await LoadDocumentSnapshotAsync(id, uid);
await InsertDocumentAsync(snapshot);
return await LoadDocumentAsync(uid);
}
结论
此示例背后的目标是提供一些有关使用 EF Core Azure Cosmos DB 提供程序的指导,并演示该提供程序大展身手的领域。 请提交问题并附上反馈或建议。 我们接受拉取请求,因此如果你想要实现缺少的功能或发现了可改进的方面,请告诉我们!