Dev GuideAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

.NET 4.8 framework to .NET 8.0+ migration

Explains how to migrate to .NET8.0+ in Optimizely Configured Commerce

Optimizely Configured Commerce is migrating from .NET Framework 4.8 to .NET 8.0+. This article is for partners and developers who maintain a Configured Commerce Extensions project. Complete the migration to keep your site supported and to run it on Linux hosting.

For ease of migration, Optimizely preserved the previous API design and architecture. Some sites need only a code rebuild, but Optimizely expects most to encounter breaking changes.

Prerequisites

Before you start, ensure you have the following:

  • A 5.2.2604.725-lts build or newer
  • A sandbox environment
  • Access to the Extensions source

High-level steps

The migration runs in seven stages, from auditing the features you lose to confirming readiness in Mission Control. Complete the stages in order, because each stage depends on the build the previous stage produces.

📘

Note

During the migration process, you must have both the .NET 4.8 and .NET 8.0+ Extensions.dll files in the repository.

  1. Identify and plan for unsupported features.
  2. Update the Extensions code to use .NET 8.0+ and build.
  3. Address the breaking changes that affect your extensions.
  4. Resolve runtime errors and regressions.
  5. Update the Windows Integration Service (WIS) to use the REST integration API.
  6. Build and push the Extensions assembly to your sandbox environment.
  7. Verify readiness in Mission Control.

Identify and plan for unsupported features

Several Configured Commerce features carried a deprecated or partially supported status. Optimizely removed these features as part of the update to .NET 8.0. Before you change any code, identify which removed features are critical to your site. Optimizely recommends running that assessment with your clients. If you still need any of these 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. Upgrade any custom code to version 2.
  • The Media Library no longer supports editing images.
  • The Admin Console no longer has experiments.
  • Integration URLs no longer have a front end and are not accessible from a browser.
  • Error Logging Modules and Handlers (ELMAH) logs are no longer available.

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

Retargeting the Extensions project is the core of the migration. This section moves the project from net48 to the framework your release requires, then produces the assembly that Mission Control checks for readiness.

InsiteCommerce.sln moved from /src/InsiteCommerce.sln to the repository root, which simplifies switching between the solution and file system views in your IDE.

  1. Update your repository to the 5.2.2604.725-lts build or newer.

  2. Switch the target framework in Extensions.csproj, which ships targeting only net48:

    ReleaseTargetFramework value
    5.2.2512 – 5.2.2604net8.0 or net48;net8.0
    5.2.2605 and laternet10.0 or net48;net10.0
  3. Remove the reference to Extensions from InsiteCommerce.Web.

    Multi-targeting (net48;net8.0 or net48;net10.0) requires conditional compilation to build the Extensions project against either framework.

  4. Build dist/netcore/Extensions.dll and push it to your sandbox environment.

  5. Address any compilation errors. The following sections cover common scenarios.

ASP.NET versus ASP.NET Core controller attribute routing

ASP.NET and ASP.NET Core use similar classes and concepts for routing requests and responses through controller attributes. However, the two frameworks differ significantly. The following class translations identify common scenarios but are not exhaustive. The attribute routing documentation resolves specific issues.

Optimizely found the following common class changes in base code, the Configured Commerce platform source that your Extensions project builds against:

  • 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 in the Microsoft.AspNetCore.Mvc namespace collides with IUrlHelper in the Insite.Core.WebApi.Interfaces namespace.

Work around the IUrlHelper collision by using an alias for either interface.

Entity Framework configuration

If you have any custom database entities, you must update your mapping classes. The following list describes the changes to Entity Framework (EF).

  • Replace System.Data.Entity.ModelConfiguration.EntityTypeConfiguration<T> with Microsoft.EntityFrameworkCore.IEntityTypeConfiguration<T>.
  • Use Insite.Data.Providers.EntityFramework.EntityMappings.EntityBaseTypeConfiguration<T>, which includes mapping for CustomProperties.
  • Define mappings in the public void Configure(EntityTypeBuilder<T> builder) method instead of the constructor.
  • Replace Insite.Common.DynamicLinq.DynamicQueryable with the System.Linq.Dynamic.Core package.

Breaking changes

Some .NET 4.8 code in Configured Commerce does not compile or behave the same way on .NET 8.0+. Review each change in this section against your extensions before you rebuild. These breaks require code edits rather than configuration changes.

Automatic transaction behavior

In .NET Framework 4.8, the platform wrapped each API request's handler chain in a single database transaction. The platform began the transaction, ran every handler, and then saved and committed. A handler exception rolled back the whole chain. Multiple Save calls in one request were atomic without extra code.

