Meet The Author

I'm Ethan Jackson, An 25 years old blogger Currently living in London, United Kingdom. I'm a Skilled Blogger, Part Time web Developer And Creating new things as a web Designer.

author

Bringing .NET Remoting and AppDomains Back to .NET 8 and 10

Leave a Comment

You have most likely encountered this obstacle if you have ever attempted to convert a long-running.NET Framework program to.NET 8 or.NET10. The code is compiled. Channel Services.In the IDE, RegisterChannel is located directly. AppDomain.The build is green, CreateDomain resolves, IntelliSense is satisfied, and when you run it for the first time, you get:

 

System.PlatformNotSupportedException: Remoting is not supported on this platform.

.NET Core removed the implementations of remoting and application domains but kept some of the type names, which produces exactly this experience: a clean compile followed by a runtime failure. The usual advice is "rewrite it as gRPC" or "rewrite it as WCF Core", and for a small surface that is fine. For an application with two hundred MarshalByRefObject-derived types, server-to-client callbacks, lifetime leases, and a plugin loader built on AppDomain.CreateDomain, "rewrite it" is a project, not a migration.

This article describes a different approach: keep the API and replace what is underneath it. I will walk through two libraries that do this — a remoting compatibility layer and a process-backed AppDomain replacement — the design constraints that shaped them, and the places where the platform simply does not allow an exact match.

The packages multi-target .NET Standard 2.0.NET 8 and .NET 10, and the whole solution — libraries, tests and samples — is built and verified against both modern runtimes.

What Actually Has to Be Replaced

Classic remoting is three things stacked together, and each one is unavailable for a different reason.

Layer.NET FrameworkWhy it is gone
ProxiesCLR transparent proxies (RealProxy)The runtime hook does not exist off .NET Framework
SerializerBinaryFormatterRemoved from the supported surface; throws outright on .NET 9+
TransportTcpChannel (SSPI-secured)The channel plumbing was never ported

So the replacement needs a proxy mechanism, a [Serializable]-aware serializer that is not BinaryFormatter, and a transport. What it does not need to change is the public API — and that is the whole point of the exercise. MarshalByRefObjectRemotingConfigurationChannelServicesObjRefILeaseISponsor and the rest keep their names and signatures.

Part 1: Remoting
Before and After

Here is a classic server registration on .NET Framework:

ChannelServices.RegisterChannel(new TcpChannel(9000), false);
RemotingConfiguration.RegisterWellKnownServiceType(
    typeof(OrderService), "orders.rem", WellKnownObjectMode.Singleton);

And the same thing running on .NET 8 or .NET 10:

using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

ChannelServices.RegisterChannel(new TcpServerChannel("app", 9000), ensureSecurity: false);
RemotingConfiguration.RegisterWellKnownServiceType(
    typeof(OrderService), "orders.rem", WellKnownObjectMode.Singleton);

The channel type is named TcpServerChannel rather than TcpChannel — separate client and server channel types were always available in classic remoting, and using them makes the direction explicit. The client side:

ChannelServices.RegisterChannel(new TcpClientChannel(), ensureSecurity: false);
var orders = (IOrderService)RemotingServices.Connect(
    typeof(IOrderService), "tcp://server:9000/orders.rem");

RemotingConfiguration.Configure("app.config") still reads a <system.runtime.remoting> section, so if your deployment is configuration-driven rather than code-driven, that keeps working untouched.

A Working Contract

Let me use the sample from the repository, because it exercises the features that usually decide whether a compatibility layer is real or a demo. The contract assembly:

[Serializable]
public class Quote
{
    public string Symbol;
    public decimal Price;
    public DateTime AsOf;
    private string _source = "unset";        // private field: must survive the wire

    public string Source => _source;
    public void SetSource(string s) => _source = s;
}

// The client implements this and hands it to the server; the server calls back into it.public interface ITicker
{
    void OnQuote(Quote quote);
    void OnClosed(string reason);
}

public interface IMarketService
{
    Quote GetQuote(string symbol);
    int Subscribe(ITicker ticker, string symbol);
    void PublishTo(int subscriptionId, decimal price);
    bool TryGetLast(string symbol, out Quote quote);     // out parameter
    void Accumulate(ref decimal running, decimal delta); // ref parameter
    T Echo<T>(T value);                                  // generic method
    void Boom(string message);                           // exception marshalling
    string WhoAmI();
}

The server implementation is ordinary code:

public class MarketService : MarshalByRefObject, IMarketService
{
    private readonly Dictionary<int, (ITicker Ticker, string Symbol)> _subscriptions = new();
    private static int _nextSubscription = 1;

    public int Subscribe(ITicker ticker, string symbol)
    {
        // `ticker` arrived as an ObjRef pointing back at the client. Calling it sends a request
        // in the reverse direction over the connection the client already opened.
        var id = _nextSubscription++;
        _subscriptions[id] = (ticker, symbol);
        ticker.OnQuote(GetQuote(symbol));
        return id;
    }

