Поделиться через


Пользовательское хранилище зерна

В руководстве по декларативному хранилищу субъектов вы узнали, как разрешить зернам хранить их состояние в таблице Azure с помощью одного из встроенных поставщиков хранилища. Хотя Azure — это отличное место для хранения данных, существует множество альтернативных вариантов. Существует так много, что поддержка их всех не является возможным. Вместо этого вы можете легко добавить поддержку предпочитаемого хранилища, Orleans написав пользовательский поставщик хранилища зерна.

В этом руководстве вы изучите, как написать небольшого файлового поставщика зернового хранилища. Файловая система не является лучшим местом для хранения состояний зерна, так как она является локальной, может иметь проблемы с блокировками файлов, и дата последнего обновления недостаточно, чтобы предотвратить несоответствие. Однако это простой пример для иллюстрации реализации поставщика хранилища зерна .

Начало работы

Поставщик Orleans хранилища зерна — это класс, который реализует IGrainStorage, включенный в пакет NuGet Microsoft.Orleans.Core. Он также наследует от ILifecycleParticipant<ISiloLifecycle>, что позволяет вам подписываться на определённые события в жизненном цикле силоса. Сначала создайте класс с именем FileGrainStorage.

using Microsoft.Extensions.Options;
using Orleans.Configuration;
using Orleans.Runtime;
using Orleans.Storage;

namespace GrainStorage;

public sealed class FileGrainStorage : IGrainStorage, ILifecycleParticipant<ISiloLifecycle>
{
    private readonly string _storageName;
    private readonly FileGrainStorageOptions _options;
    private readonly ClusterOptions _clusterOptions;

    public FileGrainStorage(
        string storageName,
        FileGrainStorageOptions options,
        IOptions<ClusterOptions> clusterOptions)
    {
        _storageName = storageName;
        _options = options;
        _clusterOptions = clusterOptions.Value;
    }

    public Task ClearStateAsync<T>(
        string stateName,
        GrainId grainId,
        IGrainState<T> grainState)
    {
        throw new NotImplementedException();
    }

    public Task ReadStateAsync<T>(
        string stateName,
        GrainId grainId,
        IGrainState<T> grainState)
    {
        throw new NotImplementedException();
    }

    public Task WriteStateAsync<T>(
        string stateName,
        GrainId grainId,
        IGrainState<T> grainState)
    {
        throw new NotImplementedException();
    }

    public void Participate(ISiloLifecycle lifecycle) =>
        throw new NotImplementedException();
}

Каждый метод реализует соответствующий метод в интерфейсе IGrainStorage , принимая параметр универсального типа для базового типа состояния. Вот эти методы:

Метод ILifecycleParticipant<TLifecycleObservable>.Participate подписывается на жизненный цикл хранилища.

Перед началом реализации создайте класс параметров, содержащий корневой каталог, в котором хранятся файлы состояния зерна. Создайте файл параметров с именем FileGrainStorageOptions , содержащий следующее:

using Orleans.Storage;

namespace GrainStorage;

public sealed class FileGrainStorageOptions : IStorageProviderSerializerOptions
{
    public required string RootDirectory { get; set; }

    public required IGrainStorageSerializer GrainStorageSerializer { get; set; }
}

Используя созданный класс параметров, изучите параметры конструктора FileGrainStorage класса:

  • storageName: указывает, какие зерна должны использовать этот поставщик хранилища, например [StorageProvider(ProviderName = "File")].
  • options: только что созданный класс параметров.
  • clusterOptions: параметры кластера, используемые для получения ServiceId.

Инициализация хранилища

Чтобы инициализировать хранилище, подпишитесь на ServiceLifecycleStage.ApplicationServices этап с помощью функции onStart. Рассмотрим следующую реализацию ILifecycleParticipant<TLifecycleObservable>.Participate.

public void Participate(ISiloLifecycle lifecycle) =>
    lifecycle.Subscribe(
        observerName: OptionFormattingUtilities.Name<FileGrainStorage>(_storageName),
        stage: ServiceLifecycleStage.ApplicationServices,
        onStart: (ct) =>
        {
            Directory.CreateDirectory(_options.RootDirectory);
            return Task.CompletedTask;
        });

Функция onStart условно создает корневой каталог для хранения состояний зерна, если он еще не существует.

Кроме того, предоставьте общую функцию для создания имени файла, обеспечивая уникальность для каждой службы, идентификатора зерна и типа зерна:

