.NET 5 Preview Breakdown – Alloy Project
Overview
The .NET 5 preview is a major update to the Optimizely CMS platform. The public preview was released on July 1st as announced by Martin Ottosen here https://world.episerver.com/blogs/martin-ottosen/dates/2021/6/-net-5-public-preview/ and with it comes a number of features as mentioned in my previous blog post.
If you’ve not read it you can read the full list here
- Admin UI Changes Breakdown - https://world.optimizely.com/blogs/scott-reed/dates/2021/7/-net-5-preview-breakdown--admin-area/
- Alloy Project Breakdown - https://world.optimizely.com/blogs/scott-reed/dates/2021/7/-net-5-preview-breakdown--alloy-project/
- DXP Changes - https://world.optimizely.com/blogs/scott-reed/dates/2021/8/-net-5-preview---dxp-changes/
In this series of blog posts, I’m going to give an overview of what to expect in this version based upon my own exploration and usage. I will compare what we had to what we have to give developers an easier understanding of where the platform has changed.
In this second post I am going to give a high-level overview and a beak down of what’s difference in the .NET 5 preview’s project structure and utilization. This is purely from my own personal breakdown of Alloy .NET 4.7.2 vs Alloy .NET 5 and the areas that I see have changed.
.NET Framework (4.*) vs .NET 5 Differences
First off, there may be a people viewing this who have no idea about the differences between the full fat .NET framework ASP.NET projects vs how .NET Core/.NET 5 works. So combined with the Optimizely CMS changes will be a bit of .NET 5 information as well to help you along.
Setup
Whereas in olden times the setting up of a new project was done purely in Visual Studio and we were all familiar with the CMS platform’s Visual Studio Addon or even cloning a version of Quicksilver/foundation this is now different. In the modern world of .NET 5 this is all handled a lot more like package managers are handled such as NPM, Nuget and done via the command line.
This require a few things to be in place for creating new website based on templates
- Template – This is essentially the same as what you get installing the CMS extension. It installs a template for the project that you can then use
- CLI – This is installation of the Episerver CLI commands such as you may have used in Powershell when working with Azure/Episerver and using the Deployment API.
Once these are installed you can use the CLI to create a new project of the preview. Be aware that with the Optimizely rebrand I would imagine at some point these will be moving to a different set of template names and CLI commands.
dotnet new epicmsempty --name ProjectName
cd projectname
dotnet-episerver create-cms-database ProjectName.csproj -S . -E
Project Structure
Packages
The package management in .NET 5 is a lot cleaner in Visual Studio and shows the packages that are installed as well as all the dependant packages that have been installed as sub nodes. This makes it a lot easier to handle the packages on a project and see the difference between packages you have installed and dependant packages
Website Root
In .NET 5 there is a special folder in the solution which is the place for you to keep all of the assets for the website. Typically, in the past on a lot of ASP.NET projects developers end up creating their own folder such as “Static” or such. In this version all of those static assets are nicely grouped in the wwwroot and will be the output files.
Dependency Injection
Who doesn’t like to be SOLID nowadays and using a dependency injection framework to allow us to model and abstract out all of our services, factories and load them in via Constructors. For years we have been using the popular StructureMap as the framework of choice and although it used to be coupled to platform a few years ago it was pulled out to a Nuget package to allow swapping and .NET Standard support.
Now we’re on .NET 5 we have the Microsoft built in DI framework which is the standard for projects https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection and this is now the default used
Configuration and Initalization
The trusty Web.Config is something ASP.NET developer have been used to using for a long time. An XML file that ends up being hundred if not thousands of lines of configuration and setup.
In .NET 5 this is gone mostly in favour a combination C# based code configuration and some JSON configuration files.
Another standard in the Optimizely CMS platform is the InitializableModule which was used as a place to configure startup code. Although the attribute still exists in the Framework library the correct place to add startup logic is in the Startup.cs file. https://docs.microsoft.com/en-us/aspnet/core/fundamentals/startup?view=aspnetcore-5.0
As the framework still has many Initializable Modules these don’t look to be going anywhere so for the time being these will still work as expected.
Startup
Most of the magic happens now in the Startup.cs file moved from both the Web.Config and the InitalizationModules with the following actions happening
- Setting up if the scheduler should run (This is the system that run Scheduled Jobs)
- Setting up the correct connection strings (Set up in code however the connectionString is in the appsettings.json file)
- Adding in the Identity provider
- Configuring MVC
- Adding in Alloy configuration (AddAlloy();) such as
- The view engine used
- The display options (Used to allow blocks to show in different sizes as set by the editor)
- The resolutions (Used for previewing in different resolutions / devices)
- Browser detection (Uses Wangkani)
- Adds CMS services (AddCms(); such as
- Host
- UI
- HTML Helpers
- Config
- TinyMCE config)
- Adding localization support for the editor
- Configuration of exception handling
- Setup of admin page for first time admin registration
- NET features (static files, routing, authentication, authorization)
- Routing configuration, Mapping of CMS content and use of Razor pages as the View Engine
There’s a lot there that’s been ported from Web.Config and a collection of other Initialization modules.
Here’s the Alloy example for you to feast your eyes on, see more by setting up the preview for yourself
using EPiServer.Cms.UI.AspNetIdentity;
using EPiServer.Data;
using EPiServer.Framework.Web.Resources;
using EPiServer.Personalization.Commerce.Tracking;
using EPiServer.Reference.Commerce.Site.Features.Market.Services;
using EPiServer.Reference.Commerce.Site.Features.Recommendations.Services;
using EPiServer.Reference.Commerce.Site.Features.Shared.Services;
using EPiServer.Reference.Commerce.Site.Infrastructure;
using EPiServer.Reference.Commerce.Site.Infrastructure.Attributes;
using EPiServer.Reference.Commerce.Site.Infrastructure.Indexing;
using EPiServer.ServiceLocation;
using EPiServer.Tracking.Commerce;
using EPiServer.Web;
using EPiServer.Web.Routing;
using Mediachase.Commerce;
using Mediachase.Commerce.Anonymous;
using Mediachase.Commerce.Core;
using Mediachase.Commerce.Orders;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using System;
using System.IO;
namespace EPiServer.Reference.Commerce.Site
{
public class Startup
{
private readonly IWebHostEnvironment _webHostingEnvironment;
private readonly IConfiguration _configuration;
public Startup(IWebHostEnvironment webHostingEnvironment, IConfiguration configuration)
{
_webHostingEnvironment = webHostingEnvironment;
_configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(_webHostingEnvironment.ContentRootPath, "App_Data"));
services.Configure<DataAccessOptions>(o =>
{
o.ConnectionStrings.Add(new ConnectionStringOptions
{
ConnectionString = _configuration.GetConnectionString("EcfSqlConnection"),
Name = "EcfSqlConnection"
});
});
services.AddCmsAspNetIdentity<ApplicationUser>(o =>
{
if (string.IsNullOrEmpty(o.ConnectionStringOptions?.ConnectionString))
{
o.ConnectionStringOptions = new ConnectionStringOptions
{
Name = "EcfSqlConnection",
ConnectionString = _configuration.GetConnectionString("EcfSqlConnection")
};
}
});
//UI
if (_webHostingEnvironment.IsDevelopment())
{
services.Configure<ClientResourceOptions>(uiOptions =>
{
uiOptions.Debug = true;
});
}
services.Configure<JsonOptions>(o =>
{
o.JsonSerializerOptions.PropertyNamingPolicy = null;
});
//Commerce
services.AddCommerce();
//site specific
services.Configure<IISServerOptions>(options => options.AllowSynchronousIO = true);
services.Configure<KestrelServerOptions>(options => options.AllowSynchronousIO = true);
services.TryAddEnumerable(Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Singleton(typeof(IFirstRequestInitializer), typeof(UsersInstaller)));
services.AddDisplayResolutions();
services.TryAddSingleton<IRecommendationContext, RecommendationContext>();
services.AddSingleton<ICurrentMarket, CurrentMarket>();
services.TryAddSingleton<ITrackingResponseDataInterceptor, TrackingResponseDataInterceptor>();
services.AddHttpContextOrThreadScoped<SiteContext, CustomCurrencySiteContext>();
services.AddTransient<CatalogIndexer>();
services.TryAddSingleton<ServiceAccessor<IContentRouteHelper>>(locator => locator.GetInstance<IContentRouteHelper>);
services.AddEmbeddedLocalization<Startup>();
services.Configure<MvcOptions>(o =>
{
o.Filters.Add(new ControllerExceptionFilterAttribute());
o.Filters.Add(new ReadOnlyFilter());
o.Filters.Add(new AJAXLocalizationFilterAttribute());
o.ModelBinderProviders.Insert(0, new ModelBinderProvider());
});
services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/util/Login";
});
services.Configure<OrderOptions>(o =>
{
o.DisableOrderDataLocalization = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAnonymousId();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(name: "Default", pattern: "{controller}/{action}/{id?}");
endpoints.MapControllers();
endpoints.MapContent();
});
}
}
public static class Extensions
{
public static void AddDisplayResolutions(this IServiceCollection services)
{
services.AddSingleton<StandardResolution>();
services.AddSingleton<IpadHorizontalResolution>();
services.AddSingleton<IphoneVerticalResolution>();
services.AddSingleton<AndroidVerticalResolution>();
}
}
}
Appsettings.json
This is a file which contains a little of what the Web.Config used to contain and allows environment versions to allow for transforms which I assume be supported in the DXP to allow transformation of environment configurations. After speaking to Optimizely I'm told this will load whatever version matched the environment name .e.g appsettings.Production.json will be loaded if the environment matches production.
The file in Alloy contains
- Logging configuration
- Connection strings
Bundleconfig.json
Whereas we used to do the bundle configuration in code as this is mostly a frontend focused task this has now been moved to JSON as well with the bundleconfig.json.
To note, I did find a stack overflow post saying that this method was removed as standard in .net core 2.1 as it uses a third party tool. With the standard being to add client files in VS to the project which uses LibMan instead.
I’m still unsure on this one but for now I’m going with the Alloy Preview setup as the standard.
.NET 6
.NET 6 is expected in november with an longer support lifespan as you can see from the image below from https://blog.inedo.com/dotnet/demystifying-lts
From what I've heard the plan is to have a .NET 6 support version by the end of the year / early 2022 so that we are on the LTS .NET
Conclusion
Although this is a huge upgrade most of the fundamentals are under the covers and have been carried out by Optimizely in the packages and aspnet CLI. I reality at least in the base CMS project there’s not huge amounts different for a developer to think about other than moving the initialization where possible to Startup, a bit of bundling changes and some light structural changes.
I’d suggest the best possible way to learn is to dive in an have a play 😊
If anything is unclear or wrong in this post then comment and I’ll update!! Thanks all!
Few few rating apart from a 3 star. If there's anything that could make this better or you didn't like feel free to let me know so I can make the blogs better
Nice post Scott. Might you be aware if Optimizely will be upgrading the Foundation code base to .Net core when the Commerce upgrade rolls out later this year? There are quite a few useful product integrations there so it will be quie an important one.
The demo project for .NET 5 includes 2 projects. Alloy and Commerce Foundation example. I imagine the .NET 5 will be kept seperate but all the core projects/templates will have .NET 5 versions
Ah ok, i was looking at the .Net Core preview here: episerver/netcore-preview (github.com)
But yes, I've now located the Foundation version.episerver/Foundation-spa-react (github.com)
Interested to see how products such as Recommendations are integrated but i do note that it's not quite there yet.