    public T Echo<T>(T value) => value;

    public void Boom(string message) => throw new InvalidOperationException(message);

    public string WhoAmI() => "server-pid-" + Environment.ProcessId;
}

That WhoAmI is not decoration. It is how you prove a remoting layer is actually remoting: the client asserts that the process id it gets back is not its own. A layer that quietly short-circuits to an in-process call would pass every other test in the suite.

The Callback Improvement

This is the part where the replacement is better than the original, so it is worth dwelling on.

In classic remoting, if you wanted the server to call back into the client, the client had to register its own receiver channel and be externally addressable. That is a firewall problem, a NAT problem, and in a container it is often simply impossible.

Here, the client registers a client channel only:

// A client channel only. Note there is no server channel here at all — callbacks still work.
ChannelServices.RegisterChannel(new TcpClientChannel("sample-client", null), ensureSecurity: false);

var market = (IMarketService)RemotingServices.Connect(typeof(IMarketService), url);

var ticker = new Ticker();                              // : MarshalByRefObject, ITickervar subscription = market.Subscribe(ticker, "ACME");    // server now holds a reference to us

market.PublishTo(subscription, 123.75m);                // server pushes to us
Console.WriteLine(ticker.Quotes[^1].Price);             // 123.75

No listening port on the client. This works because the connection is framed, duplex and multiplexed: one TCP connection carries calls in both directions, correlated by request id. When the server invokes ticker.OnQuote(...), the reverse call travels back down the connection the client already opened.

The routing rule that makes this safe is worth stating precisely, because the naive version is wrong. A client with no receiver channel marshals its objects under a bare uri with no scheme — there is nothing to dial. So ConnectionContext publishes the connection a call arrived on for the duration of dispatch, and a reference with no dialable url binds its calls back to that connection. A reference that does carry a channel url is dialed normally. That distinction is what prevents a third-party reference — A hands B a reference to C — from being misrouted through the A↔B connection.

The Serializer

BinaryFormatter is not an option, so the layer carries its own [Serializable]-aware binary serializer. It honours the full classic contract, which is more than most people remember is in there:

  • private fields and [NonSerialized]

  • ISerializable with GetObjectData / the deserialization constructor

  • IObjectReferenceIDeserializationCallback, and the [OnSerializing] / [OnDeserialized] family

  • serialization surrogates

  • object cycles and shared references (identity preserved, not duplicated)

  • arrays: single-dimension, jagged, multi-dimensional, and non-zero lower bound

Two constraints are new, and both exist because the sender should not get to decide how much work the receiver does:

RemotingConfiguration.MaxGraphDepth  = 128;         // default
RemotingConfiguration.MaxFrameLength = 64 * 1024 * 1024;

The depth bound is not paranoia about malice alone. Reading a graph is recursive, so graph depth is stack depth — 50,000 nested arrays fit in 250 KB of payload and overflow the reader's stack, which you cannot catch and which takes the process with it. Declared element counts are separately checked against the bytes actually remaining in the payload: every element costs at least one byte, so a 10-byte message claiming 100,000,000 elements is lying, and the check catches it before the allocation.

Both bounds are enforced on the writing side too, so a graph that could never be read fails on the side that can still do something about it, with an error message naming the property to raise.

Deserialization Safety

TypeFilterLevel.Low is the default for a network endpoint, matching .NET Framework. There is one deliberate improvement: a deny list that applies at every level, including Full. .NET Framework's Full had no deny list at all.

The deny list names known gadget-chain entry points — types whose deserialization turns into code execution — and matches on the base chain, not the type's own name. That detail matters: matching by name alone made the System.IO.FileSystemInfo entry inert, because it is abstract and only its differently-named subclasses can be constructed.

Two related rules:

  • Delegates are refused on the general object path. A delegate reached through an object graph is a classic gadget step. There is a dedicated delegate record where the shape is known and the receiver decides whether to invoke; a static delegate is only reconstructed at TypeFilterLevel.Full.

  • Type names on an incoming message resolve only against already-loaded assemblies. Type.GetType loads assemblies by name, so resolving a wire-supplied name would let a caller choose which assembly the server loads — and run its module initializer — before dispatch. Nothing legitimate is lost: for the server to hold an object satisfying a contract, that contract's assembly is loaded by definition.

TLS

Classic remoting's secure="true" on the TCP channel meant SSPI/Negotiate — Windows authentication, not transport encryption. There is no portable equivalent, so the encryption story here is deliberately different and deliberately named differently:

server.Security = TcpChannelSecurity.ForServer(certificate);
client.Security = TcpChannelSecurity.ForClient("service.example.com");

A client that cannot chain the certificate — a self-signed cert in a closed deployment — opts out with ForClientWithoutValidation(). That is a named setting rather than a validation callback returning true, because the callback-returning-true is the single most common way TLS gets silently disabled in production.

