Migrating ASP.NET MVC 5 projects to ASP.NET Core

This page covers the main differences between Kentico Xperience live site projects built using ASP.NET MVC 5 and ASP.NET Core. It is aimed at developers intending to transition existing projects running on MVC 5 to ASP.NET Core.

This page covers:

  • fundamental changes between the Xperience MVC 5 and Core development models related to the integration of Xperience into an application, feature development, and extensibility
  • differences between the MVC 5 and Core platforms that invalidate some of the information in our MVC 5 development documentation and recommended replacements

Every section contains links to relevant pages within the Xperience ASP.NET Core development documentation or the official ASP.NET Core documentation from Microsoft with further information and details. 

For a general migration guide for ASP.NET MVC 5 projects covering application logic migration, see Microsoft’s Migrate from ASP.NET to ASP.NET Core and Learn to migrate from ASP.NET MVC to ASP.NET Core MVC

Table of contents

Application startup and Xperience integration

In ASP.NET Core, Xperience is a set of middleware components added to the application’s middleware pipeline.

In contrast to MVC 5, where integrating Xperience requires actions across multiple classes (ApplicationConfig to enable system features, RouteConfig to register routes, Startup.Auth for membership integration, and Global.asax for general application configuration), everything is bootstrapped in the application’s Startup class and its ConfigureServices and Configure methods. 

In the ConfigureServices method, you:

  • Add Xperience services to the application’s service container (IoC) using IServiceCollection.AddKentico.
  • Enable Xperience features to use in the project via the IFeaturesBuilder delegate.
  • Add the framework features required by the system.
Adding Xperience services and enabling features



public void ConfigureServices(IServiceCollection services)
{
    // Adds Xperience services to the application's service container
    services.AddKentico(features =>
    {
        // Enables Xperience features
        features.UsePageBuilder();
        ...
    });

    // Adds framework features required by Xperience
    services.AddControllersWithViews();
    services.AddAuthentication();
    ...
}


In the Configure method you:

  • Add Xperience and framework middleware components required by the integration.
  • Map Xperience routes.
Configuring the middleware pipeline



public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseStaticFiles();
    ...

    // Adds the Xperience middleware to the pipeline. Internally adds the routing (UseRouting) and session (UseSession) middleware.
    app.UseKentico();
    ...

    app.UseCookiePolicy();
    ...

    app.UseCors();
    ...

    app.UseAuthentication();
    ...

    app.UseEndpoints(endpoints =>
    {
        // Adds system routes such as HTTP handlers and feature-specific routes.
        endpoints.Kentico().MapRoutes();
        ...
    });
}


For more information, see Starting with ASP.NET Core development.

Dependency injection (IoC) container configuration

ASP.NET Core applications come with an inversion of control (IoC) container provided by the framework. Xperience uses this container to store all its services. Since the container is available globally, you do not need to register child containers for your services (as described in Initializing Xperience services with dependency injection). All services can be placed directly into the default container.

Adding application services to the IoC container



using Kentico.Web.Mvc;

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    // Adds all Xperience services to the service container
    services.AddKentico();
    // Place to register application services
    services.AddSingleton<IMyService, MyService>();
}


The default container can be substituted for a supported third-party container if you need more robust functionality. See Replacing the default service container

Request life cycle events

Due to differences between the .NET Framework and .NET Core architecture, only a subset of events from the CMS.Base.RequestEvents class are supported for .NET Core applications. 

The following table lists available events in the order they are invoked by the application:

Event

Event type

Handler parameters

Description

Prepare

Execute

EventArgs

Occurs before request processing begins.

Begin

Execute

EventArgs

Occurs when request processing starts.

PreSendRequestHeaders

Execute

EventArgs

Occurs just before ASP.NET sends HTTP headers to the client.

Note: Added delegates are invoked during HttpResponse.OnStarting.

End

Execute

EventArgs

Occurs at the end of request processing.

RunEndRequestTasks

Execute

EventArgs

Allows you to execute background tasks at the end of request processing. Running long tasks in this event’s handlers can negatively affect the response time of requests.

