<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nurullah Türkoğlu</title>
    <description>The latest articles on DEV Community by Nurullah Türkoğlu (@nturkoglu).</description>
    <link>https://dev.to/nturkoglu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4190885%2F36d497f2-6d98-4456-8ff9-48f9c26b9e3b.jpg</url>
      <title>DEV Community: Nurullah Türkoğlu</title>
      <link>https://dev.to/nturkoglu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nturkoglu"/>
    <language>en</language>
    <item>
      <title>What I learned building an xAPI 2.0 library and LRS for .NET</title>
      <dc:creator>Nurullah Türkoğlu</dc:creator>
      <pubDate>Sat, 17 Oct 2026 17:06:09 +0000</pubDate>
      <link>https://dev.to/nturkoglu/what-i-learned-building-an-xapi-20-library-and-lrs-for-net-o20</link>
      <guid>https://dev.to/nturkoglu/what-i-learned-building-an-xapi-20-library-and-lrs-for-net-o20</guid>
      <description>&lt;p&gt;xAPI (the Experience API, now IEEE 9274.1.1) is how e-learning content reports what a learner did: "Ada completed Fire Safety, score 0.8" as a JSON statement sent to a Learning Record Store (LRS). The .NET library most projects use for it, &lt;a href="https://github.com/RusticiSoftware/TinCan.NET" rel="noopener noreferrer"&gt;TinCan.NET&lt;/a&gt;, last changed in 2018. It targets &lt;code&gt;net35&lt;/code&gt;, its calls block, and it has no xAPI 2.0 support.&lt;/p&gt;

&lt;p&gt;I wrote xapi-dotnet to fill that gap: statement models and validation, an async client with a durable outbox, and an LRS you can embed in an ASP.NET Core app with EF Core on SQLite or PostgreSQL. Version 0.2.0 is on NuGet. Most of what I ran into on the way isn't specific to xAPI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The conformance suite's exit code wraps at 256
&lt;/h2&gt;

&lt;p&gt;ADL published a conformance test suite for LRSs: a Node.js project that runs 1,365 tests for xAPI 1.0.3 and 1,435 for 2.0.0 against a live endpoint. It is the only shared, executable measure of an LRS, so I put it in CI from the start, first against &lt;a href="https://github.com/yetanalytics/lrsql" rel="noopener noreferrer"&gt;lrsql&lt;/a&gt; (a reference LRS that passes everything) to check the pipeline, then against my own.&lt;/p&gt;

&lt;p&gt;The suite's exit code is the number of failed tests. Exit codes are one byte, so 256 failures exit with 0. Tests that never ran are not counted at all, and a wrong password in the setup hook shows up as one failure and 1,434 tests that never ran.&lt;/p&gt;

&lt;p&gt;So CI ignores the exit code and reads the suite's JSON log instead. A small .NET file-based app fails the job if the log is for the wrong battery, if the run did not finish, if fewer tests exist than expected, if any test never ran, or if fewer tests passed than a floor stored in the repo. The floor is a ratchet: it only goes up, in the pull request that makes more tests pass. Upstream has no lockfile, so the repo keeps its own and installs the suite with &lt;code&gt;npm ci&lt;/code&gt; on Node 22.&lt;/p&gt;

&lt;p&gt;The LRS now passes all 1,365 and all 1,435 tests, on both databases, in four CI jobs on every pull request. One failing or skipped test fails the build.&lt;/p&gt;

