Upgrading Episerver CMS 11 to Optimizely CMS 12, porting to .net 5
Is it time to upgrade to .NET 8? This is a first-ever blog journey, transitioning from the old .NET Framework 4.8 to .NET 6/8 and from CMS 11 to 12. Is it as easy as pie? Not really, but this guide will significantly reduce 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:
- https://github.com/episerver/Foundation/tree/net5
(foundation project from Optimizely with Commerce and CMS on .net 5) - 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
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 SqlServerDbContextOptionsBuilder
so 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);
});
404 not available error on Public Site for page templates
Is it possible that the page template is only functional in edit view? Furthermore, are all other pages on the public site displaying a 404 error? To resolve this issue, please navigate to the Admin section and access the "/EPiServer/EPiServer.Cms.UI.Admin/default#/Configurations/ManageSites" URL. Here, you can add your domain or localhost and include a * host, also add lang if needed. This solution may help resolve the problem you are facing!
Conclusion
Upgrading was not completed in a single work day. It is significantly more complex than I initially anticipated, and certainly more time-consuming. However, this guide will assist you in reducing that timeframe by half.
FAQ – what everyone wants to know
What is the level of effort required to upgrade Episerver/Optimizely CMS to .NET 6?
The level of effort required for the upgrade depends on your specific solution, but in general, it tends to be greater than anticipated.
Is the site faster after upgrade to .net 6/8?
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 assist me in upgrading my website?
The developers/partners who work with your website on a daily basis possess the most comprehensive knowledge of your site and are therefore best suited to handle this task. Please reach out to your preferred partner. If you are unsure whom to contact, you can email me at luc.gosso (at) epicweb.se, and I will gladly assist you in finding a partner who would be interested in undertaking this project on your behalf.
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
- .ConfigureCmsDefaults() in program.cs
- add ConnectionStrings to appsettings.json
- add ConfigureApplicationCookie to Startup.cs
Read more
- DrewNull/optimizely-cms12-upgrade (github.com)
- Optimizely CMS 12 - Unique upgrade challenges / Blogs / Perficient
Unleash the power of AI in Optimizely to boost your content management.