Day 2: Open data feeds

Objective for Day 2

Create open data RPDE feeds live from your application.

Rationale

Open Booking API is built on top of open data feed publishing, and RPDE feeds are therefore a prerequisite to implementing the rest of the API.

Even for systems that have already implemented OpenActive open data feeds, it is highly recommended that the feed generation code is migrated to within the OpenActive.Server.NET framework, using the steps below. This will greatly simplify the rest of the implementation, and increase maintainability.

Step 1 - Understand the RPDE specification

Before continuing with this tutorial, the following video offers a good theoretical understanding of the RPDE specification. Further information can be found in the developers guide and RPDE specification.

Step 2 - Set up your Opportunity and Offer pair ID structure

In order for opportunities and offers within open RPDE feeds to be bookable via the Open Booking API, they must have IDs.

The Open Booking API is based on JSON-LD. JSON-LD IDs always take the form of a URL, and hence all IDs within the Open Booking API also take the form of a URL. The StoreBookingEngine automatically constructs and parses these URLs for you, and deserialises them into POCOs. It also handles routing based on the URL template to the relevant store depending on the type of opportunity.

In order to take advantage of these features, you need to configure the URL structure specifically for your application.

When making changes to these setting, specifically for IBookableIdComponents, consider using Visual Studio's refactoring tools to alter the example provided, to ensure that the rest of the project still compiles.

The first URLs we'll set up are for the Opportunity and Offer pairs, such as the IDs found in the Open Booking API request snippet below:

"orderedItem": [
  {
    "@type": "OrderItem",
    "acceptedOffer": {
      "@type": "Offer",
      "@id": "https://example.com/api/identifiers/api/session-series/100002#/offers/0"
    },
    "orderedItem": {
      "@type": "ScheduledSession",
      "@id": "https://example.com/api/identifiers/api/session-series/100002/scheduled-sessions/100003"
    }
  }
]

IBookablePairIdTemplate and IBookableIdComponents

There are two key components that handle JSON-LD ID serialisation and deserialisation within the StoreBookingEngine:

  • IBookablePairIdTemplate instances specify the ID URL templates used by your application and associates them with OpenActive opportunity types, as well as the hierarchy of types that are in use from the OpenActive data model. They also define the types of opportunities that are bookable, and which feeds are used to publish the opportunities.

  • IBookableIdComponents are POCO objects that you define specifically based on the underlying data model within your booking system, used to deserialise the ID URL. The properties within these are mapped by name to the placeholders within the associated IBookablePairIdTemplate URL templates. IBookableIdComponents represents an Opportunity and Offer pair, and hence includes both the Opportunity ID and Offer ID, with their matching overlapping components.

Note that the JSON-LD IDs do not need to resolve to actual endpoints, provided they are unique and exist within your domain.

Configuring IBookablePairIdTemplates

IBookablePairIdTemplate instances are configured in Startup.cs or ServiceConfig.cs within BookingEngineSettings:

new BookingEngineSettings
{
    // This assigns the ID pattern used for each ID
    IdConfiguration = new List<IBookablePairIdTemplate> {
        // Note that ScheduledSession is the only opportunity type that allows offer inheritance  
        new BookablePairIdTemplateWithOfferInheritance<SessionOpportunity> (
            // Opportunity
            new OpportunityIdConfiguration
            {
                OpportunityType = OpportunityType.ScheduledSession,
                AssignedFeed = OpportunityType.ScheduledSession,
                OpportunityIdTemplate = "{+BaseUrl}api/session-series/{SessionSeriesId}/scheduled-sessions/{ScheduledSessionId}",
                OfferIdTemplate =       "{+BaseUrl}api/session-series/{SessionSeriesId}/scheduled-sessions/{ScheduledSessionId}#/offers/{OfferId}",
                Bookable = true
            },
            // Parent
            new OpportunityIdConfiguration
            {
                OpportunityType = OpportunityType.SessionSeries,
                AssignedFeed = OpportunityType.SessionSeries,
                OpportunityIdTemplate = "{+BaseUrl}api/session-series/{SessionSeriesId}",
                OfferIdTemplate =       "{+BaseUrl}api/session-series/{SessionSeriesId}#/offers/{OfferId}",
                Bookable = false
            }),
            
         new BookablePairIdTemplate<EventOpportunity>(
            // Opportunity
            new OpportunityIdConfiguration
            {
                OpportunityType = OpportunityType.Event,
                AssignedFeed = OpportunityType.Event,
                OpportunityUriTemplate = "{+BaseUrl}api/events/{EventId}",
                OfferUriTemplate =       "{+BaseUrl}api/events/{EventId}#/offers/{OfferId}",
                Bookable = true
            }),
            
        ...
    }
}

