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
Prerequisites
- .NET 8.0 SDK
- Visual Studio 2022 with the ASP.NET and web development workload
Create the app
Start Visual Studio 2022 and select Create a new project.
On the Create a new project dialog, select ASP.NET Core Web API, and then select Next.
On the Configure your new project dialog, enter
OrleansURLShortener
for Project name, and then select Next.On the Additional information dialog, select .NET 8.0 (Long Term Support) and uncheck Use controllers, and then select Create.
Add Orleans to the project
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.
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();
Configure the silos
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();
Create the URL shortener grain
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.
- IGrainWithGuidKey
- IGrainWithIntegerKey
- IGrainWithStringKey
- IGrainWithGuidCompoundKey
- IGrainWithIntegerCompoundKey
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.
Append the following interface definition to the bottom of the Program.cs file.
public interface IUrlShortenerGrain : IGrainWithStringKey { Task SetUrl(string fullUrl); Task<string> GetUrl(); }
Create a
UrlShortenerGrain
class using the following code. This class inherits from theGrain
class provided by Orleans and implements theIUrlShortenerGrain
interface you created. The class also uses theIPersistentState
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; } = ""; }
Create the endpoints
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 namedurl
, 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();
Test the completed app
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:
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.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.Paste the shortened URL into the address bar and press enter. The page should reload and redirect you to https://learn.microsoft.com.