For same-machine communication there is also an IpcChannel speaking the same framed protocol over named pipes, with ipc://portName/objectUri urls. No port means nothing is reachable from off the machine and nothing can collide with another process binding the same port.

Part 2: Application Domains
A Domain Is Now a Process

AppDomain.CreateDomain cannot be revived. The CLR has no second domain to create — this is not a missing API, it is a missing runtime feature. So a domain becomes an operating system process, and the remoting layer above carries the calls.

// Before (.NET Framework)var domain = AppDomain.CreateDomain("plugin");
var plugin = (IPlugin)domain.CreateInstanceAndUnwrap("Acme.Plugin", "Acme.Plugin.Entry");
plugin.Run();
AppDomain.Unload(domain);

// Aftervar domain = AppDomain.CurrentDomain.CreateChildDomain("plugin");
var plugin = (IPlugin)domain.CreateInstanceAndUnwrap("Acme.Plugin", "Acme.Plugin.Entry");
plugin.Run();
domain.Unload();

Only the two statics change, and only because C# has no static extension methods. SetData / GetDataDoCallBackFriendlyNameBaseDirectoryIsDefaultAppDomain() and CreateInstanceAndUnwrap keep their names and signatures.

Static State, Isolated — the Original Reason Domains Existed

using var first  = AppDomain.CurrentDomain.CreateChildDomain("tenant-a");
using var second = AppDomain.CurrentDomain.CreateChildDomain("tenant-b");

var a = first.CreateInstanceAndUnwrap<Plugin>(new object[] { "tenant-a" });
var b = second.CreateInstanceAndUnwrap<Plugin>(new object[] { "tenant-b" });

a.Execute("one");
a.Execute("two");
b.Execute("one");

// Plugin holds a private static int _callsInThisDomain
Console.WriteLine(a.Summarise().Calls);   // 2
Console.WriteLine(b.Summarise().Calls);   // 1
Console.WriteLine(a.ProcessId != b.ProcessId);   // True
Isolation That the Original Could Not Deliver

This is where a process beats a domain outright. A .NET Framework application domain shared a process, so a stack overflow, a corrupt native heap or an Environment.FailFast in the plugin killed the host along with it. Domains offered code isolation, never fault isolation.

var domain = AppDomain.CurrentDomain.CreateChildDomain("doomed");
var plugin = domain.CreateInstanceAndUnwrap<Plugin>();

try { plugin.Crash(); }              // calls Environment.FailFast inside the childcatch (Exception) { /* the connection dies with the process; that is the point */ }

while (domain.IsAlive) Thread.Sleep(50);

try { domain.CreateInstanceAndUnwrap<Plugin>(); }
catch (AppDomainUnloadedException) { /* expected */ }

// ...and this application is still running.using var healthy = AppDomain.CurrentDomain.CreateChildDomain("recovered");

The parent observes AppDomainUnloadedException — the same exception classic code already handled — and carries on. Nothing in the calling code needs to know that the mechanism changed.

DoCallBack, and Why a Lambda Cannot Work
public static class Settings
{
    // Target of a DoCallBack. Must be static — a closure cannot cross the boundary.
    public static void ApplyDefaults()
    {
        Write("mode", "configured-by-callback");
        Write("pid", Process.GetCurrentProcess().Id.ToString());
    }
}

domain.DoCallBack(Settings.ApplyDefaults);      // runs INSIDE the domain

A lambda that captures local state compiles to a closure object which has no identity the child process can reach, so it is rejected with an explanation rather than silently misbehaving:

var captured = 1;
domain.DoCallBack(() => captured++);   // throws, message mentions MarshalByRefObject
The Complete List of Required Source Changes

This is the part of any compatibility layer that decides whether it is usable. Here it is exhaustive.

1. Members on a class contract must be virtual

The CLR's transparent proxy intercepted every member. A generated proxy is a subclass, and a subclass can only override virtual members. A non-virtual one would run locally on the caller and silently return wrong results:

public class OrderService : MarshalByRefObject
{
    public virtual Order Get(int id) { ... }   // add 'virtual'
}

Two non-obvious points. First, IsVirtual is not the test — an implicit interface implementation is compiled virtual final, which is just as un-overridable, so it is reported too. Second, remoting an interface avoids the issue entirely, because interface slots are separately re-implemented and are therefore always intercepted. An interface-typed contract is the recommended shape.

Rather than allow a silent wrong answer, proxy creation fails at creation time and names every offender. A non-overridable member cannot even be guarded: DefineMethodOverride against a non-virtual method fails at CreateTypeInfo() with TypeLoadException, and a new member is never reached through a base-typed reference. Failing loudly at the earliest possible point is the only honest option.

2. Client-activated objects need an explicit factory

new Foo() was routed through activation by the CLR, and there is no hook for that off .NET Framework:

var session = RemotingActivator.CreateInstance<Session>(userId);   // was: new Session(userId)

