Disclaimer: This website requires JavaScript to function properly. Some features may not work as expected. Please enable JavaScript in your browser settings for the best experience.

Dev GuideAPI Reference
Dev GuideAPI ReferenceUser GuideLegal TermsGitHubDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

.NET48 framework to .NET migration

Explains how to migrate to .NET8.0+

👍

Beta

.NET8 is currently in beta for Optimizely Configured Commerce. Contact your implementation partner about reviewing your custom extensions as they begin testing .NET8.

Optimizely Configured Commerce is migrating from .NET Framework 4.8 to .NET 8.0+. For ease of migration, Optimizely worked to preserve the previous API design and architecture. Some sites may need just a code rebuild, but Optimizely expects most to deal with breaking changes.

High-level steps

  1. Identify and plan for unsupported features.
  2. Update the Extensions code to use .NET 8.0+ and build.
  3. Resolve runtime errors and regressions.
  4. Build your dist/Extensions.dll using Net 8 as the target framework and push to your sandbox environment.

Identify and plan for unsupported features

Some features in Configured Commerce have existed in the platform in a deprecated or partially-supported status. Optimizely determine that updating to .NET 8.0 was a good time to remove these features from the platform. Before touching any code to upgrade to .NET 8.0, Optimizely strongly recommends that you work with your clients to identify which removed features are critical. If you still need any features after the upgrade, you must create your own alternative solutions or workflows.

Features Removed in .NET 8.0

  • Search Build Version Version 1 is no longer supported in .NET 8.0. You must upgrade any custom code must to Version 2.
  • The Media Library no longer supports editing images.
  • The Admin Console no longer has experiments.

Update the Extensions code to use .NET 8.0+ and Build

  1. Update your repo with the 5.2.2312.2363+sts build.
  2. Note that InsiteCommerce.sln has moved to the root from /src/InsiteCommerce.sln. This makes it easier to jump between the sln and file system view in your IDE.
  3. The released version of Extensions.csproj only targets net48. You can either:
    1. Switch TargetFrameworks to net8.0. Remove the reference to Extensions from InsiteCommerce.Web.
    2. Switch TargetFrameworks to net48;net8.0. This may require using conditional compilation to ensure the Extensions project can be built against etiher framework.
  4. Build Extensions and start to address any compilation errors. The following section contains some common situations we ran into that may affect you.

ASP.Net vs ASP.Net Core Controller Attribute Routing

ASP.NET and ASP.NET Core use similar classes and concepts for routing requests and responses via controller attributes. However, there are significant differences between the two frameworks. The class translations below can help identify some scenarios but are not exhaustive. The Microsoft documentation is the best resource for resolving specific issues. The common class changes we found in base code:

  • System.Web.Http RoutePrefixAttribute translates to Microsoft.AspNetCore.Mvc RouteAttribute
  • System.Web.Http.Description ResponseTypeAttribute translates to Microsoft.AspNetCore.Mvc ProducesAttribute
  • System.Web.Http IHttpActionResult translates to Microsoft.AspNetCore.Mvc IActionResult
  • IUrlHelper is an interface under the Microsoft.AspNetCore.MVC namespace that collides with the IUrlHelper interface in the Insite.Core.WebApi.Interfaces namespace.
  • This collision can be worked around by using an alias for either interface.

Entity Framework Configuration

If you have any custom database entities, you must update your mapping classes. Additional information on the changes to Entity Framework can be found below.

  • System.Data.Entity.ModelConfiguration.EntityTypeConfiguration<T> should be replaced with Microsoft.EntityFrameworkCore.IEntityTypeConfiguration<T>.
  • Insite.Data.Providers.EntityFramework.EntityMappings.EntityBaseTypeConfiguration<T> can be used which includes mapping for CustomProperties.
  • Mapping definitions now happen in the public void Configure(EntityTypeBuilder<T> builder) method instead of the constructor.
  • Insite.Common.DynamicLinq.DynamicQueryable is replaced by the package System.Linq.Dynamic.Core.

Resolve Runtime Errors and Regressions

Most difficulties with upgrading to .NET 8.0 are related to errors that only show during runtime. Optimizely suggests running full regression tests on your site to locate and resolve any possible runtime issues. Some common challenges encountered when upgrading base code are below.

ASP.NET Core

ASP.NET Core has significant differences in APIs than ASP.NET, and some matching APIs have subtle differences in behavior. APIs that use return this.Ok(object) are now converted to effectively return this.NoContent() when the passed object is null. URL encoding of the comma character is not escaped in .NET 6. Standards-compliant URL decoders are not affected by this, but custom parsers may be.

