Skip to content

Quickstart: Build your first Orleans app with ASP.NET Core

In this quickstart, you use Orleans and ASP.NET Core 8.0 Minimal APIs to build a URL shortener app. Users submit a full URL to the app’s /shorten endpoint and get a shortened version to share with others, who are redirected to the original site. The app uses Orleans grains and silos to manage state in a distributed manner to allow for scalability and resiliency. These features are critical when developing apps for distributed cloud hosting services like Azure Container Apps and platforms like Kubernetes.

At the end of the quickstart, you have an app that creates and handles redirects using short, friendly URLs. You learn how to:

  • Add Orleans to an ASP.NET Core app
  • Work with grains and silos
  • Configure state management
  • Integrate Orleans with API endpoints

  1. Start Visual Studio and select Create a new project.

  2. On the Create a new project dialog, select ASP.NET Core Web API, and then select Next.

  3. On the Configure your new project dialog, enter OrleansURLShortener for Project name, and then select Next.

  4. On the Additional information dialog, select .NET 8.0 (Long Term Support) and uncheck Use controllers, and then select Create.

  1. Inside Visual Studio Code, open the integrated terminal.

  2. Change to the directory (cd) that will contain the project.

  3. Run the following commands:

    Terminal window
    dotnet new webapi -o OrleansURLShortener
    code -r OrleansURLShortener

    The dotnet new command creates a new Minimal API project in the OrleansURLShortener folder. The code command opens the OrleansURLShortener folder in the current instance of Visual Studio Code.

    Visual Studio Code displays a dialog box that asks Do you trust the authors of the files in this folder. Select:

    • The checkbox trust the authors of all files in the parent folder
    • Yes, I trust the authors (because dotnet generated the files).

Orleans is available through a collection of NuGet packages, each of which provides access to various features. For this quickstart, add the Microsoft.Orleans.Server NuGet package to the app:

  • Right-click on the OrleansURLShortener project node in the solution explorer and select Manage NuGet Packages.
  • In the package manager window, search for Orleans.
  • Choose the Microsoft.Orleans.Server package from the search results and then select Install.

In the Visual Studio Code terminal, run the following command:

Terminal window
dotnet add package Microsoft.Orleans.Server

Or, in .NET 10+:

Terminal window
dotnet package add Microsoft.Orleans.Server

Open the Program.cs file and replace the existing content with the following code:

using Orleans.Runtime;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();

Silos are a core building block of Orleans responsible for storing and managing grains. A silo can contain one or more grains; a group of silos is known as a cluster. The cluster coordinates work between silos, allowing communication with grains as though they were all available in a single process.

At the top of the Program.cs file, refactor the code to use Orleans. The following code uses a ISiloBuilder class to create a localhost cluster with a silo that can store grains. The ISiloBuilder also uses the AddMemoryGrainStorage to configure the Orleans silos to persist grains in memory. This scenario uses local resources for development, but a production app can be configured to use highly scalable clusters and storage using services like Azure Blob Storage.

using Orleans.Runtime;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseOrleans(static siloBuilder =>
{
siloBuilder.UseLocalhostClustering();
siloBuilder.AddMemoryGrainStorage("urls");
});
using var app = builder.Build();

Grains are the most essential primitives and building blocks of Orleans applications. A grain is a class that inherits from the Grain base class, which manages various internal behaviors and integration points with Orleans. Grains should also implement one of the following interfaces to define their grain key identifier. Each of these interfaces defines a similar contract, but marks your class with a different data type for the identifier that Orleans uses to track the grain, such as a string or integer.

For this quickstart, you use the IGrainWithStringKey, since strings are a logical choice for working with URL values and short codes.

Orleans grains can also use a custom interface to define their methods and properties. The URL shortener grain interface should define two methods:

  • A SetUrl method to persist the original and their corresponding shortened URLs.
  • A GetUrl method to retrieve the original URL given the shortened URL.
  1. Append the following interface definition to the bottom of the Program.cs file.

    public interface IUrlShortenerGrain : IGrainWithStringKey
    {
    Task SetUrl(string fullUrl);
    Task<string> GetUrl();
    }
  2. Create a UrlShortenerGrain class using the following code. This class inherits from the Grain class provided by Orleans and implements the IUrlShortenerGrain interface you created. The class also uses the IPersistentState<T> interface of Orleans to manage reading and writing state values for the URLs to the configured silo storage.

    public sealed class UrlShortenerGrain(
    [PersistentState(
    stateName: "url",
    storageName: "urls")]
    IPersistentState<UrlDetails> state)
    : Grain, IUrlShortenerGrain
    {
    public async Task SetUrl(string fullUrl)
    {
    state.State = new()
    {
    ShortenedRouteSegment = this.GetPrimaryKeyString(),
    FullUrl = fullUrl
    };
    await state.WriteStateAsync();
    }
    public Task<string> GetUrl() =>
    Task.FromResult(state.State.FullUrl);
    }
    [GenerateSerializer, Alias(nameof(UrlDetails))]
    public sealed record class UrlDetails
    {
    [Id(0)]
    public string FullUrl { get; set; } = "";
    [Id(1)]
    public string ShortenedRouteSegment { get; set; } = "";
    }

