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


Руководство для разработчиков по устойчивым сущностям в .NET

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

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

В настоящее время доступно два программных интерфейса для определения сущностей:

  • Синтаксис на основе классов представляет сущности и операции как классы и методы. Этот синтаксис позволяет получить легко читаемый код и вызывать операции с помощью интерфейсов с проверкой типов.

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

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

Примечание.

Синтаксис на основе классов — это лишь один из слоев, реализуемых поверх синтаксиса на основе функций, поэтому в одном приложении можно одновременно применять оба варианта.

Определение классов сущностей

В следующем примере реализована сущность Counter, которая хранит одно целочисленное значение и поддерживает четыре операции: Add, Reset, Get и Delete.

[JsonObject(MemberSerialization.OptIn)]
public class Counter
{
    [JsonProperty("value")]
    public int Value { get; set; }

    public void Add(int amount) 
    {
        this.Value += amount;
    }

    public Task Reset() 
    {
        this.Value = 0;
        return Task.CompletedTask;
    }

    public Task<int> Get() 
    {
        return Task.FromResult(this.Value);
    }

    public void Delete() 
    {
        Entity.Current.DeleteState();
    }

    [FunctionName(nameof(Counter))]
    public static Task Run([EntityTrigger] IDurableEntityContext ctx)
        => ctx.DispatchAsync<Counter>();
}

Функция Run содержит шаблон, необходимый для использования синтаксиса на основе классов. Это должна быть статическая Функция Azure. Она выполняется однократно для каждого сообщения операции, которое сущность обрабатывает. Когда вызывается DispatchAsync<T> и сущность еще не находится в памяти, конструируется объект типа T, и его поля заполняются из последнего сохраненного файла JSON, найденного в хранилище (если таковой имеется). Затем вызывается метод с соответствующим именем.

Функция EntityTrigger в Run этом примере не должна находиться в самом классе Сущности. Он может находиться в любом допустимом расположении для функции Azure: внутри пространства имен верхнего уровня или внутри класса верхнего уровня. Однако если вложенный более глубокий (например, функция объявлена внутри вложенного класса), эта функция не будет распознана последней средой выполнения.

Примечание.

Состояние сущности на основе класса неявно создается до того, как сущность обработает операцию, и его можно явно удалить в операции путем вызова Entity.Current.DeleteState().

Примечание.

Для запуска сущностей в изолированной модели требуется Функции Azure core 4.0.5455 Tools или более поздней версии.

Существует два способа определения сущности как класса в изолированной рабочей модели C#. Они создают сущности с разными структурами сериализации состояния.

При следующем подходе весь объект сериализуется при определении сущности.

public class Counter
{
    public int Value { get; set; }

    public void Add(int amount) 
    {
        this.Value += amount;
    }

    public Task Reset() 
    {
        this.Value = 0;
        return Task.CompletedTask;
    }

    public Task<int> Get() 
    {
        return Task.FromResult(this.Value);
    }

    // Delete is implicitly defined when defining an entity this way

    [Function(nameof(Counter))]
    public static Task Run([EntityTrigger] TaskEntityDispatcher dispatcher)
        => dispatcher.DispatchAsync<Counter>();
}

Реализация TaskEntity<TState>на основе, которая упрощает внедрение зависимостей. В этом случае состояние десериализуется в State свойство, а другое свойство сериализуется или десериализировано.

public class Counter : TaskEntity<int>
{
    readonly ILogger logger; 

    public Counter(ILogger<Counter> logger)
    {
        this.logger = logger; 
    }

    public int Add(int amount) 
    {
        this.State += amount;
    }

    public Reset() 
    {
        this.State = 0;
        return Task.CompletedTask;
    }

    public Task<int> Get() 
    {
        return Task.FromResult(this.State);
    }

    // Delete is implicitly defined when defining an entity this way

    [Function(nameof(Counter))]
    public static Task Run([EntityTrigger] TaskEntityDispatcher dispatcher)
        => dispatcher.DispatchAsync<Counter>();
}

Предупреждение

При написании сущностей, производных от ITaskEntity или TaskEntity<TState>, важно не называть метод RunAsyncтриггера сущности. Это приведет к ITaskEntity ошибкам среды выполнения при вызове сущности, так как имеется неоднозначное совпадение с именем метода RunAsync из-за уже определения уровня экземпляра RunAsync.

