65.9K
CodeProject 正在变化。 阅读更多。
Home

运行中的语义数据库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (13投票s)

2014 年 11 月 3 日

CPOL

16分钟阅读

viewsIcon

27434

一个 Feed Reader 用例,部分演示了语义数据库的工作原理以及 Higher Order Programming Environment IDE。

观看视频!

视频 是了解本文所述流程的绝佳方式。它相当粗糙,所以也许我以后会重做它,使其更专业一些。出于某种原因,音频有点低,所以你需要调高音量设置。

获取代码!

git clone https://github.com/cliftonm/HOPE.git
git checkout -b feed-reader-semantic-database

引言

这是关于语义数据库的文章系列第二部分(第一部分在此)。在本文中,我们将再次讨论 Feed Reader 的概念(我之前写过关于它与自然语言处理相关的文章),但这次,我们将重点关注使用我在第一部分中写过的语义数据库来持久化和查询语义信息。

本文的格式基本上将是一个基于文本的视频教程(我还没做),但会包含一些额外的“幕后”代码示例。这种模式在文章中重复出现,以说明每个步骤中接收器中正在发生的事情。

假设您熟悉 HOPE 概念。如果不熟悉,请访问HOPE 网站获取教程、Code Project 文章链接、白皮书等。

创建一个 Feed Reader Applet

本教程的每个步骤都有一个相应的 applet 文件,因此您可以加载 applet 并只玩那个步骤中的行为。

第一步:基础先行

创建一个基本的 Feed Reader applet 只需要两个接收器

  • Feed Reader
  • Carrier List Viewer

在 HOPE IDE 中,选择 Carrier List Viewer 和 Feed Reader 接收器,然后点击 Add 按钮。

这会在 HOPE 表面上创建两个接收器,以及一个未初始化的查看器。

双击 Carrier List Viewer 会弹出其配置 UI。

为窗口名称输入“Code Project”,然后选择 RSSFeedItem 语义类型,因为我们要显示 Feed Reader 发出的 Feed 项。保存配置后,请注意列表查看器的列现在已根据选定的语义类型进行配置。

还要注意,表面现在显示两个接收器正在相互交互。从 View 菜单中,我们将选择“Show Semantics”来查看协议是什么,从而揭示

我们想禁用 Carrier List Viewer 发出的 RSSFeedUrl 信号。为什么?因为稍后,当我们通过双击 Feed 项在浏览器页面中显示它时,我们不希望 Feed URL 被发送到 Feed Reader,这会重新触发读取操作(Feed Reader 可以接收此语义结构,也称为协议,作为以编程方式触发 Feed Reader 的一种方式)。

要禁用此协议,请右键单击 Carrier List Viewer 并取消选中 protocl RSSFeedUrl。

我们现在看到 Feed Reader 不再接收此协议。