.NET 8.0+ removes this automatic per-request transaction wrapping. This change has the following consequences for extension code:

  • Multiple saves are no longer atomic – In .NET 4.8, every Save enlisted in the single wrapping transaction and committed at the end. In .NET 8.0+, each Save (or SaveAsync) commits immediately in its own transaction.
  • Mid-chain changes still persist, but no longer atomically – A UnitOfWork flushes pending changes on the shared per-request context. A later Save by base code still persists changes your extension made earlier in the chain. The difference is that each change commits immediately. A failure later in the request does not roll it back, which leaves partial writes. This consequence affects typical extensions most, because partner code usually runs inside a base handler or pipeline.
  • Committed work survives an exception – On an unhandled exception, the context discards only unsaved tracked changes. Anything a prior Save already committed stays committed.
  • The implicit end-of-request save is gone – The next Save in the chain normally flushes your changes. A lost write occurs only when you mutate entities and no later Save runs in the same request.
  • The default isolation level changed – The default transaction isolation level is READ COMMITTED rather than READ UNCOMMITTED.

Optimizely's hosted environments enable Read Committed Snapshot Isolation (RCSI). Under READ COMMITTED, a statement reads the latest committed row version from the version store instead of acquiring shared locks. Readers do not block writers, and writers do not block readers. This behavior is comparable to READ UNCOMMITTED without dirty reads. RCSI is a database-level setting. Local or on-premises SQL Server instances that do not enable it show more lock-based blocking.

If your extension depends on several saves succeeding or failing together, wrap them in an explicit transaction:

this.UnitOfWork.BeginTransaction();
try
{
    // ... perform repository changes ...
    this.UnitOfWork.Save();
    this.UnitOfWork.CommitTransaction();
}
catch (Exception)
{
    this.UnitOfWork.RollbackTransaction();
    throw;
}

The Enable Retry On Handlers system setting adds a retrying execution strategy for transient SQL errors. The setting sits in the Developer settings group and is off by default. When enabled, a transient failure re-runs the entire handler chain from the start. The retry does not roll back a committed Save, because the chain no longer runs in one transaction. The retry re-applies that work instead. Handler work must be idempotent, or you must wrap it in an explicit transaction. This setting does not restore the old whole-chain transaction wrapping. For full details, see Transactions and the unit of work.

userfiles API

The API at /api/v1/admin/userfiles has a breaking change. Previously, the client submitted the file in a form with a Content-Disposition of attachment. The client now uses the standard file-submission format: a Content-Disposition of form-data with Name="file". Update the WIS if you use IntegrationProcessorFileUpload. For C# code, set the following headers when submitting the request:

fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
{
    Name = "file",
    FileName = Uri.EscapeDataString(fileName),
};

Clone extension

Optimizely changed the Clone extension to forward its calls to CloneUsingJson instead of BinaryFormatter. Microsoft disabled BinaryFormatter by default in .NET 6 because of security issues, and Optimizely stopped using it at that point. The main functional difference is that the Clone extension no longer clones private members.

HttpContextBase to IHttpContextAccessor

Dependency injection can no longer inject HttpContextBase into classes. Use IHttpContextAccessor instead.

NavigationFilters defined in JSON

Navigation filters defined in JSON files at Extensions\NavigationFilters\Filters require a rebuild before any change takes effect.

.NET 8 and nullable reference types

A .NET 8 project sets <Nullable>enable</Nullable> by default. That setting can cause validation failures when you add or edit a CMS widget. To avoid setting <Nullable>disable</Nullable>, use the following workaround:

public class SalespersonForStateSelector : ContentWidget
{
    // Properties named Drop are excluded from validation, so this property passes.
    public virtual SalespersonForStateListDrop Drop
    {
        get => this.GetPerRequestValue<SalespersonForStateListDrop>(nameof(this.Drop));
        set => this.SetPerRequestValue(nameof(this.Drop), value);
    }

    // This property passes validation because it is nullable.
    public virtual SalespersonForStateListDrop? NullableSalespersonList
    {
        get => this.GetPerRequestValue<SalespersonForStateListDrop>(nameof(this.NullableSalespersonList));
        set => this.SetPerRequestValue(nameof(this.NullableSalespersonList), value);
    }

    // This property fails validation on the add or edit page because null is not a valid value.
    public virtual SalespersonForStateListDrop NonNullableSalespersonList
    {
        get => this.GetPerRequestValue<SalespersonForStateListDrop>(nameof(this.NonNullableSalespersonList));
        set => this.SetPerRequestValue(nameof(this.NonNullableSalespersonList), value);
    }
}

Resolve runtime errors and regressions

