.NET Remoting, AppDomains and other .NET Framework-only APIs on .NET 8 or 10

Most .NET Framework code compiles on .NET 8 or 10 once the project file is converted. A few technologies do not come along at all: Microsoft removed them and names what to use instead. This guide goes through that list, what each removal means for an existing application, and how to find which of them your code touches.

What Microsoft lists as unavailable

Microsoft keeps one page with the list, .NET Framework technologies unavailable on .NET ↗. On the day we read it, it named these, with Microsoft’s recommendation next to each:

  • Application domains. Creating more than one is not supported and there are no plans to add it. For code isolation, use separate processes or containers; to load assemblies dynamically, use AssemblyLoadContext.
  • .NET Remoting. Not supported. For two processes on one machine, System.IO.Pipes or MemoryMappedFile; for more complex cases, the open-source StreamJsonRpc; across machines, a network protocol such as HTTP on Kestrel, or System.Net.Sockets.
  • Code access security (CAS) and security transparency. Not supported and no longer treated as a security boundary. Use the boundaries the operating system gives you: virtualization, containers, user accounts.
  • System.EnterpriseServices (COM+). Not supported. The page names no replacement.
  • Windows Workflow Foundation. Not supported. The page points to CoreWF.
  • Saving assemblies built with System.Reflection.Emit. Not possible in .NET 8 and earlier; .NET 9 added PersistedAssemblyBuilder and brought back AssemblyBuilder.Save.
  • Multi-module assemblies and XSLT script blocks. Not supported.

The same page adds that WCF server code can run on .NET through the CoreWCF NuGet packages. Some of the removed APIs are still present in .NET: they compile and then throw PlatformNotSupportedException when called. Microsoft’s desktop upgrade guides say to run the application on .NET early for that reason, before the port gets far.

.NET Remoting

Remoting was built on application domains, and Microsoft’s page says it also needs runtime support that is expensive to maintain. There is no version of it in .NET. Because of that, BeginInvoke and EndInvoke on delegate objects throw PlatformNotSupportedException as well.

What is usually behind it

In an application of that era, Remoting is one of a few things: a Windows service that exposes a singleton object to a desktop client over a TCP or IPC channel; a server that raises events into its clients, which Remoting delivered through callbacks on a MarshalByRefObject; or a plugin host that creates objects in another AppDomain and talks to them through a MarshalByRefObject proxy. The library you pick matters less than deciding what crosses the process boundary. An object that was passed by reference and changed on both sides has to become a message.

Replacements

  • Pipes and sockets. System.IO.Pipes for two processes on one machine, System.Net.Sockets across machines. You define the message format yourself. These are the two Microsoft names first.
  • gRPC. A contract-first RPC framework: services and messages are defined in .proto files and the tooling generates servers and clients in C#. It supports client, server and bidirectional streaming, which covers a server that pushes events. Microsoft’s desktop upgrade guides name gRPC and ASP.NET Core for communication across machines.
  • StreamJsonRpc. Microsoft’s open-source JSON-RPC library that runs over an existing stream or pipe connection, named on the same page for the more complex cases.
  • CoreWCF. If the contract is already a WCF ServiceContract, or clients outside your control speak SOAP, the server side of WCF runs on .NET through CoreWCF ↗, a .NET Foundation project under the MIT license. Its packages include NetTcp and NetNamedPipe transports. See WCF services on .NET 8 or 10.

CoreRemoting ↗ is an open-source library (MIT license) that describes itself as an RPC library with “classic .NET Remoting flavour”, written to help move applications that use .NET Remoting to .NET. It targets .NET Standard 2.0 and supports events and delegates across the wire, sessions, a Remoting-style CallContext, TCP and named-pipe channels, and several serializers (BSON by default; a BinaryFormatter adapter is a separate package). Its readme does not mention MarshalByRefObject, and it rules out REST, SOAP and non-.NET clients. The repository was last pushed to in September 2026, and its latest release, 1.3.6, is from May 2026.

AppDomains

.NET has one application domain. AppDomain.CreateDomain throws PlatformNotSupportedException; other members still work (AppDomain.UnhandledException, for example) and some do nothing (SetCachePath). Microsoft points to the System.AppDomain source in the dotnet/runtime repository to check the members you use, on the branch of your runtime version.