现在让我们配置 Feed Reader 接收器。双击它并输入 Code Project 文章 Feed 的 URL(https://codeproject.org.cn/WebServices/ArticleRSS.aspx)。

点击 Save,并注意,几秒钟后,列表视图会用 Feed 中的项填充。您会看到发送到列表查看器的项显示为小红三角。

并观察列表查看器中的项。

此 applet 保存为“Step 1 - The Basics”,每当您将其加载到 HOPE IDE 中时,它都会重新获取 Feed 并显示文章。

当您处理语义结构时,您将获得在数据呈现方式上的巨大灵活性。最简单的选项之一是检查特定语义结构的子类型。例如,我们可以配置 Carrier List Viewer 来响应 RSSFeedItem 语义结构中的任何其他协议。如果我们只想查看 RSS 标题,它是 RSSFeedItem 所组成的语义结构之一,只需将语义类型更改为“RSSFeedTitle”。

 

现在,当我们接收 Feed 时,我们只看到它们的标题。

稍后当我们创建自定义本体查询时,我们将利用此功能(一个比“关系”或“表连接”更花哨的词,在我看来,当处理语义数据库时更准确,而且,我喜欢创建 buzzword bingo 术语的程度不亚于任何人)。

幕后

Feed Reader

Feed Reader 异步读取 Feed,返回一个 SyndicationFeed 对象。

protected async Task<SyndicationFeed> GetFeedAsync(string feedUrl)
{
  CreateCarrier("LoggerMessage", signal => 
  {
    signal.TextMessage.Text.Value = "Acquiring feed " + feedUrl + ".";
    signal.MessageTime = DateTime.Now;
  });

  SyndicationFeed feed = await Task.Run(() =>
  {
    // To handle this error:
    // For security reasons DTD is prohibited in this XML document. To enable DTD processing set the DtdProcessing property on XmlReaderSettings to Parse and pass the settings into XmlReader.Create method.
    XmlReaderSettings settings = new XmlReaderSettings();
    settings.XmlResolver = null;
    settings.DtdProcessing = DtdProcessing.Ignore;

    XmlReader xr = XmlReader.Create(feedUrl);
    SyndicationFeed sfeed = SyndicationFeed.Load(xr);
    xr.Close();

    return sfeed;
  });

  CreateCarrier("LoggerMessage", signal =>
  {
    signal.TextMessage.Text.Value = "Feed " + feedUrl + " has " + feed.Items.Count().ToString() + " items.";
    signal.MessageTime = DateTime.Now;
  });

  return feed;
}

您还会注意到一些日志消息。一旦读取器获取了项,它就会创建带有 RSSFeedItem 协议的载体,并用 syndication feed items 填充信号。

protected void EmitFeedItems(SyndicationFeed feed, int maxItems = Int32.MaxValue)
{
  // Allow -1 to also represent max items.
  int max = (maxItems == -1 ? feed.Items.Count() : maxItems);
  max = Math.Min(max, feed.Items.Count()); // Which ever is less.

  feed.Items.ForEachWithIndexOrUntil((item, idx) =>
  {
    CreateCarrier("RSSFeedItem", signal =>
    {
      signal.RSSFeedName.Name.Text.Value = FeedName;
      signal.RSSFeedTitle.Title.Text.Value = item.Title.Text;
      signal.RSSFeedUrl.Url.Value = item.Links[0].Uri.ToString();
      signal.RSSFeedDescription.Description.Text.Value = item.Summary.Text;
      signal.RSSFeedAuthors.Text.Value = String.Join(", ", item.Authors.Select(a => a.Name));
      signal.RSSFeedCategories.Text.Value = String.Join(", ", item.Categories.Select(c => c.Name));
      signal.RSSFeedPubDate.Value = item.PublishDate.LocalDateTime;
      });
  }, ((item, idx) => idx >= max));
}

Carrier List Viewer

在接收端,Carrier List Viewer 正在监听我们之前配置的协议。

protected void ConfigureBasedOnSelectedProtocol()
{
  ProtocolName = cbProtocols.SelectedValue.ToString();
  CreateViewerTable();
  ListenForProtocol();
  UpdateCaption();
}

这里的工作马函数是 `CreateViewerTable`,它查询语义结构(深入到子类型)并根据模式中设置的序数属性值来显示列。列名使用别名,同样由模式中的 Alias 属性确定。

protected virtual void CreateViewerTable()
{
  if (!String.IsNullOrEmpty(ProtocolName))
  {
    DataTable dt = new DataTable();
    List<IFullyQualifiedNativeType> columns = rsys.SemanticTypeSystem.GetFullyQualifiedNativeTypes(ProtocolName).OrderBy(fqn=>fqn.Ordinality).ToList();
uniqueKey.Clear();

    columns.ForEach(col =>
    {
      try
      {
        DataColumn dc = new DataColumn(col.FullyQualifiedName, col.NativeType.GetImplementingType(rsys.SemanticTypeSystem));

        // If no alias, then use the FQN, skipping the root protocol name.
        String.IsNullOrEmpty(col.Alias).Then(() => dc.Caption = col.FullyQualifiedName.RightOf('.')).Else(() => dc.Caption = col.Alias);
        dt.Columns.Add(dc);
        col.UniqueField.Then(() => uniqueKey.Add(col));
      }
      catch
      {
        // If the implementing type is not known by the native type system (for example, List<dynamic> used in the WeatherInfo protocol, we ignore it.
        // TODO: We need a way to support implementing lists and displaying them in the viewer as a sub-collection.
        // WeatherInfo protocol is a good example.
      }
    });

    dvSignals = new DataView(dt);
    dgvSignals.DataSource = dvSignals;

    foreach(DataColumn dc in dt.Columns)
    {
      dgvSignals.Columns[dc.ColumnName].HeaderText = dc.Caption;
    }
  }
}    

当 Carrier List Viewer 收到指定语义结构(也称为协议)的信号时,它会填充网格中的一行,只要该行不是重复项。重复项的确定方式是语义结构是否将结构、元素和/或本机类型标记为唯一(请参阅关于语义数据库的文章以了解讨论)。

protected void ShowSignal(dynamic signal)
{
  form.IfNull(() => ReinitializeUI());
  List<IFullyQualifiedNativeType> colValues = rsys.SemanticTypeSystem.GetFullyQualifiedNativeTypeValues(signal, ProtocolName);

  if (!RowExists(colValues))
  {
    try
    {
      DataTable dt = dvSignals.Table;
      DataRow row = dt.NewRow();
      colValues.ForEach(cv =>
      {
        try
        {
          row[cv.FullyQualifiedName] = cv.Value;
        }
        catch
        {
          // Ignore columns we can't handle.
          // TODO: Fix this at some point. WeatherInfo protocol is a good example.
        }
      });
    dt.Rows.Add(row);
    }
    catch (Exception ex)
    {
      EmitException(ex);
    }
  }
}

第二步:在浏览器中查看 Feed 项

显然,我们想要查看我们感兴趣的 Feed 项。默认情况下,当我们双击 Carrier List Viewer 时,它会向任何监听这些协议的接收器发出协议和子协议。在我们的例子中,让我们使用一个网页查看器,它是一个基于 .NET 的 `WebBrowser` 控件的简单查看器。首先,我们通过选择 Web Page Viewer 接收器并点击 Add 来将其添加到表面。

请注意,“Url”子协议正被我们的新接收器从 Carrier List Viewer 和 Feed Reader 接收。

我们不希望 Web Page Viewer 显示 Feed Reader 作为 RSSFeedItem 协议一部分发出的每个页面,因此我们将右键单击 Feed Reader 并禁用 Url 语义类型。

我们现在看到所需的配置。

最后,当我们双击列表中的一个项时,我们会得到一个带有项内容的浏览器窗口。

HOPE 系统的关键方面之一是接收器是轻量级组件。例如,我们不必使用 Web Page Viewer 接收器,而是可以使用 Web Page Launcher 接收器,它会在浏览器中以标签页的形式启动页面。正是这种面向组件的(另一个 buzzword!)开发方法,加上数据的完整语义,使得构建应用程序如此容易。

幕后

Carrier List Viewer Receptor

当我们在查看器中双击一个项时会发生什么?基本上,情况相反,信号会从选定的行重建。

protected virtual void OnCellContentDoubleClick(object sender, DataGridViewCellEventArgs e)
{
  ISemanticTypeStruct st = rsys.SemanticTypeSystem.GetSemanticTypeStruct(ProtocolName);
  dynamic outsignal = rsys.SemanticTypeSystem.Create(ProtocolName);
  List<IFullyQualifiedNativeType> ntList = rsys.SemanticTypeSystem.GetFullyQualifiedNativeTypes(ProtocolName);

  ntList.ForEach(nt =>
  {
    // Store the value into the signal using the FQN.
    string colName = nt.FullyQualifiedName;

    // Columns that can't be mapped to native types directly (like lists) are not part of the data table.
    if (dgvSignals.Columns.Contains(colName))
    {
      rsys.SemanticTypeSystem.SetFullyQualifiedNativeTypeValue(outsignal, nt.FullyQualifiedNameSansRoot, dvSignals[e.RowIndex][colName]);
    }
  });

  // Send the record on its way.
  rsys.CreateCarrier(this, st, outsignal);
}

Web Pager Viewer Receptor

这里的实现很简单,所以我将向您展示整个类。

public class WebBrowserReceptor : WindowedBaseReceptor
{
  protected WebBrowser browser;

  public override string Name { get { return "Web Page Viewer"; } }
  public override bool IsEdgeReceptor { get { return true; } }

  public WebBrowserReceptor(IReceptorSystem rsys)
    : base("webPageViewer.xml", false, rsys)
  {
    AddReceiveProtocol("Url", (Action<dynamic>)(signal => ShowPage(signal.Value)));
  }

  protected void ShowPage(string url)
  {
    form.IfNull(() => ReinitializeUI());
    browser.Navigate(new Uri(url));
  }

  protected override void InitializeUI()
  {
    base.InitializeUI();
    browser = (WebBrowser)mycroParser.ObjectCollection["browser"];
  }
}

请注意,它监听“Url”协议,并在接收时导航浏览器到该页面,如果表单不存在则创建它。唯一的“复杂性”是 UI 的后端 XML 代码。

<?xml version="1.0" encoding="utf-8" ?>
<MycroXaml Name="Form"
  xmlns:wf="System.Windows.Forms, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
  xmlns:def="def"
  xmlns:ref="ref">
  <wf:Form Text="Web Page Viewer" Size="400, 300" StartPosition="CenterScreen" ShowInTaskbar="false">
    <wf:Controls>
      <wf:WebBrowser def:Name="browser" Dock="Fill"/>
    </wf:Controls>
  </wf:Form>
</MycroXaml>

第三步:更多 Feed Reader

我们将添加更多 Feed Reader。

Gigaom: https://gigaom.com/feed/

Wired: http://feeds.wired.com/wired/index

InfoWorld: http://www.infoworld.com/index.rss

Ars Technica: http://feeds.arstechnica.com/arstechnica/technology-lab?format=xml

在每个新的 Feed Reader 中,我们都会禁用 URL 和 RSSFeedUrl 协议的发出,否则它们将分别被 Web Page Viewer 和 Feed Reader 接收(试着快速说出“receptors respectively”五次)。您可以按您喜欢的方式排列接收器。

并享受所有最新项的列表。

第四步:刷新 Feed

Feed Reader 接收器在某个固定间隔重新获取 Feed 会很好。幸运的是,Feed Reader 接收器将响应 RSSFeedRefresh 协议。我们可以通过添加 Interval Timer 接收器来定时发出此协议。

并配置为每 30 分钟发出一次 RSSFeedRefresh。

一旦指定了协议,我们就可以观察 Interval Timer 接收器如何将此协议发出到 Feed Reader 接收器。UI 还显示倒计时。

幕后

Feed Reader Receptor

RSSFeedRefresh 协议是一个没有结构的语义结构!它只是一个协议——没有子类型。

在 Feed Reader 接收器中,接收此协议

AddReceiveProtocol("RSSFeedRefresh", (Action<dynamic>)(s => AcquireFeed(lastFeedUrl)));

导致 Feed 被重新获取。

我们不在 Feed Reader 接收器中实现自动刷新是因为我们想保持关注点分离。为什么 Feed Reader 接收器需要知道在指定间隔重新获取 Feed 的事情?这会将接收器置于困境,因为可能有多种不同的方式触发重新获取 Feed,而与定时器无关!

Interval Timer Receptor

此接收器也非常简单——它配置一个 Timer 每秒触发一次(以便 UI 可以更新),当经过的时间发生时,它会用指定的协议触发载体。请注意,信号没有值。

protected void FireEvent(object sender, EventArgs e)
{
  if (triggerTime <= DateTime.Now)
  {
    triggerTime = DateTime.Now + new TimeSpan(days, hours, minutes, seconds);

    if (Enabled)
    {
      CreateCarrierIfReceiver(ProtocolName, signal => { });
    }
  }

  TimeSpan ts = triggerTime - DateTime.Now;

  Subname = String.Format("{0}:{1}:{2:D2}:{3:D2}", ts.Days, ts.Hours, ts.Minutes, ts.Seconds);
}

第五步:添加持久化

到目前为止都很好,但我们现在需要切入正题,即添加持久化。因此,我们将用 Semantic Database 接收器替换 Carrier List Viewer 接收器,而不是显示 Feed 项,并移除 Web Page Viewer。将 Carrier List Viewer 和 Web Page Viewer 接收器拖出表面,它们将被移除。

接下来,添加 Semantic Database 接收器。

一旦接收器出现在表面上,我们就可以配置它要持久化哪些协议(语义结构)。我们唯一感兴趣的语义结构是“RSSFeedItem”,因此我们将其配置为持久化该语义类型。

完成后,我们注意到 Feed Reader 接收器正在向 Semantic Database 接收器发出 RSSFeedItem。

幕后

这里没有“幕后”。我们没有在 Feed Reader 接收器中编写任何代码来与数据库通信,信号现在被我们正在监听 Feed Reader 发出的协议的接收器(在这种情况下是 Semantic Database)持久化。换句话说,我们可以通过监听接收器发出的协议,在应用程序的任何地方添加持久化。

此外,Semantic Database 接收器会自动检查监听的协议是否存在支持表,如果不存在,它会创建必要的表。

当然,说幕后没什么事有点撒谎——Semantic Database 接收器中有很多事情在发生,我在前一篇文章中已经描述了。

第六步:添加一些日志记录

让我们添加一些日志记录功能。Semantic Database 发出它生成的 SQL 语句,我们想查看它们,所以我们将添加一个 Carrier List Viewer 接收器和一个 Text Display 接收器。

结果(在将 Carrier List Viewer 配置为接收 LoggerMessage 协议后)有点混乱,因为几乎每个接收器都有一些日志输出。

与其关闭所有 Feed Reader 中的选择性协议,不如将 Carrier List Viewer 和 Semantic Database 放入它们自己的膜中。

现在,我们只需关闭 Semantic Database 中的 Text 协议,然后双击膜中的任何位置,就可以将 RSSFeedItem 启用为可以渗透到膜中的协议。

得到所需的配置。

现在我们只需再等 5 分钟让信号刷新,看看日志文件显示数据库在做什么。在此期间,让我们看看日志,看看 Semantic Database 对 SQLite 数据库做了什么(使用 SQLite 很好,因为您可以简单地删除整个文件并从头开始)。我们首先注意到它创建了 RSSFeedItem 结构所需的各种表。

并且我们确实使用 SQLite Database Browser 看到这些表。

接下来,一旦 Feed 重新获取(Semantic Database 的信号是它在检查数据库模式与语义模式的匹配度)。

我们看到(只出现一次,数据库创建时,因为在后续重新获取中,大部分是重复数据)很多唯一性检查和很多插入语句。

您可以看到 RSSFeedItem 的语义结构正在构建。这有很多事务,因为 RSSFeedItem 结构相当深,而且我们必须测试每个记录的唯一性,因为我们知道 Feed Reader 在很多时候会重新获取相同的 Feed 项。

幸运的是,我们不必编写这些语句,不必创建复杂的 ORM 并填充对象,不必事先弄清楚数据模型——Semantic Database 负责自动化所有这些过程。

当然,我们都知道有时自动化无法让您达到最后的 1%,但幸运的是,在本教程中我们没有遇到任何这些问题。

最后一点——我们可以双击日志中的一行项目,以便更轻松地查看 SQL 语句。

幕后

我们已经看到了在一个 Carrier List Viewer 中选择一个项如何发出一个协议。由于 LoggerMessage 语义结构包含一个 Text 语义结构,监听此协议的 Text Viewer 接收器将做出响应。其核心行为的实现非常简单。

public ReceptorDefinition(IReceptorSystem rsys) : base(rsys)
{
  AddReceiveProtocol("Text", (Action<dynamic>)(signal =>
  {
    form.IfNull(() =>
    {
      InitializeViewer();
      UpdateFormLocationAndSize();
    });

    string text = signal.Value;

    if (!String.IsNullOrEmpty(text))
    {
      tb.AppendText(text.StripHtml());
      tb.AppendText("\r\n");
    }
  }));
}

第七步:查询数据库并使用真正的 Feed Reader 控件

让我们将我们所做的工作包装在一个外部膜中,创建一个“计算岛”,因为我们不希望与下一步有任何交互。为此,我们左键单击并拖动以包围所有接收器和膜,结果如下。

现在我们可以将这个“计算岛”放在一边,然后使用 Feed List Viewer 接收器来显示 Feed 项,Feed List Viewer 是 Carrier List Viewer 的派生。我们将开始添加几个接收器。

  • Feed List Viewer
  • Semantic Database
  • Signal Creator
  • Interval Timer
  • Web Page Launcher

结果是

请注意新的对话框和未配置的接收器。但也要注意,一旦我们分离了接收器,一些事情已经发生了。

我们看到 Feed List 接收器正在向 Semantic Database 发出 Query。它立即执行此操作,并且由于它正在查询书签类别,因此正在返回响应信号。我们还注意到,“Url”协议正在被 Web Page Launcher 接收器接收(这是 RSSFeedBookmark 协议的一个子类型,因此被 Web Page Launcher 接收器“看到”。要停止此操作(否则它实际上会因为返回包含此子类型的查询数据而启动网页),我们将右键单击 Semantic Database 接收器并禁用发出的“Url”协议(这与我们之前禁用协议的方式类似,所以这里不再展示)。

此外,我们“知道”我们希望将此语义数据库实例配置为持久化以下语义结构。

  • UrlVisited
  • RSSFeedItemDisplayed
  • RssFeedBookmark

因为 Feed List Viewer 会在我们与 UI 交互时发出这些协议。我们双击 Semantic Database 接收器并添加这些协议。

我们现在在 Feed Item List 接收器和 Semantic Database Receptor 之间的交互中看到这些协议。

接下来,让我们添加一个初始查询来填充显示。我们双击 Signal Creator 并添加以下内容。

注意查询以及我们如何“连接”多个语义结构。

Semantic Database 从结构本身推断出连接,创建了这个漂亮的查询。

完成此操作后,Feed Item List 接收器会填充我们在摆弄 Feed Reader 持久化时获取的数据。

我们还注意到 Query 协议正从 Signal Creator 接收器发送到 Semantic Database 接收器。

最后一步是连接 Interval Timer 接收器。Signal Creator 监听一个“Resend”协议(同样没有实际的值类型)。在这里,我们希望每 10 分钟重新查询一次数据库,这样每 30 分钟获取的新项就能在 10 分钟内显示出来。我们双击 Interval Timer 接收器,并像以前一样进行配置。

现在我们有了一个新的计算岛(我已经将其包装在一个膜中),它可以更新我们的 Feed 显示。

我们完整的 Feed Reader Applet 现在有两个“计算岛”,一个用于显示 Feed,另一个用于读取和持久化新 Feed 项。

行为上,UI 将显示

  • 新 Feed,背景为白色。
  • 旧 Feed,背景为蓝色。
  • 已访问 Feed,背景为绿色。

 

(顺便说一下,前两项虽然看起来相同,但并非如此——描述已更改,但 URL 没有。由于 URL 相同,查看器显示两项都已访问,即使我只访问了一个。)

您可以为 Feed 添加书签,提供类别并添加注释(在查看已添加书签的项目时,注释会显示在注释字段中)。

您可以按类别查询特定的已添加书签的项目,然后点击 Show 按钮。

幕后

当您选择一个书签类别并点击 Show 时,一件有趣的事情是发生的事情。在内部,Feed Item List 接收器分派一个 Query 协议。

protected void ShowItemInCategory(object sender, EventArgs args)
{
  ClearGrid();
  string categoryName = ((ComboBox)form.Controls.Find("cbCategories", false)[0]).SelectedItem.ToString();
  CreateCarrierIfReceiver("Query", signal =>
  {
    signal.QueryText = "RSSFeedBookmark, RSSFeedItem, UrlVisited, RSSFeedItemDisplayed where [BookmarkCategory] = @0 order by RSSFeedPubDate desc, RSSFeedName";
    signal.Param0 = categoryName;
  });
}

关于 Semantic Database,第一个语义结构被认为是“根”结构——所有其他结构都与它或其他结构进行左连接。因此,要获取*仅*已添加书签的 RSSFeedItem 记录,我们从 RSSFeedBookmark 结构开始,作为查询中的第一个语义类型。

这是生成的查询。

在上一篇关于 Semantic Database 的文章中,我描述了多结构查询如何在运行时创建一个新的语义类型。上述查询之所以如此复杂,是因为它返回了结构的整个语义图,它看起来像这样。

绿色项是正在被专门查询的语义结构。

因为上面的语义图包括 RSSFeedItem,所以由于 Feed Item List 接收器监听此协议,因此会发出此子元素。但此接收器也监听 BookmarkCategory 协议,因此当收到此协议时,我们可以做一些有趣的事情来将书签注释与 URL 相关联。

protected void AssociateBookmarkNote(ICarrier carrier)
{
  string url = carrier.ParentCarrier.Signal.RSSFeedUrl.Url.Value;
  string note = carrier.ParentCarrier.Signal.BookmarkNote.Note.Text.Value;
  urlNote[url] = note ?? "";
}

注意(哈哈)我们如何导航到 RSSFeedBookmark 语义结构,然后获取注释和 Feed URL。这特别有趣,因为它说明了 Semantic Database 如何返回与语义模式定义的信号完全相同的语义结构。因此,我们完全消除了数据库模型和代码表示之间的“阻抗不匹配”,因为两者都精确地镜像了相同的语义结构。

结论

这绝对不是主流开发路径。尽管如此,我希望(双关语)它能激发想象力!感谢您的阅读或观看视频,或两者兼有。HOPE 将回归!

© . All rights reserved.