The url-taking form is named CreateInstanceAt rather than being an overload — with both present, CreateInstance<Cart>("owner") binds to the url overload and silently treats a constructor argument as an endpoint address. That bug showed up in the sample within minutes of the overload existing.

3. Domain creation and unload
AppDomain.CurrentDomain.CreateChildDomain("worker")   // was: AppDomain.CreateDomain("worker")
domain.Unload()                                       // was: AppDomain.Unload(domain)
4. A few members could not keep their signature

AssemblyAppDomainSetup, evidence and — less obviously — AssemblyName cannot cross a process boundary. AssemblyName.GetObjectData throws PlatformNotSupportedException off .NET Framework, which is the sort of thing you only discover by measuring.

ClassicReplacement
AppDomainSetupAppDomainSetup2 — same property names; the platform owns the original type on .NET 8 or .NET 10
domain.Load(name) → Assemblydomain.LoadAssembly(name) → AssemblyName
domain.GetAssemblies() → Assembly[]domain.GetLoadedAssemblies() → AssemblyName[]
AssemblyResolve returning AssemblyAssemblyResolve returning a path or raw bytes
domain.SetupInformationdomain.Setup

Settings with no meaning off .NET Framework — ShadowCopyFilesLoaderOptimization, evidence, permission sets — are deliberately not declared at all. A settable property that silently does nothing is a latent bug; a compile error is a prompt to think about what the code actually needed.

5. Suppress two obsoletion warnings
<NoWarn>$(NoWarn);SYSLIB0010;CS0672</NoWarn>
XML

MarshalByRefObject.InitializeLifetimeService is [Obsolete] on modern .NET (SYSLIB0010) and overriding it also raises CS0672. Both are harmless here: the library calls the override itself and handles the PlatformNotSupportedException the base implementation throws — which is what lets a class that returns its own lease keep working, and one that defers to base keep working too.

That is the whole list.

What You Give Up

An honest accounting matters more than a feature table, so:

  • No wire compatibility with unmodified .NET Framework peers. Both ends must reference these packages. This was the deliberate trade that allowed a clean protocol and a serializer that is not BinaryFormatter.

  • No AOT, no full trimming. Proxies are generated with Reflection.Emit.

  • No HTTP/SOAP channel, no ContextBoundObject, no context attributes. SOAP serialization has no supported equivalent off .NET Framework; ref="http" in a config file throws and names tcp and ipc as the alternatives.

  • Domains cost more to start — roughly 50–100 ms against about 1 ms. Pool and reuse them rather than creating one per unit of work.

  • A domain is not a security boundary. Domains talk over loopback TCP bound to 127.0.0.1 with unguessable capability uris. That is hardening, not isolation — another process running as the same user can still reach one. Do not use domains to sandbox code you do not trust.

  • One thread per in-flight call. IMessageSink.SyncProcessMessage is synchronous by contract, so a caller blocks a thread until the reply arrives. A call graph that bounces between processes consumes a thread per hop, and the thread pool injects new threads at roughly one per 500 ms once saturated — which surfaces as calls that take seconds and then recover. Deployments with deep callback chains should raise ThreadPool.SetMinThreads. Fixing this properly would mean abandoning the IMessageSink shape, and that shape is what makes this a compatibility layer rather than a new framework.

  • Emitted proxy types are never unloaded. The factory uses AssemblyBuilderAccess.Run and caches each contract's proxy for the process lifetime, which is what makes the second call cheap. Bounded by the number of distinct contracts — a deployment constant, not a function of traffic.

  • Lifetime leases are lazy. A lease is created only when something asks for one, so a published object with no lease is never reaped. A long-lived domain that creates a worker per unit of work must call domain.Release(instance); nothing else will.

Installing

Remoting only:

<PackageReference Include="Net4x.Runtime.Remoting" Version="1.0.0" />

Application domains — both packages are required:

<PackageReference Include="Net4x.AppDomain" Version="1.0.0" /><PackageReference Include="Net4x.AppDomain.Host" Version="1.0.0" />

Net4x.AppDomain is the API you compile against. Net4x.AppDomain.Host contains no API at all — it delivers the host program, one process of which is started per domain. It has to be a separate package because NuGet never copies a tools/ folder into your output directory, and a bare executable in lib/ would arrive without the runtimeconfig.json that dotnet exec needs. The host package carries build targets that place the complete host at tools/net8.0/ and tools/net10.0/ in your output folder, which is where the launcher looks.

Reference Net4x.AppDomain alone and it compiles fine, then throws on the first CreateChildDomain with a message listing every path it searched. Fail loudly, and name the fix.

The Host Must Match Your Runtime

Both host builds ship, and the launcher picks the one matching the runtime the parent is on: an exact match first, then the closest older build, then a newer one. That ordering is not cosmetic. A child process running .NET 8 cannot load an assembly built for .NET 10 — and the failure does not say so. The assembly loads at metadata level and then Assembly.GetType simply returns null, so what you get is:

Type 'Acme.Plugin.Entry' was not found in assembly 'Acme.Plugin' inside domain 'plugin'.

A type-not-found message for a type that plainly is in the assembly, with nothing pointing at the real cause. It is worth knowing the shape of this if you ever hand-deploy the host or pin it to one framework: the host under tools/ has to be at least as new as the code you are loading into it. The library now resolves types in the child with throwOnError: true so the underlying load failure is named, and the error mentions the host's runtime version and the tools/<tfm> layout.

How It Is Verified

Claims about a cross-process library are cheap, so here is what actually runs:

dotnet build Net4x.Runtime.Remoting.slnx -c Release
dotnet test  Net4x.Runtime.Remoting.Tests\Net4x.Runtime.Remoting.Tests.csproj   # 144 tests
dotnet test  Net4x.AppDomain.Tests\Net4x.AppDomain.Tests.csproj                 # 49 tests, real processes
pwsh -File   samples\run-cross-process-sample.ps1                               # 17 cross-process checks
pwsh -File   samples\run-appdomain-sample.ps1                                   # 24 domain checks

Every line of that runs twice, once per target framework: the test projects multi-target net8.0 and net10.0, and both sample scripts loop over the two. That is not redundancy. A domain test only exercises the host build for its own framework, so running only the newest one would leave the .NET 8 host completely untested while still shipping it in the package.

The unit tests alone cannot catch a broken cross-process path. Every one of the 49 domain tests spawns real child processes, the cross-process sample starts two separate executables talking over a real socket, and both compare Environment.ProcessId across the boundary — so they fail if anything is ever quietly served in-process. The cross-process sample is also the only thing that puts the contract in a third assembly, which is what catches an over-strict change to type or method resolution.

Latest cross-process run:

PASS  simple call returns a value
  PASS  decimal survives the wire
  PASS  DateTime survives with its Kind
  PASS  private field round-trips
  PASS  call executed in the server process        <- proves it is not an in-process shortcut
  PASS  out parameter comes back
  PASS  ref parameter comes back
  PASS  generic method (string)
  PASS  generic method (int)
  PASS  server exception propagates with type and message
  PASS  subscribe returned an id
  PASS  server called back into the client during Subscribe
  PASS  server pushed a later callback
  PASS  callback carried the pushed payload
  PASS  client-activated object keeps per-client state
  PASS  client-activated constructor argument was honoured
  PASS  each activation is a distinct instance

ALL CHECKS PASSED

Three Bugs Worth Knowing About

Because they are the kind you would hit yourself building anything similar, and each one produced a silently wrong answer rather than an exception.

Array type names on the wire. The assembly qualifier must go after the array / byref / pointer suffix. System.Int32, MyAsm[] parses back as int, not int[] — so every array-typed payload was quietly corrupted until the suffix order was fixed.

Open generic definitions. A generic method's metadata table entry is the open definition. Passing it across the wire delivers an unbound T whose parameter types have no resolvable names, and every generic call fails server-side. It must be closed at the call site.

IsVirtual as the interceptability test. As above: virtual final passes IsVirtual and cannot be overridden. Sealed overrides and implicit interface implementations sailed through the check and then ran locally on the client.

Conclusion
Not every migration should keep its remoting. If your remote surface is a handful of service calls, gRPC or ASP.NET Core minimal APIs will give you a better result and a smaller dependency footprint.

But "rewrite the distribution layer" is not always a proportionate answer to "we want to run on a supported runtime". When the remoting surface is large, deeply entangled with MarshalByRefObject identity semantics, or reliant on lifetime leases and bidirectional callbacks, a compatibility layer lets you move to .NET 8 or .NET 10 first and modernise the architecture afterwards — as a choice rather than as a prerequisite.

And in two places the replacement is simply better than what it replaces. Callbacks no longer require a reachable, addressable client, so a client behind NAT or in a container can receive server pushes over the connection it already opened. And domain isolation is now real fault isolation: a plugin that hard-crashes its process can no longer take your application down with it. 

Windows Hosting Recommendation

HostForLIFEASP.NET receives Spotlight standing advantage award for providing recommended, cheap and fast ecommerce Hosting including the latest Magento. From the leading technology company, Microsoft. All the servers are equipped with the newest Windows Server 2012 R2, SQL Server 2014, ASP.NET 7.0.4, ASP.NET MVC 6.0, Silverlight 5, WebMatrix and Visual Studio Lightswitch. Security and performance are at the core of their Magento hosting operations to confirm every website and/or application hosted on their servers is highly secured and performs at optimum level. mutually of the European ASP.NET hosting suppliers, HostForLIFE guarantees 99.9% uptime and fast loading speed. From €3.49/month , HostForLIFE provides you with unlimited disk space, unlimited domains, unlimited bandwidth,etc, for your website hosting needs.
 
https://hostforlifeasp.net/
Read More

