Getting started
This guide installs the packages, sends a statement to an LRS, and hosts an LRS inside an ASP.NET Core app. Each section links to the guide that covers its topic in full.
Install
| You want to | Package |
|---|---|
| Read, write or validate xAPI JSON | Xapi.Core |
| Send statements and documents to any LRS | Xapi.Client (brings Xapi.Core) |
| Queue statements on disk and deliver them when the LRS can be reached | Xapi.Client.Outbox (brings Xapi.Client) |
| Run an LRS in an ASP.NET Core app | Xapi.Lrs.Sqlite or Xapi.Lrs.PostgreSql (bring Xapi.Lrs) |
Xapi.Core, Xapi.Client and Xapi.Client.Outbox target netstandard2.0 (.NET Framework 4.6.2 and later) and
net10.0. The LRS packages target net10.0.
Add the package you need:
dotnet add package Xapi.Client
For Unity, see unity.md.
Send a statement
With using Xapi; and using Xapi.Client;:
using var http = new HttpClient();
var client = new XapiClient(http, new XapiClientOptions
{
Endpoint = new Uri("https://lrs.example.com/xapi/"),
Credentials = XapiCredentials.Basic(key, secret),
});
var statement = new Statement(
Agent.FromMbox("learner@example.com", "Ada Learner"),
Verbs.Completed,
new Activity(new Iri("https://example.com/courses/fire-safety")));
var id = await client.SaveStatementAsync(statement);
Console.WriteLine($"The LRS stored statement {id}");
Endpoint is the base address of the LRS; resource names such as statements are appended to it. No xAPI
version is set, so before its first request the client reads the LRS's About resource and uses 2.0.0 if the
LRS supports it, otherwise 1.0.3. The statement is validated against the rules of that version before it is
sent, and an invalid one throws XapiValidationException without a request.
Reading it back:
var stored = await client.GetStatementAsync(id);
if (stored is not null)
{
Console.WriteLine(XapiJson.SerializeStatement(stored, await client.GetVersionAsync()));
}
The client guide covers queries, documents, attachments, retries and errors.
Send from an ASP.NET Core app
In a web app, register the client with dependency injection and put an outbox in front of it, so a request
handler only writes the statement to disk and an LRS outage does not fail the request. With using Xapi;,
using Xapi.Client; and using Xapi.Client.Outbox;:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddXapiClient(options =>
{
options.Endpoint = new Uri("https://lrs.example.com/xapi/");
options.Credentials = XapiCredentials.Basic(builder.Configuration["Xapi:Key"]!, builder.Configuration["Xapi:Secret"]!);
})
.AddOutbox(Path.Combine(builder.Environment.ContentRootPath, "xapi-outbox"));
var app = builder.Build();
app.MapPost("/courses/{courseId}/complete", async (string courseId, string email, StatementOutbox outbox, CancellationToken cancellationToken) =>
{
var statement = new Statement(
Agent.FromMbox(email),
Verbs.Completed,
new Activity(new Iri($"https://courses.example.com/{courseId}")));
await outbox.EnqueueAsync(statement, cancellationToken);
return Results.Accepted();
});
app.Run();
AddXapiClient registers XapiClient with IHttpClientFactory. AddOutbox registers a
StatementOutbox that keeps its queue in the given folder and a hosted service that delivers it in the
background, with retries, backoff and dead letters. EnqueueAsync gives the statement an id before storing
it, so a statement that is sent twice after a crash is stored once. The outbox guide has the
details.
Read and validate JSON without an LRS
Xapi.Core works on its own, for example to check statements before they are stored elsewhere. With
using Xapi;, using Xapi.Json; and using Xapi.Validation;:
var parsed = XapiJson.ParseStatement(json, XapiVersion.V2_0_0);
if (!parsed.IsSuccess)
{
foreach (var error in parsed.Errors)
{
Console.WriteLine(error); // "<JSON path>: <message> (<error code>)"
}
return;
}
var statement = parsed.Value with { Timestamp = DateTimeOffset.UtcNow };
var errors = StatementValidator.Validate(statement, XapiVersion.V1_0_3);
Console.WriteLine(errors.Count == 0
? XapiJson.SerializeStatement(statement, XapiVersion.V1_0_3)
: "Not valid as xAPI 1.0.3: " + string.Join("; ", errors));
Every call takes the xAPI version whose rules apply; versions.md lists the differences.
Host an LRS
Create a web app and add the SQLite provider:
dotnet new web -n MyLrs
cd MyLrs
dotnet add package Xapi.Lrs.Sqlite
Replace Program.cs with this:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddXapiLrs(builder.Configuration.GetSection("Lrs"), options => options.MigrateOnStartup = true)
.UseSqlite("Data Source=lrs.db");
var app = builder.Build();
app.MapXapiLrs("/xapi");
app.Run();
The LRS options come from the Lrs configuration section. Add a credential, a Basic key with every scope, to
appsettings.json, and keep its secret out of the file:
{
"Lrs": {
"Credentials": [ { "Key": "my-app", "Scopes": [ "all" ] } ]
}
}
MigrateOnStartup creates the database on the first run. To try it, pass the secret on the command line and
send a statement with curl, using the address that dotnet run prints:
dotnet run -- --Lrs:Credentials:0:Secret=change-me
URL=http://localhost:5000
curl $URL/xapi/about
curl -u my-app:change-me -H "X-Experience-API-Version: 2.0.0" -H "Content-Type: application/json" \
-d '{"actor":{"mbox":"mailto:learner@example.com"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"https://example.com/courses/fire-safety"}}' \
$URL/xapi/statements
curl -u my-app:change-me -H "X-Experience-API-Version: 2.0.0" "$URL/xapi/statements?verb=http://adlnet.gov/expapi/verbs/completed"
The POST answers with the id of the stored statement. The LRS guide covers credentials and scopes, delegated authentication, PostgreSQL, migrations and limits.
Test your code against a real LRS
The embedded LRS also runs inside a test process, without Docker or a network port, so code that uses
XapiClient can be tested against real LRS behaviour. samples/FakeLrsInTests
shows how.
Samples
| Sample | Shows |
|---|---|
| ConsoleClient | Sending a statement, reading it back, and delivering one through a file outbox. Published with Native AOT in CI. |
| EmbedInAspNetCore | An app with its own endpoints and an LRS under /xapi, with credentials from configuration. |
| FakeLrsInTests | xUnit tests of an app's xAPI code against the embedded LRS hosted in memory. |
| LrsHost | A standalone LRS on SQLite or PostgreSQL; the target of the conformance runs. |