Views: 141
Number of votes: 4
Average rating:

.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 part 1 on the admin breakdown here https://world.optimizely.com/blogs/scott-reed/dates/2021/7/-net-5-preview-breakdown--admin-area/

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

  1. 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
  2. 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

  1. Setting up if the scheduler should run (This is the system that run Scheduled Jobs)
  2. Setting up the correct connection strings (Set up in code however the connectionString is in the appsettings.json file)
  3. Adding in the Identity provider
  4. Configuring MVC
  5. Adding in Alloy configuration (AddAlloy();) such as
    1. The view engine used
    2. The display options (Used to allow blocks to show in different sizes as set by the editor)
    3. The resolutions (Used for previewing in different resolutions / devices)
    4. Browser detection (Uses Wangkani)
  6. Adds CMS services (AddCms(); such as
    1. Host
    2. UI
    3. HTML Helpers
    4. Config
    5. TinyMCE config)
  7. Adding localization support for the editor
  8. Configuration of exception handling
  9. Setup of admin page for first time admin registration
  10. NET features (static files, routing, authentication, authorization)
  11. 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.

The file in Alloy contains

  1. Logging configuration
  2. 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.

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!

Jul 21, 2021

Scott Reed
( By Scott Reed, 7/22/2021 8:43:21 AM)

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

Please login to comment.