ASP.NET's resilience Core: Using Polly to Implement Timeout, Circuit Breaker, Retry, and Fallback

Leave a Comment

Rarely do modern ASP.NET Core apps function independently. They interact with third-party APIs, databases, payment gateways, cloud storage, messaging services, and authentication providers. Although these dependencies are necessary, they also bring to uncontrollable failures for your program.


Your application shouldn't crash right away due to a brief network outage, a sluggish external API, or a throttled cloud service. Applications should instead minimize the impact on users, recover gracefully, and guard against cascade failures in downstream services.

Polly is the de facto resilience library for .NET, allowing developers to implement retry, circuit breaker, timeout, fallback, and other resilience patterns with minimal code changes. Combined with HttpClientFactory, Polly helps build reliable, production-ready applications.

In this article, you'll learn how to implement resilience policies in ASP.NET Core, understand when to use each strategy, and build a resilient communication pipeline for external services.

Why Resilience Matters

The Reality of Distributed Systems

Consider an order processing application.

Customer
    │
    ▼
ASP.NET Core API
    │
 ┌──┴───────────────┐
 ▼                  ▼
SQL Database   Payment Gateway
                    │
                    ▼
             Shipping Provider

If the payment gateway becomes temporarily unavailable:

  • Orders cannot be completed.

  • Customer requests fail.

  • Application reliability decreases.

  • Support requests increase.

Not every failure requires immediate failure. Many are temporary and recover automatically within seconds.

Understanding Retry

What Is Retry?

Retry automatically repeats an operation after a transient failure.

Typical transient failures include:

  • Temporary network interruptions

  • HTTP 503 responses

  • Connection resets

  • Cloud service throttling

Instead of failing immediately, the application retries the request after a short delay.

Configuring Retry

Register an HttpClient with a retry policy.

builder.Services
    .AddHttpClient<PaymentClient>()
    .AddPolicyHandler(
        Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .OrResult(r => !r.IsSuccessStatusCode)
            .WaitAndRetryAsync(3,
                retry =>
                    TimeSpan.FromSeconds(retry)));
C#

Why Use Retry?

Many failures are temporary.

Retrying a request a small number of times often succeeds without requiring user intervention.

However, retries should remain limited because excessive retries can increase load on already struggling services.

Understanding Circuit Breaker

Retries alone cannot solve every problem.

If a service remains unavailable, repeatedly retrying requests wastes resources.

Circuit Breaker addresses this issue.

builder.Services
    .AddHttpClient<InventoryClient>()
    .AddPolicyHandler(
        Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .CircuitBreakerAsync(
                5,
                TimeSpan.FromSeconds(30)));
C#

Why Use a Circuit Breaker?

After several consecutive failures:

  • Requests stop reaching the failing service.

  • Resources are preserved.

  • Recovery time improves.

  • Cascading failures are reduced.

After the configured break period expires, the circuit allows limited traffic to determine whether the service has recovered.

Configuring Timeouts

Waiting indefinitely for an external service reduces application throughput.

builder.Services
    .AddHttpClient<ShippingClient>()
    .AddPolicyHandler(
        Policy.TimeoutAsync<HttpResponseMessage>(
            TimeSpan.FromSeconds(10)));

Why Use Timeouts?

Timeouts prevent slow external services from consuming request threads indefinitely.

Combined with retries and circuit breakers, they help maintain predictable response times during dependency failures.

Implementing Fallback

Sometimes an alternative response is preferable to a failure.

Policy<string>
    .Handle<Exception>()
    .FallbackAsync("Service temporarily unavailable.");

Why Use Fallback?

Fallback provides a controlled response when every other resilience strategy fails.

Examples include:

  • Returning cached data

  • Displaying maintenance information

  • Serving default configuration

  • Returning partial results

Fallback should provide useful behavior rather than simply masking failures.

Combining Policies

Production applications rarely use a single resilience strategy.

Typical execution order:

Request
   │
   ▼
Timeout
   │
Retry
   │
Circuit Breaker
   │
Fallback
   │
External Service

Each policy addresses a different failure scenario.

Together they create a more reliable communication pipeline.

End-to-End Implementation

Consider an online retail platform.

Architecture:

Customer
     │
     ▼
Order API
     │
HttpClientFactory
     │
Polly Policies
     │
 ┌───┴─────────────────────┐
 ▼                         ▼
Payment API         Shipping API

Workflow:

  1. A customer places an order.

  2. The Order API calls the payment service.

  3. If a transient failure occurs, Polly retries the request.

  4. If repeated failures continue, the circuit breaker opens.

  5. During the open state, requests fail immediately without contacting the external service.

  6. If all recovery attempts fail, the fallback policy returns an alternative response.

  7. Once the break interval expires, the circuit allows a limited number of requests to determine whether the external service has recovered.

This layered approach minimizes customer impact while preventing repeated failures from overwhelming dependent systems.

