6 min read
Azure Service Fabric for Microservices
Introduction
Azure Service Fabric is a distributed systems platform for packaging, deploying, and managing scalable and reliable microservices and containers. It powers many Azure services including Azure SQL Database, Cosmos DB, and Azure Event Hubs. This guide covers building and deploying applications on Service Fabric.
Service Fabric Programming Models
Reliable Services
// Stateless Service
public class OrderApiService : StatelessService
{
public OrderApiService(StatelessServiceContext context)
: base(context)
{
}
protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
{
return new[]
{
new ServiceInstanceListener(serviceContext =>
new KestrelCommunicationListener(serviceContext, "ServiceEndpoint", (url, listener) =>
{
return new WebHostBuilder()
.UseKestrel()
.ConfigureServices(services =>
{
services.AddSingleton(serviceContext);
services.AddControllers();
})
.UseServiceFabricIntegration(listener, ServiceFabricIntegrationOptions.UseUniqueServiceUrl)
.UseUrls(url)
.Build();
}))
};
}
protected override async Task RunAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
ServiceEventSource.Current.ServiceMessage(Context, "Working...");
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
}
}
}
Stateful Service with Reliable Collections
public class ShoppingCartService : StatefulService
{
public ShoppingCartService(StatefulServiceContext context)
: base(context)
{
}
protected override IEnumerable<ServiceReplicaListener> CreateServiceReplicaListeners()
{
return new[]
{
new ServiceReplicaListener(context =>
new ServiceRemotingListener<IShoppingCartService>(context, this))
};
}
public async Task<Cart> GetCartAsync(string userId)
{
var carts = await StateManager.GetOrAddAsync<IReliableDictionary<string, Cart>>("carts");
using var tx = StateManager.CreateTransaction();
var result = await carts.TryGetValueAsync(tx, userId);
return result.HasValue ? result.Value : new Cart { UserId = userId };
}
public async Task AddItemAsync(string userId, CartItem item)
{
var carts = await StateManager.GetOrAddAsync<IReliableDictionary<string, Cart>>("carts");
using var tx = StateManager.CreateTransaction();
var result = await carts.TryGetValueAsync(tx, userId);
var cart = result.HasValue ? result.Value : new Cart { UserId = userId };
cart.Items.Add(item);
cart.UpdatedAt = DateTime.UtcNow;
await carts.SetAsync(tx, userId, cart);
await tx.CommitAsync();
}
public async Task<bool> RemoveItemAsync(string userId, Guid itemId)
{
var carts = await StateManager.GetOrAddAsync<IReliableDictionary<string, Cart>>("carts");
using var tx = StateManager.CreateTransaction();
var result = await carts.TryGetValueAsync(tx, userId);
if (!result.HasValue)
return false;
var cart = result.Value;
var removed = cart.Items.RemoveAll(i => i.Id == itemId) > 0;
if (removed)
{
await carts.SetAsync(tx, userId, cart);
await tx.CommitAsync();
}
return removed;
}
public async Task ClearCartAsync(string userId)
{
var carts = await StateManager.GetOrAddAsync<IReliableDictionary<string, Cart>>("carts");
using var tx = StateManager.CreateTransaction();
await carts.TryRemoveAsync(tx, userId);
await tx.CommitAsync();
}
}
Reliable Actors
Actor Implementation
// Actor Interface
public interface IOrderActor : IActor
{
Task<Order> GetOrderAsync();
Task CreateOrderAsync(OrderDetails details);
Task AddItemAsync(OrderItem item);
Task SubmitAsync();
Task CancelAsync(string reason);
}
// Actor Implementation
[StatePersistence(StatePersistence.Persisted)]
public class OrderActor : Actor, IOrderActor, IRemindable
{
private const string OrderStateName = "OrderState";
private const string ExpirationReminderName = "OrderExpiration";
public OrderActor(ActorService actorService, ActorId actorId)
: base(actorService, actorId)
{
}
protected override async Task OnActivateAsync()
{
ActorEventSource.Current.ActorMessage(this, "OrderActor activated: {0}", Id);
// Initialize state if needed
var state = await StateManager.TryGetStateAsync<Order>(OrderStateName);
if (!state.HasValue)
{
await StateManager.SetStateAsync(OrderStateName, new Order
{
Id = Guid.Parse(Id.ToString()),
Status = OrderStatus.New,
CreatedAt = DateTime.UtcNow
});
}
}
public async Task<Order> GetOrderAsync()
{
return await StateManager.GetStateAsync<Order>(OrderStateName);
}
public async Task CreateOrderAsync(OrderDetails details)
{
var order = new Order
{
Id = Guid.Parse(Id.ToString()),
CustomerId = details.CustomerId,
Items = new List<OrderItem>(),
Status = OrderStatus.Draft,
CreatedAt = DateTime.UtcNow
};
await StateManager.SetStateAsync(OrderStateName, order);
// Set reminder for order expiration (abandon if not submitted in 30 minutes)
await RegisterReminderAsync(
ExpirationReminderName,
null,
TimeSpan.FromMinutes(30),
TimeSpan.FromMilliseconds(-1)); // One-time reminder
}
public async Task AddItemAsync(OrderItem item)
{
var order = await StateManager.GetStateAsync<Order>(OrderStateName);
if (order.Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot modify non-draft order");
order.Items.Add(item);
order.UpdatedAt = DateTime.UtcNow;
await StateManager.SetStateAsync(OrderStateName, order);
}
public async Task SubmitAsync()
{
var order = await StateManager.GetStateAsync<Order>(OrderStateName);
if (order.Status != OrderStatus.Draft)
throw new InvalidOperationException("Order already submitted");
if (!order.Items.Any())
throw new InvalidOperationException("Cannot submit empty order");
order.Status = OrderStatus.Submitted;
order.SubmittedAt = DateTime.UtcNow;
await StateManager.SetStateAsync(OrderStateName, order);
// Cancel expiration reminder
await UnregisterReminderAsync(
GetReminder(ExpirationReminderName));
// Publish event
var eventProxy = GetEvent<IOrderEvents>();
eventProxy.OrderSubmitted(order.Id, order.CustomerId, order.Items.Sum(i => i.TotalPrice));
}
public async Task CancelAsync(string reason)
{
var order = await StateManager.GetStateAsync<Order>(OrderStateName);
if (order.Status == OrderStatus.Shipped)
throw new InvalidOperationException("Cannot cancel shipped order");
order.Status = OrderStatus.Cancelled;
order.CancellationReason = reason;
order.CancelledAt = DateTime.UtcNow;
await StateManager.SetStateAsync(OrderStateName, order);
}
public async Task ReceiveReminderAsync(
string reminderName,
byte[] state,
TimeSpan dueTime,
TimeSpan period)
{
if (reminderName == ExpirationReminderName)
{
var order = await StateManager.GetStateAsync<Order>(OrderStateName);
if (order.Status == OrderStatus.Draft)
{
await CancelAsync("Order expired - not submitted within time limit");
}
}
}
}
Service Communication
Service Remoting
// Client calling a service
public class OrderServiceClient : IOrderServiceClient
{
private readonly IOrderService _orderService;
public OrderServiceClient()
{
_orderService = ServiceProxy.Create<IOrderService>(
new Uri("fabric:/MyApplication/OrderService"),
new ServicePartitionKey(0));
}
public async Task<Order> GetOrderAsync(Guid orderId)
{
return await _orderService.GetOrderAsync(orderId);
}
}
// Actor client
public class OrderActorClient
{
public async Task<Order> GetOrderAsync(Guid orderId)
{
var actorId = new ActorId(orderId);
var orderActor = ActorProxy.Create<IOrderActor>(
actorId,
new Uri("fabric:/MyApplication/OrderActorService"));
return await orderActor.GetOrderAsync();
}
}
Reverse Proxy
// HTTP call through Service Fabric reverse proxy
var client = new HttpClient();
var response = await client.GetAsync(
"http://localhost:19081/MyApplication/OrderService/api/orders/123");
Application Manifest
<!-- ApplicationManifest.xml -->
<?xml version="1.0" encoding="utf-8"?>
<ApplicationManifest ApplicationTypeName="ECommerceType" ApplicationTypeVersion="1.0.0"
xmlns="http://schemas.microsoft.com/2011/01/fabric">
<Parameters>
<Parameter Name="OrderService_InstanceCount" DefaultValue="-1" />
<Parameter Name="ShoppingCartService_PartitionCount" DefaultValue="5" />
<Parameter Name="ShoppingCartService_MinReplicaSetSize" DefaultValue="3" />
<Parameter Name="ShoppingCartService_TargetReplicaSetSize" DefaultValue="3" />
</Parameters>
<ServiceManifestImport>
<ServiceManifestRef ServiceManifestName="OrderServicePkg" ServiceManifestVersion="1.0.0" />
<ConfigOverrides />
<Policies>
<RunAsPolicy CodePackageRef="Code" UserRef="SetupAdminUser" EntryPointType="Setup" />
</Policies>
</ServiceManifestImport>
<ServiceManifestImport>
<ServiceManifestRef ServiceManifestName="ShoppingCartServicePkg" ServiceManifestVersion="1.0.0" />
<ConfigOverrides />
</ServiceManifestImport>
<DefaultServices>
<Service Name="OrderService" ServicePackageActivationMode="ExclusiveProcess">
<StatelessService ServiceTypeName="OrderServiceType" InstanceCount="[OrderService_InstanceCount]">
<SingletonPartition />
</StatelessService>
</Service>
<Service Name="ShoppingCartService" ServicePackageActivationMode="ExclusiveProcess">
<StatefulService ServiceTypeName="ShoppingCartServiceType"
TargetReplicaSetSize="[ShoppingCartService_TargetReplicaSetSize]"
MinReplicaSetSize="[ShoppingCartService_MinReplicaSetSize]">
<UniformInt64Partition PartitionCount="[ShoppingCartService_PartitionCount]"
LowKey="0" HighKey="100" />
</StatefulService>
</Service>
</DefaultServices>
<Principals>
<Users>
<User Name="SetupAdminUser">
<MemberOf>
<SystemGroup Name="Administrators" />
</MemberOf>
</User>
</Users>
</Principals>
</ApplicationManifest>
Deployment with PowerShell
# Connect to cluster
Connect-ServiceFabricCluster -ConnectionEndpoint "mycluster.australiaeast.cloudapp.azure.com:19000" `
-X509Credential `
-ServerCertThumbprint "1234567890ABCDEF" `
-FindType FindByThumbprint `
-FindValue "1234567890ABCDEF" `
-StoreLocation CurrentUser `
-StoreName My
# Copy application package
Copy-ServiceFabricApplicationPackage `
-ApplicationPackagePath ".\pkg\Release" `
-ApplicationPackagePathInImageStore "ECommerceV1" `
-ImageStoreConnectionString "fabric:ImageStore"
# Register application type
Register-ServiceFabricApplicationType -ApplicationPathInImageStore "ECommerceV1"
# Create application instance
New-ServiceFabricApplication `
-ApplicationName "fabric:/ECommerce" `
-ApplicationTypeName "ECommerceType" `
-ApplicationTypeVersion "1.0.0" `
-ApplicationParameter @{
"OrderService_InstanceCount" = "-1"
"ShoppingCartService_PartitionCount" = "10"
}
Health Monitoring
public class HealthReporter : IHostedService
{
private readonly StatelessServiceContext _context;
private Timer? _timer;
protected override Task OnOpenAsync(CancellationToken cancellationToken)
{
_timer = new Timer(ReportHealth, null, TimeSpan.Zero, TimeSpan.FromSeconds(30));
return Task.CompletedTask;
}
private void ReportHealth(object? state)
{
var healthInfo = new HealthInformation("HealthReporter", "Connectivity", HealthState.Ok)
{
Description = "Service is healthy",
TimeToLive = TimeSpan.FromMinutes(2),
RemoveWhenExpired = true
};
_context.ServicePartition.ReportInstanceHealth(healthInfo);
}
}
Conclusion
Azure Service Fabric provides a mature platform for building microservices with built-in state management, service discovery, and health monitoring. While newer platforms like Kubernetes have gained popularity, Service Fabric remains a powerful choice, especially for stateful services and actor-based systems.