private string GetKeyString(string grainType, GrainId grainId) =>
    $"{_clusterOptions.ServiceId}.{grainId.Key}.{grainType}";

Состояние прочтения

Чтобы прочитать состояние "grain", получите имя файла с помощью функции GetKeyString и объедините его с корневым каталогом из экземпляра _options.

public async Task ReadStateAsync<T>(
    string stateName,
    GrainId grainId,
    IGrainState<T> grainState)
{
    var fName = GetKeyString(stateName, grainId);
    var path = Path.Combine(_options.RootDirectory, fName!);
    var fileInfo = new FileInfo(path);
    if (fileInfo is { Exists: false })
    {
        grainState.State = (T)Activator.CreateInstance(typeof(T))!;
        return;
    }

    using var stream = fileInfo.OpenText();
    var storedData = await stream.ReadToEndAsync();
    
    grainState.State = _options.GrainStorageSerializer.Deserialize<T>(new BinaryData(storedData));
    grainState.ETag = fileInfo.LastWriteTimeUtc.ToString();
}

Используйте fileInfo.LastWriteTimeUtc как ETag, что другие функции используют для проверок несоответствий, чтобы предотвратить потерю данных.

Для десериализации используйте параметр IStorageProviderSerializerOptions.GrainStorageSerializer. Это важно для правильной сериализации и десериализации состояния.

Состояние записи

Запись состояния аналогична чтению состояния.

public async Task WriteStateAsync<T>(
    string stateName,
    GrainId grainId,
    IGrainState<T> grainState)
{
    var storedData = _options.GrainStorageSerializer.Serialize(grainState.State);
    var fName = GetKeyString(stateName, grainId);
    var path = Path.Combine(_options.RootDirectory, fName!);
    var fileInfo = new FileInfo(path);
    if (fileInfo.Exists && fileInfo.LastWriteTimeUtc.ToString() != grainState.ETag)
    {
        throw new InconsistentStateException($"""
            Version conflict (WriteState): ServiceId={_clusterOptions.ServiceId}
            ProviderName={_storageName} GrainType={typeof(T)}
            GrainReference={grainId}.
            """);
    }

    await File.WriteAllBytesAsync(path, storedData.ToArray());

    fileInfo.Refresh();
    grainState.ETag = fileInfo.LastWriteTimeUtc.ToString();
}

Аналогично чтению состояния, используйте IStorageProviderSerializerOptions.GrainStorageSerializer для записи состояния. Текущие ETag проверки сверяются с последним обновленным временем UTC файла. Если дата отличается, это означает, что другая активация одного и того же зерна одновременно изменила состояние. В этой ситуации бросьте InconsistentStateException. Это приводит к тому, что текущая активация будет прекращена, чтобы предотвратить перезапись состояния, ранее сохраненного другим активированным элементом.

Очистить состояние

Очистка состояния включает удаление файла, если он существует.

public Task ClearStateAsync<T>(
    string stateName,
    GrainId grainId,
    IGrainState<T> grainState)
{
    var fName = GetKeyString(stateName, grainId);
    var path = Path.Combine(_options.RootDirectory, fName!);
    var fileInfo = new FileInfo(path);
    if (fileInfo.Exists)
    {
        if (fileInfo.LastWriteTimeUtc.ToString() != grainState.ETag)
        {
            throw new InconsistentStateException($"""
                Version conflict (ClearState): ServiceId={_clusterOptions.ServiceId}
                ProviderName={_storageName} GrainType={typeof(T)}
                GrainReference={grainId}.
                """);
        }

        grainState.ETag = null;
        grainState.State = (T)Activator.CreateInstance(typeof(T))!;

        fileInfo.Delete();
    }

    return Task.CompletedTask;
}

По той же причине, что WriteStateAsync, также проверьте несоответствие. Перед удалением файла и сбросом ETag проверьте, соответствует ли текущее ETag последнему времени записи в формате UTC.

Соберите всё вместе

Затем создайте фабрику, которая позволяет определить параметры имени поставщика при создании экземпляра FileGrainStorage для упрощения регистрации в коллекции служб.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Orleans.Configuration.Overrides;
using Orleans.Storage;

namespace GrainStorage;