Comparing Resilience Strategies

StrategyBest ForPrevents
RetryTemporary failuresImmediate request failures
Circuit BreakerRepeated failuresCascading failures
TimeoutSlow dependenciesResource exhaustion
FallbackUnrecoverable failuresPoor user experience

Each strategy addresses a different reliability concern, and they are most effective when used together.

Best Practices

  • Retry only transient failures.

  • Use exponential backoff instead of immediate retries.

  • Configure realistic timeout values.

  • Keep circuit breaker thresholds conservative.

  • Use fallback responses only where appropriate.

  • Log resilience events for diagnostics.

  • Monitor retry and circuit breaker metrics.

  • Test resilience policies under failure conditions.

  • Combine Polly with HttpClientFactory for centralized configuration.

Common Mistakes

One common mistake is retrying every exception indiscriminately. Permanent failures, such as authentication errors or invalid requests, should not be retried because they will continue to fail.

Another issue is configuring aggressive retry policies. Multiple retries across several services can amplify traffic and worsen outages rather than improving reliability.

Developers also sometimes treat fallback responses as a replacement for proper error handling. Fallback should provide an acceptable degraded experience, not conceal operational issues.

Testing and Validation

Before deploying resilience policies, verify:

  • Retry behavior

  • Circuit breaker activation

  • Timeout handling

  • Fallback responses

  • External API failures

  • Network interruptions

  • High-concurrency scenarios

  • Recovery after dependency restoration

Chaos testing and simulated dependency failures help validate resilience strategies under realistic production conditions.

Performance Considerations

Resilience policies improve reliability, but they should be configured carefully.

Consider these recommendations:

  • Limit retry attempts.

  • Use exponential backoff to reduce pressure on recovering services.

  • Monitor timeout frequency.

  • Avoid retrying long-running operations.

  • Track circuit breaker state changes.

  • Profile external dependency latency regularly.

Properly configured resilience policies improve overall system stability without introducing unnecessary processing overhead.

Security Considerations

Resilience mechanisms should not compromise application security.

Follow these recommendations:

  • Do not retry authentication failures caused by invalid credentials.

  • Log resilience events without exposing sensitive request data.

  • Protect API keys and secrets used by external services.

  • Validate HTTPS certificates for outbound requests.

  • Monitor repeated failures for signs of abuse or attacks.

  • Combine resilience with rate limiting and request timeouts.

Reliability and security should complement each other throughout the application's communication pipeline.

Troubleshooting

Retry Never Executes

Verify that the configured policy handles the specific exception or HTTP status code being returned by the external service.

Circuit Breaker Opens Too Frequently

Review failure thresholds and timeout settings. The configured limits may be too aggressive for normal traffic patterns.

Requests Continue Timing Out

Investigate the downstream service rather than continually increasing timeout values. Persistent timeouts often indicate an underlying performance issue.

Fallback Response Is Never Returned

Ensure the fallback policy wraps the retry, timeout, and circuit breaker policies in the intended execution order.

Conclusion

Building resilient ASP.NET Core applications requires more than handling exceptions. Retry policies recover from transient failures, circuit breakers prevent cascading outages, timeouts protect server resources, and fallback strategies provide graceful degradation when dependencies remain unavailable. By combining Polly with HttpClientFactory and carefully configuring resilience policies, developers can build production-ready applications that remain responsive, reliable, and easier to operate even when external services experience failures.

Windows Hosting Recommendation

HostForLIFE.eu receives Spotlight standing advantage award for providing recommended, cheap and fast ecommerce Hosting including the latest Magento. From the leading technology company, Microsoft. All the servers are equipped with the newest Windows Server 2022 R2, SQL Server 2022, ASP.NET Core 10.0 , ASP.NET MVC, Silverlight 5, WebMatrix and Visual Studio Lightswitch. Security and performance are at the core of their Magento hosting operations to confirm every website and/or application hosted on their servers is highly secured and performs at optimum level. mutually of the European ASP.NET hosting suppliers, HostForLIFE guarantees 99.9% uptime and fast loading speed. From €3.49/month , HostForLIFE provides you with unlimited disk space, unlimited domains, unlimited bandwidth,etc, for your website hosting needs.
 
https://hostforlifeasp.net/
Read More

ASP.NET Tutorial: The Foundation of Contemporary Online and Mobile Apps is REST APIs

Leave a Comment

In contemporary applications, artificial intelligence is starting to become a regular feature. AI can enhance user experience and automate tedious processes, from intelligent chatbots and document summaries to content creation and customer service.

 

A standardized approach to creating AI-powered features is offered via the OpenAI Responses API. By utilizing a single API for text generation, content analysis, and multi-step discussions, it streamlines interactions with OpenAI models. It's easy to include the Responses API into your application if you're an ASP.NET Core developer. This article explains what the OpenAI Responses API is, how it functions, and provides a real-world example of how to utilize it in an ASP.NET Core project.