Where you these referenced in the example above...

The following annotations apply...

SessionOpportunity and EventOpportunity

Examples of an IBookableIdComponents POCO used to map to placeholders within the ID templates defined

BookablePairIdTemplate

A template that contains a hierarchy of up to three OpenActive types, in order from child to parent (e.g. ScheduledSessionSessionSeriesEventSeries). BookablePairIdTemplate accepts the generic parameter of an IBookableIdComponents type, which binds the placeholders within the URL templates specified to the properties defined within the IBookableIdComponents POCO.

BookablePairIdTemplateWithOfferInheritance

A specialism of BookablePairIdTemplate designed for the special case of ScheduledSessionSessionSeries where Offers may be inherited from SessionSeries to ScheduledSession.

OpportunityType

The OpenActive opportunity type represented

AssignedFeed

The feed within which this opportunity type is published (for example, the EventSeries may be embedded within a SessionSeries feed as shown in this example).

OpportunityUriTemplate

OfferUriTemplate

The URL Templates used to serialise/deserialise the POCO for an Opportunity ID and Offer ID. Note that the Opportunity ID and Offer ID are deserialised into the same POCO, as an Opportunity and Offer pair, and hence any shared components within these must match (e.g. an Offer is specific to an Event, and hence will include both the Event ID and and

Bookable

Whether the specified Opportunity and Offer pair is bookable within this booking system using the Open Booking API. Note that SessionSeries and FacilityUse are not bookable within the Open Booking API, which instead prefers ScheduledSessions or Slots.

BaseUrl

This is a placeholder within the ID templates that has a special function of ensuring conformity with the JsonLdIdBaseUrl setting within BookingEngineSettings, both for serialisation and deserialisation.

Configuring IBookableIdComponents

IBookableIdComponents are defined by the classes in the IdComponents directory.

These classes must be created by the booking system. There is a choice of string, long? and Uri available as the type for each component of the ID, as well as any enum type. The names of the components must exactly match the placeholders within the associated IBookablePairIdTemplates.

public class SessionOpportunity : IBookableIdComponentsWithInheritance
{
    public OpportunityType? OpportunityType { get; set; }
    public OpportunityType? OfferOpportunityType { get; set; }
    public long? SessionSeriesId { get; set; }
    public long? ScheduledSessionId { get; set; }
    public long? OfferId { get; set; }
}

In the example above

Description

OpportunityType

The OpportunityType represents the type in the hierarchy that the Opportunity and Offer pair represents (e.g. HeadlineEvent or its child Event). This property is available by the class extending either IBookableIdComponents or IBookableIdComponentsWithInheritance.

OfferOpportunityType

For SessionSeries, where the Offer can be inherited to the child ScheduledSession, OfferOpportunityType indicates which Offer is being referenced - the parent or the child. This property is only available by the class extending IBookableIdComponentsWithInheritance.

Step 3 - Set up your Seller ID structure

The Seller IDs are configured slightly differently to the Opportunity and Offer pair IDs, for simplicity.

There is a built-in POCO named SellerIdComponentsthat is defined as follows:

public class SellerIdComponents
{
    public long? SellerIdLong { get; set; }
    public string SellerIdString { get; set; }
}

Within BookingEngineSettings the SellerIdTemplate setting controls how the Seller ID is serialised and deserialised. There are two options for Seller IDs: Single Seller and Multiple Seller.

Booking systems supporting Multiple Sellers

Depending on the type of your internal Seller ID, you may use either SellerIdLong or SellerIdString within the URL template. Once you have chosen which one to use, simply reference that same property consistently wherever you use the Seller ID throughout your code, and ignore the other property.

The following example demonstrates BookingSystemSettings for Multiple Sellers:

SellerIdTemplate = new SingleIdTemplate<SellerIdComponents>(
    "{+BaseUrl}api/sellers/{SellerIdLong}"
    ),

Booking systems supporting a Single Sellers

To use Single Seller mode, simply use neither SellerIdLong or SellerIdString within the URL template, and set HasSingleSeller to true. Then simply do not reference SellerIdComponents anywhere in your code, as both SellerIdLong and SellerIdString will be null. RenderSingleSellerId is provided for scenarios where a Single Seller ID needs to be rendered.

The following example demonstrates BookingSystemSettings for a Single Seller:

HasSingleSeller = true,
SellerIdTemplate = new SingleIdTemplate<SellerIdComponents>(
    "{+BaseUrl}api/seller"
    ),

Step 4 - Create RPDE Feed Generators

The StoreBookingEngine handles the serialisation and parameter validation that would usually be required when implementing an RPDE feed. All that is required for each feed is to implement a single method within an IOpportunityDataRPDEFeedGenerator class.

If you have already implemented RPDE feeds within your application using OpenActive.NET, your existing database queries and OpenActive model mapping should easily transferable to within the generator method.

Within BookingEngineSettings within EngineConfig.cs the OpenDataFeeds setting configures the routing to the different feed generators from the GetOpenDataRPDEPageForFeed method being called in the controller.

OpenDataFeeds = new Dictionary<OpportunityType, IOpportunityDataRPDEFeedGenerator> {
    {
        OpportunityType.ScheduledSession, new AcmeScheduledSessionRPDEGenerator()
    },
    {
        OpportunityType.SessionSeries, new AcmeSessionSeriesRPDEGenerator()
    },
    {
        OpportunityType.FacilityUse, new AcmeFacilityUseRPDEGenerator()
    }
    ,
    {
        OpportunityType.FacilityUseSlot, new AcmeFacilityUseSlotRPDEGenerator()
    }
},

Three implementations of IOpportunityDataRPDEFeedGenerator are available depending on your prefered RPDE Ordering Strategy, and whether your ID is a long or string.

Using the example of the IBookableIdComponents class SessionOpportunity from Step 2, and the OpenActive feed root type of ScheduledSession, a generator is created by subclassing one of the following:

  • RPDEFeedModifiedTimestampAndIDLong<SessionOpportunity, ScheduledSession>

  • RPDEFeedModifiedTimestampAndIDString<SessionOpportunity, ScheduledSession>

  • RPDEFeedIncrementingUniqueChangeNumber<SessionOpportunity, ScheduledSession>

Any of the above would require the same GetRPDEItems method be implemented, as below:

public class AcmeScheduledSessionRPDEGenerator : 
    RPDEFeedModifiedTimestampAndIDLong<SessionOpportunity, ScheduledSession>
{
    protected override List<RpdeItem<ScheduledSession>> GetRPDEItems(long? afterTimestamp, long? afterId)
    {
        ...
    }
}

The appropriate query for the database for your chosen RPDE Ordering Strategy must be used, and the ordering of the items returned from your overridden method is automatically validated to ensure consistency with the specification.

Within the mapping of your data to the OpenActive model, there are a few helper methods available from the base class to construct IDs specific for the feed:

Method

Example

RenderOpportunityId

Id = this.RenderOpportunityId(new SessionOpportunity

{

OpportunityType = OpportunityType.SessionSeries,

SessionSeriesId = @class.Id

}),

RenderOfferId

Id = this.RenderOfferId(new SessionOpportunity

{

OfferOpportunityType = OpportunityType.SessionSeries,

SessionSeriesId = @class.Id,

OfferId = 0

}),

RenderSellerId

(for Multiple Sellers)

Id = this.RenderSellerId(new SellerIdComponents

{

SellerIdLong = seller.Id

}),

RenderSingleSellerId (for Single Seller)

Id = this.RenderSingleSellerId(),

Step 5 - Configure Dataset Site

The DatasetSiteGeneratorSettings within EngineConfig.cs can be used to configure your dataset site.

Only the OpenDataFeedBaseUrl is used by the StoreBookingEngine, so this must be accurate to proceed with testing.

All other settings are described within the documentation for OpenActive.DatasetSite.NET.

If you have already implemented a dataset site within your application using OpenActive.DatasetSite.NET, the settings should be easily transferable to EngineConfig.cs.

Step 6 - Test data feeds and dataset site

If you run your application and navigate to the dataset site endpoint (e.g. https://localhost:44307/openactive), you should find it now reflects your updated settings, and the links to the RPDE pages on that page work as expected.

Last updated