Upgrading Episerver CMS 11 to Optimizely CMS 12, porting to .net 5

Time to upgrade to .net 5? This is a first ever blog journey going from old .netFramework 4.8 to .net5 and CMS 11 to 12. Easy peasy? Not really, but this guide will halfen your problems.

Published 28th of November 2021
Optimizely CMS 12

In this blog post, I have done a proof of concept of a journey to upgrade from .net framework 4.8 project to net5. I have a small real world CMS (without commerce) with some smaller trivial plugins, so I wanted to see how much effort needs to be planned for upgrading. These are the steps I took and what I found out along the way.

You can find a conclusion and FAQ at the bottom of this blogpost

First of all – check out these resources before starting:
https://world.optimizely.com/documentation/developer-guides/archive/-net-core-preview/upgrading-cms-12/

You will be helped by having Alloy and Foundation as reference projects, install them, follow install guides here:

  1. https://github.com/episerver/Foundation/tree/net5
    (foundation project from Optimizely with Commerce and CMS on .net 5)
  2. https://github.com/episerver/netcore-preview
    (Alloy CMS template from Optimizely with CMS on .net 5)

Prerequisites

  • You’ll need a MVC project
  • Webforms won’t work
  • Webpages, you’ll need to go for Razor pages
  • Upgrade to latest CMS 11

Upgrade the project to .net 5 format with upgrade assistant

Resources: https://world.optimizely.com/documentation/developer-guides/archive/-net-core-preview/upgrading-cms-12/upgrade-assistent/installing-and-running/

Download the latest version of Optimizely upgrade-assistant-extensions

Create this folder scheme and unzip the downloaded file here:

Make sure you have the Optimizely nuget feed to your NuGet config file: (https://nuget.optimizely.com/feed/packages.svc/)

Start CMD from the project root:

Download/install the upgrade assistant:

Run the upgrade assistant

   > upgrade-assistant upgrade {projectName}.csproj –extension “{extensionPath}” –ignore-unsupported-features

Apply all the steps following the previous command

After this step you should be done with upgrade assistent

Resolve dependencies in visual studio

Open the new solution in VS. You will have a lot of dependencies marked red … Welcome to the .net 5 world! Get ready for dependency hell!

Start by removing all dependencies to legacy libs and libs that you know have been removed and the ones that you know not exists in .net 5 / net standards. List of removed apis, you find here https://world.optimizely.com/documentation/developer-guides/archive/-net-core-preview/upgrading-cms-12/breaking-changes-cms-12/

Its okey to uninstall to much, package referenced will be reinstalled anyways.

Uninstall following

  • Uninstall ImageResizer.Plugin.EpiserverBlobReader
  • EPiServer.GoogleAnalytics
  • Remove logging log4net
  • EPiServer.ServiceLocation.StructureMap 2.0.3
  • Remove struturemap.signed and web.signed
  • EPiServer.Search
  • EPiServer.Packaging.UI
  • remove nuget.core
  • EPiServer.Forms.Samples, it is not upgraded yet
  • Uninstal Tinymce 2
  • other addon that is not upgraded to .net5, like AddOn.Edit.ContextMenuOpenInNewTab (my favo)

I hade to unistall/reinstall also the following (even if the upgrade assistant probably should have solved this)

  • Successfully uninstalled ‘EPiServer.CMS.AspNetCore.HtmlHelpers 12.0.3’
  • Successfully uninstalled ‘EPiServer.CMS.AspNetCore.Mvc 12.0.3’
  • Successfully uninstalled ‘EPiServer.CMS.AspNetCore.Routing 12.0.3’
  • Successfully uninstalled ‘EPiServer.CMS.AspNetCore.Templating 12.0.3’

It can be tricky to uninstall dependencies, so you have to remove them in the right order. Follow the Package Console Management tool message:

Now Update/install all Episerver.CMS for latest 12.

  • Successfully installed ‘EPiServer.CMS 12.0.4’

Now it should look something like this:

Now try to compile, you will get 50 error or more.

Out with the old, in with the new

Optional installing EPiServer.Cms.AspNetCore.Migration

The package contains some old APIs such as DataFactory, FilterForVisitor and support for XML-based .config files. If you use an existing web.config, you should rename the config file to app.config so that ConfigurationManager will load it.

If you install EPiServer.CMS.AspNetCore.Migration 12 you might get following error if you used FilterForVisitor in your code

‘FilterForVisitor’ exists in both ‘EPiServer.Cms.AspNetCore.Migration, Version=12.0.0.0’ and ‘EPiServer.Cms.AspNetCore.Templating, Version=12.0.4.0’

That is because, yes, it exists in two dlls, with same namespace. problematic? yes.
(this has been updated to EPiServer.CMS.AspNetCore.Migration 12.0.1 after my report.)

Replace all log4net to Episerver.Logging

using log4net => using EPiServer.Logging (Use the “replace in Entire Solution”)

Replace General MVC APIs to .net 5

These mappings represent ASP.NET types that should be replaced when upgrading to ASP.NET Core / .net 5

System.Web.Http.ApiController => Microsoft.AspNetCore.Mvc.ControllerBase
System.Web.Mvc.Controller => Microsoft.AspNetCore.Mvc.Controller
System.Web.Mvc.ResultExecutingContext => Microsoft.AspNetCore.Mvc.Filters.ResultExecutingContext
System.Web.Mvc.ResultExecutedContext => Microsoft.AspNetCore.Mvc.Filters.ResultExecutedContext
System.Web.Mvc.IResultFilter => Microsoft.AspNetCore.Mvc.Filters.IResultFilter
System.Web.Mvc.ActionFilterAttribute => Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute
System.Web.Mvc.ActionExecutingContext => Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext
System.Web.Mvc.ActionExecutedContext => Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext
System.Web.Mvc.IActionFilter => Microsoft.AspNetCore.Mvc.Filters.IActionFilter
System.Web.WebPages.HelperResult => Microsoft.AspNetCore.Mvc.Razor.HelperResult
System.Web.HtmlString => Microsoft.AspNetCore.Html.HtmlString
System.Web.IHtmlString => Microsoft.AspNetCore.Html.HtmlString
System.Web.Mvc.AuthorizeAttribute => Microsoft.AspNetCore.Authorization.AuthorizeAttribute
System.Web.Mvc.BindAttribute => Microsoft.AspNetCore.Mvc.BindAttribute
System.Web.Mvc.MvcHtmlString => Microsoft.AspNetCore.Html.HtmlString
System.Web.Mvc.ActionResult => Microsoft.AspNetCore.Mvc.ActionResult
System.Web.Mvc.AllowHtmlAttribute => Microsoft.AspNetCore.Mvc.AllowHtmlAttribute
System.Web.Mvc.ContentResult => Microsoft.AspNetCore.Mvc.ContentResult
System.Web.Mvc.FileResult => Microsoft.AspNetCore.Mvc.FileResult
System.Web.Mvc.HttpNotFoundResult => Microsoft.AspNetCore.Mvc.NotFoundResult
System.Web.Mvc.HttpStatusCodeResult => Microsoft.AspNetCore.Mvc.StatusCodeResult
System.Web.Mvc.HttpUnauthorizedResult => Microsoft.AspNetCore.Mvc.UnauthorizedResult
System.Web.Mvc.OutputCacheAttribute => Microsoft.AspNetCore.Mvc.ResponseCacheAttribute
System.Web.Mvc.RedirectResult => Microsoft.AspNetCore.Mvc.RedirectResult
System.Web.Mvc.ViewResult => Microsoft.AspNetCore.Mvc.ViewResult
System.Web.Mvc.ValidateInputAttribute => Microsoft.AspNetCore.Mvc.ValidateInputAttribute
System.Web.Mvc.ViewResult => Microsoft.AspNetCore.Mvc.ViewResult

Replace System.Web.HttpContextBase

HttpContextBase is history, long live Microsoft.AspNetCore.Http.HttpContext
If you use HttpContextBase somewhere, try to replace and do you thing with Microsoft.AspNetCore.Http.HttpContext.
Example accessing HttpContext.Referer the new .net 5 way => request.GetTypedHeaders().Referer, extension form using Microsoft.AspNetCore.Http;

The upgrade-assitant adds a temporary helper class static HttpContext.Current, in root file HttpContextHelper

Read more about http-context

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-6.0

Replace Request.RawUrl

Request.RawUrl is replaced with Request.GetDisplayUrl() by using Microsoft.AspNetCore.Http.Extensions;

Replace TemplateTypeCategories.MvcPartialController

TemplateTypeCategories.MvcPartialController => TemplateTypeCategories.MvcPartialComponent

Replace BlockController

BlockController<TBlock> => BlockComponent<TBlock>

BlockControllers change to

public class ContactAreaBlockController : BlockComponent<ContactAreaBlock>
{
protected override IViewComponentResult InvokeComponent(ContactAreaBlock currentContent)
{
return View("ContactAreaBlockIndex", currentContent);
}
}

from

public class ContactAreaBlockController : BlockController<ContactAreaBlock>
{
public override ActionResult Index(ContactAreaBlock currentBlock)
{
return PartialView("ContactAreaBlockIndex", currentBlock);
}
}

InvokeComponent must be protected not public

BlockComponent exists also as AsyncBlockComponent

Replace PartialContentController

PartialContentController => PartialContentComponent (Use the “replace in Entire Solution”)

‘IHtmlContent’ does not contain a definition for ‘ToHtmlString’

IHtmlContent.ToHtmlString() is missing since it is part of elder version System.Web dll.
You might add your own extension:

using Microsoft.AspNetCore.Html;
using System.IO;

namespace MyNameSpace
{
    public static class HtmlContentExtensions
    {
        public static string ToHtmlString(this IHtmlContent htmlContent)
        {
            if (htmlContent is HtmlString htmlString)
            {
                return htmlString.Value;
            }

            using (var writer = new StringWriter())
            {
                htmlContent.WriteTo(writer, System.Text.Encodings.Web.HtmlEncoder.Default);
                return writer.ToString();
            }
        }
    }
}

EPiServer.Global is obsolete

Everything in here needs to be moved to .net startup class.

‘ServiceProviderHelper’ does not contain a definition for ‘TemplateResolver’

public void Initialize(InitializationEngine context)
{
context.Locate.TemplateResolver()
.TemplateResolved += TemplateCoordinator.OnTemplateResolved;
}

change to:

public void Initialize(InitializationEngine context)
{
context.Locate.Advanced.GetInstance<ITemplateResolverEvents>()
.TemplateResolved += TemplateCoordinator.OnTemplateResolved;
}

Port your IResultFilter to .net 5

You might have an IResultFilter to do some last changes or populate some LayoutModel

An object reference is required for the non-static field, method, or property ‘Controller.ViewData’

To get the view model in the IResultFilter:

public void OnResultExecuting(ResultExecutingContext filterContext)
{
   //get model from filterContext
  object viewModel = (filterContext.Controller as Microsoft.AspNetCore.Mvc.Controller)?.ViewData.Model;

  var model = viewModel as IPageViewModel<BasePageData>;//depending of your viewmodel, IContentViewModel<IRoutableContent>;
   if (model?.CurrentPage == null)
       return;

   //do some stuff
}

Register the Filter in startup.cs:

services.AddTransient<MyResultFilter>();

‘ResultExecutingContext’ does not contain a definition for ‘RequestContext’

filterContext.RequestContext => filterContext.HttpContext

Get Current Content Reference to the current routed content instance

public void OnResultExecuting(ResultExecutingContext filterContext)
{
var currentContentLink = filterContext.HttpContext.GetContentLink();
}

PrincipalInfo.HasEditAccess is obsolete

Use IContextModeResolver to detect if in edit mode, or user.IsInRole(Roles.CmsEditors) to detect a role

PrincipalInfo.CurrentPrincipal.IsInRole(“CmsEditor”)

PageEditing.PageIsInEditMode is obsolete

Use IContextModeResolver instead.

if (_contextModeResolver.CurrentMode == ContextMode.Edit) …

In views you can add @inject

@inject IContextModeResolver contextModeResolver
@if ((Model.CurrentContent.ContentArea != null && !Model.CurrentContent.ContentArea.IsEmpty) || contextModeResolver.CurrentMode == ContextMode.Edit)
{
<div class="row product-detail__contentarea">
<div class="col-12">
@Html.PropertyFor(x => x.CurrentContent.ContentArea)
</div>
</div>
}

CSHTML view errors and warnings

After a while in this process you get to “view errors and warnings” only, thats a good thing. You are now near to have it runing.

Add a _viewImports.cshtml

The _ViewImports file allows you to apply a range of the Razor directives to all your Views automatically, e.g. using statements and TagHelper functionality.

Add it to you root feature folder or views folder
(_viewImports.cshtml)

@using EPiServer.Cms.AspNetCore.Mvc
@using EPiServer.AddOns.Helpers
@using EPiServer.Core
@using EPiServer.Framework.Localization
@using EPiServer.Framework.Web.Mvc.Html
@using EPiServer.Framework.Web.Resources
@using EPiServer.Shell.Web.Mvc.Html
@using EPiServer.Web
@using EPiServer.Web.Mvc
@using EPiServer.Web.Mvc.Html
@using EPiServer.Web.Routing
@using Microsoft.AspNetCore.Mvc.Razor
@using Microsoft.AspNetCore.Html
@using Microsoft.AspNetCore.Http.Extensions
@using System.Net

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

About Taghelpers in .net 5: Tag Helpers in ASP.NET Core MVC – Dot Net Tutorials

Change @Request => @Context.Request

Razorpages does not have the public Request variable instead use @Context.Request.

@Request.Url => @Context.Request.GetDisplayUrl() and it works if you have @using Microsoft.AspNetCore.Http.Extensions in _viewImports.cshtml

Replace Html.RenderPartial

Use of IHtmlHelper.RenderPartial may result in application deadlocks. Consider using <partial> Tag Helper or IHtmlHelper.RenderPartialAsync.

@{Html.RenderPartial(“MyView”, Model)} => @{await Html.RenderPartialAsync(“MyView”, Model);}

Replace Html.RenderEPiServerQuickNavigator

@Html.RenderEPiServerQuickNavigator() => @await Html.RenderEPiServerQuickNavigatorAsync()

Remove legacy css and javascript bundlers

You probably have an old bundler not compatible, remove it, and comment it await, add to //todo:

The code compiles!

Hurray! if you think you are done, you are not! a few things to get going.

Add ConnectionsString to appsettings.json

appsettings in web.config is moved to appsettings.json, add your connstring at root level, you might think the upgrade assistant does that for you, but no.

"ConnectionStrings": {
"EPiServerDB": "Data Source=.;Initial Catalog=dbEpiserver;Integrated Security=False;User ID=x;Password=y;MultipleActiveResultSets=True"
}

Add login page

startup.cs:

//add login
services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/util/Login";
});

