练习 - 导入外部内容
在本练习中,你将使用用于将本地 markdown 文件导入到 Microsoft 365 的代码扩展自定义 Microsoft Graph 连接器。
准备工作
在执行本练习之前,请务必完成本模块中的上一个练习。
若要遵循本练习,请从 GitHub 复制本练习中使用的示例内容文件,并将其存储在项目中名为 content 的文件夹中。
若要使代码正常工作,必须将 内容 文件夹及其内容复制到生成输出文件夹。
在代码编辑器中:
打开 .csproj 文件,并在
</Project>
标记之前添加以下代码:<ItemGroup> <ContentFiles Include="content\**" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup> <Target Name="CopyContentFolder" AfterTargets="Build"> <Copy SourceFiles="@(ContentFiles)" DestinationFiles="@(ContentFiles->'$(OutputPath)\content\%(RecursiveDir)%(Filename)%(Extension)')" /> </Target>
保存所做的更改。
添加库以分析 Markdown 和 YAML
要生成的 Microsoft Graph 连接器会将本地 Markdown 文件导入到 Microsoft 365。 其中每个文件都包含一个标头,其中包含 YAML 格式的元数据,也称为 frontmatter。 此外,每个文件的内容都写入 Markdown。 若要提取元数据并将正文转换为 HTML,请使用自定义库:
- 打开终端并将工作目录更改为项目。
- 若要添加 Markdown 处理库,请运行以下命令:
dotnet add package Markdig
。 - 若要添加 YAML 处理库,请运行以下命令:
dotnet add package YamlDotNet
。
定义类以表示导入的文件
为了简化使用导入的 markdown 文件及其内容,让我们定义一个具有必要属性的类。
在代码编辑器中:
打开 ContentService.cs 文件。
using
添加 语句,并更新 类,Document
如下所示:using YamlDotNet.Serialization; public interface IMarkdown { string? Markdown { get; set; } } class Document : IMarkdown { [YamlMember(Alias = "title")] public string? Title { get; set; } [YamlMember(Alias = "description")] public string? Description { get; set; } public string? Markdown { get; set; } public string? Content { get; set; } public string? RelativePath { get; set; } public string? IconUrl { get; set; } public string? Url { get; set; } }
接口
IMarkdown
表示本地 markdown 文件的内容。 需要单独定义它以支持反序列化文件内容。 类Document
表示具有分析的 YAML 属性和 HTML 内容的最终文档。YamlMember
属性将属性映射到每个文档标题中的元数据。保存所做的更改。
实现加载 Markdown 文件
下一步是实现加载本地 Markdown 文件、提取元数据并将内容转换为 HTML 的逻辑。
首先,添加帮助程序方法以轻松使用 Markdig
和 YamlDotNet
库。
在代码编辑器中:
创建名为 MarkdownExtensions.cs 的新文件。
在 文件中,添加以下代码:
// from: https://khalidabuhakmeh.com/parse-markdown-front-matter-with-csharp using Markdig; using Markdig.Extensions.Yaml; using Markdig.Syntax; using YamlDotNet.Serialization; public static class MarkdownExtensions { private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder() .IgnoreUnmatchedProperties() .Build(); private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder() .UseYamlFrontMatter() .Build(); }
属性
YamlDeserializer
为要提取的每个 markdown 文件中的 YAML 块定义新的反序列化程序。 将反序列化程序配置为忽略不属于文件反序列化到的类的所有属性。属性
Pipeline
为 markdown 分析程序定义处理管道。 将其配置为分析 YAML 标头。 如果没有此配置,标头中的信息将被丢弃。接下来,使用以下代码扩展
MarkdownExtensions
类:public static T GetContents<T>(this string markdown) where T : IMarkdown, new() { var document = Markdown.Parse(markdown, Pipeline); var block = document .Descendants<YamlFrontMatterBlock>() .FirstOrDefault(); if (block == null) return new T { Markdown = markdown }; var yaml = block // this is not a mistake // we have to call .Lines 2x .Lines // StringLineGroup[] .Lines // StringLine[] .OrderByDescending(x => x.Line) .Select(x => $"{x}\n") .ToList() .Select(x => x.Replace("---", string.Empty)) .Where(x => !string.IsNullOrWhiteSpace(x)) .Aggregate((s, agg) => agg + s); var t = YamlDeserializer.Deserialize<T>(yaml); t.Markdown = markdown.Substring(block.Span.End + 1); return t; }
方法
GetContents
将标头中带有 YAML 元数据的 markdown 字符串转换为实现 接口的IMarkdown
指定类型。 从 markdown 字符串中提取 YAML 标头并将其反序列化为指定的类型。 然后,它将提取项目正文,并将其设置为Markdown
属性以供进一步处理。保存所做的更改。
提取 markdown 和 YAML 内容
使用帮助程序方法后,实现 Extract
方法以加载本地 markdown 文件并从中提取信息。
在代码编辑器中:
打开 ContentService.cs 文件。
在文件顶部,添加以下 using 语句:
using Markdig;
接下来,在 类中
ContentService
使用以下代码实现Extract
方法:static IEnumerable<Document> Extract() { var docs = new List<Document>(); var contentFolder = "content"; var baseUrl = new Uri("https://learn.microsoft.com/graph/"); var contentFolderPath = Path.Combine(Directory.GetCurrentDirectory(), contentFolder); var files = Directory.GetFiles(contentFolder, "*.md", SearchOption.AllDirectories); foreach (var file in files) { try { var contents = File.ReadAllText(file); var doc = contents.GetContents<Document>(); doc.Content = Markdown.ToHtml(doc.Markdown ?? ""); doc.RelativePath = Path.GetRelativePath(contentFolderPath, file); doc.Url = new Uri(baseUrl, doc.RelativePath!.Replace(".md", "")).ToString(); doc.IconUrl = "https://raw.githubusercontent.com/waldekmastykarz/img/main/microsoft-graph.png"; docs.Add(doc); } catch (Exception ex) { Console.WriteLine(ex.Message); } } return docs; }
方法从 内容 文件夹中加载 markdown 文件开始。 对于每个文件,它会以字符串的形式加载其内容。 它使用
GetContents
类前面定义的扩展方法,将字符串转换为一个对象,其中元数据和内容存储在单独的属性中MarkdownExtensions
。 接下来,它将 markdown 字符串转换为 HTML。 它使用文件的相对路径在 Internet 上生成其 URL。 最后,它存储文件的相对路径,并将 对象添加到集合中以供进一步处理。保存所做的更改。
将内容转换为外部项
读取外部内容后,下一步是将其转换为外部项,这些项将加载到 Microsoft 365。
首先,实现 方法, GetDocId
该方法基于其相对文件路径为每个外部项生成唯一 ID。
在代码编辑器中:
确认正在编辑 ContentService.cs 文件。
ContentService
在 类中,添加以下方法:static string GetDocId(Document doc) { var id = doc.RelativePath! .Replace(Path.DirectorySeparatorChar.ToString(), "__") .Replace(".md", ""); return id; }
GetDocId
方法使用文档的相对文件路径,并将所有目录分隔符替换为双下划线。 这是必需的,因为不能在外部项 ID 中使用路径分隔符。保存所做的更改。
现在,实现 Transform
方法,该方法将表示本地 markdown 文件的对象从 Microsoft Graph 转换为外部项。
在代码编辑器中:
确认你位于 ContentService.cs 文件中。
Transform
使用以下代码实现 方法:static IEnumerable<ExternalItem> Transform(IEnumerable<Document> documents) { return documents.Select(doc => { var docId = GetDocId(doc); return new ExternalItem { Id = docId, Properties = new() { AdditionalData = new Dictionary<string, object> { { "title", a.Title ?? "" }, { "description", a.Description ?? "" }, { "url", new Uri(baseUrl, a.RelativePath!.Replace(".md", "")).ToString() } } }, Content = new() { Value = a.Content ?? "", Type = ExternalItemContentType.Html }, Acl = new() { new() { Type = AclType.Everyone, Value = "everyone", AccessType = AccessType.Grant } } }; }); }
首先,定义基 URL。 使用此 URL 为每个项生成完整的 URL,以便在向用户显示该项时,他们可以导航到原始项。 接下来,将每个项从
DocsArticle
转换为ExternalItem
。 首先,根据项的相对文件路径获取每个项的唯一 ID。 然后创建 的新实例ExternalItem
,并使用 中DocsArticle
的信息填充其属性。 然后,将项的内容设置为从本地文件提取的 HTML 内容,并将项目内容类型设置为 HTML。 最后,配置项目的权限,使其对组织中的每个人都可见。保存所做的更改。
将外部项加载到 Microsoft 365
处理内容的最后一步是将转换的外部项加载到 Microsoft 365。
在代码编辑器中:
验证是否正在编辑 ContentService.cs 文件。
ContentService
在 类中,使用以下代码实现Load
方法:static async Task Load(IEnumerable<ExternalItem> items) { foreach (var item in items) { Console.Write(string.Format("Loading item {0}...", item.Id)); try { await GraphService.Client.External .Connections[Uri.EscapeDataString(ConnectionConfiguration. ExternalConnection.Id!)] .Items[item.Id] .PutAsync(item); Console.WriteLine("DONE"); } catch (Exception ex) { Console.WriteLine("ERROR"); Console.WriteLine(ex.Message); } } }
对于每个外部项,请使用 Microsoft Graph .NET SDK 调用 Microsoft Graph API 并上传该项。 在请求中,指定以前创建的外部连接的 ID、要上传的项的 ID 以及完整项的内容。
保存所做的更改。
添加内容加载命令
在测试代码之前,需要使用调用内容加载逻辑的命令扩展控制台应用程序。
在代码编辑器中:
打开 Program.cs 文件。
使用以下代码添加新命令以加载内容:
var loadContentCommand = new Command("load-content", "Loads content into the external connection"); loadContentCommand.SetHandler(ContentService.LoadContent);
使用以下代码将新定义的命令注册到根命令,以便可以调用它:
rootCommand.AddCommand(loadContentCommand);
保存所做的更改。
测试代码
最后一件事是测试 Microsoft Graph 连接器是否正确导入外部内容。
- 打开终端。
- 将工作目录更改为项目。
- 通过运行
dotnet build
命令生成项目。 - 通过运行
dotnet run -- load-content
命令开始加载内容。 - 等待命令完成并加载内容。