AppDomains were used for two different things, and .NET replaces them separately.

Loading and unloading plugins

AssemblyLoadContext loads a set of assemblies into its own context, so two plugins can bring different versions of the same dependency. Created with isCollectible: true, the context can be unloaded. AssemblyDependencyResolver reads the plugin’s .deps.json and finds its dependencies next to it. This is the class from Microsoft’s documentation, with the two pieces put together:

using System.Reflection;
using System.Runtime.Loader;

class PluginLoadContext : AssemblyLoadContext
{
    private readonly AssemblyDependencyResolver _resolver;

    public PluginLoadContext(string pluginPath) : base(isCollectible: true)
    {
        _resolver = new AssemblyDependencyResolver(pluginPath);
    }

    protected override Assembly? Load(AssemblyName name)
    {
        string? path = _resolver.ResolveAssemblyToPath(name);
        return path != null ? LoadFromAssemblyPath(path) : null;
    }
}

Loading and unloading then takes three lines:

var context = new PluginLoadContext(pluginPath);
Assembly assembly = context.LoadFromAssemblyPath(pluginPath);
context.Unload();

Two things differ from AppDomain.Unload. With AppDomains the unload was forced: threads in the domain were aborted. With AssemblyLoadContext it is cooperative. Unload only starts it, and it finishes when no thread has a method from the context on its stack and nothing outside the context holds a reference to its assemblies, types or instances, other than weak references. A static field, a cached MethodInfo, a running thread, a strong GC handle or a local the JIT kept alive keeps the whole context loaded. Microsoft’s page on unloadability shows how to find the culprit with WinDbg or LLDB and the SOS gcroot command. Two limits apply to collectible contexts: C++/CLI assemblies are not supported, and ReadyToRun code is ignored.

The plugin project needs <EnableDynamicLoading>true</EnableDynamicLoading>, and its reference to the shared interface assembly needs <Private>false</Private> and <ExcludeAssets>runtime</ExcludeAssets>. Without them the plugin loads its own copy of the interface, the runtime sees two different types, and the host’s type check fails. Plugins should target a runtime such as net8.0 or net10.0; a .NET Standard target gives a .deps.json that may miss the implementation assemblies of packages.

Isolation

An AppDomain also kept a misbehaving plugin away from the host, and a restricted PermissionSet limited what it could do. AssemblyLoadContext gives none of that. Microsoft’s plugin tutorial says that untrusted code cannot be safely loaded into a trusted .NET process, and that a security or reliability boundary has to come from the operating system or a virtualization platform. In practice that means a separate process per plugin, or per group of plugins, that talks to the host over pipes or gRPC, so the plugin host ends up with the same design as the Remoting server above.

BinaryFormatter

The state depends on the version you move to:

  • .NET 8: Serialize and Deserialize throw NotSupportedException in every project type except Windows Forms and WPF, and the APIs are obsolete as an error everywhere. The EnableUnsafeBinaryFormatterSerialization switch from .NET 7 still turns them back on.
  • .NET 9 and later: the implementation is gone from the runtime. The types still exist, and every use throws PlatformNotSupportedException, whatever the switch says. To keep using it, the application project references the System.Runtime.Serialization.Formatters NuGet package and sets EnableUnsafeBinaryFormatterSerialization to true. Microsoft labels the package unsupported and unsafe. The package does not change the type identity of BinaryFormatter, so libraries do not have to reference it; only the application does.

Microsoft recommends four serializers in its place, depending on the data: System.Text.Json (JSON), DataContractSerializer (XML), MessagePack and protobuf-net (binary). All four assume you control both the writer and the reader of the data. Its security guide lists SoapFormatter, NetDataContractSerializer, LosFormatter and ObjectStateFormatter as equally dangerous, so moving from one of those to another gains nothing.

Data you already have