Add ConfigureCmsDefaults() to Program.cs

this sets up the CMS and registers all services:

Host.CreateDefaultBuilder(args)
.ConfigureCmsDefaults()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});

Without this you get 397 of following errors:

  • Error while validating the service descriptor ServiceType: Microsoft.AspNetCore.Routing.Matching.DfaMatcherBuilder
  • No constructor for type ‘EPiServer.Events.Internal.RemoteCacheSynchronization’ can be instantiated using services from the service container and default values.
  • InvalidOperationException: Unable to resolve service for type ‘EPiServer.Web.Routing.IUrlResolver’ while attempting to activate ‘EPiServer.Web.Routing.Matching.Internal.ContentMatcherPolicy’.
  • (Inner Exception #397) System.InvalidOperationException: Error while validating the service descriptor ‘ServiceType: EPiServer.Cms.UI.Admin.Tools.Internal.IExportService

Add ViewEngine paths

Startup.cs:

services.AddMvc(o =>
{
o.Conventions.Add(new FeatureConvention());
}).AddRazorOptions(ro => ro.ViewLocationExpanders.Add(new FeatureViewLocationExpander()));

Example FeatureConvention and FeatureViewLocationExpander: https://gist.github.com/LucGosso/48544606987af8f4ec774b1b82a6fee2

Other tips

Add runtime compilation on razor views

in startup.cs:

if (_webHostingEnvironment.IsDevelopment())
{
//Add development configuration
#if DEBUG
services.AddControllersWithViews().AddRazorRuntimeCompilation();
#endif
}

Upgrade AspNetIdentity

(EDIT 2022-06-05) This step has been handled by Optimizely to work better in CMS version 12.7 https://world.optimizely.com/documentation/Release-Notes/ReleaseNote/?releaseNoteId=CMS-21895
(but ive not testet)  Optimizely will update the schema automatically (ASP.NET Identity to ASP.NET Core Identity), if you have that enabled. I think it’s enabled by default. They’re also exposing the SqlServerDbContextOptionsBuilderso you can use it for running EF migrations.

Original blog text: (EDIT end)

This part is a little bigger issue. There might be migration steps you need to do and prepare before starting the upgrade. For Optimizely, you may use the following guide from GETA: Upgrading to Optimizely 12 ASP.NET Core Identity (getadigital.com)

Migrate ASP.NET Identity to .net 5: https://docs.microsoft.com/en-us/aspnet/core/migration/identity

Migrate from ASP.NET Membership follow this guide: https://docs.microsoft.com/en-us/aspnet/core/migration/proper-to-2x/membership-to-core-identity

Runtime error when log in:

SqlException: Invalid column name 'NormalizedUserName'.
Invalid column name 'ConcurrencyStamp'.
Invalid column name 'LockoutEnd'.
Invalid column name 'NormalizedEmail'.
Invalid column name 'NormalizedUserName'.

Move your TinyMceConfiguration to Startup

Example:

services.Configure<TinyMceConfiguration>(config =>
{
config.Default().AddSetting("extended_valid_elements",
"iframe[src|alt|title|width|height|align|name],picture,source[srcset|media],span")
.AddPlugin(
"epi-link epi-image-editor epi-dnd-processor epi-personalized-content print preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media template codesample table charmap hr pagebreak nonbreaking anchor toc insertdatetime advlist lists textcolor " +
"wordcount imagetools contextmenu colorpicker textpattern help code")
.Toolbar(
"bold italic strikethrough forecolor | epi-link image epi-image-editor epi-personalized-content | imagevault-insert-media imagevault-edit-media | bullist numlist | searchreplace fullscreen ",
"styleselect formatselect | alignleft aligncenter alignright alignjustify | removeformat | table toc | code"
, "")
.Menubar("edit insert view format table tools help")
.BodyClass("main-content-wrapper")
//.AddSetting("image_class_list", new[]
//{
// new {title = "None", value = ""},
// new {title = "class one", value = "one"}
//})
.AddSetting("image_title", false)
.AddSetting("image_dimensions", true)
.ContentCss("/css/bundle.css?" + DateTime.Now.Ticks);
});


Conclusion

Upgrading wasn’t made over one work day. It is much more complex than I first thought, and definitely more time consuming. Anyhow, this guide will help you halfen that time.

FAQ – what everyone wants to know

How much effort upgrading Episerver/Optimizely CMS to .net5?

It depends on your solution, but generally more than you think.

Is the site faster after upgrade to .net 5?

YES! Much faster startup.

Should I update my CMS site or should I start from scatch?

That is the question, it depends on:

  • how old your CMS site is
    • what other Frontend frameworks (do they exist in newer version)
    • CSS framework compatibility with .net 5
  • how much customization you have
  • which plugins you use
  • other benefits rebuilding

Who can help me upgrading the my site?

The developers/Partners working with your site on daily basis are the one that have the best knowledge of your site. They are the one best fitted to do this. Contact your favourite partner, or if you don’t know who to contact, email me luc (at) gosso.se and I’m willing to help finding a partner that would be interested in doing the job for you.

New Optimizely branding

When you’re done, CMS 12 will have the new branding:

Optimizely upgrade assistant missing features

The assistant is probably doing more job than we anticipate, but some more things could be included to https://github.com/episerver/upgrade-assistant-extensions

  1. .ConfigureCmsDefaults() in program.cs
  2. add ConnectionStrings to appsettings.json
  3. add ConfigureApplicationCookie to Startup.cs

About the author

Luc Gosso
– Independent Senior Web Developer
working with Azure and Episerver

Twitter: @LucGosso
LinkedIn: linkedin.com/in/luc-gosso/
Github: github.com/lucgosso

 

Leave a Reply

Your email address will not be published. Required fields are marked *