internal static class FileGrainStorageFactory
{
    internal static IGrainStorage Create(
        IServiceProvider services, string name)
    {
        var optionsMonitor =
            services.GetRequiredService<IOptionsMonitor<FileGrainStorageOptions>>();

        return ActivatorUtilities.CreateInstance<FileGrainStorage>(
            services,
            name,
            optionsMonitor.Get(name),
            services.GetProviderClusterOptions(name));
    }
}

Наконец, чтобы зарегистрировать хранилище зерна, создайте расширение в ISiloBuilder. Это расширение внутренне регистрирует хранилище зерна в качестве именованной службы с использованием AddSingletonNamedService, предоставляемого расширением Orleans.Core.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Orleans.Runtime;
using Orleans.Storage;

namespace GrainStorage;

public static class FileSiloBuilderExtensions
{
    public static ISiloBuilder AddFileGrainStorage(
        this ISiloBuilder builder,
        string providerName,
        Action<FileGrainStorageOptions> options) =>
        builder.ConfigureServices(
            services => services.AddFileGrainStorage(
                providerName, options));

    public static IServiceCollection AddFileGrainStorage(
        this IServiceCollection services,
        string providerName,
        Action<FileGrainStorageOptions> options)
    {
        services.AddOptions<FileGrainStorageOptions>(providerName)
            .Configure(options);

        services.AddTransient<
            IPostConfigureOptions<FileGrainStorageOptions>,
            DefaultStorageProviderSerializerOptionsConfigurator<FileGrainStorageOptions>>();

        return services.AddSingletonNamedService(providerName, FileGrainStorageFactory.Create)
            .AddSingletonNamedService(providerName,
                (p, n) =>
                    (ILifecycleParticipant<ISiloLifecycle>)p.GetRequiredServiceByName<IGrainStorage>(n));
    }
}

FileGrainStorage реализует два интерфейса: IGrainStorage и ILifecycleParticipant<ISiloLifecycle>. Таким образом, зарегистрируйте две именованные службы, по одному для каждого интерфейса:

return services.AddSingletonNamedService(providerName, FileGrainStorageFactory.Create)
    .AddSingletonNamedService(providerName,
        (p, n) => (ILifecycleParticipant<ISiloLifecycle>)p.GetRequiredServiceByName<IGrainStorage>(n));

Это позволяет добавлять хранилище файлов с помощью расширения в ISiloBuilder:

using GrainStorage;
using Microsoft.Extensions.Hosting;

using IHost host = new HostBuilder()
    .UseOrleans(builder =>
    {
        builder.UseLocalhostClustering()
            .AddFileGrainStorage("File", options =>
            {
                string path = Environment.GetFolderPath(
                    Environment.SpecialFolder.ApplicationData);

                options.RootDirectory = Path.Combine(path, "Orleans/GrainState/v1");
            });
    })
    .Build();

await host.RunAsync();

Теперь вы можете оформлять свои зерна с помощью провайдера [StorageProvider(ProviderName = "File")], и он сохраняет состояние зерна в корневом каталоге, заданном в параметрах. Рассмотрим полную реализацию FileGrainStorage:

using Microsoft.Extensions.Options;
using Orleans.Configuration;
using Orleans.Runtime;
using Orleans.Storage;

namespace GrainStorage;

public sealed class FileGrainStorage : IGrainStorage, ILifecycleParticipant<ISiloLifecycle>
{
    private readonly string _storageName;
    private readonly FileGrainStorageOptions _options;
    private readonly ClusterOptions _clusterOptions;

    public FileGrainStorage(
        string storageName,
        FileGrainStorageOptions options,
        IOptions<ClusterOptions> clusterOptions)
    {
        _storageName = storageName;
        _options = options;
        _clusterOptions = clusterOptions.Value;
    }

