Host Orleans on Service Fabric
Orleans can run on Azure Service Fabric as an unpartitioned stateless Reliable Service. Each Service Fabric service instance hosts one Orleans silo in a normal .NET generic host.
There is no Orleans Service Fabric hosting or clustering package. The integration is application-authored using:
Microsoft.ServiceFabric.Servicesfor the Reliable Services runtime.Microsoft.Orleans.Serverfor the silo.- One supported Orleans clustering provider for membership and gateway discovery.
Use multiple stateless service instances across nodes, fault domains, and update domains. Don’t use a stateful Reliable Service to host Orleans merely to obtain Service Fabric state replication: Orleans grain state uses an Orleans storage provider, independently of the Service Fabric service type.
Separate platform and Orleans responsibilities
Section titled “Separate platform and Orleans responsibilities”Service Fabric and Orleans have complementary roles:
| Concern | Owner |
|---|---|
| Process placement, restart, service instance lifecycle, application upgrade | Service Fabric |
| Per-instance port allocation and node address | Service Fabric service manifest and runtime context |
| Service Fabric service endpoint publication | ICommunicationListener.OpenAsync and the Service Fabric Naming Service |
| Silo membership, failure detection, and Orleans gateway discovery | Orleans and the selected clustering provider |
| Grain activation and placement | Orleans |
| Durable grain state, reminders, and streams | Configured Orleans providers |
Service Fabric Naming Service isn’t an Orleans clustering provider. Orleans silos and clients must use the same external clustering provider, ServiceId, and ClusterId. Don’t use CoreHostingExtensions.UseLocalhostClustering in a deployed service.
Implement the generic-host integration
Section titled “Implement the generic-host integration”The sample under snippets/service-fabric uses Azure Table Storage as one concrete clustering provider so that the code compiles end to end. Replace it with another supported provider when appropriate for the environment.
The compiled example targets Windows Service Fabric nodes and publishes self-contained for win-x64, so the nodes don’t need a separately installed .NET runtime. For a Linux cluster, select the corresponding Linux runtime identifier, publish a Linux executable, and update the service manifest entry point. Keep the service lifecycle and Orleans configuration pattern unchanged.
The service manifest declares two dynamically allocated TCP endpoints:
<?xml version="1.0" encoding="utf-8"?><ServiceManifest Name="OrleansSiloPkg" Version="1.0.0" xmlns="http://schemas.microsoft.com/2011/01/fabric" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ServiceTypes> <StatelessServiceType ServiceTypeName="OrleansSiloType" /> </ServiceTypes>
<CodePackage Name="Code" Version="1.0.0"> <EntryPoint> <ExeHost> <Program>ServiceFabricSilo.exe</Program> <WorkingFolder>CodePackage</WorkingFolder> </ExeHost> </EntryPoint> <EnvironmentVariables> <EnvironmentVariable Name="ORLEANS_SERVICE_ID" Value="OrleansService" /> <EnvironmentVariable Name="ORLEANS_CLUSTER_ID" Value="production" /> <EnvironmentVariable Name="ORLEANS_TABLE_SERVICE_URI" Value="https://orleansstorage.table.core.windows.net" /> </EnvironmentVariables> </CodePackage>
<Resources> <Endpoints> <Endpoint Name="OrleansSiloEndpoint" Protocol="tcp" /> <Endpoint Name="OrleansGatewayEndpoint" Protocol="tcp" /> </Endpoints> <ManagedIdentities DefaultIdentity="OrleansSiloServiceIdentity"> <ManagedIdentity Name="OrleansSiloServiceIdentity" /> </ManagedIdentities> </Resources></ServiceManifest>The stateless service creates an ICommunicationListener:
using System.Fabric;using Microsoft.Extensions.Hosting;using Microsoft.ServiceFabric.Services.Communication.Runtime;using Microsoft.ServiceFabric.Services.Runtime;
namespace ServiceFabricSilo;
internal sealed class OrleansStatelessService( StatelessServiceContext context, Func<StatelessServiceContext, IHost> createHost) : StatelessService(context){ protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners() { yield return new ServiceInstanceListener( serviceContext => new OrleansCommunicationListener( serviceContext, () => createHost(serviceContext)), "Orleans"); }}The listener owns the Orleans generic host. ICommunicationListener.OpenAsync starts it, ICommunicationListener.CloseAsync requests graceful shutdown, and ICommunicationListener.Abort disposes it without assuming graceful work can complete:
using System.Fabric;using System.Text.Json;using Microsoft.Extensions.Hosting;using Microsoft.ServiceFabric.Services.Communication.Runtime;
namespace ServiceFabricSilo;
internal sealed class OrleansCommunicationListener( StatelessServiceContext context, Func<IHost> createHost) : ICommunicationListener{ private readonly object _lock = new(); private ListenerState? _state;
public async Task<string> OpenAsync(CancellationToken cancellationToken) { var state = new ListenerState(createHost(), new CancellationTokenSource()); lock (_lock) { if (_state is not null) { state.Host.Dispose(); state.Abort.Dispose(); throw new InvalidOperationException("The listener is already open."); }
_state = state; state.ActiveOperations++; }
try { using var linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, state.Abort.Token); await state.Host.StartAsync(linkedCancellation.Token); var activationContext = context.CodePackageActivationContext; var address = context.NodeContext.IPAddressOrFQDN; var siloPort = activationContext.GetEndpoint("OrleansSiloEndpoint").Port; var gatewayPort = activationContext.GetEndpoint("OrleansGatewayEndpoint").Port;
return JsonSerializer.Serialize(new { Endpoints = new { Silo = $"tcp://{address}:{siloPort}", Gateway = $"tcp://{address}:{gatewayPort}", }, }); } catch { RemoveState(state); throw; } finally { CompleteOperation(state); } }
public async Task CloseAsync(CancellationToken cancellationToken) { ListenerState? state; lock (_lock) { state = _state; if (state is not null) { state.ActiveOperations++; } }
if (state is null) { return; }
try { using var linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, state.Abort.Token); await state.Host.StopAsync(linkedCancellation.Token); } finally { RemoveState(state); CompleteOperation(state); } }
public void Abort() { ListenerState? state; lock (_lock) { state = _state; if (state is not null) { _state = null; state.Removed = true; state.AbortRequested = true; } }
if (state is not null) { state.Abort.Cancel(); lock (_lock) { state.AbortSignaled = true; }
DisposeIfComplete(state); } }
private void RemoveState(ListenerState state) { lock (_lock) { if (ReferenceEquals(_state, state)) { _state = null; }
state.Removed = true; } }
private void CompleteOperation(ListenerState state) { lock (_lock) { state.ActiveOperations--; }
DisposeIfComplete(state); }
private void DisposeIfComplete(ListenerState state) { var dispose = false; lock (_lock) { if (state.Removed && state.ActiveOperations == 0 && (!state.AbortRequested || state.AbortSignaled) && !state.Disposed) { state.Disposed = true; dispose = true; } }
if (dispose) { state.Host.Dispose(); state.Abort.Dispose(); } }
private sealed record ListenerState(IHost Host, CancellationTokenSource Abort) { public int ActiveOperations { get; set; }
public bool Removed { get; set; }
public bool AbortRequested { get; set; }
public bool AbortSignaled { get; set; }
public bool Disposed { get; set; } }}Finally, the process registers the Service Fabric service type and constructs the Orleans host:
using System.Fabric;using Azure.Data.Tables;using Azure.Identity;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;using Microsoft.ServiceFabric.Services.Runtime;using Orleans.Configuration;using Orleans.Hosting;
namespace ServiceFabricSilo;
internal static class Program{ private const string ServiceTypeName = "OrleansSiloType";
public static async Task Main() { try { await ServiceRuntime.RegisterServiceAsync( ServiceTypeName, context => new OrleansStatelessService(context, CreateHost));
await Task.Delay(Timeout.InfiniteTimeSpan); } catch (Exception exception) { Console.Error.WriteLine(exception); throw; } }
private static IHost CreateHost(StatelessServiceContext context) { var activationContext = context.CodePackageActivationContext; var siloEndpoint = activationContext.GetEndpoint("OrleansSiloEndpoint"); var gatewayEndpoint = activationContext.GetEndpoint("OrleansGatewayEndpoint"); var advertisedHost = context.NodeContext.IPAddressOrFQDN;
var serviceId = GetRequiredSetting("ORLEANS_SERVICE_ID"); var clusterId = GetRequiredSetting("ORLEANS_CLUSTER_ID"); var tableServiceUri = new Uri(GetRequiredSetting("ORLEANS_TABLE_SERVICE_URI"));
var builder = Host.CreateApplicationBuilder(); builder.UseOrleans(siloBuilder => { siloBuilder .Configure<ClusterOptions>(options => { options.ServiceId = serviceId; options.ClusterId = clusterId; }) .UseAzureStorageClustering(options => options.TableServiceClient = new TableServiceClient( tableServiceUri, new DefaultAzureCredential())) .ConfigureEndpoints( advertisedHost, siloEndpoint.Port, gatewayEndpoint.Port, listenOnAnyHostAddress: true); });
builder.Services.Configure<HostOptions>(options => { options.ShutdownTimeout = TimeSpan.FromSeconds(120); });
return builder.Build(); }
private static string GetRequiredSetting(string name) => Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : throw new InvalidOperationException( $"The required setting '{name}' isn't configured.");}The example uses:
- The node address from NodeContext.IPAddressOrFQDN as the advertised address.
- Ports allocated from the service manifest for the silo and gateway endpoints.
listenOnAnyHostAddress: truebecause the advertised node address might not be an address the process can bind directly.- DefaultAzureCredential with an Azure Table service URI, avoiding a storage account key in configuration.
- A 120-second .NET host shutdown timeout.
The application manifest should create a singleton-partition stateless service with multiple instances and ServicePackageActivationMode="ExclusiveProcess". This example also wires the Orleans settings, a dedicated local RunAs user, and the service identity binding used by DefaultAzureCredential:
<?xml version="1.0" encoding="utf-8"?><ApplicationManifest ApplicationTypeName="OrleansApplicationType" ApplicationTypeVersion="1.0.0" xmlns="http://schemas.microsoft.com/2011/01/fabric" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Parameters> <Parameter Name="OrleansSilo_InstanceCount" DefaultValue="3" /> <Parameter Name="Orleans_ServiceId" DefaultValue="OrleansService" /> <Parameter Name="Orleans_ClusterId" DefaultValue="production" /> <Parameter Name="Orleans_TableServiceUri" DefaultValue="https://orleansstorage.table.core.windows.net" /> </Parameters>
<ServiceManifestImport> <ServiceManifestRef ServiceManifestName="OrleansSiloPkg" ServiceManifestVersion="1.0.0" /> <EnvironmentOverrides CodePackageRef="Code"> <EnvironmentVariable Name="ORLEANS_SERVICE_ID" Value="[Orleans_ServiceId]" /> <EnvironmentVariable Name="ORLEANS_CLUSTER_ID" Value="[Orleans_ClusterId]" /> <EnvironmentVariable Name="ORLEANS_TABLE_SERVICE_URI" Value="[Orleans_TableServiceUri]" /> </EnvironmentOverrides> <Policies> <RunAsPolicy CodePackageRef="Code" UserRef="OrleansSiloUser" EntryPointType="Main" /> <IdentityBindingPolicy ServiceIdentityRef="OrleansSiloServiceIdentity" ApplicationIdentityRef="OrleansSiloApplicationIdentity" /> </Policies> </ServiceManifestImport>
<DefaultServices> <Service Name="OrleansSilo" ServicePackageActivationMode="ExclusiveProcess"> <StatelessService ServiceTypeName="OrleansSiloType" InstanceCount="[OrleansSilo_InstanceCount]"> <SingletonPartition /> </StatelessService> </Service> </DefaultServices>
<Principals> <Users> <User Name="OrleansSiloUser" /> </Users> <ManagedIdentities> <ManagedIdentity Name="OrleansSiloApplicationIdentity" /> </ManagedIdentities> </Principals></ApplicationManifest>Exclusive process activation avoids sharing one host process and endpoint resources among service instances. Size the instance count for capacity and redundancy; three is only an example.
Configure topology and endpoints
Section titled “Configure topology and endpoints”Each service instance needs:
- A unique silo identity. Orleans generates one if the application doesn’t set SiloName.
- A silo endpoint reachable from every other silo.
- A gateway endpoint reachable from every Orleans client when gateways are enabled.
- Connectivity to the clustering provider and every configured state, reminder, and stream provider.
When an endpoint omits Port in ServiceManifest.xml, Service Fabric allocates a port from the application port range. The application reads that allocation from GetEndpoint. Don’t hard-code the Orleans default ports unless the cluster reserves them and prevents conflicts.
ICommunicationListener.OpenAsync returns a string that Service Fabric publishes through its Naming Service. The sample publishes the allocated Orleans endpoints for diagnostics. Orleans clients don’t consume that value; they discover gateways through the Orleans clustering provider.
Apply network controls so only trusted silos can reach the silo ports and only trusted clients can reach gateway ports. Don’t expose either port directly to the public internet. Configure Orleans TLS when the network boundary alone doesn’t provide the required authentication and encryption.
Health and readiness
Section titled “Health and readiness”Service Fabric opens the communication listener before calling StatelessService.RunAsync. In this integration, OpenAsync completes only after the Orleans host starts, so Service Fabric doesn’t publish the listener address while silo startup is still in progress.
That lifecycle boundary is necessary but isn’t a complete application health model. Add application-authored Service Fabric health reports for sustained conditions that operators or monitored upgrades must evaluate, such as:
- Failure to join the intended Orleans cluster.
- Loss of a required storage or clustering dependency.
- Local saturation or inability to make forward progress.
- A prolonged degraded mode.
Keep transient dependency failures from causing synchronized restarts. If the process also exposes HTTP ingress, implement separate startup, readiness, liveness, and dependency checks as described in Health and observability.
Set Service Fabric upgrade health policies from application signals. Built-in platform health can detect process and deployment failures, but it can’t infer Orleans request correctness or capacity.
Shutdown and scale-in
Section titled “Shutdown and scale-in”During graceful stateless service shutdown, Service Fabric calls ICommunicationListener.CloseAsync. The listener calls StopAsync, allowing Orleans to leave membership and stop within the supplied deadline.
Graceful shutdown isn’t guaranteed:
Abortcan occur after a process, node, or lifecycle failure.- Service Fabric can terminate a code package after configured timeouts.
- The host can crash or lose network access before leaving membership.
Therefore, correctness must tolerate abrupt silo loss and unknown call outcomes. Configure Service Fabric close and upgrade timeouts to exceed the measured Orleans shutdown duration, and align them with ShutdownTimeout.
Scale in one instance or update domain at a time where possible. Wait for Orleans membership and application latency to stabilize before removing more capacity.
Rolling upgrades
Section titled “Rolling upgrades”Use Service Fabric monitored rolling upgrades so each update domain must satisfy the application health policy before the next proceeds. Maintain enough instances outside one update domain to serve the workload and absorb reactivated grains.
Old and new silos coexist during a rolling upgrade. They must be compatible at every boundary:
- Grain interfaces and serialized payloads.
- Persisted grain state.
- Clustering, reminder, stream, and storage schemas.
- External side effects and deduplication records.
Increment the application type version and service manifest version for a release. Also increment the version of every changed code, configuration, or data package; changing binaries without changing the CodePackage version doesn’t identify a new code package to Service Fabric. Test automatic rollback with mixed versions and with state written by the new version. See Graceful shutdown and upgrades and Service Fabric application upgrades.
For an incompatible release, use a separately named Service Fabric application and a distinct Orleans ClusterId, then follow the blue-green guidance. Don’t let incompatible clusters concurrently own the same mutable grain state.
Identity, secrets, and configuration
Section titled “Identity, secrets, and configuration”Two identities have different purposes:
- A RunAs identity is the local operating-system account for the code package. The sample creates a dedicated local user and applies a
RunAsPolicyinstead of running under Service Fabric’s default account. - A managed identity authenticates the application to Azure resources. The sample maps
OrleansSiloApplicationIdentityin the application manifest toOrleansSiloServiceIdentityin the service manifest.
The manifest mapping alone doesn’t create or assign an Azure identity. The sample is configured for a user-assigned identity. Deploy the application as an Azure resource, assign the user-assigned identity in the Azure Resource Manager deployment, and map its friendly name to OrleansSiloApplicationIdentity. Applications not deployed as Azure resources can’t use Service Fabric application managed identities. See Deploy a Service Fabric application with a user-assigned managed identity.
A system-assigned identity uses the reserved application identity name SystemAssigned; update the application principal and identity-binding policy accordingly if you choose that model.
Grant the managed identity only the provider permissions it needs. For the sample’s Azure Table clustering provider, assign a role containing table data actions, such as Storage Table Data Contributor, at the narrowest practical scope. A management-plane Contributor role doesn’t grant table data access.
Don’t put credentials in source, manifests, application parameters, or command lines. When workload identity isn’t available, use an external secret store or Service Fabric encrypted secrets, and plan rotation and expiry alerts.
Treat ServiceId as the stable application identity and ClusterId as the environment or deployment identity. The sample declares nonsecret defaults in the service manifest and parameterized environment overrides in the application manifest. Replace the example table URI and set environment-specific application parameters at deployment. Preserve the effective parameter map during application upgrades because Service Fabric doesn’t automatically carry application parameters forward. Validate effective configuration before starting the host and fail startup explicitly when required values are absent.
Diagnostics
Section titled “Diagnostics”Correlate:
- Service Fabric application, service, partition, instance, node, code package, and version.
- Orleans service ID, cluster ID, silo name, and advertised endpoints.
- Deployment update domain and application upgrade operation.
Use Service Fabric Explorer and health events to inspect placement, restarts, package activation, endpoint allocation, and upgrade decisions. Export Orleans logs, metrics, and traces as described in Orleans observability. Preserve telemetry outside the Service Fabric cluster so it remains available during cluster incidents.
During an incident, compare the endpoints published by the listener with Orleans membership and test the exact advertised silo and gateway addresses. See Troubleshoot deployments.
