Umbraco dotnet core config

Heads Up!

This article is several years old now, and much has happened since then, so please keep that in mind while reading it.

The Unicore project started in 2020 with the aim to migrate Umbraco from .NET Framework to .NET Core. Bjarke Berg leads the Unicore project and stewards the recently refreshed community team. We’ve had some great contributions from within the team and from the wider Umbraco community already.

Cut to December 2020 and we have an Alpha Umbraco release running on .NET Core. This opens Umbraco up to a range of exciting options, as well as encouraging community involvement from .NET Core developers who may be new to Umbraco. It also gives those Umbraco developers who are more well-versed in .NET Framework the chance to learn .NET Core in a familiar, supportive place.

If your world has been Umbraco and .NET Framework for so long, the idea of having to learn .NET Core might be intimidating. How long will it take to adjust? Will you need to relearn everything you know about developing Umbraco?

We are here to demystify this in advance.

 

Lego Unicorn

 

Unicore aims

We want to help the Umbraco developer on their journey to become as confident developing Umbraco on .NET Core as they are on .NET Framework (hopefully even more!).

We aim to:

  • Enable .NET Framework developers to adjust to developing in .NET Core
  • Utilise the best of .NET Core for Umbraco
  • Encourage new community contributions
  • Ensure Umbraco package developers can make changes for their packages to run on .NET Core

There are many existing integrations using Umbraco, and whilst we aren’t able to avoid breaking changes entirely, we’ve kept them in mind at every turn.

How?

We are in contact with the other Umbraco community teams to ensure a smoother transition during the upgrade.

From the start of the Unicore project large-scale refactoring of Umbraco was out of scope, as was a rewrite of the back-office AngularJS code. By keeping the changes to Umbraco Web API and the database as minimal as possible we reduce the need for integrations to change.

Utilising core strengths

Although we’re trying to minimise breaking changes, there are some areas where we want to utilise the strengths of .NET Core.

It is worth noting that Microsoft have named the next major version of .NET Core after 3.1 .NET 5, but for the purpose of this article I'll still be referring to it as .NET Core. 

For historical reasons Umbraco has implemented things its own way in a few areas within the codebase. Now we have alternatives available in .NET Core itself. Rather than porting the existing Umbraco code in these areas, we're adopting the standard .NET Core approach. It also allows us to offload responsibility for some lower-level concerns to the underlying platform.  

We're also keeping an eye on the future, when new .NET Core developers come to the Umbraco ecosystem to contribute to its development. Standard .NET Core approaches will be more familiar, lowering the slope of the on-ramp to understanding the Umbraco code base.

Some examples of these standard .NET Core approaches are:

This article focuses on the changes we’re making to Umbraco configuration.

You might not often need to edit Umbraco config files, relying instead on the default Umbraco installation with an updated connection string to your database. That is totally fine, and in the future .NET Core Umbraco build, if you rarely need to update config you’ll have a similar experience. However, if you’re a package developer, or tend to build solutions with extensive integrations, then you’ll want to hear how config is changing. Or maybe you’re just curious? Come along for the ride and find out!

Umbraco on .NET Framework

Before moving forward, it is good to look back. So, before we get into the specifics of how configuration is changing for .NET Core, we’ll look at Umbraco as it is right now, and talk about what is happening under the hood.

When you run any Umbraco site version 8 or below, it is powered by Microsoft .NET Framework, the original implementation of .NET now on its last release version. For background into how .NET Framework runs and compiles, you can delve into more detail in the Microsoft docs.

Umbraco currently runs on .NET Framework. But what framework is the web application built in? Enter ASP.NET Framework.

ASP.NET Framework

Until the Unicore project, the Umbraco web application always ran on ASP.NET Framework. ASP.NET is Microsoft’s .NET Framework technology for creating web apps, and it shares the core functionality of .NET. ASP.NET comes in three main flavours: 

  • Web Forms
  • Web Pages
  • MVC

There is also Web API, which is a separate area of ASP.NET. Umbraco currently uses a combination of MVC and the ASP.NET Web API framework. 

Config in .NET Framework Umbraco

As an ASP.NET Framework application, Umbraco 8 uses configuration files to store settings. Config files are editable XML files that contain elements. Within these element structures you can edit attributes to control various configuration settings within your application.

