When Umbraco V8 released back in February 2019, one of the biggest changes that it brought about was the introduction of Components and Composers. This feature effectively replaces the ApplicationEventHandler which existed in the previous versions of Umbraco. Umbraco, the application, is a Composition of many different collections and many single items called Components. If your Umbraco V8 site or backoffice needs any customisations, using Components and Composers is the right way to achieve it.
There can be 2 parts to any customisation which you wish to achieve on your Umbraco V8 site or backoffice.
- Component - A C# class which contains your customisations
- Composer - A C# class which registers your customisations/components
By using a combination of Components and Composers you can add, update or even replace existing functionality in Umbraco with your own customisations.
What is a component?
A component is a C# class which inherits the IComponent interface. There are two methods which needs implementation - Initialize() and Terminate(). You can use the Initialize() method to implement your customisation.
Registering your customisations
A Component on its own cannot add any functionality to your Umbraco V8 website or backoffice. You need a Composer to register your Component.
There are different types of Composers
-
InitialComposers - The CoreInitialComposer and the WebInitialComposer which sets up everything required for Umbraco to run. The CoreInitialComposer runs before the WebInitialComposer . Registering the database builder, registering the various services in Umbraco, registering the routes for RenderMvcControllers, APIControllers and SurfaceControllers in Umbraco etc are some of the responsibilities of these composers. These composers runs before any other composers and there should only be one instance of these composers. This was called the 'IRuntimeComposer' initially and changed to the "InitialComposers" with a better composing order in a later release.
-
ICoreComposer - The ICoreComposers run after the InitialComposers have composed. Some of the responsibilities covered by ICoreComposers include registering ModelsBuilder, registering the log viewer, registering the component responsible for writing into the audit logs etc.
-
IUserComposer - These composers are the last to compose. These composers can be used by implementors/developers to register their customisations and by package developers for extending the functionality in Umbraco.
Combining the power of Components and Composers a wide variety of things can be achieved. Let's have a look at some of those.
Cross-Origin Resource Sharing (CORS)
CORS is a mechanism that allows resources on a domain to be accessed by another domain safely. For security reasons, when a web page at a domain abc.com tries to access a resource at xyz.com using scripts, browsers restrict and block such requests. This can be overcome using additional HTTP headers on resources at abc.com. A very common use case is a javascript method trying to call an API method in a different domain. In this case the domain from which the javascript method calls the API has to be allowed as an origin on your API.
The first step to enabling an origin on the API is installing the WebAPI CORS nuget package. Once this is done I can use a Component to enable CORS and register the component using a Composer.
public class CorsComponent :IComponent
{
/// <summary>Initializes the component.</summary>
public void Initialize()
{
//enable all origins, headers and methods
var corsAttr = new EnableCorsAttribute("*", "*", "*");
GlobalConfiguration.Configuration.EnableCors(corsAttr);
}
/// <summary>Terminates the component.</summary>
public void Terminate()
{
}
}
public class CorsComposer :IUserComposer
{
/// <summary>Compose.</summary>
public void Compose(Composition composition)
{
composition.Components().Append<CorsComponent>();
}
}
I am allowing my API to be very open. In reality you will want to lock it down to specific origins.
I also added the following to the <customHeaders> in the web.config as the final step. Now I can call my API using a script in a completely different domain.
<remove name="Access-Control-Allow-Origin" />
<remove name="Access-Control-Allow-Credentials" />
<remove name="Access-Control-Allow-Methods" ></remove>
<add name="Access-Control-Allow-Credentials" value="true" />
Message Handlers
A message handler is a class that accepts a HTTP request and serves an HTTP response. It derives from the abstract HTTPMessageHandler class. Message handlers are chained, one message handler accepts a request, processes it and handles the request to next. At some point when all handlers in the pipeline execute a response goes back. This pattern is called a delegating handler.
Earlier this year I did another Skrift article on how we can use Umbraco Heartcore to power an Alexa Skill. In order to get an Alexa Skill live on the Skills store, the API must pass certain security requirements. One of the checks include verifying the request signature and confirming that it matches the pattern used by Alexa. This needs intercepting of the incoming request from Alexa and is not something we can handle at the controller level. I used a message handler in this case to verify the request signature and pass the request on to the controller if it verifies successfully.
As an example let us look at a message handler which does some processing every time an action in a controller is hit. I am using writing to the logs as an example here as its easy to verify using the LogViewer in the backoffice.
public class MyRequestValidationHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken)
{
// I want it to log only when I call this api method, otherwise it will do for every single call made including every call within the back office too
if (requestMessage.RequestUri.AbsolutePath.InvariantContains("umbraco/api/testapi"))
{
Current.Logger.Info(typeof(MyRequestValidationHandler),"I have been in the request handler");
}
return await base.SendAsync(requestMessage, cancellationToken);
}
}
Now I can add my message handlers to the collection of message handlers. I also have to register my component using a composer.
public class MyValidationHandlerComponent : IComponent
{
public void Initialize()
{
GlobalConfiguration.Configuration.MessageHandlers.Add(new MyRequestValidationHandler());
}
public void Terminate()
{
}
}
public class MyValidationHandlerComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.Components().Append<MyValidationHandlerComponent>();
}
}
Handling events in the back office
I personally think this is one of the most common use cases of Composers in Umbraco. Very often in our projects, we might want to hook into specific processes in the back office. For eg: process something when a node is published or a media item is saved or a member is saved. In V7 ApplicationEventHandler would be the place to handle them but in V8 we need to use the combination of Components and Composers.
As an example let us look at how I can do some processing every time the member information is saved. For this I can use the Saved event in the MemberService. This event gets called when the member information has been saved and data has been persisted. I use a component to handle the Saved event and register it using a Composer.
Note that I am also injecting an ILogger into my Component which helps me write to the Umbraco logs.
public class MyMemberSaveEventComponent : IComponent
{
private readonly ILogger _logger;
public MyMemberSaveEventComponent(ILogger logger)
{
this._logger = logger;
}
public void Initialize()
{
MemberService.Saved += this.MemberService_Saved;
}
private void MemberService_Saved(IMemberService sender, SaveEventArgs<IMember> e)
{
foreach (var member in e.SavedEntities)
{
this._logger.Info<MyPublishEventComponent>("member {member} has been saved and event fired!", member.Name);
}
}
public void Terminate()
{ }
}
public class MyEventHandlerComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.Components().Append<MyMemberSaveEventComponent>();
}
}
Adding new fields to the Examine Index
Another area where Components and Composers can be of help is when we want to add a new field into the Examine index. A very common use case for this is enable sorting of blog posts or articles on a website based on a date using Examine. With every field in a document type and its value written into the External Index in Umbraco this sounds straightforward. But there is a catch here that all fields written into the index are in full text format - be it a number or a date. To store field value as an integer or datetime in the index the some work needs to be done. In V7 this is something we would have done using the ExamineIndex.config. This config no longer exists in V8 so we have to write a fair bit of C# code to achieve this.
In my Component I try and get the External Index first. All operations which involves changing values or adding values to an Index involves handling the TransformingIndexValues event. In V7 it was called GatheringNodeData. This event is available on the BaseIndexProvider so I cast my external index to a BaseIndexProvider. I also specify a new FieldDefinition called blogDateLong and add it to the FieldDefinitionCollection of the external index. The field definition is of type Long because I plan to store the value of the blog date as DateTime.Ticks. In my event handler I get the value of the blogDate field if the item being indexed is a blogPost and set the value of the blogDateLong field which I added to the FieldDefinitionCollection to blogDate.Date.Ticks. Being a Component it needs to be registered using a Composer . Now I can use the blogDateLong field to sort the blog posts in the front end.
Note: I picked up this little trick of storing Datetime as a long value from the fantastic Nik Rimington. Thank you Nik :-)
public class ExamineEventsComponent : IComponent
{
private readonly IExamineManager _examineManager;
private readonly IUmbracoContextFactory _umbracoContextFactory;
public ExamineEventsComponent(IExamineManager examineManager, IUmbracoContextFactory umbracoContextFactory)
{
this._examineManager = examineManager;
this._umbracoContextFactory = umbracoContextFactory;
}
/// <summary>Initializes the component.</summary>
public void Initialize()
{
//get the external index
if (!_examineManager.TryGetIndex(Constants.UmbracoIndexes.ExternalIndexName, out IIndex index))
return;
//we need to cast because BaseIndexProvider contains the TransformingIndexValues event
if (!(index is BaseIndexProvider indexProvider))
throw new InvalidOperationException("Could not cast");
//event handler
indexProvider.TransformingIndexValues += IndexProviderTransformingIndexValues;
//add a new field definition to the index
index.FieldDefinitionCollection.TryAdd(new FieldDefinition("blogDateLong", FieldDefinitionTypes.Long));
}
private void IndexProviderTransformingIndexValues(object sender, IndexingItemEventArgs e)
{
if (e.ValueSet.Category == IndexTypes.Content)
{
//get node id
if (int.TryParse(e.ValueSet.Id, out var nodeId))
{
switch (e.ValueSet.ItemType)
{
case "blogpost": //check whether the node being published is blogpost
using (var umbracoContext = _umbracoContextFactory.EnsureUmbracoContext())
{
//get the content node
var contentNode = umbracoContext.UmbracoContext.Content.GetById(nodeId);
if (contentNode != null)
{
//get the date
var blogDate = contentNode.Value<DateTime>("blogDate");
//set the value for the newly added field
e.ValueSet.Set("blogDateLong", blogDate.Date.Ticks);
}
}
break;
}
}
}
}
/// <summary>Terminates the component.</summary>
public void Terminate()
{
}
}
public class ExamineEventComposer : IUserComposer
{
/// <summary>Compose.</summary>
public void Compose(Composition composition)
{
composition.Components().Append<ExamineEventsComponent>();
}
}
Scheduled Tasks
One of the new features with V8 is the ability to run background tasks. This would have been achieved in V7 using Hangfire or using a application pool relative task specified in umbracosettings.config. With V8 we can achieve this using a BackgroundTaskRunner registered using a Component and a Composer.
The first part to this is setting up a RecurringTask. This can be done by using a C# class which inherits from the RecurringTaskBase. This class provides a base class for recurring background tasks and can be implemented by overriding the PerformRun
method. The task can also run asynchronously. In this instance the IsAsync must be overridden and set to true and the PerformRunAsync method should be overridden. Now I need to add this task to a BackgroundTaskRunner
using a Component and register it using a Composer. A BackgroundTaskRunner
is responsible for running my ScheduledLogger
task.
public class ScheduledLogger : RecurringTaskBase
{
private ILogger _logger;
public ScheduledLogger(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayBeforeWeStart, int howOftenWeRepeat, ILogger logger)
: base(runner, delayBeforeWeStart, howOftenWeRepeat)
{
_logger = logger;
}
public override bool PerformRun()
{
_logger.Info<ScheduledLogger>("The scheduler has ran");
return true;
}
public override bool IsAsync => false;
}
public class ScheduledLoggerComponent : IComponent
{
private readonly ILogger _logger;
//background task runner is what actually runs a task, you define a background task runner, create a recurring task and add the recurring task to the task runner
private BackgroundTaskRunner<IBackgroundTask> _loggerRunner;
public ScheduledLoggerComponent(ILogger logger)
{
_logger = logger;
_loggerRunner = new BackgroundTaskRunner<IBackgroundTask>("Scheduled Logger", _logger);
}
public void Initialize()
{
int delayBeforeWeStart = 60000; // 60000ms = 1min
int howOftenWeRepeat = 300000; //300000ms = 5mins
var task = new ScheduledLogger(_loggerRunner, delayBeforeWeStart, howOftenWeRepeat, _logger);
//As soon as we add our task to the runner it will start to run (after its delay period)
_loggerRunner.TryAdd(task);
}
public void Terminate()
{
throw new NotImplementedException();
}
}
public class ScheduledLoggerComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.Components().Append<ScheduledLoggerComponent>();
}
}
I am using logging as an example here again as I can easily verify it using the LogViewer in the backoffice. So everytime my task runs it writes into the logs.
Dependency Injection
One of the biggest enhancements to Umbraco v8 is that Dependency Injection is supported out of the box. LightInject is now shipped with Umbraco. Dependency Injection is really the superpower in Umbraco V8. All of the above examples is made possible by Dependency Injection. The composition object exposes the LightInject container by implementing the IRegister interface and the various Register overloads can be used to register the dependencies.
By making use of a Composer we can register our own dependencies. In my example I can register my service type IMyService and the implementing type MyService to the DI container using a Composer. We do not need a Component in this instance.
public interface IMyService
{
List<string> GetSomeString();
}
public class MyService :IMyService
{
public List<string> GetSomeString()
{
return new List<string>(){"string 1", "string 2"};
}
}
public class MyServiceComposer :IUserComposer
{
public void Compose(Composition composition)
{
composition.Register<IMyService, MyService>();
}
}
Once this has been registered I can inject my service into controllers.
public class DemoSurfaceController : SurfaceController
{
private readonly IMyService _myService;
//inject my service DI works out the implementation
public DemoSurfaceController(IMyService myService)
{
this._myService = myService;
}
// GET: DemoSurface
public ActionResult GetVersions()
{
var someString = this._myService.GetSomeString();
return this.PartialView("GetVersions", someString);
}
}
DI in Umbraco can help you achieve a lot more. See the documentation to gain an understanding about this in greater detail.
Custom Sections
The Umbraco backoffice consists of many sections or applications like Content, Media, Settings. We can extend this functionality and create custom sections. In Umbraco V7 the starting point for creating a custom section was to use applications.config. This config file no longer exist in v8 and we have to use a package.manifest or write a C# class to accomplish this.
To add a custom section in Umbraco we need a class that derives from the ISection interface. My custom section needs to be appended to the SectionsCollectionBuilder using a Composer.
public class CustomCodeSection : ISection
{
/// <inheritdoc />
public string Alias => "customCodeSection";
/// <inheritdoc />
public string Name => "Custom Code Section";
}
public class SectionComposer : IUserComposer
{
/// <summary>Compose.</summary>
public void Compose(Composition composition)
{
composition.Sections().Append<CustomCodeSection>();
}
}
The above code adds my custom section as the last item in the list of sections. We can use some other methods available on the SectionsCollectionBuilder to control the order of sections in the back office.
//insert a section at a particular position 4 (based on a zero index)
composition.Sections().Insert<CustomCodeSection>(4);
//insert after packages section
composition.Sections().InsertAfter<PackagesSection, CustomCodeSection>();
//insert before packages section
composition.Sections().InsertBefore<SettingsSection, CustomCodeSection>();
I picked this up during a conversation in the Community Slack channel where I was told about this little tip by Matt Brailsford and Anders Bjerner. I can see that Anders has also updated the Docs on Our. #h5yr both of you!
Custom Dashboards
Every section in Umbraco has dashboards. In V7 dashboards.config was the starting point for adding custom dashbaords. In V8 we have to use package.manifest or write a C# class which derives from the IDashboard interface to create custom dashboards. But unlike custom sections, dashboards are type scanned at start up which means Umbraco will automatically register them for you. We don't need to use a Composer . However if we need to remove a dashboard or change something about an existing dashboard we need to use a Composer. This example shows how we can remove the Welcome dashboard in the settings section.
public class DashboardComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.Dashboards().Remove<SettingsDashboard>();
}
}
We can also update permissions for existing dashboards. Suppose I want a "Settings Only" user group who has access to the Settings section in Umbraco but not want access to the Examine Management Dashboard I can update the AccessRules to deny access to the user group and then re-add the dashboard after removing the existing one.
[Weight(20)]
public class MyExamineManagementDashboard : ExamineDashboard, IDashboard
{
public new IAccessRule[] AccessRules
{
get
{
var rules = new IAccessRule[]
{
new AccessRule {Type = AccessRuleType.Deny, Value = "settingsOnly"}
};
return rules;
}
}
}
public class DashboardComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.Dashboards().Remove<ExamineDashboard>().Add<MyExamineManagementDashboard>();
}
}
Note that I have added a the Weight attribute with a value of 20 to my override Examine Dashboard. The Weight attribute controls the order of appearance of dashboards. The out of the box Examine Dashboard has a weight of 20 so by keeping the weight same for my override dashboard it re-adds it into the original position but with the extra access rules.
This might not represent a complete real-world scenario but its a part of my experiments where I was trying to see whether I can give a user access to the Settings section without any tree menu options or the ability to save.
Bundling and Minification
Bundling and Minification is a ASP.Net feature whereby you can create bundles of css and script files. This improves load time as there are fewer HTTP requests and reduced size of the files. While ClientDependency ships with Umbraco and aids in minification its always good to know that we can use this ASP.Net feature in Umbraco.
As a starting point to this the nuget package Microsoft.AspNet.Web.Optimization needs installing.
Install-Package Microsoft.AspNet.Web.Optimization
Now I can create a Component to create my bundles and register it using a Composer. In my Component I create a StyleBundle in the path ~/bundles/styles
and add it to the Tables collection in BundleTable. I have only one stylesheet specified in the Include method but it can accept a string array if we want to add multiple stylesheets into the bundle. I have also created a ScriptBundle with a single script file but it can accept a string array too. This takes care of the concatenation and minification of my stylesheets and scripts. I have also set BundleTable.EnableOptimizations = true; to force minification in debug mode.
public class BundleComponent : IComponent
{
public void Initialize()
{
BundleTable.Bundles.Add(new StyleBundle("~/bundles/styles").Include(
"~/css/umbraco-starterkit-style.css"));
BundleTable.Bundles.Add(new ScriptBundle("~/bundles/scripts").Include("~/scripts/umbraco-starterkit-app.js"));
BundleTable.EnableOptimizations = true;
}
public void Terminate()
{
throw new System.NotImplementedException();
}
}
public class BundleComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.Components().Append<BundleComponent>();
}
}
I also add ~/bundles to umbracoReservedPaths appSetting in web.config.
<add key="umbracoReservedPaths" value="~/bundles/" />
Now I can use my script and stylesheet bundles in my view.
@Scripts.Render("~/bundles/scripts)
@Styles.Render("~/bundles/style")
File System Providers
File system providers tells Umbraco where to store certain files. The first thing that comes running into our minds is probably the media storage. By default Umbraco stores media to a media folder in the file system. It can be changed by using a Composer
public class SetMediaFileSystemComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.SetMediaFileSystem(() => new PhysicalFileSystem("~/your-folder-name"));
}
}
We can also write our own FileSystem. Some of the most popular packages around this are the UmbracoFileSystemProviders.Azure.Media and UmbracoFileSystemProviders.Azure.Forms which saves the media and Umbraco Forms Data into Azure blob storage.
Content Apps
Content Apps are companions to the editing experience in Umbraco. This was probably the most spoken about features in V8. We can create our own Content Apps for content, media and members and may be even doctypes going ahead, thanks to this wonderful PR by patrickdemooij9.
Content Apps can be created using a package.manifest or by writing a C# class. To create a Content App the C# way I start with a class which derives from IContentAppFactory and implement the GetContentAppFor method. My Content App is available to all user groups but it can restricted to specific user groups. The Content App is available on Content, Media and Member nodes and restricting it to content or media or member alone is something that can be achieved. Content Apps always appear between the Content and Info content apps in the backoffice with the order of appearance controlled by the Weight. I also append my Content App to the ContentApps collection builder in my composer for it to be picked up in the back office.
public class CustomCodeContentApp : IContentAppFactory
{
public ContentApp GetContentAppFor(object source, IEnumerable<IReadOnlyUserGroup> userGroups)
{
var contentApp = new ContentApp
{
Alias = "customCodeContentApp",
Name = "Custom Code Content App",
Icon = "icon-hearts",
View = "/App_Plugins/ContentApps/CustomCodeContentApp/view.html",
Weight = 0
};
return contentApp;
}
}
public class CustomCodeContentAppComposer : IUserComposer
{
public void Compose(Composition composition)
{
// Append the content app to the collection
composition.ContentApps().Append<CustomCodeContentApp>();
}
}
Attribute Routing in Web APIs
Attribute routing is an MVC5 feature whereby you can define the routes using attributes. This gives greater control on the resulting route urls. The Umbraco Web API controllers are auto-routed and the url is in the format ~/Umbraco/Api/[YourControllerName]/[ActionName]. Using Attribute Routing we can make this url simpler.
To enableAttribute Routing for Web APIs I start off with a Component, calling theMapHttpAttributeRoutes() method. I also have to register my Component using a Composer. Now I can add a route attribute to my Web API controller action. I have specified a route urlapi/getdata which means my api will return some results at this url rather than the auto-routed url umbraco/api/TestAPI/GetData .
public class AttributeRoutingComponent :IComponent
{
public void Initialize()
{
GlobalConfiguration.Configuration.MapHttpAttributeRoutes();
}
public void Terminate()
{
}
}
public class AttributeRoutingComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.Components().Append<AttributeRoutingComponent>(); ;
}
}
public class TestAPIController : UmbracoApiController
{
[Route("api/getdata")]
[HttpGet]
public string GetData()
{
return "Hello there from API via attribute routing";
}
}
Use Custom MVC Controllers
There could be a scenario where we need some custom mvc controllers to be used in our web application. Let us see how we can achieve that in Umbraco V8. The aim is to have a url /non-umbraco-page which has a custom MVC controller and view to be rendered while keeping the Umbraco part intact.
I start off with my Custom MVC controller which has an Index action. Note that this is an MVC controller not the Umbraco RenderMvcController or SurfaceController. Now I specify a component which registers the custom route for me. In this example the route url is non-umbraco-page which is served up by the NonUmbracoPage controller and the index action. I also need to register this Component. In my composer, in addition to registering my Component, I also need to register my custom MVC controller as custom MVC controllers are not registered automatically to the LightInject DI container in Umbraco. Now if I browse to the url /non-umbraco-page, the view specified in my Index action should show up.
public class NonUmbracoPageController : Controller
{
// GET
public ActionResult Index()
{
return View("~/Views/NonUmbracoPage.cshtml");
}
}
public class NonUmbracoRouteComponent : IComponent
{
public void Initialize()
{
RouteTable.Routes.MapRoute("NonUmbracoRoute", "non-umbraco-page",
new { controller = "NonUmbracoPage", action = "Index" });
}
public void Terminate()
{
}
}
public class NonUmbracoRouteComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.Register<NonUmbracoPageController>();
composition.Components().Append<NonUmbracoRouteComponent>(); ;
}
}
Quicker way of adding a Component
All through my article I have been deriving my composer from IComposer and implementing the Compose method explicitly to register my Component. There is a quicker way of doing it by deriving the Composer from ComponentComposer<MyComponentType>. This will automatically add MyComponentType to the collection of Components. As an example, my Composer for CORS can be rewritten like below.
public class CorsComposer :ComponentComposer<CorsComponent>, IUserComposer
{
}
There is more that you can achieve with Composer like controlling the order of Composers. I have added a link to the official Umbraco Docs in the Resources section. Most of these samples are also present in the GitHub repo which I put together for my talk at the London and Manchester meet-up earlier this year. I have added the links to it in the resources section.
As the name of the article suggests Components and Composers are everywhere in Umbraco V8. Think of a customisation that you wish to do and it will involve Components and Composers. Happy composing!
Resources
- https://our.umbraco.com/Documentation/Reference/Events/Application-Startup-v7
- https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
- https://www.codecademy.com/articles/what-is-cors
- https://docs.microsoft.com/en-us/aspnet/web-api/overview/advanced/http-message-handlers
- https://our.umbraco.com/documentation/Reference/Events/
- https://our.umbraco.com/documentation/Reference/Searching/Examine/
- https://our.umbraco.com/documentation/Reference/Scheduling/
- https://www.tutorialsteacher.com/ioc/dependency-injection
- https://our.umbraco.com/Documentation/Reference/Using-Ioc/
- https://our.umbraco.com/documentation/Extending/Section-Trees/sections
- https://our.umbraco.com/documentation/Extending/Dashboards/
- https://24days.in/umbraco-cms/2019/dashboards-plus-migration/dashboards-and-sections/
- https://our.umbraco.com/documentation/Extending/FileSystemProviders/
- https://our.umbraco.com/documentation/Extending/Content-Apps/
- https://our.umbraco.com/documentation/implementation/composing
- https://github.com/poornimanayar/UmbracoLondonMeetup.Demo.V8Way
- https://devblogs.microsoft.com/aspnet/attribute-routing-in-asp-net-mvc-5/
- https://our.umbraco.com/Documentation/Reference/Routing/