Small to medium sized development teams often grapple with the complexities of microservices architecture. While the distributed approach offers scalability and flexibility, it can quickly become overwhelming, especially without a large team to manage the infrastructure. Enter Microsoft Orleans, a framework designed to simplify building scalable, resilient, and distributed backends. This post delves into how Orleans can be a game-changer for teams seeking the benefits of distributed systems without the overhead of traditional microservices.
The Drawbacks of Microservices
Typically, a microservices infrastructure involves splitting your application into smaller, independent services, each running in its own environment and possibly using different technologies. For example, a user service might handle all user-related operations, running in its own Docker container with separate monitoring, deployment, and API documentation. As your application grows, you could end up managing a plethora of services like event, post, subscription, and billing services, each with its own complexities. This fragmentation can quickly become a management nightmare, especially for smaller teams.
Now with real interest rates, the backlash against microservices and the associated complexity is getting real.
Microsoft Orleans: A Simplified Approach
Microsoft Orleans (re)introduces the actor model that simplifies the design and deployment of distributed systems. In Orleans, entities like users are represented as grains, which are essentially instances of C# classes that can be instantiated as needed. These grains are identified by a primary key (e.g., a user ID) and can live in the system’s memory, being activated or deactivated as required.
You could call it a “distributed modular monolith”, in that there is a single deployable unit (unless you want to get fancy) but certain parts are activated on certain load balanced servers as required.
Benefits of Using Orleans
- Reduced Boilerplate: Unlike microservices, which require extensive setup for each service (Docker files, API docs, etc.), Orleans grains share resources within the same deployment, minimizing the need for additional boilerplate.
- Built-in Caching: The grain storage pattern keeps frequently accessed data in memory, eliminating the need for a separate caching layer.
- Automatic Scalability: Grains are automatically distributed across the available servers in the cluster, ensuring efficient use of resources without manual configuration.
- Ease of Testing: Since grains are based on C# interfaces, they can be easily mocked for unit testing, simplifying the testing process compared to microservices.
- It’s F5-able! You can just run it locally. No docker compose. No minikube. It just works and behaves just like it will spread over 50 servers. Nice!
Real-World Use Cases
Orleans isn’t just a theoretical solution; it’s used in major Microsoft products like Xbox Live and Azure, proving its effectiveness at scale. Its adoption underscores the framework’s reliability and performance, making it an attractive option for developers looking for proven solutions.
This is the key differentiator between Orleans and other products that Microsoft pump out (see MAUI, Blazor etc) – they actually use it themselves for large production public facing systems!
A quick Orleans refactor
Consider the scenario where a traditional MVC application accesses a database using Entity Framework for user management. We’ll see how this approach can be refactored using Microsoft Orleans.
Traditional MVC Users Controller
In a typical MVC application, you might have a UsersController
that interacts directly with an Entity Framework DbContext
to perform CRUD operations on users. This is an Index action that loads a single user from the database.
public class UsersController : ApiController
{
private readonly ApplicationDbContext _context;
public UsersController(ApplicationDbContext context)
{
_context = context;
}
[HttpPost]
public async Task<IActionResult> Create([FromForm] User user)
{
_context.Users.Add(user);
await _context.SaveChangesAsync();
return Ok();
}
[HttpGet]
public async Task<IActionResult> Index(string id)
{
return Ok(await _context.Users.FindAsync(id));
}
}
Refactoring with Orleans
With Orleans, you abstract user operations into a grain interface and implementation, removing direct database access from the controller and instead relying on Orleans to manage user information in Grain state.
Defining the User Grain Interface and Implementation
First, define an interface for the user grain operations:
public interface IUserGrain : IGrainWithStringKey
{
Task<User> GetUserAsync();
Task SetUserAsync(User user);
}
Then, implement the grain:
public class UserGrain : Grain, IUserGrain
{
private readonly IPersistentState<User> _userState;
public UserGrain(
[PersistentState("user", "userStore")] IPersistentState<User> userState)
{
_userState = userState;
}
public Task<User> GetUserAsync()
{
return Task.FromResult(_userState.State);
}
public async Task SetUserAsync(User user)
{
_userState.State = user;
await _userState.WriteStateAsync();
}
}
Modifying the Controller to Use the Grain
Instead of directly accessing the database, the controller now interacts with the user grain:
public class UsersController : Controller
{
private readonly IClusterClient _client;
public UsersController(IClusterClient client)
{
_client = client;
}
[HttpPost]
public async Task<IActionResult> Create([FromForm] User user)
{
var userGrain = _client.GetGrain<IUserGrain>(id);
userGrain.SetUserAsync(user);
return Ok();
}
[HttpGet]
public async Task<IActionResult> Index(string id)
{
var userGrain = _client.GetGrain<IUserGrain>(id);
var user = await userGrain.GetUserAsync();
return View(user);
}
}
This refactoring moves the saving and loading out of Entity Framework and into grain state – which will be persisted to your preferred backing data store. Of course, ADO.NET / MSSQL is supported but also lots of cloudy blob storage solutions. One major benefit is that the user information here is cached – if the user does not change between calls to the grain, and it has not been deactivated, the result is served from memory. That’s your Redis cache you can send off to pastures new too.
The team behind it
The strength of a development framework is not just in its technology but also in the community and leadership behind it. Microsoft Orleans benefits greatly from the expertise and dedication of individuals like Reuben Bond and Brady Gaster. Reuben is not just a senior developer but a central figure in the Orleans community. His active participation in the Orleans Discord channel, where he answers questions and provides guidance, is testament to his commitment to the project. You can actually ask him questions!
On the management side, Brady Gaster’s involvement brings a wealth of experience. Known for his significant contributions to the .NET world, Brady’s role in overseeing Orleans’ development ensures that the framework not only meets current developer needs but also anticipates future trends and challenges. The collaboration between these two industry veterans, along with the broader team and community, is a significant reason why Microsoft Orleans has emerged as a powerful and reliable solution.
In summary
.NET dev shops wanting to build distributed applications should take a look at Orleans before going down the garden path of microservices. By abstracting the complexities of distributed systems into manageable grains, Orleans allows developers to focus on building features rather than managing infrastructure.
For those interested in diving deeper into Microsoft Orleans, take a look at the docs on the Microsoft website, which is pleasantly well written.
Leave a Reply