Finalize

Execute

EventArgs

Occurs when the request is finalized. Use to clean up and release any resources used by the request. Running long tasks in this event’s handlers can negatively affect the response time of requests.

Handling HTTP errors

For MVC 5 applications, we advise handling HTTP errors through IIS. This approach is strongly coupled with the application’s hosting server.

For ASP.NET Core, use the approach described in Handle errors in ASP.NET Core from the ASP.NET Core documentation. For example:

Example error handling



if (env.IsDevelopment())
{
    // If the application is running in the Development environment, displays a special page with a detailed breakdown of the error
    app.UseDeveloperExceptionPage();
}
else
{
    // In all other cases, redirects to a URL handled by a conventional controller action method or a Razor page
    // You can access exception details within the handler and serve a relevant page based on the error
    app.UseExceptionHandler("/Error");
}

...

// The 'UseKentico()' middleware registration must be called after the error handlers are registered
app.UseKentico();


This approach is independent of the application’s hosting environment and comes integrated directly into the framework.

Making Xperience classes and modules discoverable

For MVC 5 projects, we recommend the AssemblyInfo class as the location for the AssemblyDiscoverable assembly attribute. The attribute marks assemblies containing custom implementations (interfaces, providers, etc.), modules, or generated classes intended to be consumed by Xperience.

Class libraries targeting the .NET Standard or .NET Core platforms do not have dedicated AssemblyInfo classes included in the project by default. Instead, the build process generates an assembly info file directly to the output. However, s ince the AssemblyDiscoverable attribute provides information about an assembly, its location is not tied to any particular class. You are free to place it anywhere within the class library project.

For example, you can create an empty AssemblyDiscoverable dummy class within the project and place the attribute there:

AssemblyDiscoverable.cs



using CMS;

[assembly: AssemblyDiscoverable]


Web.config application configuration

ASP.NET Core projects use configuration providers to work with application settings. If your MVC 5 application makes use of web.config application keys provided by Xperience to modify certain behavior or functionality, migrate these keys to one of the configuration files registered for the Core application (Core web project templates come with an appsettings.json file registered by default).

For example:

Example appSettings section in a web.config



<appSettings>
    <add key="CMSHashStringSalt" value="1c0af037-71f0-4b91-95d3-a818657de1a7" />
    ...
</appSettings>
<connectionStrings>
  <add name="CMSConnectionString" connectionString="..." />
</connectionStrings>


Is converted into:

Example configuration file



{
    "CMSHashStringSalt": "1c0af037-71f0-4b91-95d3-a818657de1a7",
    "ConnectionStrings": {
        "CMSConnectionString": "<value>"
    }
}


The database connection string (CMSConnectionString) must be nested within a ConnectionStrings object.

Output caching

Xperience Core sites use and extend native output caching support provided by ASP.NET Core. The MVC 5 approach using the OutputCache attribute and GetVaryByCustomString implementations (as described on Caching the output of controller actions and Caching the output of personalized content) is not supported. 

Instead, use the built-in cache Tag Helper to cache page output.

cache Tag Helper example



@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<cache priority="High" expires-sliding="@TimeSpan.FromSeconds(60)" vary-by-user="true">
    @* Cached content... *@
</cache>


This approach also comes with the advantage of being able to cache only specific parts of the output (known as donut caching) instead of the whole markup returned by controller actions.

If you need to add cache dependencies on Xperience objects, use the cache-dependency Tag Helper within sections encapsulated by the cache tag.

cache-dependency Tag Helper example



@* Adds the Tag Helper to the view *@
@addTagHelper  Kentico.Web.Mvc.Caching.CacheDependencyTagHelper, Kentico.Web.Mvc

@{
    // Creates cache dummy keys targetting all user objects in the system and the /Home page
    var userCacheKeys = new[] { "cms.user|all", "node|mysite|/home" };
}

<cache>
    @* Ensures that contents of this cached section are evicted whenever any user in the system or the /Home page is modified *@
    <cache-dependency cache-keys="@userCacheKeys" />

    Time: @DateTime.Now