Удаление сущностей в изолированной модели

Удаление сущности в изолированной модели выполняется путем задания состояния nullсущности. Как это достигается, зависит от используемого пути реализации сущности.

  • При использовании синтаксиса ITaskEntityна основе функций удаление выполняется путем вызова.TaskEntityOperation.State.SetState(null)
  • При производных от TaskEntity<TState>удаления неявно определяется. Однако его можно переопределить, определив метод Delete для сущности. Состояние также можно удалить из любой операции с помощью this.State = null.
    • Чтобы удалить состояние null, необходимо TState иметь значение NULL.
    • Неявно определяемая операция удаления удаляет ненулевое значение TState.
  • При использовании POCO в качестве состояния (не производных от TaskEntity<TState>), удаление неявно определено. Можно переопределить операцию удаления, определив метод Delete в POCO. Однако в маршруте POCO невозможно задать состояние null , поэтому неявно определенная операция удаления является единственным истинным удалением.

Требования к классам

Классы сущностей — это объекты POCO, для которых не требуются специальные суперклассы, интерфейсы или атрибуты. Тем не менее

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

  • В операции должно быть не более одного аргумента, и она не должна иметь никаких перегрузок или аргументов универсального типа.
  • Операция, которая будет вызываться из оркестрации с помощью интерфейса, должна возвращать Task или Task<T>.
  • Аргументы и возвращаемые значения должны быть сериализуемыми значениями или объектами.

Возможности операций

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

Операции также имеют доступ к функциональным возможностям, предоставляемым контекстом Entity.Current:

  • EntityName: возвращает имя сущности, выполняющейся в данный момент.
  • EntityKey: возвращает ключ сущности, выполняющейся в данный момент.
  • EntityId: идентификатор сущности, выполняющейся в данный момент (включает имя и ключ).
  • SignalEntity: отправляет одностороннее сообщение сущности.
  • CreateNewOrchestration: запускает новую оркестрацию.
  • DeleteState: удаляет состояние сущности.

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

public void Add(int amount) 
{
    if (this.Value < 100 && this.Value + amount >= 100)
    {
        Entity.Current.StartNewOrchestration("MilestoneReached", Entity.Current.EntityId);
    }
    this.Value += amount;      
}

Прямой доступ к сущностям

К сущностям на основе классов можно обращаться напрямую, используя явные имена строк для сущности и ее операций. В этом разделе приведены примеры. Более подробное описание базовых понятий (таких как сигналы и вызовы), см. в обсуждении сущностей Access.

Примечание.

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

Пример: клиент передает сигнал в сущность

Следующая функция HTTP Azure реализует операцию DELETE с использованием соглашений REST. Она отправляет сигнал об удалении в сущность счетчика, ключ которой передается в URL-адресе.

[FunctionName("DeleteCounter")]
public static async Task<HttpResponseMessage> DeleteCounter(
    [HttpTrigger(AuthorizationLevel.Function, "delete", Route = "Counter/{entityKey}")] HttpRequestMessage req,
    [DurableClient] IDurableEntityClient client,
    string entityKey)
{
    var entityId = new EntityId("Counter", entityKey);
    await client.SignalEntityAsync(entityId, "Delete");    
    return req.CreateResponse(HttpStatusCode.Accepted);
}

Пример: клиент считывает состояние сущности

Следующая функция HTTP Azure реализует операцию GET с использованием соглашений REST. Она считывает текущее состояние сущности счетчика, ключ которой передается в URL-адресе.

[FunctionName("GetCounter")]
public static async Task<HttpResponseMessage> GetCounter(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = "Counter/{entityKey}")] HttpRequestMessage req,
    [DurableClient] IDurableEntityClient client,
    string entityKey)
{
    var entityId = new EntityId("Counter", entityKey);
    var state = await client.ReadEntityStateAsync<Counter>(entityId); 
    return req.CreateResponse(state);
}

Примечание.

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

Пример: оркестрация первых сигналов, а затем вызывает сущность

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

[FunctionName("IncrementThenGet")]
public static async Task<int> Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var entityId = new EntityId("Counter", "myCounter");

    // One-way signal to the entity - does not await a response
    context.SignalEntity(entityId, "Add", 1);

    // Two-way call to the entity which returns a value - awaits the response
    int currentValue = await context.CallEntityAsync<int>(entityId, "Get");

    return currentValue;
}

