快速入门:使用 ASP.NET Core 生成第一个 Orleans 应用

在本快速入门中,你将使用 Orleans 和 ASP.NET Core 8.0 Minimal API 生成 URL 缩短器应用。 用户将完整的 URL 提交到应用应用的 /shorten 终结点,并获取缩短的版本,以便与重定向到原始站点的其他人共享。 该应用使用 Orleans grain 和接收器以分布式方式管理状态,以实现可伸缩性和复原能力。 为 Azure 容器应用等分布式云托管服务和 Kubernetes 等平台开发应用时,这些功能至关重要。

在快速入门结束后,你将拥有一个应用程序,它可以使用简短易记的 URL 来创建和管理重定向。 你将学习如何执行以下操作:

  • 将 Orleans 添加到 ASP.NET Core 应用
  • 使用粒度和接收器
  • 配置状态管理
  • 将 Orleans 与 API 终结点集成

先决条件

创建应用

  1. 启动 Visual Studio 2022 并选择“创建新项目”。

  2. 在“新建项目”对话框中,选择“ASP.NET Core Web API”,然后选择“下一步”。

  3. 在“配置新项目”对话框中,输入 作为“项目名称”,然后选择“下一步”。

  4. 在“其他信息”对话框中,选择“.NET 8.0 (长期支持)”,取消选中“使用控制器”,然后选择“创建”。

将 Orleans 添加到项目中

可通过一系列 NuGet 包获取 Orleans,每个包都提供对各种功能的访问。 在此快速入门中,请将 Microsoft.Orleans.Server NuGet 包添加到应用中:

  • 右键单击解决方案资源管理器中的“OrleansURLShortener”项目节点,然后选择“管理 NuGet 包”。
  • 在包管理器窗口中,搜索“Orleans”。
  • 从搜索结果中选择“Microsoft.Orleans.Server”包,然后选择“安装”。

打开 Program.cs 文件,将现有内容替换为以下代码:

using Orleans.Runtime;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

配置接收器

筒仓是 Orleans 的核心组成部分,负责存储和管理谷物。 筒仓可以容纳一种或多种谷物;一组筒仓被称为集群。 群集协调接收器之间的工作,允许与粒度通信,就像它们在单个进程中都可用一样。

在 Program.cs 文件的顶部,重构代码以使用 Orleans。 以下代码使用 ISiloBuilder 类创建包含可存储粒度的接收器的 localhost 群集。 ISiloBuilder 还使用 AddMemoryGrainStorage 配置 Orleans 接收器以将 grain 永久保存在内存中。 此方案使用本地资源进行开发,但生产应用可以配置为通过 Azure Blob 存储等服务使用高度可缩放的群集和存储。

using Orleans.Runtime;

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseOrleans(static siloBuilder =>
{
    siloBuilder.UseLocalhostClustering();
    siloBuilder.AddMemoryGrainStorage("urls");
});

using var app = builder.Build();

创建 URL 缩短器粒度

grain 是 Orleans 应用程序的最基本的基元和基础部分。 "Grain 是一个继承自 Grain 基类的类,它管理着各种内部行为和与 Orleans 的集成点。" grain 还应实现下列接口之一,以定义 grain 键标识符。 其中每个接口都定义了一个类似的协定,但对 Orleans 用于跟踪 grain 的标识符使用不同的数据类型来标记类,例如字符串或整数。

对于本快速入门,你将使用 IGrainWithStringKey,因为字符串是处理 URL 值和短代码的合理选择。

Orleans grain 还可以使用自定义接口来定义其方法和属性。 URL 缩短器粒度接口应定义两个方法:

  • 用于永久保存原始 URL 和相应缩短 URL 的 SetUrl 方法。
  • 用于在给定缩短 URL 的情况下检索原始 URL 的 GetUrl 方法。
  1. 将以下接口定义追加到 Program.cs 文件的底部。

    public interface IUrlShortenerGrain : IGrainWithStringKey
    {
        Task SetUrl(string fullUrl);
    
        Task<string> GetUrl();
    }
    
  2. 使用以下代码创建 UrlShortenerGrain 类。 此类继承自 Grain 提供的 Orleans 类,并实现你创建的 IUrlShortenerGrain 接口。 该类还使用 IPersistentState 的 Orleans 接口来管理 URL 状态值的读写,以将其存储到已配置的隔离存储中。

    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; } = "";
    }
    

创建终结点

接下来,创建两个终结点以利用 Orleans grain 和接收器配置:

  • /shorten 终结点,用于创建和存储缩短版的 URL。 原始的完整 URL 作为名为 url 的查询字符串参数提供,缩短的 URL 返回给用户供后续使用。
  • /go/{shortenedRouteSegment:required} 终结点,用于通过作为参数提供的缩短 URL 将用户重定向到原始 URL。

IGrainFactory 接口注入到两个端点中。 使用粒度工厂可以检索和管理对接收器中存储的各个粒度的引用。 在调用 方法之前,将以下代码追加到 Program.cs 文件:

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();

测试已完成的应用

应用的核心功能现已完成,可以进行测试。 最终的应用代码应与以下示例匹配:

// <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>

使用以下步骤在浏览器中测试应用程序:

  1. 使用 Visual Studio 顶部的“运行”按钮启动应用。 应用应在浏览器中启动,显示熟悉的 Hello world! 文本。

  2. 在浏览器地址栏中,通过输入 URL 路径(例如 shorten)来测试 {localhost}/shorten?url=https://learn.microsoft.com 终结点。 此时页面应会重新加载并提供缩短的 URL。 将缩短的 URL 复制到剪贴板。

    该屏幕截图显示了从 Visual Studio 启动的 URL 缩短器的结果。

  3. 将缩短的 URL 粘贴到地址栏中,然后按 Enter 键。 此时页面应会重新加载并重定向到 https://learn.microsoft.com