    // <clearstateasync>
    public Task ClearStateAsync<T>(
        string stateName,
        GrainId grainId,
        IGrainState<T> grainState)
    {
        var fName = GetKeyString(stateName, grainId);
        var path = Path.Combine(_options.RootDirectory, fName!);
        var fileInfo = new FileInfo(path);
        if (fileInfo.Exists)
        {
            if (fileInfo.LastWriteTimeUtc.ToString() != grainState.ETag)
            {
                throw new InconsistentStateException($"""
                    Version conflict (ClearState): ServiceId={_clusterOptions.ServiceId}
                    ProviderName={_storageName} GrainType={typeof(T)}
                    GrainReference={grainId}.
                    """);
            }

            grainState.ETag = null;
            grainState.State = (T)Activator.CreateInstance(typeof(T))!;

            fileInfo.Delete();
        }

        return Task.CompletedTask;
    }
    // </clearstateasync>
    // <readstateasync>
    public async Task ReadStateAsync<T>(
        string stateName,
        GrainId grainId,
        IGrainState<T> grainState)
    {
        var fName = GetKeyString(stateName, grainId);
        var path = Path.Combine(_options.RootDirectory, fName!);
        var fileInfo = new FileInfo(path);
        if (fileInfo is { Exists: false })
        {
            grainState.State = (T)Activator.CreateInstance(typeof(T))!;
            return;
        }

        using var stream = fileInfo.OpenText();
        var storedData = await stream.ReadToEndAsync();
        
        grainState.State = _options.GrainStorageSerializer.Deserialize<T>(new BinaryData(storedData));
        grainState.ETag = fileInfo.LastWriteTimeUtc.ToString();
    }
    // </readstateasync>
    // <writestateasync>
    public async Task WriteStateAsync<T>(
        string stateName,
        GrainId grainId,
        IGrainState<T> grainState)
    {
        var storedData = _options.GrainStorageSerializer.Serialize(grainState.State);
        var fName = GetKeyString(stateName, grainId);
        var path = Path.Combine(_options.RootDirectory, fName!);
        var fileInfo = new FileInfo(path);
        if (fileInfo.Exists && fileInfo.LastWriteTimeUtc.ToString() != grainState.ETag)
        {
            throw new InconsistentStateException($"""
                Version conflict (WriteState): ServiceId={_clusterOptions.ServiceId}
                ProviderName={_storageName} GrainType={typeof(T)}
                GrainReference={grainId}.
                """);
        }

        await File.WriteAllBytesAsync(path, storedData.ToArray());

        fileInfo.Refresh();
        grainState.ETag = fileInfo.LastWriteTimeUtc.ToString();
    }
    // </writestateasync>
    // <participate>
    public void Participate(ISiloLifecycle lifecycle) =>
        lifecycle.Subscribe(
            observerName: OptionFormattingUtilities.Name<FileGrainStorage>(_storageName),
            stage: ServiceLifecycleStage.ApplicationServices,
            onStart: (ct) =>
            {
                Directory.CreateDirectory(_options.RootDirectory);
                return Task.CompletedTask;
            });
    // </participate>
    // <getkeystring>
    private string GetKeyString(string grainType, GrainId grainId) =>
        $"{_clusterOptions.ServiceId}.{grainId.Key}.{grainType}";
    // </getkeystring>
}
using System;
using System.Threading.Tasks;
using Orleans;
using Orleans.Storage;
using Orleans.Runtime;

namespace GrainStorage;

public class FileGrainStorage
    : IGrainStorage, ILifecycleParticipant<ISiloLifecycle>
{
    private readonly string _storageName;
    private readonly FileGrainStorageOptions _options;
    private readonly ClusterOptions _clusterOptions;
    private readonly IGrainFactory _grainFactory;
    private readonly ITypeResolver _typeResolver;
    private JsonSerializerSettings _jsonSettings;

    public FileGrainStorage(
        string storageName,
        FileGrainStorageOptions options,
        IOptions<ClusterOptions> clusterOptions,
        IGrainFactory grainFactory,
        ITypeResolver typeResolver)
    {
        _storageName = storageName;
        _options = options;
        _clusterOptions = clusterOptions.Value;
        _grainFactory = grainFactory;
        _typeResolver = typeResolver;
    }

    public Task ClearStateAsync(
        string grainType,
        GrainReference grainReference,
        IGrainState grainState)
    {
        throw new NotImplementedException();
    }

    public Task ReadStateAsync(
        string grainType,
        GrainReference grainReference,
        IGrainState grainState)
    {
        throw new NotImplementedException();
    }

    public Task WriteStateAsync(
        string grainType,
        GrainReference grainReference,
        IGrainState grainState)
    {
        throw new NotImplementedException();
    }

    public void Participate(
        ISiloLifecycle lifecycle)
    {
        throw new NotImplementedException();
    }
}

Перед началом реализации создайте класс параметров, содержащий корневой каталог, в котором хранятся файлы состояния зерна. Создайте файл параметров с именем FileGrainStorageOptions:

public class FileGrainStorageOptions
{
    public string RootDirectory { get; set; }
}

