As orquestrações de funções duráveis são implementadas em código e podem usar os recursos internos de tratamento de erros da linguagem de programação. Realmente não há novos conceitos que você precise aprender para adicionar tratamento de erros e compensação em suas orquestrações. No entanto, existem alguns comportamentos que você deve estar ciente.
Nota
A versão 4 do modelo de programação Node.js para o Azure Functions está disponível em geral. O novo modelo v4 foi projetado para ter uma experiência mais flexível e intuitiva para desenvolvedores de JavaScript e TypeScript. Saiba mais sobre as diferenças entre v3 e v4 no guia de migração.
Nos trechos de código a seguir, JavaScript (PM4) indica o modelo de programação V4, a nova experiência.
Erros nas funções de atividade e sub-orquestrações
Em Funções Duráveis, exceções não tratadas lançadas dentro de funções de atividade ou suborquestrações são encaminhadas de volta para a função orquestradora utilizando tipos de exceção padronizados.
Por exemplo, considere a seguinte função orquestradora que executa uma transferência de fundos entre duas contas:
Em Durable Functions C# em processo, exceções não tratadas são lançadas como FunctionFailedException.
A mensagem de exceção normalmente identifica quais funções de atividade ou suborquestrações causaram a falha. Para acessar informações de erro mais detalhadas, inspecione a InnerException propriedade.
[FunctionName("TransferFunds")]
public static async Task Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
var transferDetails = context.GetInput<TransferOperation>();
await context.CallActivityAsync("DebitAccount",
new
{
Account = transferDetails.SourceAccount,
Amount = transferDetails.Amount
});
try
{
await context.CallActivityAsync("CreditAccount",
new
{
Account = transferDetails.DestinationAccount,
Amount = transferDetails.Amount
});
}
catch (FunctionFailedException)
{
// Refund the source account.
// Another try/catch could be used here based on the needs of the application.
await context.CallActivityAsync("CreditAccount",
new
{
Account = transferDetails.SourceAccount,
Amount = transferDetails.Amount
});
}
}
Nota
Os exemplos anteriores de C# são para Durable Functions 2.x. Para Durable Functions 1.x, você deve usar DurableOrchestrationContext em vez de IDurableOrchestrationContext. Para obter mais informações sobre as diferenças entre versões, consulte o artigo Durable Functions versions .
Em Durable Functions C# Isolated, exceções não tratadas são apresentadas como TaskFailedException.
A mensagem de exceção normalmente identifica quais funções de atividade ou suborquestrações causaram a falha. Para acessar informações de erro mais detalhadas, inspecione a propriedade FailureDetails .
[FunctionName("TransferFunds")]
public static async Task Run(
[OrchestrationTrigger] TaskOrchestrationContext context, TransferOperation transferDetails)
{
await context.CallActivityAsync("DebitAccount",
new
{
Account = transferDetails.SourceAccount,
Amount = transferDetails.Amount
});
try
{
await context.CallActivityAsync("CreditAccount",
new
{
Account = transferDetails.DestinationAccount,
Amount = transferDetails.Amount
});
}
catch (TaskFailedException)
{
// Refund the source account.
// Another try/catch could be used here based on the needs of the application.
await context.CallActivityAsync("CreditAccount",
new
{
Account = transferDetails.SourceAccount,
Amount = transferDetails.Amount
});
}
}
Nota
- A mensagem de exceção normalmente identifica quais funções de atividade ou suborquestrações causaram a falha. Para acessar informações de erro mais detalhadas, inspecione a
FailureDetails propriedade.
- Por padrão,
FailureDetails inclui o tipo de erro, a mensagem de erro, o rastreamento de pilha e quaisquer exceções internas aninhadas (cada uma representada como um objeto recursivo). FailureDetails Se desejar incluir propriedades de exceção adicionais na saída da falha, consulte Incluir propriedades de exceção personalizadas para FailureDetails (.NET isolado).
const df = require("durable-functions");
module.exports = df.orchestrator(function* (context) {
const transferDetails = context.df.getInput();
yield context.df.callActivity("DebitAccount", {
account: transferDetails.sourceAccount,
amount: transferDetails.amount,
});
try {
yield context.df.callActivity("CreditAccount", {
account: transferDetails.destinationAccount,
amount: transferDetails.amount,
});
} catch (error) {
// Refund the source account.
// Another try/catch could be used here based on the needs of the application.
yield context.df.callActivity("CreditAccount", {
account: transferDetails.sourceAccount,
amount: transferDetails.amount,
});
}
})
const df = require("durable-functions");
df.app.orchestration("transferFunds", function* (context) {
const transferDetails = context.df.getInput();
yield context.df.callActivity("debitAccount", {
account: transferDetails.sourceAccount,
amount: transferDetails.amount,
});
try {
yield context.df.callActivity("creditAccount", {
account: transferDetails.destinationAccount,
amount: transferDetails.amount,
});
} catch (error) {
// Refund the source account.
// Another try/catch could be used here based on the needs of the application.
yield context.df.callActivity("creditAccount", {
account: transferDetails.sourceAccount,
amount: transferDetails.amount,
});
}
});
import azure.functions as func
import azure.durable_functions as df
def orchestrator_function(context: df.DurableOrchestrationContext):
transfer_details = context.get_input()
yield context.call_activity('DebitAccount', {
'account': transfer_details['sourceAccount'],
'amount' : transfer_details['amount']
})
try:
yield context.call_activity('CreditAccount', {
'account': transfer_details['destinationAccount'],
'amount': transfer_details['amount'],
})
except:
yield context.call_activity('CreditAccount', {
'account': transfer_details['sourceAccount'],
'amount': transfer_details['amount']
})
main = df.Orchestrator.create(orchestrator_function)
Por padrão, os cmdlets no PowerShell não geram exceções que podem ser detetadas usando blocos try/catch. Você tem duas opções para alterar esse comportamento:
- Use o sinalizador
-ErrorAction Stop ao invocar cmdlets, como Invoke-DurableActivity.
- Defina a
$ErrorActionPreference variável de preferência como "Stop" na função orchestrator antes de invocar cmdlets.
param($Context)
$ErrorActionPreference = "Stop"
$transferDetails = $Context.Input
Invoke-DurableActivity -FunctionName 'DebitAccount' -Input @{ account = transferDetails.sourceAccount; amount = transferDetails.amount }
try {
Invoke-DurableActivity -FunctionName 'CreditAccount' -Input @{ account = transferDetails.destinationAccount; amount = transferDetails.amount }
} catch {
Invoke-DurableActivity -FunctionName 'CreditAccount' -Input @{ account = transferDetails.sourceAccount; amount = transferDetails.amount }
}
Para obter mais informações sobre o tratamento de erros no PowerShell, consulte a documentação do Try-Catch-Finally PowerShell.
@FunctionName("TransferFunds")
public void transferFunds(
@DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
TransferOperation transfer = ctx.getInput(TransferOperation.class);
ctx.callActivity(
"DebitAccount",
new OperationArgs(transfer.sourceAccount, transfer.amount)).await();
try {
ctx.callActivity(
"CreditAccount",
new OperationArgs(transfer.destinationAccount, transfer.amount)).await();
} catch (TaskFailedException ex) {
// Refund the source account on failure
ctx.callActivity(
"CreditAccount",
new OperationArgs(transfer.sourceAccount, transfer.amount)).await();
}
}
Se a primeira chamada da função CreditAccount falhar, a função orchestrator compensará creditando os fundos de volta na conta de origem.
Erros em funções de entidade
O comportamento de tratamento de exceções para funções de entidade difere com base no modelo de hospedagem de Funções Duráveis:
Em Durable Functions using C# in-process, os tipos de exceção originais lançados por funções de entidade são retornados diretamente ao orquestrador.
[FunctionName("Function1")]
public static async Task<string> RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
try
{
var entityId = new EntityId(nameof(Counter), "myCounter");
await context.CallEntityAsync(entityId, "Add", 1);
}
catch (Exception ex)
{
// The exception type will be InvalidOperationException with the message "this is an entity exception".
}
return string.Empty;
}
[FunctionName("Counter")]
public static void Counter([EntityTrigger] IDurableEntityContext ctx)
{
switch (ctx.OperationName.ToLowerInvariant())
{
case "add":
throw new InvalidOperationException("this is an entity exception");
case "get":
ctx.Return(ctx.GetState<int>());
break;
}
}
Em Durable Functions C# Isolated, as exceções são apresentadas ao orquestrador como EntityOperationFailedException. Para acessar os detalhes originais da exceção, inspecione sua FailureDetails propriedade.
[Function(nameof(MyOrchestrator))]
public static async Task<List<string>> MyOrchestrator(
[Microsoft.Azure.Functions.Worker.OrchestrationTrigger] TaskOrchestrationContext context)
{
var entityId = new Microsoft.DurableTask.Entities.EntityInstanceId(nameof(Counter), "myCounter");
try
{
await context.Entities.CallEntityAsync(entityId, "Add", 1);
}
catch (EntityOperationFailedException ex)
{
// Add your error handling
}
return new List<string>();
}
df.app.orchestration("counterOrchestration", function* (context) {
const entityId = new df.EntityId(counterEntityName, "myCounter");
try {
const currentValue = yield context.df.callEntity(entityId, "get");
if (currentValue < 10) {
yield context.df.callEntity(entityId, "add", 1);
}
} catch (err) {
context.log(`Entity call failed: ${err.message ?? err}`);
}
});
df.app.orchestration("counterOrchestration", function* (context) {
const entityId = new df.EntityId(counterEntityName, "myCounter");
try {
const currentValue = yield context.df.callEntity(entityId, "get");
if (currentValue < 10) {
yield context.df.callEntity(entityId, "add", 1);
}
} catch (err) {
context.log(`Entity call failed: ${err.message ?? err}`);
}
});
@myApp.orchestration_trigger(context_name="context")
def run_orchestrator(context):
try:
entityId = df.EntityId("Counter", "myCounter")
yield context.call_entity(entityId, "get")
return "finished"
except Exception as e:
# Add your error handling
Atualmente, não há suporte para funções de entidade no PowerShell.
Atualmente, as funções de entidade não são suportadas em Java.
Repetição automática em caso de falha
Ao chamar funções de atividade ou funções de suborquestração, você pode especificar uma política de repetição automática. O exemplo a seguir tenta chamar uma função até três vezes e aguarda 5 segundos entre cada nova tentativa:
[FunctionName("TimerOrchestratorWithRetry")]
public static async Task Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
var retryOptions = new RetryOptions(
firstRetryInterval: TimeSpan.FromSeconds(5),
maxNumberOfAttempts: 3);
await context.CallActivityWithRetryAsync("FlakyFunction", retryOptions, null);
// ...
}
Nota
Os exemplos anteriores de C# são para Durable Functions 2.x. Para Durable Functions 1.x, você deve usar DurableOrchestrationContext em vez de IDurableOrchestrationContext. Para obter mais informações sobre as diferenças entre versões, consulte o artigo Durable Functions versions .
[FunctionName("TimerOrchestratorWithRetry")]
public static async Task Run([OrchestrationTrigger] TaskOrchestrationContext context)
{
var options = TaskOptions.FromRetryPolicy(new RetryPolicy(
maxNumberOfAttempts: 3,
firstRetryInterval: TimeSpan.FromSeconds(5)));
await context.CallActivityAsync("FlakyFunction", options: options);
// ...
}
const df = require("durable-functions");
module.exports = df.orchestrator(function*(context) {
const firstRetryIntervalInMilliseconds = 5000;
const maxNumberOfAttempts = 3;
const retryOptions =
new df.RetryOptions(firstRetryIntervalInMilliseconds, maxNumberOfAttempts);
yield context.df.callActivityWithRetry("FlakyFunction", retryOptions);
// ...
});
const df = require("durable-functions");
df.app.orchestration("callActivityWithRetry", function* (context) {
const firstRetryIntervalInMilliseconds = 5000;
const maxNumberOfAttempts = 3;
const retryOptions = new df.RetryOptions(firstRetryIntervalInMilliseconds, maxNumberOfAttempts);
yield context.df.callActivityWithRetry("flakyFunction", retryOptions);
// ...
});
import azure.functions as func
import azure.durable_functions as df
def orchestrator_function(context: df.DurableOrchestrationContext):
first_retry_interval_in_milliseconds = 5000
max_number_of_attempts = 3
retry_options = df.RetryOptions(first_retry_interval_in_milliseconds, max_number_of_attempts)
yield context.call_activity_with_retry('FlakyFunction', retry_options)
main = df.Orchestrator.create(orchestrator_function)
param($Context)
$retryOptions = New-DurableRetryOptions `
-FirstRetryInterval (New-TimeSpan -Seconds 5) `
-MaxNumberOfAttempts 3
Invoke-DurableActivity -FunctionName 'FlakyFunction' -RetryOptions $retryOptions
@FunctionName("TimerOrchestratorWithRetry")
public void timerOrchestratorWithRetry(
@DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
final int maxAttempts = 3;
final Duration firstRetryInterval = Duration.ofSeconds(5);
RetryPolicy policy = new RetryPolicy(maxAttempts, firstRetryInterval);
TaskOptions options = new TaskOptions(policy);
ctx.callActivity("FlakeyFunction", options).await();
// ...
}
A chamada de função de atividade no exemplo anterior usa um parâmetro para configurar uma política de repetição automática. Há várias opções para personalizar a política de repetição automática:
-
Número máximo de tentativas: o número máximo de tentativas. Se definido como 1, não haverá nova tentativa.
-
Primeiro intervalo de tentativas: o tempo de espera antes da primeira tentativa.
-
Coeficiente de backoff: O coeficiente usado para determinar a taxa de aumento de backoff. O padrão é 1.
-
Intervalo máximo de repetição: o tempo máximo de espera entre as tentativas de repetição.
-
Tempo limite de repetição: a quantidade máxima de tempo para fazer tentativas. O comportamento padrão é tentar novamente indefinidamente.
Manipuladores de repetição personalizados
Ao usar o .NET ou Java, você também tem a opção de implementar manipuladores de repetição no código. Isso é útil quando as políticas declarativas de repetição não são expressivas o suficiente. Para idiomas que não oferecem suporte a manipuladores de repetição personalizados, você ainda tem a opção de implementar políticas de repetição usando loops, tratamento de exceções e temporizadores para injetar atrasos entre tentativas.
RetryOptions retryOptions = new RetryOptions(
firstRetryInterval: TimeSpan.FromSeconds(5),
maxNumberOfAttempts: int.MaxValue)
{
Handle = exception =>
{
// True to handle and try again, false to not handle and throw.
if (exception is TaskFailedException failure)
{
// Exceptions from TaskActivities are always this type. Inspect the
// inner Exception to get more details.
}
return false;
};
}
await ctx.CallActivityWithRetryAsync("FlakeyActivity", retryOptions, null);
TaskOptions retryOptions = TaskOptions.FromRetryHandler(retryContext =>
{
// Don't retry anything that derives from ApplicationException
if (retryContext.LastFailure.IsCausedBy<ApplicationException>())
{
return false;
}
// Quit after N attempts
return retryContext.LastAttemptNumber < 3;
});
try
{
await ctx.CallActivityAsync("FlakeyActivity", options: retryOptions);
}
catch (TaskFailedException)
{
// Case when the retry handler returns false...
}
Atualmente, o JavaScript não suporta manipuladores de repetição personalizados. No entanto, você ainda tem a opção de implementar a lógica de repetição diretamente na função orchestrator usando loops, manipulação de exceções e temporizadores para injetar atrasos entre tentativas.
Atualmente, o JavaScript não suporta manipuladores de repetição personalizados. No entanto, você ainda tem a opção de implementar a lógica de repetição diretamente na função orchestrator usando loops, manipulação de exceções e temporizadores para injetar atrasos entre tentativas.
Atualmente, o Python não suporta manipuladores de repetição personalizados. No entanto, você ainda tem a opção de implementar a lógica de repetição diretamente na função orchestrator usando loops, manipulação de exceções e temporizadores para injetar atrasos entre tentativas.
Atualmente, o PowerShell não oferece suporte a manipuladores de repetição personalizados. No entanto, você ainda tem a opção de implementar a lógica de repetição diretamente na função orchestrator usando loops, manipulação de exceções e temporizadores para injetar atrasos entre tentativas.
RetryHandler retryHandler = retryCtx -> {
// Don't retry anything that derives from RuntimeException
if (retryCtx.getLastFailure().isCausedBy(RuntimeException.class)) {
return false;
}
// Quit after N attempts
return retryCtx.getLastAttemptNumber() < 3;
};
TaskOptions options = new TaskOptions(retryHandler);
try {
ctx.callActivity("FlakeyActivity", options).await();
} catch (TaskFailedException ex) {
// Case when the retry handler returns false...
}
Tempos limite de função
Você pode querer abandonar uma chamada de função dentro de uma função orchestrator se ela estiver demorando muito para ser concluída. A maneira correta de fazer isso hoje é criando um temporizador durável com um seletor de tarefas "qualquer", como no exemplo a seguir:
[FunctionName("TimerOrchestrator")]
public static async Task<bool> Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
TimeSpan timeout = TimeSpan.FromSeconds(30);
DateTime deadline = context.CurrentUtcDateTime.Add(timeout);
using (var cts = new CancellationTokenSource())
{
Task activityTask = context.CallActivityAsync("FlakyFunction");
Task timeoutTask = context.CreateTimer(deadline, cts.Token);
Task winner = await Task.WhenAny(activityTask, timeoutTask);
if (winner == activityTask)
{
// success case
cts.Cancel();
return true;
}
else
{
// timeout case
return false;
}
}
}
Nota
Os exemplos anteriores de C# são para Durable Functions 2.x. Para Durable Functions 1.x, você deve usar DurableOrchestrationContext em vez de IDurableOrchestrationContext. Para obter mais informações sobre as diferenças entre versões, consulte o artigo Durable Functions versions .
[Function("TimerOrchestrator")]
public static async Task<bool> Run([OrchestrationTrigger] TaskOrchestrationContext context)
{
TimeSpan timeout = TimeSpan.FromSeconds(30);
DateTime deadline = context.CurrentUtcDateTime.Add(timeout);
using (var cts = new CancellationTokenSource())
{
Task activityTask = context.CallActivityAsync("FlakyFunction");
Task timeoutTask = context.CreateTimer(deadline, cts.Token);
Task winner = await Task.WhenAny(activityTask, timeoutTask);
if (winner == activityTask)
{
// success case
cts.Cancel();
return true;
}
else
{
// timeout case
return false;
}
}
}
const df = require("durable-functions");
const moment = require("moment");
module.exports = df.orchestrator(function*(context) {
const deadline = moment.utc(context.df.currentUtcDateTime).add(30, "s");
const activityTask = context.df.callActivity("FlakyFunction");
const timeoutTask = context.df.createTimer(deadline.toDate());
const winner = yield context.df.Task.any([activityTask, timeoutTask]);
if (winner === activityTask) {
// success case
timeoutTask.cancel();
return true;
} else {
// timeout case
return false;
}
});
const df = require("durable-functions");
const { DateTime } = require("luxon");
df.app.orchestration("timerOrchestrator", function* (context) {
const deadline = DateTime.fromJSDate(context.df.currentUtcDateTime).plus({ seconds: 30 });
const activityTask = context.df.callActivity("flakyFunction");
const timeoutTask = context.df.createTimer(deadline.toJSDate());
const winner = yield context.df.Task.any([activityTask, timeoutTask]);
if (winner === activityTask) {
// success case
timeoutTask.cancel();
return true;
} else {
// timeout case
return false;
}
});
import azure.functions as func
import azure.durable_functions as df
from datetime import datetime, timedelta
def orchestrator_function(context: df.DurableOrchestrationContext):
deadline = context.current_utc_datetime + timedelta(seconds = 30)
activity_task = context.call_activity('FlakyFunction')
timeout_task = context.create_timer(deadline)
winner = yield context.task_any(activity_task, timeout_task)
if winner == activity_task:
timeout_task.cancel()
return True
else:
return False
main = df.Orchestrator.create(orchestrator_function)
param($Context)
$expiryTime = New-TimeSpan -Seconds 30
$activityTask = Invoke-DurableActivity -FunctionName 'FlakyFunction'-NoWait
$timerTask = Start-DurableTimer -Duration $expiryTime -NoWait
$winner = Wait-DurableTask -Task @($activityTask, $timerTask) -NoWait
if ($winner -eq $activityTask) {
Stop-DurableTimerTask -Task $timerTask
return $True
}
else {
return $False
}
@FunctionName("TimerOrchestrator")
public boolean timerOrchestrator(
@DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
Task<Void> activityTask = ctx.callActivity("SlowFunction");
Task<Void> timeoutTask = ctx.createTimer(Duration.ofMinutes(30));
Task<?> winner = ctx.anyOf(activityTask, timeoutTask).await();
if (winner == activityTask) {
// success case
return true;
} else {
// timeout case
return false;
}
}
Nota
Na verdade, esse mecanismo não encerra a execução da função de atividade em andamento. Em vez disso, ele simplesmente permite que a função orquestradora ignore o resultado e siga em frente. Para obter mais informações, consulte a documentação dos temporizadores .
Exceções não processadas
Se uma função orchestrator falhar com uma exceção não tratada, os detalhes da exceção serão registrados e a instância será concluída com um Failed status.
Incluir propriedades de exceção personalizadas para FailureDetails (.NET isolado)
Ao executar fluxos de trabalho de Tarefa Durável no modelo Isolado do .NET, as falhas de tarefa são serializadas automaticamente em um objeto FailureDetails. Por padrão, esse objeto inclui campos padrão, como:
- ErrorType — o nome do tipo de exceção
- Mensagem — a mensagem de exceção
- StackTrace — o traço de pilha serializado
- InnerFailure – um objeto FailureDetails aninhado para excepções internas e recursivas
A partir de Microsoft.Azure.Functions.Worker.Extensions.DurableTask v1.9.0, você pode estender esse comportamento implementando um IExceptionPropertiesProvider (definido no pacote Microsoft.DurableTask.Worker a partir do v1.16.1). Esse provedor define quais tipos de exceção e quais de suas propriedades devem ser incluídas no dicionário FailureDetails.Properties.
Nota
- Esse recurso está disponível somente no .NET Isolado . O suporte para Java será adicionado em uma versão futura.
- Certifique-se de que está a utilizar Microsoft.Azure.Functions.Worker.Extensions.DurableTask v1.9.0 ou posterior.
- Certifique-se de que está a utilizar Microsoft.DurableTask.Worker v1.16.1 ou posterior.
Implementar um provedor de propriedades de exceção
Implemente um IExceptionPropertiesProvider personalizado para extrair e retornar propriedades selecionadas para as exceções que lhe interessam. O dicionário retornado será serializado no campo Properties de FailureDetails quando um tipo de exceção correspondente for lançado.
using Microsoft.DurableTask.Worker;
public class CustomExceptionPropertiesProvider : IExceptionPropertiesProvider
{
public IDictionary<string, object?>? GetExceptionProperties(Exception exception)
{
return exception switch
{
ArgumentOutOfRangeException e => new Dictionary<string, object?>
{
["ParamName"] = e.ParamName,
["ActualValue"] = e.ActualValue
},
InvalidOperationException e => new Dictionary<string, object?>
{
["CustomHint"] = "Invalid operation occurred",
["TimestampUtc"] = DateTime.UtcNow
},
_ => null // Other exception types not handled
};
}
}
Registar o Fornecedor
Registre seu IExceptionPropertiesProvider personalizado em seu host de trabalho isolado do .NET, normalmente em Program.cs:
using Microsoft.DurableTask.Worker;
using Microsoft.Extensions.DependencyInjection;
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults(builder =>
{
// Register custom exception properties provider
builder.Services.AddSingleton<IExceptionPropertiesProvider, CustomExceptionPropertiesProvider>();
})
.Build();
host.Run();
Uma vez registrada, qualquer exceção que corresponda a um dos tipos manipulados incluirá automaticamente as propriedades configuradas em seus FailureDetails.
Exemplo de FalhaSaída de Detalhes
Quando ocorre uma exceção que corresponde à configuração do seu provedor, a orquestração recebe uma estrutura FailureDetails serializada como esta:
{
"errorType": "TaskFailedException",
"message": "Activity failed with an exception.",
"stackTrace": "...",
"innerFailure": {
"errorType": "ArgumentOutOfRangeException",
"message": "Specified argument was out of range.",
"properties": {
"ParamName": "count",
"ActualValue": 42
}
}
}
Próximos passos