Пример: клиент передает сигнал в сущность

Следующая функция HTTP Azure реализует операцию DELETE с использованием соглашений REST. Она отправляет сигнал об удалении в сущность счетчика, ключ которой передается в URL-адресе.

[Function("DeleteCounter")]
public static async Task<HttpResponseData> DeleteCounter(
    [HttpTrigger(AuthorizationLevel.Function, "delete", Route = "Counter/{entityKey}")] HttpRequestData req,
    [DurableClient] DurableTaskClient client, string entityKey)
{
    var entityId = new EntityInstanceId("Counter", entityKey);
    await client.Entities.SignalEntityAsync(entityId, "Delete");
    return req.CreateResponse(HttpStatusCode.Accepted);
}

Пример: клиент считывает состояние сущности

Следующая функция HTTP Azure реализует операцию GET с использованием соглашений REST. Она считывает текущее состояние сущности счетчика, ключ которой передается в URL-адресе.

[Function("GetCounter")]
public static async Task<HttpResponseData> GetCounter(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = "Counter/{entityKey}")] HttpRequestData req,
    [DurableClient] DurableTaskClient client, string entityKey)
{
    var entityId = new EntityInstanceId("Counter", entityKey);
    EntityMetadata<int>? entity = await client.Entities.GetEntityAsync<int>(entityId);
    HttpResponseData response = request.CreateResponse(HttpStatusCode.OK);
    await response.WriteAsJsonAsync(entity.State);

    return response;
}

Пример: оркестрация первых сигналов, а затем вызывает сущность

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

[Function("IncrementThenGet")]
public static async Task<int> Run([OrchestrationTrigger] TaskOrchestrationContext context)
{
    var entityId = new EntityInstanceId("Counter", "myCounter");

    // One-way signal to the entity - does not await a response
    await context.Entities.SignalEntityAsync(entityId, "Add", 1);

    // Two-way call to the entity which returns a value - awaits the response
    int currentValue = await context.Entities.CallEntityAsync<int>(entityId, "Get");

    return currentValue; 
}

Доступ к сущностям через интерфейсы

Интерфейсы можно использовать для доступа к сущностям через созданные прокси-объекты. Такой подход гарантирует, что имя и тип аргумента операции будут совпадать с реализуемыми. Рекомендуется использовать интерфейсы для доступа к сущностям всегда, когда это возможно.

Например, счетчик в этом примере можно изменить следующим образом:

public interface ICounter
{
    void Add(int amount);
    Task Reset();
    Task<int> Get();
    void Delete();
}

public class Counter : ICounter
{
    ...
}

Классы сущностей и интерфейсы сущностей похожи на интервалы и интерфейсы интервалов, широко используемые в Orleans. Дополнительные сведения о сходстве и различиях между устойчивыми сущностями и Orleans см. в статье Сравнение с виртуальными субъектами.

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

Пример: клиент передает сигнал в сущность через интерфейс

Клиентский код может использовать SignalEntityAsync<TEntityInterface> для отправки сигналов в сущности, реализующие TEntityInterface. Например:

[FunctionName("DeleteCounter")]
public static async Task<HttpResponseMessage> DeleteCounter(
    [HttpTrigger(AuthorizationLevel.Function, "delete", Route = "Counter/{entityKey}")] HttpRequestMessage req,
    [DurableClient] IDurableEntityClient client,
    string entityKey)
{
    var entityId = new EntityId("Counter", entityKey);
    await client.SignalEntityAsync<ICounter>(entityId, proxy => proxy.Delete());    
    return req.CreateResponse(HttpStatusCode.Accepted);
}

В этом примере параметр proxy является динамически создаваемым экземпляром ICounter, который внутренне преобразует вызов Delete в сигнал.

Примечание.

Программные интерфейсы SignalEntityAsync можно использовать только для односторонних операций. Даже если операция возвращает Task<T>, значение параметра T всегда будет равно NULL или default, а не фактическому результату. Например, не имеет смысла передавать сигнал операции Get, поскольку не возвращается никакое значение. Вместо этого клиенты могут использовать ReadStateAsync для прямого доступа к состоянию счетчика или запускать функцию оркестратора, которая вызывает операцию Get.

Пример: оркестрация первых сигналов, а затем вызывает сущность через прокси-сервер.

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