You don’t have to compile your application whenever a setting needs to change. At runtime, ASP.NET uses the information provided by the config files.

There are three types of configuration files in .NET Framework apps, in hierarchy order:

  • Machine
  • Application
  • Security

We’re interested in the application config file, which contains settings specific to the application. As an ASP.NET Framework hosted web app, Umbraco uses an application-specific web.config file.

Web.config

The Umbraco web.config controls settings such as:

  • Length of time until users in the back-office are logged out
  • Location of the Umbraco database using a connection string
  • Version and location of assemblies Umbraco uses at runtime
    <connectionStrings>
        <remove name="umbracoDbDSN" />
        <add name="umbracoDbDSN" connectionString="" providerName="" />
    </connectionStrings>

Umbraco web.config file

The release of Umbraco 8 updated several XML configuration files to be configurable through code (Trees.config and Dashboards.config, for example). The following config files remain in the Umbraco 8 /config folder:

  • tinyMceConfig.config 
  • umbracoSettings.config 
  • clientdependency.config
  • healthchecks.config
  • imageprocessor:
    • cache.config
    • security.config
    • processing.config

This doesn’t include any other config files you may reference via installation of a third-party package, such as uSync. The Umbraco web.config references several of these configuration files via the configSource attribute:

    <umbracoConfiguration>
        <settings configSource="config\umbracoSettings.config" />
        <HealthChecks configSource="config\HealthChecks.config" />
    </umbracoConfiguration>
    <clientDependency configSource="config\ClientDependency.config" />

Example of Umbraco web.config referencing other config files

Accessing config values in Umbraco 8

Umbraco 8 accesses configuration values in several ways. If you’re building a standard Umbraco website with no customisation required, you can usually let Umbraco handle the access of config values with no custom code required.

However, when building a custom Umbraco 8 application, you might want access to a particular config value, perhaps for a package you’re building. In this instance, you would use the Umbraco 8 composition technique to add the config to the composition.Configs property:

composition.Configs.Add<IModelsBuilderConfig>(() => new ModelsBuilderConfig());

Adding the config you need to composition.Configs (ModelsBuilder example)

Behind the scenes, Umbraco 8 binds the config class to the Configs property. Now you can access the config in two ways: 

1. Injecting the interface of the config class you need, e.g. IGlobalSettings

internal BackOfficeServerVariables(
	UrlHelper urlHelper, 
	IRuntimeState runtimeState, 
	UmbracoFeatures features, 
	IGlobalSettings globalSettings)

Injecting IGlobalSettings into an Umbraco 8 class

2. By directly calling the Configs object on the static class, e.g. the static property Configs.Global():

if (Current.Configs.Global().UseHttps)

Using the static Current class to access the global config

For various reasons (including testability), the ideal is always to inject the config dependency rather than to access the static object. This style of injecting the config will be more matched to the .NET Core approach that Umbraco will take in future.

You can find an example of how Kevin Jump's uSync package currently uses this config Dependency Injection (DI) approach in HistoryComponent.cs:

public HistoryComponent(SyncFileService syncFileService,
            IUmbracoContextFactory umbracoContextFactory,
            IGlobalSettings globalSettings)
        {
            this.umbracoContextFactory = umbracoContextFactory;

            this.syncFileService = syncFileService;
            historyFolder = Path.Combine(globalSettings.LocalTempPath, "usync", "history");

            uSyncService.ImportComplete += USyncService_ImportComplete;
        }

uSync HistoryComponent.cs example by Kevin Jump on GitHub

This approach will likely make switching the package to .NET Core easier. We’ll dive into injectable config later, but essentially, if a package uses Current.Configs.Global (relying on the static Current class), the approach will need to change to remove the use of Current and switch to the injected config object instead.

Package developers can do this now in readiness for the .NET Core migration – remove the use of Current in their packages. You can review a similar pull request where Benjamin Carleski removed many references to Current.Services, replacing them with injected services. 

We've covered how to access config in .NET Framework Umbraco 8 during external development. However, if you’re working within the Umbraco 8 source code itself, you’ll see that the code accesses the config using either of the above two methods (dependency injection and Current.Configs()), along with three more ways:

3. Accessing an instance of the config type via Umbraco’s custom GetInstance() method (often used in the unit tests):

var umbracoSettings = Factory.GetInstance<IUmbracoSettingsSection>();
var globalSettings = Factory.GetInstance<IGlobalSettings>();

Getting the config instance from the Umbraco factory

4. Using the default .NET Framework ConfigurationManager:

var configuredTypeName = ConfigurationManager.AppSettings[Constants.AppSettings.RegisterType];

Using the ConfigurationManager to access a required app setting

5. Using the default .NET Framework WebConfigurationManager:

var config = WebConfigurationManager.OpenWebConfiguration(appPath);

Getting the config instance from the Umbraco factory

As you can tell, there are a few different implementations to currently access Umbraco configuration. 

We’ve recapped how we work with config within Umbraco as an ASP.NET Framework web application today. Next, we will share how in we’re going to be moving away from the existing XML config files. You’ll learn how we’re utilising some powerful features of .NET Core to optimise the Umbraco configuration. But first, some background.

.NET Core

.NET Core is an open source, cross-platform implementation of .NET that was released in June 2016. NET Core is an entire redesign of .NET Framework to make it leaner, more modular, more performant, faster and more testable. Being cross-platform means .NET Core can run websites, services, and apps on Linux, MacOS and Windows alike, whereas .NET Framework only allows you to run on Windows.

The web framework side of things has also changed since .NET Framework and ASP.NET Framework. On top of .NET Core is the web app framework ASP.NET Core.

ASP.NET Core

ASP.NET Core is a redesign of ASP.NET 4.x, built on top of the .NET Core runtime. It builds on top of the ideas introduced by OWINthe Open Web Interface for .NET 

ASP.NET Core is open source and, like .NET Core, has also been re-architected to be cross-platform, more testable, leaner, and modular. This simplification makes it a cleaner, more united journey to build web UI and web APIs 

Another change is that in ASP.NET Framework, Core MVC (Model-View-Controller) and WebAPI were separate, but now they are merged into the new ASP.NET MVC. This unification helps us build web apps and APIs using the same approach.

As well as Microsoft's ASP.NET Core Fundamentals, for a concise read on the subject try ASP.NET Core Succinctly by Unicore’s very own Simone Chiaretta and his co-writer, Ugo Lattanzi. It’s a free eBook so you can dive straight in.

.NET Core and ASP.NET Core come with a myriad of improvements, letting us build apps using patterns that make extensibility and testing much easier. There are a variety of changes to the project structure in ASP.NET Core, and we’re tackling these on the Unicore project for the Umbraco migration.

This article is focused on config, though. So how has config changed in .NET Core, and what does that mean for Umbraco?

Config in .NET Core Umbraco

There are some overriding changes needed for config in .NET Core. One of the main changes is moving from the .NET Framework config to the .NET Core configuration providers approach.

Configuration provider approach

In .NET Core Umbraco we can utilise powerful config dependency injection features. Umbraco now uses configuration providers to read configuration data using a set of available configuration sources. These key value pairs can come from a whole list of places but we’re going to focus on:

  • Settings files (e.g. appsettings.json)
  • Environment variables
  • Azure Key Vault and Azure App Configuration

HostBuilder

When you create a new ASP.NET Core web application, it will add a default HostBuilder to the Program.cs:

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureLogging(x =>
                {
                    x.ClearProviders();
                })
                .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
                .UseUmbraco();

HostBuilder in Program.cs in Umbraco on .NET Core

This HostBuilder lets us build and configure the whole Umbraco application. If you’d like to zoom in on this, the Generic Host docs are a good start, but for this article we’ll focus on the parts important for loading the Umbraco configuration settings.

CreateDefaultBuilder

The CreateDefaultBuilder() method within CreateHostBuilder() follows a set of default instructions, loading various configurations in a predefined order. The later loaded configuration providers override the earlier loaded ones. Umbraco loads the configuration provider in the default .NET Core order:

  1. Load the default host configuration, IConfiguration (includes all "DOTNET_" prefixed environment variables)
  2. Load appsettings.json using the JSON configuration provider
  3. Load appsettings.{Environment}.json, which overrides appsettings.json configuration
  4. Load App secrets (when the app is running in Development)
  5. Load environment variables
  6. Load command-line arguments, such as if you ran Umbraco using the dotnet run command

