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.

Create new project dialog

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.

.NET Aspire Dashboard

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:

  1. appsettings.json - To add the Sitecore configuration and connection details
  2. Program.cs - To add the necessary services and middleware capabilities of the SDK
  3. Controllers/DefaultController.cs - An MVC Controller that will capture and handle all traffic routed through the application
  4. Models/SitecoreSettings.cs - A model to reflect the appsettings configuration
  5. 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!