The harder case is data on disk or in a database that BinaryFormatter wrote: saved documents, cached objects, settings, queued messages. .NET 9 added NrbfDecoder in the System.Formats.Nrbf package. It reads the format (NRBF, “.NET Remoting: Binary Format”) without loading or instantiating any type, so it is safe on untrusted input. The package also targets .NET Standard 2.0 and .NET Framework, so the conversion can start before the runtime changes. It works when the set of serialized types is known and closed: you read the records and build your own objects from them. Microsoft’s suggested pattern is to check each payload with NrbfDecoder.StartsWithPayloadHeader, convert an old payload on first read, serialize it with the new serializer and write it back. The same page names SettingsPropertyValue, which can hold any object from a configuration file, as a case that cannot be migrated without a breaking change to the API.

Windows Forms and WPF have their own pages in the migration guide for the clipboard, drag and drop and .resx resources. Strings and icons in resources work without BinaryFormatter; resources of custom types need the compatibility package and switch.

COM+, MSMQ and Workflow Foundation

System.EnterpriseServices (COM+)

Not supported, and Microsoft’s list gives no replacement. The desktop upgrade guides say that an application which uses COM+ for object pooling, transactions or role-based security has to be redesigned: System.Transactions for distributed transactions, ASP.NET Core or worker services for hosting. Distributed transactions in System.Transactions exist on .NET from version 7 and are opt-in: TransactionManager.ImplicitDistributedTransactions is false by default, escalation then throws NotSupportedException, and the setter is marked Windows only.

System.Messaging (MSMQ)

The System.Messaging namespace is documented for .NET Framework only, and the Windows Compatibility Pack does not list messaging among its areas. The community package MSMQ.Messaging ↗ (MIT license) calls itself a drop-in replacement for System.Messaging for .NET on Windows; its latest version on NuGet, 1.0.4, is from June 2022, and the repository was last pushed to in January 2024. CoreWCF ships an MSMQ transport as well. If the queue itself can change, Microsoft’s list of open-source messaging projects for .NET names MassTransit, NServiceBus, Rebus and Brighter, and clients for Azure Service Bus and RabbitMQ.

Workflow Foundation

Not supported. Microsoft points to CoreWF ↗, a port of the WF runtime maintained by UiPath under the MIT license. Its readme says it targets .NET 6, runs XAML workflows through ActivityXamlServices, and does not include the designer; XAML produced by the Visual Studio designer runs “with some limitations”. The repository was last pushed to in August 2026. The NuGet package UiPath.Workflow is unlisted by its owner, and its page describes the project as experimental, so look at the repository for how the current code is built and shipped.

Code Access Security

CAS is gone, and most of its types are obsolete from .NET 5 on, with warning SYSLIB0003: PermissionSet, SecurityPermissionAttribute, FileIOPermission, PrincipalPermission and the rest of System.Security.Permissions. What that means for old code:

  • A Demand always succeeds at run time. Remove it. PrincipalPermission is the exception and has its own guidance under SYSLIB0002.
  • An Assert does nothing. Remove the attribute or the call.
  • A Deny or PermitOnly is not honored. Microsoft says to review these with a security advisor, because code that relied on CAS to restrict access now runs without the restriction.
  • Security transparency attributes (SecurityTransparent, SecurityCritical, SecuritySafeCritical) mark a boundary that .NET no longer enforces.

If the application sandboxed plugins with a restricted PermissionSet in a separate AppDomain, both halves of that protection are gone. The replacement is a process boundary under an account that has only the rights it needs, or a container.

Windows-only APIs and the compatibility pack

A different group of APIs exists on .NET only on Windows. The Microsoft.Windows.Compatibility package adds about 20,000 of them: registry, event log, WMI, performance counters, directory services, ACLs, Windows services, Windows cryptography, WCF, ODBC, CodeDom, configuration, serial ports, drawing. Microsoft says existing code compiles with almost no modifications once the package is referenced, and that it is meant for applications that stay on Windows, at least as a first step.

  • System.Configuration and app.config keep working through the System.Configuration.ConfigurationManager package, which has the same API. Moving to appsettings.json is a separate step and not required.
  • System.Drawing.Common is Windows only from .NET 6. On Linux or macOS the first call throws TypeInitializationException with PlatformNotSupportedException inside. The System.Drawing.EnableUnixSupport switch existed in .NET 6 only and was removed in .NET 7. For image work outside Windows Microsoft names SkiaSharp, ImageSharp, Aspose.Drawing and Microsoft.Maui.Graphics.
  • The compiler reports all of this as CA1416, on by default from .NET 5. A call to a Windows-only API from code that could run elsewhere is a warning until you wrap it in if (OperatingSystem.IsWindows()), mark the method [SupportedOSPlatform("windows")], or target net8.0-windows, which marks the whole assembly as Windows only.

