Skip to content
Back to Blog
1 min read

Azure Service Fabric for Microservices

I wrote “2021-06-26-azure-service-fabric” to share practical, production-minded guidance on this topic.

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.

References

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.