What Is the OpenAI Responses API?

The OpenAI Responses API is a unified API that allows developers to send prompts to an AI model and receive intelligent responses.

It can be used to build features such as:

  • AI chat assistants

  • Content generation

  • Document summarization

  • Code explanations

  • Text classification

  • Translation

  • Question answering

  • Product recommendations

Instead of managing multiple APIs for different AI capabilities, developers can use a single endpoint for many common tasks.

Why Use the Responses API?

The Responses API offers several advantages for developers:

  • Simple integration

  • Consistent request format

  • Support for multiple AI tasks

  • Easy conversation management

  • Scalable for enterprise applications

  • Works well with ASP.NET Core APIs

This makes it a great choice for adding AI capabilities without significantly increasing application complexity.

Setting Up an ASP.NET Core Project

Create a new Web API project using the .NET CLI:

dotnet new webapi -n OpenAIResponseDemo

Navigate to the project folder:

cd OpenAIResponseDemo

Store your OpenAI API key securely using configuration or environment variables. Avoid hardcoding secrets directly into your source code.

For example, in appsettings.json:

{
  "OpenAI": {
    "ApiKey": "YOUR_API_KEY"
  }
}

For production applications, use secure secret management solutions instead of storing keys in configuration files.

Creating an AI Service

A common approach is to create a service that handles communication with the OpenAI API.

For example:

public class OpenAIService
{
    private readonly HttpClient _httpClient;

    public OpenAIService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<string> GetResponseAsync(string prompt)
    {
        // Send request to the Responses API
        // Process the response
        // Return generated text

        return "AI response";
    }
}

Keeping AI-related logic inside a dedicated service makes your application easier to maintain and test.

Creating an API Endpoint

Next, expose an endpoint that accepts user prompts.

[ApiController]
[Route("api/ai")]
public class AIController : ControllerBase
{
    private readonly OpenAIService _service;

    public AIController(OpenAIService service)
    {
        _service = service;
    }

    [HttpPost]
    public async Task<IActionResult> Generate(string prompt)
    {
        var result = await _service.GetResponseAsync(prompt);

        return Ok(result);
    }
}

When a client sends a prompt, the controller forwards it to the AI service and returns the generated response.

Practical Example

Imagine you're building a customer support application.

A user enters the following question:

How can I reset my password?

Your ASP.NET Core API sends this prompt to the OpenAI Responses API.

The AI might generate a response such as:

To reset your password, select the "Forgot Password" option on the login page, enter your registered email address, and follow the instructions sent to your inbox.

This allows your application to provide quick, intelligent assistance without requiring predefined responses for every question.

Common Use Cases

The OpenAI Responses API can power a variety of features, including:

  • Customer support chatbots

  • FAQ assistants

  • Email drafting

  • Product descriptions

  • Knowledge base search

  • Document summaries

  • Code generation

  • Content recommendations

Because the same API supports multiple scenarios, it can simplify AI integration across different parts of an application.

Best Practices

When building AI-powered applications, keep these recommendations in mind:

  • Store API keys securely.

  • Validate all user input before sending it to the AI service.

  • Handle API errors and timeouts gracefully.

  • Avoid exposing sensitive business data in prompts.

  • Cache responses when appropriate to reduce unnecessary requests.

  • Log requests and responses responsibly without storing confidential information.

  • Review AI-generated content before using it in critical business workflows.

  • Keep prompts clear and specific for better results.

Following these practices helps improve reliability, security, and user experience.

Things to Consider

Although AI is powerful, it is not always perfect.

Keep the following in mind:

  • AI-generated responses may contain inaccuracies.

  • Responses should be validated for business-critical applications.

  • Usage costs may vary depending on request volume.

  • Network latency can affect response times.

  • Responsible AI practices should always be followed.

Design your application so that users understand when content has been generated by AI and provide human review where necessary.

Conclusion

The OpenAI Responses API makes it easier than ever to add intelligent features to ASP.NET Core applications. Whether you're building a chatbot, summarizing documents, generating content, or creating an AI-powered assistant, the unified API provides a flexible foundation for a wide range of use cases.

By organizing AI functionality into reusable services, securing your API credentials, validating user input, and following best practices, you can build reliable and scalable AI-powered applications. As AI continues to become a core part of modern software development, integrating the OpenAI Responses API into your ASP.NET Core projects is an excellent way to deliver smarter and more engaging user experiences.

HostForLIFE is Best Option for ASP.NET Core 10.0 Hosting in Europe

Frankly speaking, HostForLIFE is best option to host your ASP.NET Core 10.0 Hosting in Europe. You just need to spend €2.97/month to host your site with them and you can install the latest ASP.NET Core 10.0 via their Plesk control panel. We would highly recommend them as your ASP.NET Core 9.0 Hosting in Europe.

http://hostforlifeasp.net/European-ASPNET-Core-2-Hosting


Read More
Previous PostOlder Posts Home