.NET classes and resources supporting RPDE feed creation and Open Booking API implementation, to create something like the feeds available here (or any of the other feeds available at the examples listed here).
This package is intended to simplify the creation of OpenActive RPDE Feeds using templates.
Please find full documentation that covers creation and scaling of RPDE feeds at https://developer.openactive.io/publishing-data/data-feeds/.
OpenActive aims to provide strongly typed models for all classes defined in the Modelling Specification and Open Booking API Specification across the PHP, Ruby, and .NET languages. This library is specific to .NET; see also the PHP and Ruby implementations.
- Requirements
- Models
- RPDE Feed Publishing
- Serialization Methods
- Empty Lists
OpenActiveSerializer.Serialize<T>(T obj)
OpenActiveSerializer.SerializeList<T>(List<T> obj)
OpenActiveSerializer.SerializeToHtmlEmbeddableString<T>(T obj)
OpenActiveSerializer.SerializeRpdePage(RpdePage obj)
OpenActiveSerializer.Deserialize<T>(string str)
OpenActiveSerializer.DeserializeList<T>(string str)
OpenActiveSerializer.DeserializeRpdePage(string str)
- Contributing
This library is compatible with .NET Standard 1.1 and later (.NET Framework 4.5 and .NET Core 1.0).
OpenActive.NET includes OpenActive.io objects as strongly typed C# POCO classes for use in .NET. All classes can be serialized into JSON-LD, to provide easy conformance with the Modelling Specification and Open Booking API Specification.
Note that empty strings are automatically ignored during serialization, however empty lists need to be explicitly set to null
in order to conform to the OpenActive serialization rules.
An extension method .ToListOrNullIfEmpty()
is provided for this purpose, which should be used on any lists being set on the model.
Classes for all OpenActive classes are available in the OpenActive.NET
namespace, and can be easily serialized to JSON-LD, as follows. Enumerations are available as enum
s for properties that require their use.
var event = new Event()
{
Name = "Virtual BODYPUMP",
EventStatus = Schema.NET.EventStatusType.EventScheduled
};
var jsonLd = OpenActiveSerializer.Serialize(event);
Value of jsonLd
:
{
"@context": "https://openactive.io/",
"@type": "Event",
"name": "Virtual BODYPUMP",
"eventStatus": "https://schema.org/EventScheduled"
}
The OpenActive data model builds on top of Schema.org, which means that you are free to use additional schema.org properties within OpenActive published data.
To reflect this, OpenActive.NET uses inheritance to build on top of Schema.NET, and so makes it easy to use additional properties from schema.org on any given type.
To avoid naming conflicts between OpenActive.NET and Schema.NET, it is recommended that you import using OpenActive.NET;
to reference OpenActive model types, and use fully qualified references for Schema.NET types (e.g. Schema.NET.Thing
).
var event = new Event()
{
Name = "Virtual BODYPUMP",
Review = new Schema.NET.Review
{
ReviewBody = "So was I really there?"
}
};
var jsonLd = OpenActiveSerializer.Serialize(event);
Value of jsonLd
:
{
"@context": "https://openactive.io/",
"@type": "Event",
"name": "Virtual BODYPUMP",
"review": {
"@type": "Review",
"reviewBody": "So was I really there?"
}
}
var event = new Event()
{
Name = "Virtual BODYPUMP",
Description = "This is the virtual version of the original barbell class, which will help you get lean, toned and fit - fast. Les Mills™ Virtual classes are designed for people who cannot get access to our live classes or who want to get a ‘taste’ of a Les Mills™ class before taking a live class with an instructor. The classes are played on a big video screen, with dimmed lighting and pumping surround sound, and are led onscreen by the people who actually choreograph the classes.",
Duration = TimeSpan.FromDays(1),
StartDate = new DateTimeOffset(2017, 4, 24, 19, 30, 0, TimeSpan.FromHours(-8)),
Location = new Place()
{
Name = "Santa Clara City Library, Central Park Library",
Address = new PostalAddress()
{
StreetAddress = "2635 Homestead Rd",
AddressLocality = "Santa Clara",
PostalCode = "95051",
AddressRegion = "CA",
AddressCountry = "US"
}
},
Image = new List<ImageObject>() { new ImageObject { Url = new Uri("http://www.example.com/event_image/12345") } },
EndDate = new DateTimeOffset(2017, 4, 24, 23, 0, 0, TimeSpan.FromHours(-8)),
Offers = new List<Offer>() {
new Offer()
{
Url = new Uri("https://www.example.com/event_offer/12345_201803180430"),
Price = 30,
PriceCurrency = "USD",
ValidFrom = new DateTimeOffset(2017, 1, 20, 16, 20, 0, TimeSpan.FromHours(-8))
}
}.ToListOrNullIfEmpty(),
AttendeeInstructions = "Ensure you bring trainers and a bottle of water.",
MeetingPoint = ""
};
var jsonLd = OpenActiveSerializer.Serialize(event);
The code above outputs the following JSON-LD:
{
"@context": "https://openactive.io/",
"@type": "Event",
"name": "Virtual BODYPUMP",
"description": "This is the virtual version of the original barbell class, which will help you get lean, toned and fit - fast. Les Mills™ Virtual classes are designed for people who cannot get access to our live classes or who want to get a ‘taste’ of a Les Mills™ class before taking a live class with an instructor. The classes are played on a big video screen, with dimmed lighting and pumping surround sound, and are led onscreen by the people who actually choreograph the classes.",
"attendeeInstructions": "Ensure you bring trainers and a bottle of water.",
"duration": "P1D",
"image": [
{
"@type": "ImageObject",
"url": "http://www.example.com/event_image/12345"
}
],
"location": {
"@type": "Place",
"name": "Santa Clara City Library, Central Park Library",
"address": {
"@type": "PostalAddress",
"addressCountry": "US",
"addressLocality": "Santa Clara",
"addressRegion": "CA",
"postalCode": "95051",
"streetAddress": "2635 Homestead Rd"
}
},
"offers": [
{
"@type": "Offer",
"price": 30.0,
"priceCurrency": "USD",
"url": "https://www.example.com/event_offer/12345_201803180430",
"validFrom": "2017-01-20T16:20:00-08:00"
}
],
"startDate": "2017-04-24T19:30:00-08:00",
"endDate": "2017-04-24T23:00:00-08:00"
}
To publish an OpenActive data feed (see this video explainer), OpenActive.NET provides a drop-in solution to render the feed pages. This also includes validation for the underlying feed query.
RpdePage(feedBaseUrl, afterTimestamp, afterId, items)
Creates a new RPDE Page based on the RPDE Items provided using the Modified Timestamp and ID Ordering Strategy, given the afterTimestamp
and afterId
parameters of the current query. Also validates that the items are in the correct order, throwing a SerializationException
if this is not the case.
var items = new List<RpdeItem>
{
new RpdeItem
{
Id = "1",
Modified = 3,
State = RpdeState.Updated,
Kind = RpdeKind.SessionSeries,
Data = @event
},
new RpdeItem
{
Id = "2",
Modified = 4,
State = RpdeState.Deleted,
Kind = RpdeKind.SessionSeries,
Data = null
}
};
var jsonLd = new RpdePage(new Uri("https://www.example.com/feed"), 1, "1", items).ToString();
RpdePage(feedBaseUrl, afterChangeNumber, items)
Creates a new RPDE Page based on the RPDE Items provided using the Incrementing Unique Change Number Ordering Strategy, given the afterChangeNumber
parameter of the current query. Also validates that the items are in the correct order, throwing a SerializationException
if this is not the case.
var items = new List<RpdeItem>
{
new RpdeItem
{
Id = "1",
Modified = 3,
State = RpdeState.Updated,
Kind = RpdeKind.SessionSeries,
Data = @event
},
new RpdeItem
{
Id = "2",
Modified = 4,
State = RpdeState.Deleted,
Kind = RpdeKind.SessionSeries,
Data = null
}
};
var jsonLd = new RpdePage(new Uri("https://www.example.com/feed"), 2, items).ToString();
Implementation requires implementing ConvertToOpenActiveModel
to return an instance of e.g. OpenActive.NET.ScheduledSession
or OpenActive.NET.Event
as per the OpenActive.NET Model section below.
using OpenActive.NET.Rpde.Version1;
public abstract class RPDEBase<DatabaseType> where DatabaseType : RPDEBase<DatabaseType>, new()
{
protected abstract RpdeKind RpdeKind { get; }
protected abstract Schema.NET.Thing ConvertToOpenActiveModel(DatabaseType record, string baseUrl);
private async Task<RpdePage> GetRPDEPage(long? afterChangeNumber, string feedUrl, string baseUrl, int limit)
{
var items = new List<RpdeItem>();
DatabaseFactory.ColumnSerializer = new NPocoSerializer();
using (Database db = new Database("DefaultConnection"))
{
// Get the table name manually as we are constructing the query by hand
var tableName = db.PocoDataFactory.ForType(typeof(DatabaseType)).TableInfo.TableName;
// Query for paging as shown in https://www.openactive.io/realtime-paged-data-exchange/#incrementing-unique-change-number
// Note due to the issues with big endian vs little endian conversion when converting from rowversion in SQL Server to int64 in C#, it is best to do the
// conversion in both directions in SQL Server, as below
var whereClause = afterChangeNumber != null ? "WHERE Modified > Convert(ROWVERSION, @0)" : "";
var results = await db.QueryAsync<DatabaseType>($"SELECT TOP {limit} Convert(BIGINT,Modified) as ChangeNumber, u.* from {tableName} u (nolock) {whereClause} ORDER BY Modified", afterChangeNumber);
items = results.Select(row => new RpdeItem
{
Id = row.ID,
Modified = row.modified,
Data = row.Deleted ? null : ConvertToOpenActiveModel(row, baseUrl),
State = row.Deleted ? RpdeState.Deleted : RpdeState.Updated,
Kind = RpdeKind
}).ToList();
}
return new RpdePage(feedUrl, afterChangeNumber, items);
}
public static async Task<HttpResponseMessage> ServeRPDEPage(HttpRequestMessage req, int limit)
{
// parse query parameter 'afterChangeNumber'
long? afterChangeNumber = Convert.ToInt64(req.GetQueryNameValuePairs()
.FirstOrDefault(q => string.Compare(q.Key, "afterChangeNumber", true) == 0)
.Value);
// Get base Urls
var urlHelper = new System.Web.Mvc.UrlHelper(HttpContext.Current.Request.RequestContext);
var baseUrl = req.RequestUri.RewriteHttps().GetLeftPart(UriPartial.Authority) + urlHelper.Content("~/");
var feedUrl = req.RequestUri.RewriteHttps().GetLeftPart(UriPartial.Path);
// Instantiate DatabaseType to make use of inheritance in GetRPDEPage
var rpdePage = await (new DatabaseType()).GetRPDEPage(afterChangeNumber, feedUrl, baseUrl, limit);
// If no page returned, return error
if (rpdePage == null)
{
return req.CreateResponse(HttpStatusCode.BadRequest, "Invalid parameters");
}
// Render reponse as JSON
var response = req.CreateResponse(HttpStatusCode.OK);
response.Content = rpdePage.ToStringContent();
// Always use MaxAge cache header for a full page
if (rpdePage?.Items?.Count > 0)
{
response.Headers.CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromHours(1),
SharedMaxAge = TimeSpan.FromHours(1)
};
}
else
{
response.Headers.CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(8),
SharedMaxAge = TimeSpan.FromSeconds(8)
};
}
return response;
}
}
Empty strings are automatically ignored during serialization, however empty lists need to be explicitly set to null
in order to conform to the OpenActive specification.
An extension method .ToListOrNullIfEmpty()
is provided for this purpose, which should be used on any lists being set on the model.
var event = new Event()
{
Name = "Virtual BODYPUMP"
Offers = offers.ToListOrNullIfEmpty();
};
var jsonLd = OpenActiveSerializer.Serialize(event);
Returns the JSON-LD representation of a JsonLdObject
.
var event = new Event()
{
Name = "Virtual BODYPUMP"
};
var jsonLd = OpenActiveSerializer.Serialize(event);
Value of jsonLd
:
{
"@context": "https://openactive.io/",
"@type": "Event",
"name": "Virtual BODYPUMP"
}
Returns the JSON-LD representation of a list of JsonLdObject
.
var concepts = new List<Concept>
{
new Concept
{
Id = new Uri("https://openactive.io/facility-types#37bbed12-270b-42b1-9af2-70f0273990dd"),
PrefLabel = "Grass",
InScheme = new Uri("https://openactive.io/facility-types")
}
};
var jsonLd = OpenActiveSerializer.SerializeList(event);
Value of jsonLd
:
[
{
"@context":"https://openactive.io/",
"@type":"Concept",
"@id":"https://openactive.io/facility-types#37bbed12-270b-42b1-9af2-70f0273990dd",
"inScheme":"https://openactive.io/facility-types",
"prefLabel":"Grass"
}
]
Returns the JSON-LD representation of an JsonLdObject
, including "https://schema.org"
in the "@context"
property,
to make the output compatible with search engines, for SEO.
This method should be used when you want to embed the output raw (as-is) into a web page. It uses serializer settings with HTML escaping to avoid Cross-Site Scripting (XSS) vulnerabilities if the object was constructed from an untrusted source.
var event = new Event()
{
Name = "Virtual BODYPUMP"
};
var jsonLd = OpenActiveSerializer.SerializeToHtmlEmbeddableString(event);
Value of jsonLd
:
{
"@context": ["https://schema.org", "https://openactive.io/"],
"@type": "Event",
"name": "Virtual BODYPUMP"
}
Returns the serialized representation of an RpdePage
. Note that OpenActiveSerializer.Serialize<T>
cannot be used on an RpdePage
, as RPDE itself is not a JSON-LD based format.
Returns a strongly typed model of the JSON-LD representation provided.
Note this will return null if the deserialized JSON-LD class cannot be assigned to T
.
Hence OpenActiveSerializer.Deserialize<SessionSeries>(eventJson);
would return null where eventJson
represented an Event
.
Returns a strongly typed list of models of the given type of the JSON-LD representation provided.
Returns a strongly typed model of the RPDE page provided.
Running npm start
in the OpenActive.NET.Generator
project will regenerate the models from the OpenActive model files.
npm install
npm start
Contributions are very welcome. Please raise an issue or pull request as appropriate.