Having a fully working implementation allows you to easily see what needs to be achieved, and also provides an easy way to ensure that the test suite and libraries are behaving as expected within your environment.
Step 1 - Copy files
Copy the files within the Feeds, Helpers, IdComponents, Settings, and Stores directories from the example project into your application, and add the dependencies OpenActive.Server.NET and OpenActive.FakeDatabase.NET. FakeDatabase.NET is an in-memory fake database that is not persisted, and will allow you to create a quick working Open Booking API.
Inspect the Controllers and copy the files (or the contents of the files) as appropriate for your application.
A further endpoint is required to meet the recommendations outlined for authentication, however this can be added as part of Day 8 as it has it is not a dependency of Days 1-7:
Dynamic Client Update
Step 2 - Copy configuration
Inspect the Startup.cs (.NET Core) or ServiceConfig.cs (.NET Framework) and copy the services.AddSingleton<IBookingEngine>(...) configuration into your own Startup.cs or ServiceConfig.cs.
The initial objective is to get a working version of the Open Booking API using entirely fake data. So when copying these files do not modify the configuration values at this stage - simply use them as-is.
Step 2 - Controller endpoint bindings
The ResponseContent class provides a .NET version agnostic representation of HTTP responses from the Booking Engine. Two example helper methods are provided to be used with your version of .NET. See ResponseContentHelper.csin the example projects.
.NET Core
The following extension method can be used to return an Mvc.ContentResult.
public static Microsoft.AspNetCore.Mvc.ContentResult GetContentResult(this OpenActive.Server.NET.OpenBookingHelper.ResponseContent response)
{returnnewMicrosoft.AspNetCore.Mvc.ContentResult {StatusCode= (int)response.StatusCode,Content=response.Content,ContentType=response.ContentType };}
.NET Framework
The following extension method can be used to return an HttpResponseMessage.
public static HttpResponseMessage GetContentResult(this OpenActive.Server.NET.OpenBookingHelper.ResponseContent response)
{varresp=newHttpResponseMessage {Content=response.Content==null?null:newStringContent(response.Content),StatusCode=response.StatusCode };resp.Content.Headers.ContentType=MediaTypeHeaderValue.Parse(response.ContentType);returnresp;}
Step 3 - Input Formatter
The Booking Engine accepts input JSON as string in order to fully control deserialisation. In order to allow the web framework to capture the body of the request as a string, for the Open Booking API's media type, an InputFormatter is required. See OpenBookingInputFormatter.csin the example projects.
The Booking Engine does not handle authentication, and instead accepts claims from an authentication layer in front of it (this will be covered in more detail in Day 8). An AuthenticationHelper is provided for .NET Core and .NET Framework that extracts OAuth 2.0 claims from access tokens, and also provides simple header alternatives to facilitate development and testing.
Note that these test header alternatives are not secure and must not be used in production, however they will be used for Day 1-7 of this guide.
.NET Core
.NET Core implementations can leverage the authentication middleware, using the provided helpers:
(string clientId, Uri sellerId) =User.GetAccessTokenOpenBookingClaims();returnbookingEngine.ProcessCheckpoint1(clientId, sellerId, uuid, orderQuote).GetContentResult();
To speed development, the following allows test headers to be used for accessing the API, and can be set up to allow development to proceed until Day 8 of this tutorial.
Startup.cs
publicvoidConfigureServices(IServiceCollection services){ // DO NOT USE THIS IN PRODUCTION. // This passes test API headers straight through to claims, and provides no security whatsoever.services.AddAuthentication(options => {options.DefaultAuthenticateScheme=TestHeaderAuthenticationOptions.DefaultScheme;options.DefaultChallengeScheme=TestHeaderAuthenticationOptions.DefaultScheme; }) .AddTestHeaderAuthenticationSupport(options => { });services.AddAuthorization(options => { // No authorization checks are performed, this just ensures that the required claims are suppliedoptions.AddPolicy(OpenActiveScopes.OpenBooking, policy => {policy.RequireClaim(OpenActiveCustomClaimNames.ClientId);policy.RequireClaim(OpenActiveCustomClaimNames.SellerId); }); options.AddPolicy(OpenActiveScopes.OrdersFeed, policy => policy.RequireClaim(OpenActiveCustomClaimNames.ClientId));
});}
TestHeaderAuthentication.cs
/*** DO NOT USE THIS FILE IN PRODUCTION. This approach is for testing only, and provides no security whatsoever.*/publicclassTestHeaderAuthenticationOptions:AuthenticationSchemeOptions{publicconststring DefaultScheme ="Test Headers";publicstring Scheme => DefaultScheme;publicstring AuthenticationType = DefaultScheme;}publicstaticclassAuthenticationBuilderExtensions{ public static AuthenticationBuilder AddTestHeaderAuthenticationSupport(this AuthenticationBuilder authenticationBuilder, Action<TestHeaderAuthenticationOptions> options)
{ return authenticationBuilder.AddScheme<TestHeaderAuthenticationOptions, TestHeaderAuthenticationHandler>(TestHeaderAuthenticationOptions.DefaultScheme, options);
}}publicclassTestHeaderAuthenticationHandler:AuthenticationHandler<TestHeaderAuthenticationOptions>{publicTestHeaderAuthenticationHandler(IOptionsMonitor<TestHeaderAuthenticationOptions> options,ILoggerFactory logger,UrlEncoder encoder,ISystemClock clock) : base(options, logger, encoder, clock) { }protectedoverrideasyncTask<AuthenticateResult> HandleAuthenticateAsync() { // Get the claims from headers if they existRequest.Headers.TryGetValue(AuthenticationTestHeaders.ClientId,outvar testClientId);Request.Headers.TryGetValue(AuthenticationTestHeaders.SellerId,outvar testSellerId);var clientId =testClientId.FirstOrDefault();var sellerId =testSellerId.FirstOrDefault(); // This just passes the test headers through to the claims - it does not provide any security.var claims =newList<Claim>();if (clientId !=null) claims.Add(newClaim(OpenActiveCustomClaimNames.ClientId, clientId));if (sellerId !=null) claims.Add(newClaim(OpenActiveCustomClaimNames.SellerId, sellerId));var identity =newClaimsIdentity(claims,Options.AuthenticationType);var identities =newList<ClaimsIdentity> { identity };var principal =newClaimsPrincipal(identities);var ticket =newAuthenticationTicket(principal,Options.Scheme); // No checks are made, so this always succeeds. It's just setting the claims if they exist.returnAuthenticateResult.Success(ticket); }protectedoverrideasyncTaskHandleChallengeAsync(AuthenticationProperties properties) {Response.StatusCode=401;awaitResponse.WriteAsync(OpenActiveSerializer.Serialize(newInvalidAPITokenError())); }protectedoverrideasyncTaskHandleForbiddenAsync(AuthenticationProperties properties) {Response.StatusCode=403;awaitResponse.WriteAsync(OpenActiveSerializer.Serialize(newUnauthenticatedError())); }}
.NET Framework
(string clientId, Uri sellerId) =AuthenticationHelper.GetIdsFromAuth(Request, User);return_bookingEngine.ProcessCheckpoint1(clientId, sellerId, uuid, orderQuote).GetContentResult();
publicstaticclassAuthenticationHelper{ private static (string clientId, Uri sellerId) GetIdsFromAuth(HttpRequestMessage request, IPrincipal principal, bool requireSellerId)
{ // NOT FOR PRODUCTION USE: Please remove this block in productionIEnumerable<string> testSellerId =null;if (request.Headers.TryGetValues(AuthenticationTestHeaders.ClientId,outIEnumerable<string> testClientId) && testClientId.Count() == 1 && (!requireSellerId || (request.Headers.TryGetValues(AuthenticationTestHeaders.SellerId, out testSellerId) && testSellerId.FirstOrDefault().ParseUrlOrNull() != null))
) {return (testClientId.FirstOrDefault(),testSellerId?.FirstOrDefault().ParseUrlOrNull()); } // For production use: Get Ids from JWTvar claimsPrincipal = principal asClaimsPrincipal;var clientId =claimsPrincipal.GetClientId();var sellerId =claimsPrincipal.GetSellerId().ParseUrlOrNull();if (clientId != null && (sellerId != null || !requireSellerId)) {return (clientId, sellerId); } else { throw newOpenBookingException(new InvalidAPITokenError()); } }publicstatic (string clientId,Uri sellerId) GetIdsFromAuth(HttpRequestMessage request,IPrincipal principal) {returnGetIdsFromAuth(request, principal,true); }publicstaticstringGetClientIdFromAuth(HttpRequestMessage request,IPrincipal principal) {returnGetIdsFromAuth(request, principal,false).clientId; }}
Step 5 - Run Application
If you run your application and navigate to the dataset site endpoint (e.g. https://localhost:44307/openactive), you should find the following page:
Clicking on the ScheduledSessions feed should return JSON in the following format:
If some links appear broken, try updating BaseUrl within Startup.cs or ServiceConfig.cs with the correct port number for your local test environment.
Follow the instructions below to set up the OpenActive Test Suite:
You will need Node.js version 14 or above installed to do this - which can installed with the Visual Studio installer.
In Steps 7 and 8, the header configuration can use the default values in order to work with the Booking Engine, except that the Seller @id must be replaced with a valid Seller @id from your booking system.
In Step 9, your Dataset Site is automatically created and configured by the Booking Engine, so simply update the value of datasetSiteUrl based on the port number and path used by your .NET application when running:
At this stage we are still using FakeDatabase, so all tests for whichever 'implemented' features you have configured should pass. Assuming configuration values have not been changed from the copied files, issues with the tests at this stage will very likely be due to the controller or other application configuration.