[FunctionName("IncrementThenGet")]
public static async Task<int> Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var entityId = new EntityId("Counter", "myCounter");
    var proxy = context.CreateEntityProxy<ICounter>(entityId);

    // One-way signal to the entity - does not await a response
    proxy.Add(1);

    // Two-way call to the entity which returns a value - awaits the response
    int currentValue = await proxy.Get();

    return currentValue;
}

Сигнал неявно передается всем операциям, которые возвращают void, и вызываются все операции, которые возвращают Task или Task<T>. Это поведение по умолчанию можно изменить и передавать сигналы операциям, даже если они возвращают задачу. Для этого необходимо использовать метод SignalEntity<IInterfaceType> явным образом.

Сокращенный вариант указания целевого объекта

При вызове или сигнализации сущности с помощью интерфейса в первом аргументе должна быть указана целевая сущность. Целевой объект можно указать с либо помощью идентификатора сущности, либо (в тех случаях, когда существует только один класс, реализующий сущность) с помощью ключа сущности:

context.SignalEntity<ICounter>(new EntityId(nameof(Counter), "myCounter"), ...);
context.SignalEntity<ICounter>("myCounter", ...);

Если указан только ключ сущности и не удается найти уникальную реализацию в среде выполнения, выдается InvalidOperationException.

Ограничения для интерфейсов сущностей

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

Мы также применяем некоторые другие правила:

  • Интерфейсы сущности должны быть определены в той же сборке, что и класс сущностей.
  • Интерфейсы сущностей должны определять только методы.
  • Интерфейсы сущностей не должны содержать универсальные параметры.
  • Методы интерфейса сущности должны иметь не более одного параметра.
  • Методы интерфейса сущности должны возвращать void, Task или Task<T>.

Если какое-либо из этих правил нарушается, в среде выполнения выдается InvalidOperationException, когда интерфейс используется в качестве аргумента типа для SignalEntity, SignalEntityAsync или CreateEntityProxy. В сообщении об исключении объясняется, какое правило было нарушено.

Примечание.

Методы интерфейса, возвращающие void, могут только принимать сигналы (односторонняя передача), но не могут принимать вызовы (двусторонняя передача). Методы интерфейса, возвращающие Task или Task<T>, могут принимать либо сигналы, либо вызовы. При вызове они возвращают результат операции или повторно выдают исключения, созданные операцией. Однако в случае приема сигнала они не возвращают фактический результат или исключение операции, возвращается только значение по умолчанию.

В настоящее время это не поддерживается в изолированной рабочей роли .NET.

Сериализация сущностей

Поскольку состояние сущности устойчиво сохраняется, класс сущности должен быть сериализуемым. Среда выполнения Устойчивые функции использует библиотеку Json.NET для этой цели, которая поддерживает политики и атрибуты для управления процессом сериализации и десериализации. Основные типы данных C# (включая типы массивов и коллекций) уже являются сериализуемыми, и их можно использовать для определения состояния устойчивых сущностей.

Например, Json.NET позволяет легко сериализовать и десериализовать следующий класс:

[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class User
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("yearOfBirth")]
    public int YearOfBirth { get; set; }

    [JsonProperty("timestamp")]
    public DateTime Timestamp { get; set; }

    [JsonProperty("contacts")]
    public Dictionary<Guid, Contact> Contacts { get; set; } = new Dictionary<Guid, Contact>();

    [JsonObject(MemberSerialization = MemberSerialization.OptOut)]
    public struct Contact
    {
        public string Name;
        public string Number;
    }

    ...
}

Атрибуты сериализации

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

  • К классу добавлено примечание [JsonObject(MemberSerialization.OptIn)], которое указывает, что класс должен быть сериализуемым и должны сохраняться только элементы, явно помеченные как свойства JSON.
  • К сохраняемым полям добавлено примечание [JsonProperty("name")], которое напоминает, что поле является частью сохраняемого состояния сущности, и указывает имя свойства, которое будет использоваться в представлении JSON.

Однако эти атрибуты необязательны. Можно использовать другие соглашения или атрибуты при условии, что они совместимы с Json.NET. Например, можно использовать [DataContract] атрибуты или не использовать атрибуты вообще:

[DataContract]
public class Counter
{
    [DataMember]
    public int Value { get; set; }
    ...
}

public class Counter
{
    public int Value;
    ...
}