Создайте конструктор, содержащий два поля: storageName чтобы указать, какие зерна должны использовать это хранилище ([StorageProvider(ProviderName = "File")]) и directoryкаталог, в котором сохраняются состояния зерна.

IGrainFactory и ITypeResolver используются в следующем разделе для инициализации хранилища.

Кроме того, в качестве аргументов следует использовать два варианта: ваш собственный FileGrainStorageOptions и ClusterOptions. Они необходимы для реализации функций хранилища.

При сериализации и десериализации в формате JSON вам также нужен JsonSerializerSettings.

Это важно

JSON — это деталь реализации. Вы можете решить, какой протокол сериализации/десериализации соответствует приложению. Другой распространенный формат — двоичный.

Инициализация хранилища

Чтобы инициализировать хранилище, зарегистрируйте функцию Init в жизненном цикле ApplicationServices.

public void Participate(ISiloLifecycle lifecycle)
{
    lifecycle.Subscribe(
        OptionFormattingUtilities.Name<FileGrainStorage>(_storageName),
        ServiceLifecycleStage.ApplicationServices,
        Init);
}

Функция Init задает объект _jsonSettings, используемый для настройки сериализатора JSON. В то же время создайте папку для хранения состояний зерна, если она еще не существует.

private Task Init(CancellationToken ct)
{
    // Settings could be made configurable from Options.
    _jsonSettings =
        OrleansJsonSerializer.UpdateSerializerSettings(
            OrleansJsonSerializer.GetDefaultSerializerSettings(
                _typeResolver,
                _grainFactory),
            false,
            false,
            null);

    var directory = new System.IO.DirectoryInfo(_rootDirectory);
    if (!directory.Exists)
        directory.Create();

    return Task.CompletedTask;
}

Кроме того, предоставьте общую функцию для создания имени файла, обеспечивая уникальность для каждой службы, идентификатора зерна и типа зерна.

private string GetKeyString(string grainType, GrainReference grainReference)
{
    return $"{_clusterOptions.ServiceId}.{grainReference.ToKeyString()}.{grainType}";
}

Состояние прочтения

Чтобы прочитать состояние зерна, используйте ранее определённую функцию для получения имени файла и объедините его с корневым каталогом, указанным в параметрах.

public async Task ReadStateAsync(
    string grainType,
    GrainReference grainReference,
    IGrainState grainState)
{
    var fName = GetKeyString(grainType, grainReference);
    var path = Path.Combine(_options.RootDirectory, fName);

    var fileInfo = new FileInfo(path);
    if (!fileInfo.Exists)
    {
        grainState.State = Activator.CreateInstance(grainState.State.GetType());
        return;
    }

    using (var stream = fileInfo.OpenText())
    {
        var storedData = await stream.ReadToEndAsync();
        grainState.State = JsonConvert.DeserializeObject(storedData, _jsonSettings);
    }

    grainState.ETag = fileInfo.LastWriteTimeUtc.ToString();
}

Используйте fileInfo.LastWriteTimeUtc в качестве ETag, которые другие функции используют для проверок несоответствий, чтобы предотвратить потерю данных.

Обратите внимание, что для десериализации используйте _jsonSettings набор в Init функции. Это важно для правильной сериализации или десериализации состояния.

Состояние записи

Запись состояния аналогична чтению состояния.

public async Task WriteStateAsync(
    string grainType,
    GrainReference grainReference,
    IGrainState grainState)
{
    var storedData = JsonConvert.SerializeObject(grainState.State, _jsonSettings);

    var fName = GetKeyString(grainType, grainReference);
    var path = Path.Combine(_options.RootDirectory, fName);

    var fileInfo = new FileInfo(path);

    if (fileInfo.Exists && fileInfo.LastWriteTimeUtc.ToString() != grainState.ETag)
    {
        throw new InconsistentStateException(
            $"Version conflict (WriteState): ServiceId={_clusterOptions.ServiceId} " +
            $"ProviderName={_storageName} GrainType={grainType} " +
            $"GrainReference={grainReference.ToKeyString()}.");
    }

    using (var stream = new StreamWriter(fileInfo.Open(FileMode.Create, FileAccess.Write)))
    {
        await stream.WriteAsync(storedData);
    }

    fileInfo.Refresh();
    grainState.ETag = fileInfo.LastWriteTimeUtc.ToString();
}

