Setting up Sitecore ASP.NET Core SDK in a .NET Aspire project
I’ve recently been working on a project using .NET Aspire. This experience showed me the great potential of Aspire and how it can speed up the development process of large .NET platforms. During this time, I did wonder how Sitecore could fit into an Aspire architecture, so in this guide we will walk through the steps to get started with a new .NET Rendering Host for Sitecore on .NET Aspire.
Prerequisites
Before you begin, ensure you have the following installed:
- .NET SDK version 8+
- .NET Aspire templates installed
If you haven’t already, you can run this command
dotnet new install Aspire.ProjectTemplates
- An OCI compliant container runtime, such as Docker Desktop
- A Sitecore instance
In this article we’ll be using an XM Cloud instance - Visual Studio 2022+ (v17.9+)
Create a new .NET Aspire project
We start of creating a new, clean, .NET Aspire project using the dotnet CLI.
dotnet new aspire --output SitecoreAspireApp
This will give us a .NET solution with the Aspire AppHost and ServiceDefaults projects.
Next we will want to add an ASP.NET web project. Through Visual Studio I’m adding a new ASP.NET Core Empty project with the name SitecoreAspireApp.Web
to the solution.
In the creation dialog, you will notice a checkbox name Enlist in .NET Aspire orchestration
. Since we started off with an empty Aspire template, this won’t do anything.
Configure Aspire orchestration
Now that we have our base project setup created, we can configure the Aspire orchestration.
Orchestration happens by defining which modules, projects and/or containers to run in the SitecoreAspireApp.AppHost/AppHost.cs file.
Add a project reference from SitecoreAspireApp.AppHost
to SitecoreAspireApp.Web
and update the AppHost.cs file with the following code.
var builder = DistributedApplication.CreateBuilder(args);
builder.AddProject<Projects.SitecoreAspireApp_Web>("webfrontend")
.WithExternalHttpEndpoints();
builder.Build().Run();
In the Web project we will want to make use of the ServiceDefaults, a clean way to add base configuration and packages to any project, such as OpenTelemetry configuration.
Add a project reference from SitecoreAspireApp.Web
to SitecoreAspireApp.ServiceDefaults
and update the Program.cs in the Web project with the following code.
var builder = WebApplication.CreateBuilder(args);
// Add service defaults
builder.AddServiceDefaults();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Running the AppHost project will now result in Aspire running your Web project including Tracing and Healthchecks.
Adding the Sitecore .NET Core SDK
Next, we can setup the Sitecore .NET Core SDK in our Web project.
To do so, add the following NuGet packages to the Web project:
- Sitecore.AspNetCore.SDK.LayoutService.Client
- Sitecore.AspNetCore.SDK.RenderingEngine
The minimal setup of a Sitecore .NET Core SDK based application requires us to add/update five files:
- appsettings.json - To add the Sitecore configuration and connection details
- Program.cs - To add the necessary services and middleware capabilities of the SDK
- Controllers/DefaultController.cs - An MVC Controller that will capture and handle all traffic routed through the application
- Models/SitecoreSettings.cs - A model to reflect the appsettings configuration
- Models/Layout.cs - A model that will serve as the base for a Layout Service response
Add and/or update the files with the following code:
appsettings.json
Make sure to update the Sitecore settings according to your environment.
{
"Sitecore": {
"EdgeContextId": "",
"DefaultSiteName": "",
"EditingSecret": "",
"EnableEditingMode": true,
"EditingPath": "/api/editing/config"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Models/SitecoreSettings.cs
namespace SitecoreAspireApp.Web.Models;
public class SitecoreSettings
{
public static readonly string Key = "Sitecore";
public string? DefaultSiteName { get; set; }
public string? EditingSecret { get; set; }
public string? EdgeContextId { get; set; }
public bool EnableEditingMode { get; set; }
public string? EditingPath { get; set; }
public bool EnableLocalContainer { get; set; }
public Uri? LocalContainerLayoutUri { get; set; }
}
Models/Layout.cs
using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes;
namespace SitecoreAspireApp.Web.Models;
public class Layout
{
[SitecoreRouteField]
public string? Name { get; set; }
[SitecoreRouteField]
public string? DisplayName { get; set; }
[SitecoreRouteField]
public string? ItemId { get; set; }
[SitecoreRouteField]
public string? ItemLanguage { get; set; }
[SitecoreRouteField]
public string? TemplateId { get; set; }
[SitecoreRouteField]
public string? TemplateName { get; set; }
}
Controllers/DefaultController.cs
using Microsoft.AspNetCore.Mvc;
using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions;
using Sitecore.AspNetCore.SDK.RenderingEngine.Attributes;
using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions;
using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces;
using SitecoreAspireApp.Web.Models;
namespace SitecoreAspireApp.Web.Controllers;
public class DefaultController : Controller
{
private readonly SitecoreSettings? _settings;
private readonly ILogger<DefaultController> _logger;
public DefaultController(ILogger<DefaultController> logger, IConfiguration configuration)
{
_settings = configuration.GetSection(SitecoreSettings.Key).Get<SitecoreSettings>();
ArgumentNullException.ThrowIfNull(_settings);
_logger = logger;
}
[UseSitecoreRendering]
public IActionResult Index(Layout model)
{
IActionResult result = Empty;
ISitecoreRenderingContext? request = HttpContext.GetSitecoreRenderingContext();
if ((request?.Response?.HasErrors ?? false) && !IsPageEditingRequest(request))
{
foreach (SitecoreLayoutServiceClientException error in request.Response.Errors)
{
switch (error)
{
case ItemNotFoundSitecoreLayoutServiceClientException:
result = View("NotFound");
break;
default:
_logger.LogError(error, "{Message}", error.Message);
throw error;
}
}
}
else
{
result = View(model);
}
return result;
}
public IActionResult Error()
{
return View();
}
private bool IsPageEditingRequest(ISitecoreRenderingContext request)
{
return request.Controller?.HttpContext.Request.Path == (_settings?.EditingPath ?? string.Empty);
}
}
Program.cs
using Microsoft.AspNetCore.Localization;
using Sitecore.AspNetCore.SDK.GraphQL.Extensions;
using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions;
using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions;
using SitecoreAspireApp.Web.Models;
using System.Globalization;
var builder = WebApplication.CreateBuilder(args);
// Add service defaults
builder.AddServiceDefaults();
SitecoreSettings? sitecoreSettings = builder.Configuration.GetSection(SitecoreSettings.Key).Get<SitecoreSettings>();
ArgumentNullException.ThrowIfNull(sitecoreSettings);
builder.Services.AddRouting()
.AddLocalization()
.AddMvc();
builder.Services.AddGraphQLClient(configuration =>
{
configuration.ContextId = sitecoreSettings.EdgeContextId;
})
.AddMultisite();
if (sitecoreSettings.EnableLocalContainer)
{
// Register the GraphQL version of the Sitecore Layout Service Client for use against local container endpoint
builder.Services.AddSitecoreLayoutService()
.AddGraphQLHandler("default", sitecoreSettings.DefaultSiteName!, sitecoreSettings.EdgeContextId!, sitecoreSettings.LocalContainerLayoutUri!)
.AsDefaultHandler();
}
else
{
// Register the GraphQL version of the Sitecore Layout Service Client for use against experience edge
builder.Services.AddSitecoreLayoutService()
.AddGraphQLWithContextHandler("default", sitecoreSettings.EdgeContextId!, siteName: sitecoreSettings.DefaultSiteName!)
.AsDefaultHandler();
}
builder.Services.AddSitecoreRenderingEngine()
.ForwardHeaders();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseMultisite();
app.UseStaticFiles();
const string defaultLanguage = "en";
app.UseRequestLocalization(options =>
{
// If you add languages in Sitecore which this site / Rendering Host should support, add them here.
List<CultureInfo> supportedCultures = [new CultureInfo(defaultLanguage)];
options.DefaultRequestCulture = new RequestCulture(defaultLanguage, defaultLanguage);
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.UseSitecoreRequestLocalization();
});
app.MapControllerRoute(
"error",
"error",
new { controller = "Default", action = "Error" }
);
app.MapSitecoreLocalizedRoute("sitecore", "Index", "Default");
app.MapFallbackToController("Index", "Default");
app.Run();
After making these changes, you should be able to start the AppHost and see your Web application running.
From this point you will be able to follow the official Sitecore documentation to continue building out your Head application.
Final thoughts
The Aspire framework offers great value in being able to easily use modules to integrate with various 3rd party tools, such as Redis, Entity Framework, Kafka and many others. This alone makes it a great starting point for any Sitecore platform.
The only thing that may feel odd is how to fit the AppHost and ServiceDefaults projects in the Sitecore Helix guidelines. These projects provide features that could fit in any of the Helix layers, so you yourself will have to find out where it fits best for your solution.
Overall, I highly recommend using Aspire in your next .NET based project!