По умолчанию имя класса не сохраняется в составе представления JSON: то есть мы используем TypeNameHandling.None в качестве параметра по умолчанию. Это поведение по умолчанию можно переопределить с помощью атрибутов JsonObject или JsonProperty.

Внесение изменений в определения классов

При внесении изменений в определение класса после запуска приложения требуется некоторое внимание, так как сохраненный объект JSON больше не может соответствовать определению нового класса. Тем не менее, часто можно правильно справиться с изменением форматов данных, пока он понимает процесс десериализации, используемый JsonConvert.PopulateObject.

Ниже приведено несколько примеров изменений и их последствий.

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

Существует множество вариантов настройки поведения Json.NET. Например, чтобы принудительно заставить исключение, если сохраненный JSON содержит поле, которое отсутствует в классе, укажите атрибут JsonObject(MissingMemberHandling = MissingMemberHandling.Error). Также можно написать пользовательский код для десериализации, который может считывать JSON, хранящийся в произвольных форматах.

Поведение сериализации по умолчанию изменилось сNewtonsoft.Json System.Text.Json. Для получения дополнительных сведений см. здесь.

Создание сущностей

Иногда требуется больший контроль над созданием объектов сущностей. Рассмотрим несколько вариантов изменения поведения по умолчанию при создании объектов сущностей.

Пользовательская инициализация при первом доступе

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

[FunctionName(nameof(Counter))]
public static Task Run([EntityTrigger] IDurableEntityContext ctx)
{
    if (!ctx.HasState)
    {
        ctx.SetState(...);
    }
    return ctx.DispatchAsync<Counter>();
}

Привязки в классах сущностей

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

В следующем примере показано, как можно сделать ссылку CloudBlobContainer из входной привязки BLOB-объекта доступной для сущности на основе класса.

public class BlobBackedEntity
{
    [JsonIgnore]
    private readonly CloudBlobContainer container;

    public BlobBackedEntity(CloudBlobContainer container)
    {
        this.container = container;
    }

    // ... entity methods can use this.container in their implementations ...

    [FunctionName(nameof(BlobBackedEntity))]
    public static Task Run(
        [EntityTrigger] IDurableEntityContext context,
        [Blob("my-container", FileAccess.Read)] CloudBlobContainer container)
    {
        // passing the binding object as a parameter makes it available to the
        // entity class constructor
        return context.DispatchAsync<BlobBackedEntity>(container);
    }
}

Дополнительные сведения о привязках в Функциях Azure см. в статьеAzure Functions triggers and bindings concepts (Основные понятия триггеров и привязок в Функциях Azure).

Внедрение зависимостей в классы сущностей

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

[assembly: FunctionsStartup(typeof(MyNamespace.Startup))]

namespace MyNamespace
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddHttpClient();
        }
    }
}

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

public class HttpEntity
{
    [JsonIgnore]
    private readonly HttpClient client;

    public HttpEntity(IHttpClientFactory factory)
    {
        this.client = factory.CreateClient();
    }

    public Task<int> GetAsync(string url)
    {
        using (var response = await this.client.GetAsync(url))
        {
            return (int)response.StatusCode;
        }
    }

    [FunctionName(nameof(HttpEntity))]
    public static Task Run([EntityTrigger] IDurableEntityContext ctx)
        => ctx.DispatchAsync<HttpEntity>();
}

Пользовательская инициализация при первом доступе

public class Counter : TaskEntity<int>
{
    protected override int InitializeState(TaskEntityOperation operation)
    {
        // This is called when state is null, giving a chance to customize first-access of entity.
        return 10;
    }
}

Привязки в классах сущностей

В следующем примере показано, как использовать входную привязку BLOB-объектов в сущности на основе классов.

public class BlobBackedEntity : TaskEntity<object?>
{
    private BlobContainerClient Container { get; set; }

    [Function(nameof(BlobBackedEntity))]
    public Task DispatchAsync(
        [EntityTrigger] TaskEntityDispatcher dispatcher, 
        [BlobInput("my-container")] BlobContainerClient container)
    {
        this.Container = container;
        return dispatcher.DispatchAsync(this);
    }
}

Дополнительные сведения о привязках в Функциях Azure см. в статьеAzure Functions triggers and bindings concepts (Основные понятия триггеров и привязок в Функциях Azure).

Внедрение зависимостей в классы сущностей