ConfigureWebHostDefaults

The ConfigureWebHostDefaults() method on the Host Builder hosts Umbraco as a web app using the default .NET Core settings, telling Umbraco what web server to use and how to configure everything needed to run by:

  • Loading the assets from wwwroot in Umbraco.Web.UI.NetCore project
  • Enabling both cross-platform Kestrel and Internet Information Services (IIS) as the web server
  • Configuring Umbraco using the default configuration providers, reading from the ASPNETCORE_ENVIRONMENT variable
  • Specifying the startup class to use when the app's host is built via UseStartup<TStartup>
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })

ConfigureWebHostDefaults() method on the Host Builder in Umbraco .NET Core

There are a few areas here that are new in .NET Core Umbraco, so now we’ll cover the key changes. Firstly, where is the web.config?

Web.config is now appsettings.json

Umbraco will come installed with a default appsettings.json. This will replace the various .NET Framework configs.

Although you can read XML files using a .NET Core configuration provider, we were keen to use appsettings.json file loaded by the JsonConfigurationProvider to match the standard .NET Core implementation. Also, the hostbuilder knows to load the appsettings.json file by default.

It is worth knowing that although we have a set of values in appsettings.json, behind the scenes in Umbraco core there are a set of matching default values in GlobalSettings.cs. However, you can override these values via the Umbraco appsettings.json file.

We only include one default appsettings.json file in Umbraco but you can override it with settings that are customised for your environments.

Environmental Variables

You will often need to override app configuration for specific environments. To update an ASP.NET Framework Umbraco config for a live deployment, you likely use some form of XML config transform.  In ASP.NET Core, this changes.   

ASP.NET Core uses the following environment variables to configure the Umbraco app based on the runtime:

The environment names are usually Development, Staging or Production (although they can be set to anything). If you’re curious, look in the Umbraco.Web.UI.NetCore project properties for launchSettings.json. It sets the ASPNETCORE_ENVIRONMENT to “Development”:

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:9000",
      "sslPort": 44331
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "Umbraco.Web.UI.NetCore": {
      "commandName": "Project",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "https://localhost:44354;http://localhost:9000"
    }
  }
}

Example launchsettings.json

Just like you would use web.config transforms in .NET Framework, you can override the default appsettings.json values with your own environment-specific configuration. The application configuration files follow the pattern:

appsettings.{Environment}.json

For example, configuration of production can be set in appsettings.Production.json and will override the default appsettings.json (such as when deploying Umbraco to Azure). However, sensitive production variables like secrets should not be stored in source code. Instead, these can be stored in Environment variables on the production server, such as in Azure Portal application settings or in Azure Key Vault, for example.

Azure Key Vault and Azure App Configuration

If you use Azure, then you might have heard of Azure App Configuration. Using the Azure.Identity package, you can separate your configuration from code. This enables the storage and management of application settings centrally by using Azure Key Vault and Azure App Configuration. This setup is possible by adding another configuration provider to the host builder, following a clean and standardised pattern.

Options pattern

Now we've covered how Umbraco utilises the .NET Core configuration provider approach to read configuration data, we'll now move onto another .NET Core pattern that lets us access this config - the options pattern.

The options pattern is a clean way to access groups of config settings via the Microsoft recommended IOptions<> approach. This helps ensure a separation of concerns, keeping related config settings together instead of having one huge config for many different purposes.

If you’re new to the options pattern, Andrew Lock’s article was written for an older .NET Core (ASP.NET Core RC2), but is still a good starting point. As Lock explains, you start off with a strongly typed class that represents your config object. In Umbraco, for example, we now have a class called GlobalSettings.cs, with properties that match the section in the appsettings.json file:

"Global": {
        "DefaultUILanguage": "en-us",
        "HideTopLevelNodeFromPath": true,
        "UmbracoPath": "~/umbraco",
        "TimeOutInMinutes": 20,
        "UseHttps": false
}

Global section of the Umbraco .NET Core appsettings.json file