Windows Forms, WPF and Windows services

Both desktop frameworks are part of .NET and still receive new work, and both remain Windows only. The upgrade guides for Windows Forms ↗ and WPF ↗ list the same preparation: target .NET Framework 4.7.2 first, move from packages.config to PackageReference, convert to the SDK-style project file while still on .NET Framework, then update packages and prefer ones that target .NET Standard 2.0 or .NET. The pitfalls they name:

  • Third-party controls and libraries that target only .NET Framework may not work. Audit them before anything else; a vendor without a .NET build is a blocker you cannot code around.
  • ClickOnce still exists: Visual Studio 2019 version 16.8 and later publish .NET desktop apps with it. The System.Deployment API does not. From .NET 7 the launcher passes the deployment properties to the application as environment variables such as ClickOnce_IsNetworkDeployed and ClickOnce_ActivationUri, and Microsoft provides a sample ApplicationDeployment class that reads them. The methods of the old class have no equivalent.
  • The WPF WebBrowser control is based on Internet Explorer; Microsoft points to WebView2.
  • BinaryFormatter in the clipboard, drag and drop and resources, as above.

A Windows service becomes a Worker Service: dotnet new worker, the Microsoft.Extensions.Hosting.WindowsServices package, builder.Services.AddWindowsService(...), and sc.exe create pointing at the published executable. Microsoft recommends publishing it as a single self-contained file. From .NET 6 an unhandled exception in a BackgroundService stops the host cleanly, so the Service Control Manager does not restart it; call Environment.Exit(1) from the catch block if you rely on recovery options.

ASP.NET Web Forms and the server side of WCF have their own guides here: ASP.NET Web Forms on .NET 8 or 10 and WCF services on .NET 8 or 10.

Not sure what your application relies on?

Send us the list of projects in the solution, or a description of the application, and we answer by email with what blocks the move and what we propose for each item.

The check is free and does not commit you to anything. If you want us to do the work, we agree a fixed price for the agreed scope by email.

  • What does the application do and who uses it?
  • Which .NET Framework version and project types (ASP.NET MVC, Web Forms, WCF, Windows service, WinForms/WPF)?
  • Which third-party packages or components does it depend on?
  • What is the goal: .NET 8/10 on Windows, Linux, containers, cloud?

How to find these in your own code

Start by searching the source and the project files:

rg -l "System.Runtime.Remoting|MarshalByRefObject|RemotingConfiguration" --type cs
rg -l "AppDomain.CreateDomain|AppDomain.Unload|CreateInstanceAndUnwrap" --type cs
rg -l "BinaryFormatter|SoapFormatter|NetDataContractSerializer" --type cs
rg -l "System.EnterpriseServices|ServicedComponent" --type cs
rg -l "System.Messaging|MessageQueue" --type cs
rg -l "System.Activities|WorkflowInvoker|WorkflowApplication" --type cs
rg -l "PermissionSet|SecurityPermission|SecurityTransparent|SecurityCritical" --type cs
rg -l "System.Deployment|ApplicationDeployment" --type cs
rg -l "System.Runtime.Remoting|System.EnterpriseServices|System.Messaging|System.Activities|System.Workflow" --glob "*.csproj" --glob "*.config"

A hit in a .csproj or .config reference list counts even when the namespace never appears in C#: the reference may be there for a vendor library that uses it. The .NET Upgrade Assistant, a Visual Studio extension and CLI tool, analyzes a solution and reports incompatibilities before changing anything. Microsoft has deprecated it in favor of the GitHub Copilot app modernization agent, which does the same assessment and then applies changes. Whichever you use, build the application on .NET early and run it: several of the APIs above compile and fail only when called.

If the list you end up with is longer than you expected, the migration service starts with the same check and takes the work from there.

Sources

More guides