Using Swagger in Sitecore solution as a helix feature

Lets say your Sitecore solution needs to have some API services. The obvious thing is to implement them using .net Web API. Sometimes the understanding of various methods of an API can be a challenge for a developer when building a consuming app. Swagger is a machine-readable representation of a RESTful API that enables support for interactive documentation, client SDK generation, and discoverability.

Generating good documentation and help pages for your Web API, using Swagger is as easy as adding a NuGet package.

As long as Sitecore is still an asp.net application, we can use Swagger with it! So I implement a Web API controller that I want to host it within my Sitecore instance and to document it somehow:

namespace Feature.WebApi.Controllers
{
	using System.Web.Http;

	[RoutePrefix("-/api/v1")]
	public class MyServicesApiController : ApiController
	{
		[HttpGet]
		[Route("hello-world")]
		public IHttpActionResult HelloWorld()
		{
			return this.Ok("Hello world");
		}
	}
}

Okay, this is my “hello world” API method. Now I will install Swagger to my Feature.Swagger project using nuget:

install-package Swashbuckle

The default nuget package will create a startup config at /App_Start/SwaggerConfig.cs. By default it uses WebActivator extensions to bind it to the application start so it executes the configuration when your web application is being initialized.

By default it won’t run smoothly with Sitecore, but we can make it to work properly with few configurations. The entry point to Swagger UI would be {your.domain.com}/swagger. I run it and see next picture:

swagger-error1

In order to resolve it, we need to modify /App_Start/SwaggerConfig.cs and un-comment next line:

c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());

Run it again – and we see next issue:

swagger-error-schemas-conflict

This time we need to un-comment another line of config:

c.UseFullTypeNameInSchemaIds();

Ok, now its up and running, however there is one annoing error in the bottom which tels that there is a schema validation errors. The Swagger UI still works, but we don’t really want to see any error messages right? We can bypass it by un-commenting one more line of config:

c.DisableValidator();

swagger-services-mixed

Now it looks up and running, and I am even able to find my service method. However, Sitecore has a lot of Web API services that it uses itself and its quite hard to find my own service among others. It would be nice to separate Sitecore native Web API controllers from mine. Swagger supports API versioning, and from my perspective, this is one of ways how we can separate that. We can add a method to SwaggerConfig which will declare versions of API.

I will call Sitecore services as version “sc” and my own as “my_services“:

private static bool ResolveVersionSupportByRouteConstraint(ApiDescription apiDesc, string targetApiVersion)
{
	if (apiDesc.Route.RouteTemplate.StartsWith("-/api/v1") && targetApiVersion == "my_services") return true;
	if (apiDesc.Route.RouteTemplate.StartsWith("sitecore") && targetApiVersion == "sc") return true;
	return false;
}

And in the config we need to comment/remove this line

c.SingleApiVersion("v1", "Feature.Swagger");

And add this:

c.MultipleApiVersions(
	ResolveVersionSupportByRouteConstraint,
	vc =>
	{
		vc.Version("my_services", "My services API (you can switch to Sitecore API /swagger/docs/sc)");
		vc.Version("sc", "Sitecore services (you can switch to My services API /swagger/docs/my_services)");
	});

So now we have 2 separated views for Sitecore API and My API. They are treated as API versions by Swagger UI:

swagger-my-services

swagger-sitecore-services

Giving that documentation publicly, perhaps, is not a very good idea as it creates a security issue. So what we need to do is to hide Swagger UI behind a login and make it available only for Sitecore administrator users. We can re-use Sitecore’s defaul login page for this. From my point of view, the best way of doing it is using HTTP MessageHandlers. We can implement next message handler:

namespace Feature.Swagger
{
	using System;
	using System.Net;
	using System.Net.Http;
	using System.Threading;
	using System.Threading.Tasks;
	using Sitecore;

	public class SwaggerSitecoreAuthMessageHandler : DelegatingHandler
	{
		protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
		{
			var isAuthenticated = Context.User?.IsAuthenticated ?? false;
			var isAdministrator = Context.User?.IsAdministrator ?? false;

			if (this.IsSwaggerRequest(request) && (!isAuthenticated || !isAdministrator))
			{
				var response = new HttpResponseMessage(HttpStatusCode.Redirect);
				var uri = request.RequestUri;
				var currentOrigin = uri.AbsoluteUri.Replace(uri.PathAndQuery, string.Empty);
				var logiPage = currentOrigin + @"/sitecore/login?returnurl=/swagger";
				response.Headers.Location = new Uri(logiPage);
				return Task.FromResult(response);
			}
			else
			{
				return base.SendAsync(request, cancellationToken);
			}
		}

		private bool IsSwaggerRequest(HttpRequestMessage request)
		{
			return request.RequestUri.PathAndQuery.ToLowerInvariant().StartsWith("/swagger");
		}
	}
}

And then add a line in SwaggerConfig class like this:

GlobalConfiguration.Configuration.MessageHandlers.Add(new SwaggerSitecoreAuthMessageHandler());

Now we have successfully installed Swagger to a Sitecore solution. However, as I mentioned before, it uses WebActivator extensions, which is more like anti-pattern for web solutions and I would better get rid of it in order to keep helix principles in place. Instead, we can make it more Sitecore native way – run the config on Sitecore initialize pipeline.

So first we need to do is to remove this line:

[assembly: PreApplicationStartMethod(typeof(SwaggerConfig), "Register")]

And then create a super-simple initialize pipeline processor that runs Swagger initialization:

namespace Feature.Swagger.Pipelines.Initialize
{
	using Sitecore.Pipelines;

	public class InitializeSwagger
	{
		public void Process(PipelineArgs args)
		{
			SwaggerConfig.Register();
		}
	}
}

Of course we need to add that to a configuration file /App_Config/Include/Feature/Feature.Swagger.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="Feature.Swagger.Pipelines.Initialize.InitializeSwagger, Feature.Swagger"/>
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

Lastly, my full SwaggerConfig.cs file looks like this:

namespace Feature.Swagger
{
	using System.Web.Http;
	using System.Linq;
	using Swashbuckle.Application;
	using System.Web.Http.Description;

	public class SwaggerConfig
    {
        public static void Register()
        {
            var thisAssembly = typeof(SwaggerConfig).Assembly;

	        GlobalConfiguration.Configuration.MessageHandlers.Add(new SwaggerSitecoreAuthMessageHandler());

			GlobalConfiguration.Configuration
                .EnableSwagger(c =>
                    {
						c.MultipleApiVersions(
							ResolveVersionSupportByRouteConstraint,
							vc =>
							{
								vc.Version("my_services", "My services API (you can switch to Sitecore API /swagger/docs/sc)");
								vc.Version("sc", "Sitecore services (you can switch to My services API /swagger/docs/my_services)");
							});

						c.UseFullTypeNameInSchemaIds();

                        c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
                    })
                .EnableSwaggerUi(c =>
                    {
                        c.DisableValidator();
                    });
        }

	    private static bool ResolveVersionSupportByRouteConstraint(ApiDescription apiDesc, string targetApiVersion)
	    {
		    if (apiDesc.Route.RouteTemplate.StartsWith("-/api/v1") && targetApiVersion == "my_services") return true;
		    if (apiDesc.Route.RouteTemplate.StartsWith("sitecore") && targetApiVersion == "sc") return true;
		    return false;
	    }
	}
}

This is it, Swagger is integrated with our Sitecore solution. Read more about all Swagger capabilities at their website https://swagger.io

I have been using Swagger with Sitecore 8.x and Sitecore 9 versions, it all works!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s