Аналогично состоянию чтения, используйте _jsonSettings для записи состояния. Текущий ETag проверяет время последнего обновления файла в формате UTC. Если дата отличается, это означает, что другая активация одного и того же зерна одновременно изменила состояние. В этой ситуации выбрасывается исключение InconsistentStateException, что приводит к остановке текущей операции активации, чтобы предотвратить перезапись состояния, ранее сохраненного другой активированной зерницей.

Очистить состояние

Очистка состояния включает удаление файла, если он существует.

public Task ClearStateAsync(
    string grainType,
    GrainReference grainReference,
    IGrainState grainState)
{
    var fName = GetKeyString(grainType, grainReference);
    var path = Path.Combine(_options.RootDirectory, fName);

    var fileInfo = new FileInfo(path);
    if (fileInfo.Exists)
    {
        if (fileInfo.LastWriteTimeUtc.ToString() != grainState.ETag)
        {
            throw new InconsistentStateException(
                $"Version conflict (ClearState): ServiceId={_clusterOptions.ServiceId} " +
                $"ProviderName={_storageName} GrainType={grainType} " +
                $"GrainReference={grainReference.ToKeyString()}.");
        }

        grainState.ETag = null;
        grainState.State = Activator.CreateInstance(grainState.State.GetType());
        fileInfo.Delete();
    }

    return Task.CompletedTask;
}

По той же причине, что WriteStateAsync, также проверьте несоответствие. Перед удалением файла и сбросом ETag проверьте, совпадает ли текущий ETag с временем последнего изменения файла в формате UTC.

Соберите всё вместе

Затем создайте фабрику, которая позволяет определить параметры имени поставщика при создании экземпляра FileGrainStorage для упрощения регистрации в коллекции служб.

public static class FileGrainStorageFactory
{
    internal static IGrainStorage Create(
        IServiceProvider services, string name)
    {
        IOptionsSnapshot<FileGrainStorageOptions> optionsSnapshot =
            services.GetRequiredService<IOptionsSnapshot<FileGrainStorageOptions>>();

        return ActivatorUtilities.CreateInstance<FileGrainStorage>(
            services,
            name,
            optionsSnapshot.Get(name),
            services.GetProviderClusterOptions(name));
    }
}

Наконец, чтобы зарегистрировать хранилище зерна, создайте расширение в ISiloHostBuilder. Это расширение внутренне регистрирует хранилище зерна в качестве именованной службы с использованием .AddSingletonNamedService(...), предоставляемого расширением Orleans.Core.

public static class FileSiloBuilderExtensions
{
    public static ISiloHostBuilder AddFileGrainStorage(
        this ISiloHostBuilder builder,
        string providerName,
        Action<FileGrainStorageOptions> options)
    {
        return builder.ConfigureServices(
            services => services.AddFileGrainStorage(providerName, options));
    }

    public static IServiceCollection AddFileGrainStorage(
        this IServiceCollection services,
        string providerName,
        Action<FileGrainStorageOptions> options)
    {
        services.AddOptions<FileGrainStorageOptions>(providerName).Configure(options);

        return services.AddSingletonNamedService(providerName, FileGrainStorageFactory.Create)
            .AddSingletonNamedService(
                providerName,
                (s, n) => (ILifecycleParticipant<ISiloLifecycle>)s.GetRequiredServiceByName<IGrainStorage>(n));
    }
}

FileGrainStorage реализует два интерфейса: IGrainStorage и ILifecycleParticipant<ISiloLifecycle>. Таким образом, зарегистрируйте две именованные службы, по одному для каждого интерфейса:

return services.AddSingletonNamedService(providerName, FileGrainStorageFactory.Create)
    .AddSingletonNamedService(
        providerName,
        (s, n) => (ILifecycleParticipant<ISiloLifecycle>)s.GetRequiredServiceByName<IGrainStorage>(n));

Это позволяет добавлять хранилище файлов с помощью расширения в ISiloHostBuilder:

var silo = new HostBuilder()
    .UseOrleans(builder =>
    {
        builder.UseLocalhostClustering()
            .AddFileGrainStorage("File", opts =>
            {
                opts.RootDirectory = "C:/TestFiles";
            });
    })
    .Build();

Теперь вы можете оформлять свои зерна с помощью провайдера [StorageProvider(ProviderName = "File")], и он сохраняет состояние зерна в корневом каталоге, заданном в параметрах.