</cache>


Moreover, Xperience Core introduces caching support for page builder widgets. Caching is enabled per widget and caching strategies can be configured individually for each editable area. The system also contains robust and extensible support for cache personalization.

See Caching page output in ASP.NET Core applications.

Hosting and deployment

Xperience Core sites retain the dual-application model from ASP.NET MVC 5. However, where the administration application needs to be hosted under Internet Information Services (IIS) servers due to being built using ASP.NET WebForms, the Core live site application is more flexible and can be hosted on any platform supported by the ASP.NET Core framework. Of course, both applications still need to be able to connect to the shared database.

See Deploying and hosting ASP.NET Core applications and Host and deploy ASP.NET Core.

Page and form builder component development

The page and form builder features in ASP.NET Core contain several key differences related to component development.

Page builder – widgets and sections

Widgets and sections are implemented as:

  • standalone views with optional properties classes (injected via the ComponentViewModel<TPropertyModel> model class) – this scheme is identical to MVC 5.

  • view components – this scheme serves as a counterpart to widgets and sections using a custom controller derived from the WidgetController class. Instead of using the Index action method to render the default state of a widget, the system uses view components and their Invoke or InvokeAsync methods (both the synchronous and asynchronous approach is supported).

    When converting controller Index methods to view component Invoke(Async) methods, the method’s signature must accept the  ComponentViewModel  parameter. For widgets with custom properties, you also need to specify the properties class as the generic parameter.

    
    
    
      // The signature of a view component's InvokeAsync method for widgets without custom properties
      public async Task<IViewComponentResult> InvokeAsync(ComponentViewModel widgetProperties)
    
      // The signature of a view component's InvokeAsync method for widgets with custom properties
      public async Task<IViewComponentResult> InvokeAsync(ComponentViewModel<TWidgetPropertiesClass> widgetProperties)
    
    
      

    Furthermore, the method’s view result must specify the full relative path to the view rendered by the view component (the widget’s or section’s partial view file).

    
    
    
      return View("~/Components/Widgets/MyWidget/_MyWidget.cshtml", model);
    
    
      

    For handling POST requests, you need to create a dedicated controller class (together with corresponding routes). The system also provides injectable services that allow you to retrieve the component’s properties and the context of the page where the component is rendered in POST actions.

See Developing widgets and Developing page builder sections for details.

Page builder – selectors

Displaying of items in selectors is performed asynchronously in ASP.NET Core. Most of the system’s selectors handle this internally, with the exception of the General selector. When implementing general selector data providers for ASP.NET Core projects, you need to:

  • Use the GetSelectedItemsAsync method to retrieve and format selected items, instead of the GetSelectedItems method.
  • If you use the ObjectQuery API to retrieve items for the selector, call asynchronous methods to execute (materialize) your queries. For example, GetEnumerableTypedResultAsync instead of GetEnumerableTypedResult.

Page builder – page templates

Page templates consist of a view file and a properties class (if required). By default, no controller is necessary. In fact, page templates with a controller cannot be registered.

If your page template needs additional business logic that was previously handled by a controller, for example to

  • react to the configuration of the  template’s properties
  • perform interactions based on the page where the  template is rendered
  • execute general business logic not suitable for views (e.g., database operations)

create dedicated service or view component classes and place the logic there. To access the template’s properties, use the ComponentViewModel<TPropertyModel> model class. For handling POST requests, you need to create a dedicated controller class. The system also provides services that allow you to retrieve the template’s properties and the page context in POST actions.

See Developing page templates for details.

Form builder – form sections

Form sections that retrieve their markup via a custom controller’s Index action are not supported in ASP.NET Core. Instead, sections can be implemented as view components. Render the section by implementing the view component’s Invoke or InvokeAsync methods.

See Developing custom form layouts for details.

Components folder

We recommend storing all files related to a single component in a dedicated folder (e.g., ~/Components/Widgets/MyComponent).

The folder can store:

  • the main code files (view components, controllers, models, helper classes)
  • properties classes
  • corresponding views rendered by the component