Классы сущностей поддерживают внедрение зависимостей Функций Azure.

Ниже показано, как настроить импорт HttpClient в program.cs файле позже в классе сущностей.

public class Program
{
    public static void Main()
    {
        IHost host = new HostBuilder()
            .ConfigureFunctionsWorkerDefaults((IFunctionsWorkerApplicationBuilder workerApplication) =>
            {
                workerApplication.Services.AddHttpClient<HttpEntity>()
                    .ConfigureHttpClient(client => {/* configure http client here */});
             })
            .Build();

        host.Run();
    }
}

Вот как включить внедренную службу в класс сущностей.

public class HttpEntity : TaskEntity<object?>
{
    private readonly HttpClient client;

     public HttpEntity(HttpClient client)
    {
        this.client = client;
    }

    public async Task<int> GetAsync(string url)
    {
        using var response = await this.client.GetAsync(url);
        return (int)response.StatusCode;
    }

    [Function(nameof(HttpEntity))]
    public static Task Run([EntityTrigger] TaskEntityDispatcher dispatcher)
        => dispatcher.DispatchAsync<HttpEntity>();
}

Примечание.

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

Примечание.

В отличие от внедрения конструктора в обычных Функциях Azure .NET, метод точки входа функций для сущностей на основе класса необходимо объявить static. Объявление точки входа нестатических функций может привести к конфликтам между обычным инициализатором объектов Функции Azure и инициализатором объектов Устойчивых сущностей.

Синтаксис на основе функций

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

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

[FunctionName("Counter")]
public static void Counter([EntityTrigger] IDurableEntityContext ctx)
{
    switch (ctx.OperationName.ToLowerInvariant())
    {
        case "add":
            ctx.SetState(ctx.GetState<int>() + ctx.GetInput<int>());
            break;
        case "reset":
            ctx.SetState(0);
            break;
        case "get":
            ctx.Return(ctx.GetState<int>());
            break;
        case "delete":
            ctx.DeleteState();
            break;
    }
}

Объект контекста сущности

Доступ к функциональным возможностям сущности можно получить через объект контекста типа IDurableEntityContext. Этот объект контекста доступен в качестве параметра функции сущности, а также в асинхронном локальном свойстве Entity.Current.

Следующие элементы предоставляют сведения о текущей операции и позволяют указать возвращаемое значение.

  • EntityName: возвращает имя сущности, выполняющейся в данный момент.
  • EntityKey: возвращает ключ сущности, выполняющейся в данный момент.
  • EntityId: идентификатор сущности, выполняющейся в данный момент (включает имя и ключ).
  • OperationName: имя текущей операции.
  • GetInput<TInput>(): получает входные данные для текущей операции.
  • Return(arg): возвращает значение для оркестрации, вызывающей операцию.

Следующие элементы управляют состоянием сущности (создание, чтение, обновление, удаление).

  • HasState: показывает, существует ли сущность (то есть имеет определенное состояние).
  • GetState<TState>(): возвращает текущее состояние сущности. Если он еще не существует, он создается.
  • SetState(arg): создает или обновляет состояние сущности.
  • DeleteState(): удаляет состояние сущности, если оно существует.

Если GetState возвращает состояние, которое является объектом, его можно напрямую изменить с помощью кода приложения. Нет необходимости снова вызывать SetState в конце (но и не вред). Если GetState<TState> вызывается несколько раз, необходимо использовать один и тот же тип.

Наконец, следующие элементы используются для передачи сигналов другим сущностям или запуска новых оркестраций:

  • SignalEntity(EntityId, operation, input): отправляет одностороннее сообщение сущности.
  • CreateNewOrchestration(orchestratorFunctionName, input): запускает новую оркестрацию.
[Function(nameof(Counter))]
public static Task DispatchAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
{
    return dispatcher.DispatchAsync(operation =>
    {
        if (operation.State.GetState(typeof(int)) is null)
        {
            operation.State.SetState(0);
        }

        switch (operation.Name.ToLowerInvariant())
        {
            case "add":
                int state = operation.State.GetState<int>();
                state += operation.GetInput<int>();
                operation.State.SetState(state);
                return new(state);
            case "reset":
                operation.State.SetState(0);
                break;
            case "get":
                return new(operation.State.GetState<int>());
            case "delete": 
                operation.State.SetState(null);
                break; 
        }

        return default;
    });
}

Следующие шаги