public int TimeOutInMinutes { get; set; } = 20;

public string DefaultUILanguage { get; set; } = "en-US";

public bool HideTopLevelNodeFromPath { get; set; } = false;

public bool UseHttps { get; set; } = false;

public int VersionCheckPeriod { get; set; } = 7;

public string UmbracoPath { get; set; } = "~/umbraco";

Matching part of the GlobalSettings.cs file

You can also inject specialised config classes, such as SecuritySettings.cs, which has default values that can be overridden in appsettings.json:

namespace Umbraco.Core.Configuration.Models
{
    public class SecuritySettings
    {
        public bool KeepUserLoggedIn { get; set; } = false;

        public bool HideDisabledUsersInBackOffice { get; set; } = false;

        public bool AllowPasswordReset { get; set; } = true;

        public string AuthCookieName { get; set; } = "UMB_UCONTEXT";

        public string AuthCookieDomain { get; set; }

        public bool UsernameIsEmail { get; set; } = true;

        public UserPasswordConfigurationSettings UserPassword { get; set; }

        public MemberPasswordConfigurationSettings MemberPassword { get; set; }
    }
}

SecuritySettings.cs class in Umbraco .NET Core currently

Note how the Security section in appsettings.json matches the properties in the strongly-typed SecuritySettings.cs class:

{
    "Security": {
        "KeepUserLoggedIn": false,
        "UsernameIsEmail": true,
        "HideDisabledUsersInBackoffice": false,
        "UserPassword": {
          "RequiredLength": 10,
          "RequireNonLetterOrDigit": false,
          "RequireDigit": false,
          "RequireLowercase": false,
          "RequireUppercase": false,
          "MaxFailedAccessAttemptsBeforeLockout": 5
        },
        "MemberPassword": {
          "RequiredLength": 10,
          "RequireNonLetterOrDigit": false,
          "RequireDigit": false,
          "RequireLowercase": false,
          "RequireUppercase": false,
          "MaxFailedAccessAttemptsBeforeLockout": 5
        }
    }
}

Security appsettings.json in Umbraco .NET Core currently

How does Umbraco use this strongly typed config class? From a high level, the class is bound inside the ConfigureServices() method in the Startup.cs that is specified in the host builder.

Setting up the config class

The .NET Core Umbraco Startup.cs class is split into several areas. In a less complex solution, you could directly bind the config within the ConfigureServices method. However, we have split things up into logical parts due to the number of things that Umbraco needs to run. 

First, the Startup class extends the default builder via the .AddUmbraco() method. This method adds the extension methods that the builder will needs to load various components:

// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940

public void ConfigureServices(IServiceCollection services)
{
	var umbracoBuilder = services.AddUmbraco(_env, _config);
            umbracoBuilder
                .WithAllBackOfficeComponents()
                .WithAllWebsiteComponents()
                .Build();

}

ConfigureServices method in the Umbraco .NET Core Startup.cs file

The builder calls these methods as they need loading. When the WithAllBackOfficeComponents() method is called on the builder within the ConfigureServices() method, it returns a builder decorated with methods including WithConfiguration():

public static IUmbracoBuilder WithConfiguration(this IUmbracoBuilder builder)
            => builder.AddWith(nameof(WithConfiguration), () => builder.Services.AddUmbracoConfiguration(builder.Config));

WithConfiguration method in Umbraco .NET Core UmbracoBuilderExtensions.cs

This in turn calls AddUmbracoConfiguration() to register an instance of the strongly-typed config classes for <TOptions> to bind against:

...
// Register configuration sections.
services.Configure<GlobalSettings>(configuration.GetSection(Constants.Configuration.ConfigGlobal));
...

Register configuration sections example in Umbraco .NET Core

To bind the config values, we use the configuration.GetSection() method from Microsoft.Extensions.Configuration.IConfiguration and everything is bound and ready for us to use.

Injecting the config class

Now we can access config values via our controllers and other consuming classes using the IOptions<TOptions> approach. We inject the options type into the constructor, and dependency injection assigns the values to our property, such as in this DefaultCultureAccessor example:

public class DefaultCultureAccessor : IDefaultCultureAccessor
    {
        private readonly ILocalizationService _localizationService;
        private readonly IOptions<GlobalSettings> _options;
        private readonly RuntimeLevel _runtimeLevel;

        /// <summary>
        /// Initializes a new instance of the <see cref="DefaultCultureAccessor"/> class.
        /// </summary>
        public DefaultCultureAccessor(
		ILocalizationService localizationService, 
		IRuntimeState runtimeState, 
		IOptions<GlobalSettings> options)
        {
            _localizationService = localizationService;
            _options = options;
            _runtimeLevel = runtimeState.Level;
        }
...
}

Example Dependency Injection of config using IOptions

Using IOptions, the config won’t need to be referenced directly again during the application lifetime, until it starts up and begins this process again. However, there are times when we want to retrieve the config value on every request, rather than when the app starts. For this scenario, we would use IOptionsSnapshot, such as when retrieving cookie options in preview mode: 

var cookieOptions = context.RequestServices.GetRequiredService<IOptionsSnapshot<CookieAuthenticationOptions>>()
	.Get(Constants.Security.BackOfficeAuthenticationType);

Example use of IOptionsSnapshot in Umbraco .NET Core

Another scenario is when we want to be notified that a config value has changed, or if the class is a singleton but we require the config value to be updateable. In this case, we would use IOptionsMonitor. An example of this is for health checks such as MacroErrorsCheck.cs: 

public class MacroErrorsCheck : AbstractSettingsCheck
    {
        private readonly ILocalizedTextService _textService;
        private readonly ILoggerFactory _loggerFactory;
        private readonly IOptionsMonitor<ContentSettings> _contentSettings;

        public MacroErrorsCheck(ILocalizedTextService textService, ILoggerFactory loggerFactory,
            IOptionsMonitor<ContentSettings> contentSettings)
            : base(textService, loggerFactory)
        {
            _textService = textService;
            _loggerFactory = loggerFactory;
            _contentSettings = contentSettings;
        }
...
}

Example use of IOptionsMonitor in Umbraco .NET Core

The main takeaway from all this is that because we're using IOptions<T> in Umbraco .NET Core now, future implementations of Umbraco and packages will be able to follow this config DI approach. So a future package that needs the global settings will be able to add a custom class file and inject IOptions<GlobalSettings>. Nice, right?

Serilog config

It is worth mentioning that the config for our chosen logging framework, Serilog, is read by default from the Serilog section of the appsettings.json, meaning no additional IOptions<T> binding is required:

"Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "Microsoft.Hosting.Lifetime": "Information",
        "System": "Warning"
      }
    }
}

Serilog config section in Umbraco .NET Core

Summary

We hope this article helped you understand how config is changing, and how a new .NET Core Umbraco project will work behind the scenes in future.

You've learned about the changes to configuration within the .NET Core Umbraco solution. We’ve also shared the changes to the configuration approach that will be needed for package developers and those building Umbraco solutions that utilise custom configuration.

You might not currently need to extend the config in your own projects, but knowing what has changed and how things work behind the scenes in .NET Core Umbraco can give you confidence for future. And of course, changes to Umbraco implementations will be backed up with comprehensive documentation nearer release.

Once you get used to Umbraco’s updated .NET Core configuration approach, hopefully it will start to feel intuitive…and you might even want to mirror the config class injection approach on your .NET Framework projects using your own DI container.

Get Involved!

The Unicore team helped put this article together, so big thanks for their contributions, especially Bjarke Berg, Andy Butland and Simone Chiaretta.

As ever, if you have any questions at all, or you’d like to get involved in migrating Umbraco to .NET Core, please contact the Unicore team in your preferred way:

  • Look for the Umbraco GitHub tasks labelled with project/net-core and community/up-for-grabs – here’s a handy pre-filtered GitHub link to help find them!
  • Following the #projectunicore Twitter hashtag
  • Reach out to the Unicore team at unicore@umbraco.com
  • Contact Bjarke Berg or other team members on Twitter

Also, please let us know if you’d like us to consider another .NET Core demystification topic. We’ve got several chunky areas that would be good candidates for articles, so we’d like to hear your feedback.

Thanks for reading, now you can go and have fun with Umbraco on .NET Core!

Emma Garland

Emma is on Twitter as