---
title: .NET 4.8 framework to .NET 8.0+ migration
slug: configured-commerce/docs/net7-framework-to-net-migration
docTags: 
createdAt: 2026-08-05T19:19:02.000Z
---

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.

:::hint{type="info"}
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](docId\:yR5txbyljjyNjPSMiVTeG) 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`:

| Release             | TargetFramework value        |
| ------------------- | ---------------------------- |
| 5.2.2512 – 5.2.2604 | `net8.0` or `net48;net8.0`   |
| 5.2.2605 and later  | `net10.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](https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-8.0#attribute-routing-for-rest-apis) 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:

```csharp
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](docId\:p3YkdL3n05GsjwOJy-DoY).

### `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:

```csharp
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:

```csharp
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:

```csharp
// 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:

```csharp
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:

```csharp
[Produces("application/json")]
```

The [response formatting](https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-8.0) 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:

```text
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:

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

EF Core requires a solution similar to the following:

```csharp
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.

:::hint{type="info"}
**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](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/DzAFPa65GhV9AcH0d6svQ-20260813-104719.png "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](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/ujdx7irFcjUqktKGdzUbe-20260813-104719.png "screenshot of the Mission Control instance detail page where the Ready for Linux Migration column displays No Linux build found")