&lt;p&gt;In two places the suite expects more than IEEE 9274.1.1 requires. It still rejects &lt;code&gt;context.revision&lt;/code&gt; and &lt;code&gt;context.platform&lt;/code&gt; on statements whose object is not an Activity, a 1.0.3 rule that 2.0 dropped (&lt;a href="https://github.com/adlnet/lrs-conformance-test-suite/issues/278" rel="noopener noreferrer"&gt;suite issue #278&lt;/a&gt;), and it expects a 400 for a statement &lt;code&gt;version&lt;/code&gt; of &lt;code&gt;1.1.0&lt;/code&gt;. I followed the suite in both cases and wrote down why in the &lt;a href="https://nurullahturkoglu.github.io/xapi-dotnet/conformance.html" rel="noopener noreferrer"&gt;conformance docs&lt;/a&gt;. Otherwise those tests would fail on purpose and need an allowlist, and the allowlists are empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading JSON by hand
&lt;/h2&gt;

&lt;p&gt;The statement models are read and written with &lt;code&gt;Utf8JsonReader&lt;/code&gt; and &lt;code&gt;Utf8JsonWriter&lt;/code&gt;, without reflection. The &lt;code&gt;net10.0&lt;/code&gt; builds had to work with Native AOT and trimming, and the &lt;code&gt;netstandard2.0&lt;/code&gt; builds had to survive Unity's IL2CPP code stripping. CI publishes a sample console client with Native AOT and fails on any warning.&lt;/p&gt;

&lt;p&gt;Validation also needs to see things a serializer usually smooths over: duplicate keys, unknown properties, &lt;code&gt;null&lt;/code&gt; where the standard forbids it, and property name casing. Errors should carry the JSON path of the bad value, for example &lt;code&gt;$.actor.mbox&lt;/code&gt;, and the rules differ between 1.0.3 and 2.0.0.&lt;/p&gt;

&lt;p&gt;The first version was correct and slow: 8.85 μs and 19.7 KB allocated to read a 1 KB statement. Most of that went into JSON paths built as strings for every property, a hash set per object to find duplicate keys, and property names turned into strings to look them up. Now paths are kept as segments and only formatted when there is an error, known property names are matched on their UTF-8 bytes, and duplicate checks use a bit mask. Reading the same statement takes 5.48 μs and 5.2 KB, about four times the cost of &lt;code&gt;JsonDocument.Parse&lt;/code&gt; over the same bytes, and most of what is left is the model itself. Strict mode, which validates, is 2 to 3% slower than lenient mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  An outbox that survives a crash
&lt;/h2&gt;

&lt;p&gt;Learners lose the network, and apps get suspended or closed in the middle of a lesson. A client that only sends while the network is up loses statements, so &lt;code&gt;Xapi.Client.Outbox&lt;/code&gt; puts a queue on disk in front of the client. &lt;code&gt;EnqueueAsync&lt;/code&gt; stores the statement and returns; a background loop delivers in order and in batches, retries with backoff, and keeps dead letters with the LRS's response.&lt;/p&gt;

&lt;p&gt;Delivery is at least once: a statement leaves the store only after the LRS has answered for it. Resends are safe because the outbox assigns the statement id (a UUIDv7) before storing it. A statement sent twice reaches the LRS with the same id, and a conforming LRS keeps the first copy and answers the second with success or 409. On a 409 the outbox fetches the stored statement and compares it under the standard's equivalence rules, and only treats it as delivered if they match.&lt;/p&gt;

&lt;p&gt;The file store flushes its journal to disk before &lt;code&gt;EnqueueAsync&lt;/code&gt; returns, and that turned out to be almost all of the cost: 4.7 ms per enqueue on my M1 Pro. On macOS, .NET implements &lt;code&gt;FileStream.Flush(true)&lt;/code&gt; with &lt;code&gt;fcntl(F_FULLFSYNC)&lt;/code&gt;, which asks the SSD to empty its write cache. A plain &lt;code&gt;fsync&lt;/code&gt; on the same machine took 52 μs. One outbox therefore stores about 200 statements per second on macOS. I kept the flush and documented the cost. Code that produces statements faster than that should not await each enqueue on a hot path.&lt;/p&gt;

&lt;h2&gt;
  
  
  SERIALIZABLE on PostgreSQL, and what it costs
&lt;/h2&gt;

&lt;p&gt;Statement writes touch shared rows: the activities and agents that many statements mention. On PostgreSQL the store runs those writes in &lt;code&gt;SERIALIZABLE&lt;/code&gt; transactions and retries on serialization failures (SQLSTATE 40001).&lt;/p&gt;

&lt;p&gt;With 512 single-statement POSTs from 8 concurrent writers, the store retried about 0.11 times per statement, every request still succeeded, and throughput was about three times that of a single writer. In a one-off comparison, the same load at &lt;code&gt;READ COMMITTED&lt;/code&gt; stored 1.4 to 1.9 times as many statements per second with no retries. It would also let through write skews on that path, so &lt;code&gt;SERIALIZABLE&lt;/code&gt; stays. SQLite takes the lock up front with &lt;code&gt;BEGIN IMMEDIATE&lt;/code&gt;, so writers queue and throughput stays flat at about 640 to 700 statements per second, with no retries.&lt;/p&gt;

&lt;p&gt;The benchmark numbers come from one noisy laptop run and are &lt;a href="https://nurullahturkoglu.github.io/xapi-dotnet/benchmarks.html" rel="noopener noreferrer"&gt;in the docs&lt;/a&gt; with the machine details. They say nothing about other LRS products, which I did not measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unity
&lt;/h2&gt;

&lt;p&gt;xAPI is also used from games and simulations built in Unity, and several of the open TinCan.NET issues are about Unity builds. The &lt;code&gt;netstandard2.0&lt;/code&gt; packages install with NuGetForUnity. Tests that parse, validate, sign, send and queue statements pass in the Unity 6.3 editor and in IL2CPP players on macOS and Linux with Managed Stripping Level High and no &lt;code&gt;link.xml&lt;/code&gt;. The Linux runs happen in CI on every change.&lt;/p&gt;

&lt;p&gt;WebGL took more work. &lt;code&gt;HttpClientHandler&lt;/code&gt; can't open connections in a browser, so a small Unity package adds &lt;code&gt;UnityWebRequestHandler&lt;/code&gt;, an &lt;code&gt;HttpMessageHandler&lt;/code&gt; built on &lt;code&gt;UnityWebRequest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Even with that handler, the request went out and the response came back, but the &lt;code&gt;await&lt;/code&gt; never resumed. The completion callback runs on Unity's main thread, which has Unity's own &lt;code&gt;SynchronizationContext&lt;/code&gt;. .NET runs a &lt;code&gt;ConfigureAwait(false)&lt;/code&gt; continuation inline only when the thread has no custom context, and queues it to the thread pool otherwise. A WebGL player has no thread pool, so the continuation never ran. The handler now completes its task with the context cleared for that moment. Unity's async file reads also go through the thread pool, so the &lt;code&gt;netstandard2.0&lt;/code&gt; file outbox now reads its journal synchronously.&lt;/p&gt;

&lt;p&gt;With those two changes the tests pass in a WebGL build running in headless Chromium. That run is local for now; CI covers the editor and the Linux player. Android and iOS are still untested. The &lt;a href="https://nurullahturkoglu.github.io/xapi-dotnet/unity.html" rel="noopener noreferrer"&gt;Unity guide&lt;/a&gt; has the setup and the test results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Xapi.Client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Xapi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Xapi.Client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;XapiClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;XapiClientOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://lrs.example.com/xapi/"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Credentials&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;XapiCredentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Basic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveStatementAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"learner@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Ada Learner"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Verbs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Iri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/courses/fire-safety"&lt;/span&gt;&lt;span class="p"&gt;))));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The LRS also ships as a container image:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;LrsHost__Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-key &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;LrsHost__Secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-secret &lt;span class="nt"&gt;-v&lt;/span&gt; xapi-lrs-data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/nurullahturkoglu/xapi-lrs:0.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The license is Apache-2.0. The version is still 0.x, so the API can change before 1.0. The code and the design decisions are on GitHub, and the docs include a &lt;a href="https://nurullahturkoglu.github.io/xapi-dotnet/migrating-from-tincan.html" rel="noopener noreferrer"&gt;migration guide for TinCan.NET users&lt;/a&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nurullahturkoglu" rel="noopener noreferrer"&gt;
        nurullahturkoglu
      &lt;/a&gt; / &lt;a href="https://github.com/nurullahturkoglu/xapi-dotnet" rel="noopener noreferrer"&gt;
        xapi-dotnet
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Experience API (xAPI 2.0 / IEEE 9274.1.1 and 1.0.3) for .NET: models, validation, an LRS client with a durable outbox, and an embeddable LRS
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;xapi-dotnet&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/nurullahturkoglu/xapi-dotnet/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/nurullahturkoglu/xapi-dotnet/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://github.com/nurullahturkoglu/xapi-dotnet/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/798509b4df525f56802b56f8096862487f08023e3d7561c68656f8dab10d0d6e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4170616368652d2d322e302d626c75652e737667" alt="License: Apache-2.0"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Experience API (xAPI 2.0 / IEEE 9274.1.1, and 1.0.3) for .NET: models and validation, an async LRS client
with a durable outbox, and a Learning Record Store you can embed in an ASP.NET Core app.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Status: 0.x.&lt;/strong&gt; &lt;code&gt;0.2.0&lt;/code&gt; is on &lt;a href="https://www.nuget.org/packages?q=Xapi." rel="nofollow noopener noreferrer"&gt;NuGet&lt;/a&gt; and
the LRS image is on &lt;a href="https://github.com/nurullahturkoglu/xapi-dotnet/pkgs/container/xapi-lrs" rel="noopener noreferrer"&gt;ghcr.io&lt;/a&gt;. The API can
change between minor versions until 1.0; the &lt;a href="https://github.com/nurullahturkoglu/xapi-dotnet/milestone/2" rel="noopener noreferrer"&gt;1.0 milestone&lt;/a&gt; lists
what is left. Documentation: &lt;a href="https://nurullahturkoglu.github.io/xapi-dotnet/" rel="nofollow noopener noreferrer"&gt;https://nurullahturkoglu.github.io/xapi-dotnet/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The 2.0 standard is IEEE 9274.1.1-2023, also adopted as ISO/IEC/IEEE 39274-1-1:2025.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Packages&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Targets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Xapi.Core&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Statement model, strict JSON reading and writing for 1.0.3 and 2.0.0, validation, statement comparison&lt;/td&gt;
&lt;td&gt;netstandard2.0, net10.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Xapi.Client&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Async client for every LRS resource, version negotiation, attachments, statement signing&lt;/td&gt;
&lt;td&gt;netstandard2.0, net10.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Xapi.Client.Outbox&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Durable outbox: retries, error classification, dead letters, idempotent resend&lt;/td&gt;
&lt;td&gt;netstandard2.0, net10.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Xapi.Lrs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AddXapiLrs&lt;/code&gt; / &lt;code&gt;MapXapiLrs&lt;/code&gt;: an LRS inside your ASP.NET Core app&lt;/td&gt;
&lt;td&gt;net10.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Xapi.Lrs.Sqlite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SQLite storage and migrations for &lt;code&gt;Xapi.Lrs&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;net10.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Xapi.Lrs.PostgreSql&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nurullahturkoglu/xapi-dotnet" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;If you send xAPI statements from .NET today, I'd like to hear what you use and what is missing.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>aspnetcore</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
