练习 - 导入外部内容

已完成

在本练习中,你将使用用于将本地 markdown 文件导入到 Microsoft 365 的代码扩展自定义 Microsoft Graph 连接器。

准备工作

在执行本练习之前,请务必完成本模块中的上一个练习。

若要遵循本练习,请从 GitHub 复制本练习中使用的示例内容文件,并将其存储在项目中名为 content 的文件夹中。

代码编辑器的屏幕截图,其中显示了本练习中使用的内容文件。

若要使代码正常工作,必须将 内容 文件夹及其内容复制到生成输出文件夹。

在代码编辑器中:

  1. 打开 .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>
    
  2. 保存所做的更改。

添加库以分析 Markdown 和 YAML

要生成的 Microsoft Graph 连接器会将本地 Markdown 文件导入到 Microsoft 365。 其中每个文件都包含一个标头,其中包含 YAML 格式的元数据,也称为 frontmatter。 此外,每个文件的内容都写入 Markdown。 若要提取元数据并将正文转换为 HTML,请使用自定义库:

  1. 打开终端并将工作目录更改为项目。
  2. 若要添加 Markdown 处理库,请运行以下命令: dotnet add package Markdig
  3. 若要添加 YAML 处理库,请运行以下命令: dotnet add package YamlDotNet

定义类以表示导入的文件

为了简化使用导入的 markdown 文件及其内容,让我们定义一个具有必要属性的类。

在代码编辑器中:

  1. 打开 ContentService.cs 文件。

  2. 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 属性将属性映射到每个文档标题中的元数据。

  3. 保存所做的更改。

实现加载 Markdown 文件

下一步是实现加载本地 Markdown 文件、提取元数据并将内容转换为 HTML 的逻辑。

首先,添加帮助程序方法以轻松使用 MarkdigYamlDotNet 库。

在代码编辑器中:

  1. 创建名为 MarkdownExtensions.cs 的新文件。

  2. 在 文件中,添加以下代码:

    // 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 标头。 如果没有此配置,标头中的信息将被丢弃。

  3. 接下来,使用以下代码扩展 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 属性以供进一步处理。

  4. 保存所做的更改。

提取 markdown 和 YAML 内容

使用帮助程序方法后,实现 Extract 方法以加载本地 markdown 文件并从中提取信息。

在代码编辑器中:

  1. 打开 ContentService.cs 文件。

  2. 在文件顶部,添加以下 using 语句:

    using Markdig;
    
  3. 接下来,在 类中 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。 最后,它存储文件的相对路径,并将 对象添加到集合中以供进一步处理。

  4. 保存所做的更改。

将内容转换为外部项

读取外部内容后,下一步是将其转换为外部项,这些项将加载到 Microsoft 365。

首先,实现 方法, GetDocId 该方法基于其相对文件路径为每个外部项生成唯一 ID。

在代码编辑器中:

  1. 确认正在编辑 ContentService.cs 文件。

  2. ContentService在 类中,添加以下方法:

    static string GetDocId(Document doc)
    {
      var id = doc.RelativePath!
        .Replace(Path.DirectorySeparatorChar.ToString(), "__")
        .Replace(".md", "");
      return id;
    }
    

    GetDocId方法使用文档的相对文件路径,并将所有目录分隔符替换为双下划线。 这是必需的,因为不能在外部项 ID 中使用路径分隔符。

  3. 保存所做的更改。

现在,实现 Transform 方法,该方法将表示本地 markdown 文件的对象从 Microsoft Graph 转换为外部项。

在代码编辑器中:

  1. 确认你位于 ContentService.cs 文件中。

  2. 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。 最后,配置项目的权限,使其对组织中的每个人都可见。

  3. 保存所做的更改。

将外部项加载到 Microsoft 365

处理内容的最后一步是将转换的外部项加载到 Microsoft 365。

在代码编辑器中:

  1. 验证是否正在编辑 ContentService.cs 文件。

  2. 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 以及完整项的内容。

  3. 保存所做的更改。

添加内容加载命令

在测试代码之前,需要使用调用内容加载逻辑的命令扩展控制台应用程序。

在代码编辑器中:

  1. 打开 Program.cs 文件。

  2. 使用以下代码添加新命令以加载内容:

    var loadContentCommand = new Command("load-content", "Loads content   into the external connection");
    loadContentCommand.SetHandler(ContentService.LoadContent);
    
  3. 使用以下代码将新定义的命令注册到根命令,以便可以调用它:

    rootCommand.AddCommand(loadContentCommand);
    
  4. 保存所做的更改。

测试代码

最后一件事是测试 Microsoft Graph 连接器是否正确导入外部内容。

  1. 打开终端。
  2. 将工作目录更改为项目。
  3. 通过运行 dotnet build 命令生成项目。
  4. 通过运行 dotnet run -- load-content 命令开始加载内容。
  5. 等待命令完成并加载内容。