Parameter binding in controllers has undergone several changes. IEnumerables no longer bind as null in the example below:

public ActionResult PublishMultiple( 
 
    // will never be null in ASP.Net Core 
    IEnumerable<KeyContentContextModel> contextsToPublish = null  
    // will never be in ASP.Net Core, but you can check it for empty and  
    // convert it to null to keep code mostly the same 
    IEnumerable<KeyContentContextModel>? contextsToPublish = null  

HttpPost methods on controllers do not auto bind parameters from the body. You must add the FromBodyAttribute:

public ActionResult ResetPasswordSubmit([FromBody] ResetData resetData)

When using Microsoft.AspNetCore.Mvc IActionResult and returning response with string return type, the response is formatted with Microsoft.AspNetCore.Mvc.Formatters StringOutputFormatter by default. This sets the Content-Type header to text/plain. This may cause issues on the client-side if the expected Content-Type is application/json, which ASP.NET currently returns. To fix this, force the API endpoint to return JSON data type response by using the Produces attribute on the Controller Action:

[Produces("application/json")]

Additional information can be found in the Microsoft Documentation for response formatting.

Entity Framework Core

Some queries that worked in EF 6 are not compatible with EF 7. These issues will not be exposed until the offending code is executed.

Queries with complex GroupBy behavior do not always work as they did in EF 6. The error messages often look something like this:

The LINQ expression 'DbSet<Entity>() 
    .Where(predicate) 
    .GroupBy(o => groupByPredicate)' could not be translated. Either rewrite the query in a form that can be translated 

In most cases, you can prevent this error by adding .AsEnumerable() before .GroupBy(). However, this can cause noticeable loss of performance. More performant alternatives include converting the query to a normal SQL statement or creating a stored procedure.

❗️

Warning

Multiple Active Result Sets is disabled because a single connection cannot have multiple concurrent running queries. You can open additional connections as needed as a workaround. Base code did not require any changes to account for this.

Attempting to lazy load related data when using GetTableAsNoTracking as a starting point for your LINQ queries is no longer supported in EF Core. You can resolve this by eager loading your related data or switching the statement to use GetTable instead. You should note that eager loading via Include and ThenInclude might degrade performance of your query.

EF Core does not have a translated SQL comparer for string.Equals('value', StringComparison.OrdinalIgnoreCase). You can still use this type of comparer with in-memory IEnumerables. However, using it on an IQueryable results in a runtime error. Configured Commerce uses Microsoft SQL Server with a case-insensitive collation. Any string comparisons that are translated to a SQL query from an IQueryable are case-insensitive. Any string comparisons made in C# are not case-insensitive.

Breaking changes

Some .NET 4.8 code in Configured Commerce is not compatible with the .NET 8.0 implementation.

The Clone extension

The Clone extension, which was previously built around the BinaryFormatter, has been changed to forward its calls to CloneUsingJson. BinaryFormatter has well-known security issues and is disabled by default in .NET 6. Optimizely stopped using it with the .NET 6 branch of base code. The main functional difference is that private members are no longer cloned.

HttpContextBase to IHttpContextAccessor

HttpContextBase can no longer be injected in to classes using dependency injection. IHttpContextAccessor is the replacement for this.

NavigationFilters defined in JSON

Navigation filters defined via JSON files at Extensions\\NavigationFilters\\Filters now require a rebuild for any changes to take effect.

.NET 8 and Nullable

.NET 8 project uses <nullable>enable</nullable> by default. This can cause validation failures when adding or editing a CMS widget. If you do not wish to set <nullable>disable</nullable>, see the examples below for a workaround:

public class SalespersonForStateSelector : ContentWidget 
{ 
    // this passes validation because Properties named Drop are excluded from validation 
    public virtual SalespersonForStateListDrop Drop 
    { 
        get => this.GetPerRequestValue<SalespersonForStateListDrop>(nameof(this.Drop)); 
        set => this.SetPerRequestValue(nameof(this.Drop), value); 
    } 
  
    // this passes validation because it is nullable 
    public virtual SalespersonForStateListDrop? SalespersonForStateLis 
    { 
        get => this.GetPerRequestValue<SalespersonForStateListDrop>(nameof(this.SalespersonForStateLis)); 
        set => this.SetPerRequestValue(nameof(this.SalespersonForStateLis), value); 
    } 
  
    // this will fail validation on the add/edit screen because null is not considered a valid value 
    public virtual SalespersonForStateListDrop SalespersonForStateLis 
    { 
        get => this.GetPerRequestValue<SalespersonForStateListDrop>(nameof(this.SalespersonForStateLis)); 
        set => this.SetPerRequestValue(nameof(this.SalespersonForStateLis), value);