Most upgrade difficulties come from errors that surface only at runtime. Run full regression tests on your site to locate and resolve runtime issues. The following sections describe common challenges in base code.

ASP.NET Core

ASP.NET Core APIs differ significantly from ASP.NET APIs, and some matching APIs behave differently. The following differences appear most often:

  • return this.Ok(object) now behaves like return this.NoContent() when the passed object is null.
  • URL encoding no longer escapes the comma character. Standards-compliant URL decoders handle the unescaped character correctly, but custom parsers may not.

Parameter binding in controllers changed in several ways. IEnumerable parameters no longer bind as null, as the following example shows:

// The parameter never binds as null in ASP.NET Core.
public ActionResult PublishMultiple(IEnumerable<KeyContentContextModel> contextsToPublish = null)
{
    return this.Ok();
}

// The nullable parameter never binds as null either. Check for an empty collection
// and convert it to null to keep existing code mostly unchanged.
public ActionResult PublishMultipleNullable(IEnumerable<KeyContentContextModel>? contextsToPublish = null)
{
    if (contextsToPublish != null && !contextsToPublish.Any())
    {
        contextsToPublish = null;
    }

    return this.Ok();
}

[HttpPost] methods on controllers do not automatically bind parameters from the body. Add the [FromBody] attribute:

public ActionResult ResetPasswordSubmit([FromBody] ResetData resetData)

A controller action that returns IActionResult with a string return type uses StringOutputFormatter by default. That formatter sets the Content-Type header to text/plain. A client that expects application/json can fail as a result, because ASP.NET returns application/json. To return JSON instead, apply the Produces attribute to the controller action:

[Produces("application/json")]

The response formatting documentation covers the remaining formatters.

Entity Framework Core

Some queries that worked in Entity Framework 6 do not work in EF Core. These issues surface only when the offending code runs.

Queries with complex GroupBy behavior do not always work as they did in EF 6. The error message resembles the following:

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, or switch to client evaluation explicitly by inserting a call to
'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'.

Work around EF Core limitations with GroupBy by decomposing the query manually. For example, to find user profiles that share an email address, EF 6 can use the following:

userProfiles.GroupBy(o => o.Email).Where(g => g.Count() > 1).SelectMany(g => g);

EF Core requires a solution similar to the following:

var counts = userProfiles
    .GroupBy(x => x.Email)
    .Select(g => new { Email = g.Key, Count = g.Count() });
return userProfiles
    .Join(
        counts,
        u => u.Email,
        c => c.Email,
        (userprofile, count) => new { userprofile, count }
    )
    .Where(o => o.count.Count > 1)
    .Select(x => x.userprofile);

A SQL statement or stored procedure is sometimes a clearer alternative. EF Core does not support the simpler syntax.

📘

Note

Multiple Active Result Sets is disabled because a single connection cannot run concurrent queries. Open additional connections as needed. Base code required no changes for this behavior.

EF Core no longer supports lazy loading related data when GetTableAsNoTracking is the starting point for a LINQ query. To resolve the error, eager-load the related data or switch the statement to GetTable. Eager loading through Include and ThenInclude can degrade query performance.

EF Core does not have a translated SQL comparer for string.Equals("value", StringComparison.OrdinalIgnoreCase). This comparer still works with in-memory IEnumerable collections. However, using it on an IQueryable results in a runtime error. Configured Commerce uses Microsoft SQL Server with a case-insensitive collation. String comparisons that translate to a SQL query from an IQueryable are case-insensitive. String comparisons made in C# are case-sensitive.

Update the WIS

.NET 8.0+ dropped support for the Windows Communication Foundation (WCF) integration endpoint. Optimizely updated the WIS to try the REST integration API first and fall back to WCF when REST is unavailable. Update your WIS to use the REST integration API so it does not depend on the fallback.

Verify readiness in Mission Control

Mission Control, the Configured Commerce management portal, reports whether Optimizely can migrate your environment to Linux hosting. Check the readiness flag after you push the rebuilt assembly, because the flag confirms that your build produced a .NET 8.0+ assembly in the expected location.

  1. Log in to Mission Control.
  2. Go to your instance's detail page.
  3. Locate the Ready for Linux Migration column.

A checkmark confirms that your environment is ready for migration.

screenshot of the Mission Control instance detail page where the Ready for Linux Migration column displays a checkmark

No Linux build found – rebuild to verify means Mission Control did not find the .NET 8.0+ Extensions.dll file. Confirm the file is in the correct location, then re-trigger a build.

screenshot of the Mission Control instance detail page where the Ready for Linux Migration column displays No Linux build found

Did this page help you?