Desempenho: Toda regra tem exceção?
Tags: csharp, performance, dotnet, braziliandevs
Published: 2022-11-04 20:36:17

Olá!

Este é mais um post da seção **Desempenho** e, desta vez, trago uma perspectiva diferente para o uso do que alguns chamam de "*business exceptions*" ou "*domain exceptions*", que são exceções customizadas, criadas por desenvolvedores, para indicar a violação de uma regra de negócio.

Vamos lá!

## O que são "*business exceptions*"?

Este é um termo, longe de ser convencional, que descreve exceções customizadas, que são lançadas como reação adversa à validação de uma regra de negócio ou invariante do domínio.

Vejamos o exemplo abaixo:

```c#
public static void ValidateUser(UserCredentials userCredentials)
{
	if(string.IsNullOrWhiteSpace(userCredentials.UserName))
		throw new UserValidationException("A username must be provided.");
}

```

Parece comum. Certo?

Vamos verificar agora, como este código se comporta diante de diversas chamadas. Para isso, vamos utilizar um benckmark (link para o código no final do post), e simular as duas situações possíveis no bloco acima: o nome do usuário estar, ou não, preenchido.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i1mlxt9ztesv5w31wb7n.png)

Veja que impressionante! Apenas por lançar uma exception, o código apresentou um desempenho inferior em uma ordem de grandeza. É muita coisa!

## Por que tão caro?

A pergunta que salta aos olhos neste momento é: por que esse custo todo para lançar uma simples exceção? O problema não está no lançamento, mas sim no que acontece em seguida.

Quando uma exceção é lançada, o runtime do .NET vai buscar por um bloco `catch` capaz de lidar com ela. Essa busca começa no método onde a exceção foi lançada, e percorre a pilha de chamadas em sentido inverso até encontrar um bloco `catch` e transferir o controle da execução a ele ou, então, encerrar a aplicação por falta de tratamento.

Este percurso da pilha de chamadas, e a busca pelo bloco `catch` é bem custoso e, por isso, lançar exceções no caminho da crítico da aplicação, para tratar de erros previsíveis em suas regras de negócio não é uma boa ideia.

## O que fazer?

Uma alternativa, que se mantém semanticamente coerente com a programação orientada a objeto, não conflita com o modelo de exceções do C# e, ao mesmo tempo, economiza recursos, é tipo `Result`, que indica se o resultado de uma operação foi um sucesso ou erro e, em sua versão `Result<T>`, além de servir como indicador de sucesso ou falha, também age como um envelope, carregando um valor do tipo `T` em caso de retorno satisfatório.

Vamos testar?

Aqui utilizo uma versão simples de `Result`, que não carrega qualquer resultado, e apenas indica se a operação de validação do usuário foi ou não bem sucedida.

```c#
public readonly ref struct Result
{
    private Result(bool isOk, string? errorMessage) 
    {
        IsOk = isOk;
        ErrorMessage = errorMessage;
    }

    public bool IsOk { get; }
    public string? ErrorMessage { get; }

    public static Result Ok() => new (true, default);
    public static Result Failure(string errorMessage) => new (false, errorMessage);
}
```

Abaixo, a implementação responsável por retornar `Result`:

```c#
public static Result ValidateUser(UserCredentials userCredentials)
{
    if (string.IsNullOrWhiteSpace(userCredentials.UserName))
        return Result.Failure("A username must be provided.");

    return Result.Ok();
}
```

Agora, o benchmark atualizado:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nyni1cf5xag9087dribx.png)

Impressionante! Não?

## Considerações sobre `Result` e design

Ótimo! Então, agora, basta trocar exceções por `Result`e estará tudo resolvido. Certo?

Pois bem: **não exatamente!**

Apesar de ser um ótimo recurso, `Result` demanda certa atenção. Isso porque é possível que seu uso se confunda com a possibilidade do lançamento de exceções, fazendo com que as mesmas não sejam previstas pelo método invocador e acabem não sendo tratadas se não o forem no método que as lança.

Imagine o seguinte exemplo:

```c#
public Result<User> GetById(Guid id)
{
	...
	var user = connection.QuerySingle<User>(sql);
	...
}

```

No código acima, onde usa-se Dapper para obter um usuário junto a uma base de dados, em uma borda da aplicação, é possível haver exceções como `SqlException` ou `InvalidOperationException`, caso o número de linhas retornadas seja diferente de 1. Neste caso, o que fazer?

Minha recomendação é que as exceções possíveis sejam tratadas dentro do método que retornará `Result<User>`, fazendo com que, ao tratá-las, seja retornado um resultado padrão para o método invocador. Desta forma, o código acima mudaria para o seguinte:

```c#
public Result<User> GetById(Guid id)
{
	...
	try
	{
		var user = connection.QuerySingle<User>(sql);
	}
	catch
	{
		/*Log the exception*/
		return Result<User>.Failure("Unable to find the user.");
	}
	...
}

```

Com isso, as exceções lançadas dentro do método `GetById` seriam devidamente registradas, e um erro indicaria a impossibilidade do retorno de um usuário.

## Considerações sobre modelo de domínio

É possível que seu modelo de domínio atue com *guard clauses* que, uma vez violadas, lancem exceções como `ArgumentNullException` ou `InvalidOperationException`. Nestes casos, a solução é mais simples: pode-se substituir as exceções por `Result` cuja mensagem de erro seria a mesma da exceção antes lançada.

Entretanto, há situações como `OverflowException` que são um tanto mais difíceis de prever, e que podem ocorrer em seu modelo de domínio. Minha recomendação, neste caso, é deixar a exceção ser lançada normalmente, tratando-a na borda da aplicação, onde a operação é iniciada (em seu Controller, no caso de uma WebAPI, por exemplo), mantendo o uso de `Result` em sua operação para o caso dela ser concluída.

## Considerações finais

Exceções tem uma razão de ser: indicar que houve uma falha em nível de infraestrutura na aplicação. Exceções são necessárias para ajudar a entender onde ocorrem erros que demandam ações do time de desenvolvimento para restaurar a saúde da aplicação. Por este motivo, o ideal é que se mantenham excepcionais. Para erros de domínio conhecidos, que não demandem atenção urgente do time, `Result` é uma ótima alternativa, evitando o desperdício de recursos gerado pelo lançamento de exceções e, ao mesmo tempo, mantendo um isolamento satisfatório entre auditoria e falhas.

[Aqui](https://github.com/wsantosdev/performance-excecoes) você encontra o código de exemplo deste post no Github. E, [aqui](https://www.nuget.org/packages/CSharpFunctionalExtensions/), o pacote NuGet CSharpFunctionalExtensions, que contém a implementação completa de `Result` e `Result<T>`.

Gostou? Me deixe saber pelos indicadores. Tem dúvidas ou sugestões? Deixe um comentário ou me procure pelas redes sociais.

Até a próxima!