This structure offers more clarity and a better developer experience than having individual files scattered across the solution.

When transitioning to this structure, you need to explicitly set the customViewName property (for basic components not based on a view component) in the corresponding Register* attributes since the system cannot automatically detect the view to serve based on location conventions.

Setting customViewName



using Kentico.PageBuilder.Web.Mvc;

[assembly: RegisterWidget("MyProject.Widgets.MyWidget",
                         "Widget Name",
                         typeof(WidgetProperties),
                         customViewName: "~/Components/Widgets/MyWidget/_MyWidget.cshtml")]


Asynchronous extension methods

ASP.NET Core does not support synchronous rendering of views within Razor syntax (.cshtml files). As a result, all system extension methods used by the builder features that render a (partial) view are asynchronous:

  • FormZone -> FormZoneAsync
  • EditableArea -> EditableAreaAsync
  • WidgetZone -> WidgetZoneAsync
  • RenderStandaloneWidget -> RenderStandaloneWidgetAsync
  • RenderNestedWidget -> RenderNestedWidgetAsync

These methods need to be prefixed with the @await directive when called within Razor syntax (alternatively, use their corresponding tag helpers).

Example.cshtml



using Kentico.PageBuilder.Web.Mvc;

@await Html.Kentico().EditableAreaAsync("area1")


Tag helpers for builder extension methods

Most builder extension methods intended to be used in views have corresponding tag helper alternatives. Using tag helpers removes the need to @await individual *Async methods and provides an alternative that more neatly fits into HTML-heavy Razor syntax.

Example.cshtml



@addTagHelper Kentico.Content.Web.Mvc.EditableAreaTagHelper, Kentico.Content.Web.Mvc

<editable-area area-identifier="area1" />


Bundling support for page and form builder assets

In MVC 5 projects, static assets for page and form builder components are gathered from predetermined locations (~/Content/<component>folders) and automatically bundled using native ASP.NET bundling support. The bundles are then linked on all pages where the Html.Kentico().PageBuilderScripts and Html.Kentico().PageBuilderStyles methods are called.

For ASP.NET Core, Xperience provides no default bundling or minification support. The system only provides customizable “pickup points” on the filesystem where it expects individual bundles. The actual implementation of the bundling and minification process itself is left to the developers. This also means that there are no longer any predetermined locations for builder component assets. You can organize the folder structure according to your requirements.

See Bundling static assets of builder components for more information, a detailed description of individual bundles expected by the system, and a sample bundling process using Node.js and the Grunt automation library.

Xperience membership

In Xperience MVC 5 applications, membership authentication and identity features are configured in Startup.Auth.cs, located in the App_Start folder. In ASP.NET Core, you configure these features in the application’s startup class.

In MVC 5, you work with identity classes prefixed Kentico*.In ASP.NET Core, identity classes adapted by the system are prefixed with Application*: ApplicationUserStore, ApplicationRoleStore, ApplicationUserManager, and ApplicationUser. Additionally, there is the default SignInManager type.

The membership integration is registered in ConfigureServices in the application’s startup. See Integrating Xperience membership for more information.

Preventing memory problems – Cache compacting

Xperience uses in-memory caching to optimize the performance of data loading operations. Caching occurs automatically when using most of the default Xperience functionality or API, and you can also leverage the Caching API in your own code when loading data.

However, the system does not limit the size of the cached data, which can potentially consume large amounts of your server’s memory and possibly lead to “Out of Memory” exceptions.

If such issues occur in your application, we recommend that you implement a custom solution that monitors memory usage and performs Cache compacting when it is necessary to free up memory. Compacting attempts to remove a specified percentage of the cache, starting from expired and low priority items.

To compact the Xperience memory cache, call the CompactCache method of the CacheHelper class. We recommend compacting with a relatively low percentage of removed items, based on your environment and the exact nature of your memory problems.

Example



using CMS.Helpers;

// Removes 5% of cached items
CacheHelper.CompactCache(.05);


The CompactCache method returns a bool value indicating whether the underlying environment supports cache compacting.

Miscellaneous differences in functionality between the platforms