Next, create two endpoints to utilize the Orleans grain and silo configurations:

  • A /shorten endpoint to handle creating and storing a shortened version of the URL. The original, full URL is provided as a query string parameter named url, and the shortened URL is returned to the user for later use.
  • A /go/{shortenedRouteSegment:required} endpoint to handle redirecting users to the original URL using the shortened URL that is supplied as a parameter.

Inject the IGrainFactory interface into both endpoints. Grain Factories enable you to retrieve and manage references to individual grains that are stored in silos. Append the following code to the Program.cs file before the app.Run() method call:

app.MapGet("/", static () => "Welcome to the URL shortener, powered by Orleans!");
app.MapGet("/shorten",
static async (IGrainFactory grains, HttpRequest request, string url) =>
{
var host = $"{request.Scheme}://{request.Host.Value}";
// Validate the URL query string.
if (string.IsNullOrWhiteSpace(url) ||
Uri.IsWellFormedUriString(url, UriKind.Absolute) is false)
{
return Results.BadRequest($"""
The URL query string is required and needs to be well formed.
Consider, ${host}/shorten?url=https://www.microsoft.com.
""");
}
// Create a unique, short ID
var shortenedRouteSegment = Guid.NewGuid().GetHashCode().ToString("X");
// Create and persist a grain with the shortened ID and full URL
var shortenerGrain =
grains.GetGrain<IUrlShortenerGrain>(shortenedRouteSegment);
await shortenerGrain.SetUrl(url);
// Return the shortened URL for later use
var resultBuilder = new UriBuilder(host)
{
Path = $"/go/{shortenedRouteSegment}"
};
return Results.Ok(resultBuilder.Uri);
});
app.MapGet("/go/{shortenedRouteSegment:required}",
static async (IGrainFactory grains, string shortenedRouteSegment) =>
{
// Retrieve the grain using the shortened ID and url to the original URL
var shortenerGrain =
grains.GetGrain<IUrlShortenerGrain>(shortenedRouteSegment);
var url = await shortenerGrain.GetUrl();
// Handles missing schemes, defaults to "http://".
var redirectBuilder = new UriBuilder(url);
return Results.Redirect(redirectBuilder.Uri.ToString());
});
app.Run();

The core functionality of the app is now complete and ready to be tested. The final app code should match the following example:

// <configuration>
using Orleans.Runtime;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseOrleans(static siloBuilder =>
{
siloBuilder.UseLocalhostClustering();
siloBuilder.AddMemoryGrainStorage("urls");
});
using var app = builder.Build();
// </configuration>
// <endpoints>
app.MapGet("/", static () => "Welcome to the URL shortener, powered by Orleans!");
app.MapGet("/shorten",
static async (IGrainFactory grains, HttpRequest request, string url) =>
{
var host = $"{request.Scheme}://{request.Host.Value}";
// Validate the URL query string.
if (string.IsNullOrWhiteSpace(url) ||
Uri.IsWellFormedUriString(url, UriKind.Absolute) is false)
{
return Results.BadRequest($"""
The URL query string is required and needs to be well formed.
Consider, ${host}/shorten?url=https://www.microsoft.com.
""");
}
// Create a unique, short ID
var shortenedRouteSegment = Guid.NewGuid().GetHashCode().ToString("X");
// Create and persist a grain with the shortened ID and full URL
var shortenerGrain =
grains.GetGrain<IUrlShortenerGrain>(shortenedRouteSegment);
await shortenerGrain.SetUrl(url);
// Return the shortened URL for later use
var resultBuilder = new UriBuilder(host)
{
Path = $"/go/{shortenedRouteSegment}"
};
return Results.Ok(resultBuilder.Uri);
});
app.MapGet("/go/{shortenedRouteSegment:required}",
static async (IGrainFactory grains, string shortenedRouteSegment) =>
{
// Retrieve the grain using the shortened ID and url to the original URL
var shortenerGrain =
grains.GetGrain<IUrlShortenerGrain>(shortenedRouteSegment);
var url = await shortenerGrain.GetUrl();
// Handles missing schemes, defaults to "http://".
var redirectBuilder = new UriBuilder(url);
return Results.Redirect(redirectBuilder.Uri.ToString());
});
app.Run();
// </endpoints>
// <graininterface>
public interface IUrlShortenerGrain : IGrainWithStringKey
{
Task SetUrl(string fullUrl);
Task<string> GetUrl();
}
// </graininterface>
// <grain>
public sealed class UrlShortenerGrain(
[PersistentState(
stateName: "url",
storageName: "urls")]
IPersistentState<UrlDetails> state)
: Grain, IUrlShortenerGrain
{
public async Task SetUrl(string fullUrl)
{
state.State = new()
{
ShortenedRouteSegment = this.GetPrimaryKeyString(),
FullUrl = fullUrl
};
await state.WriteStateAsync();
}
public Task<string> GetUrl() =>
Task.FromResult(state.State.FullUrl);
}
[GenerateSerializer, Alias(nameof(UrlDetails))]
public sealed record class UrlDetails
{
[Id(0)]
public string FullUrl { get; set; } = "";
[Id(1)]
public string ShortenedRouteSegment { get; set; } = "";
}
// </grain>

Test the application in the browser using the following steps:

  1. Start the app using the run button at the top of Visual Studio. The app should launch in the browser and display the familiar Hello world! text.

  2. In the browser address bar, test the shorten endpoint by entering a URL path such as {localhost}/shorten?url=https://learn.microsoft.com. The page should reload and provide a shortened URL. Copy the shortened URL to your clipboard.

    A screenshot showing the result of the URL shortener launched from Visual Studio.

  3. Paste the shortened URL into the address bar and press enter. The page should reload and redirect you to https://learn.microsoft.com.

  1. Inside the Visual Studio Code terminal, run the dotnet run command again to launch the app. The app should launch in the browser and display the familiar Hello world! text.

    Terminal window
    dotnet run
  2. In the browser address bar, test the shorten endpoint by entering a URL path such as {localhost}/shorten?url=https://learn.microsoft.com. The page should reload and provide a shortened URL. Copy the shortened URL to your clipboard.

    A screenshot showing the result of the URL shortener launched from Visual Studio Code.

  3. Paste the shortened URL into the address bar and press enter. The page should reload and redirect you to https://learn.microsoft.com.


Applies to: Orleans 8.0, Orleans 9.0, Orleans 10.0

Next steps: Production-ready Orleans with Aspire

Section titled “Next steps: Production-ready Orleans with Aspire”

The quickstart above uses in-memory storage and localhost clustering, which works well for development but isn’t suitable for production deployments. When you’re ready to build production-ready Orleans applications, Aspire provides a streamlined approach with:

  • Resource management: Easily add Redis, Azure Storage, or SQL databases as backing stores for clustering, grain storage, and reminders
  • Service discovery: Automatic configuration of Orleans silos and clients without manual endpoint configuration
  • Observability: Built-in OpenTelemetry integration for distributed tracing, metrics, and logging
  • Health checks: Automatic health check endpoints for your Orleans cluster
  • Multi-replica support: Scale your Orleans silos horizontally with WithReplicas()

Here’s how the URL shortener app would look with Aspire and Redis:

AppHost project (Program.cs):

var builder = DistributedApplication.CreateBuilder(args);
// Add Redis for clustering and grain storage
var redis = builder.AddRedis("redis");
// Configure Orleans to use Redis
var orleans = builder.AddOrleans("cluster")
.WithClustering(redis)
.WithGrainStorage("urls", redis);
// Add the Orleans silo project
builder.AddProject<Projects.OrleansURLShortener>("silo")
.WithReference(orleans)
.WithReference(redis);
builder.Build().Run();

Silo project (Program.cs):

var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddKeyedRedisClient("redis");
builder.UseOrleans();
var app = builder.Build();
// ... rest of app configuration

With Aspire, Orleans automatically picks up the cluster configuration from environment variables injected by the AppHost, eliminating the need for manual configuration code.

Applies to: Orleans 3.x, Orleans 7.0