Árvore de páginas

Introdução

Conexão e seu gerenciamento é um dos conceitos mais importantes de uma aplicação que utiliza um banco de dados relacional. Para disponibilizar essa infraestrutura no TNF utilizamos o pattern Unit Of Work (UoW).

Conexão e Gerenciamento de Transação

O TNF abre uma conexão com o banco de dados e inicia uma transação ao entrar em um método que utiliza o padrão. No final do método, a transação é confirmada e a conexão é descartada. Se o método lançar qualquer exceção, a transação é revertida e a conexão é descartada. Desta forma, todo UoW é tido como atômico.

Se uma um método que utiliza o UoW chamar outro método que também o utiliza, ambos usam a mesma conexão e transação. Neste cenário o primeiro método fará o gerenciamento da conexão e da transação.

Uso Convencional

Alguns métodos utilizam UoW por default:

  • ASP NET Core Controller Actions.
  • Serviços de Aplicação.
  • Todos os Métodos de Repositórios.

Vamos utilizar como exemplo o serviço de aplicação abaixo:

PersonAppService.cs
public class PersonAppService : IPersonAppService
{
    private readonly IPersonRepository _personRepository;
    private readonly IStatisticsRepository _statisticsRepository;

    public PersonAppService(IPersonRepository personRepository, IStatisticsRepository statisticsRepository)
    {
        _personRepository = personRepository;
        _statisticsRepository = statisticsRepository;
    }

    public void CreatePerson(CreatePersonInput input)
    {
        var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
        _personRepository.Insert(person);
        _statisticsRepository.IncrementPeopleCount();
    }
}

No método CreatePerson, estamos inserindo uma pessoa usando o repositório IPersonRepository e incrementando a contagem total de pessoas usando o repositório IStatisticsRepository. Ambos os repositórios compartilham mesma conexão e transação neste exemplo, já que o método de serviço de aplicação é um UoW por padrão. O TNF abre uma conexão de banco de dados e inicia uma transação ao inserir o método CreatePerson e confirmar a transação no final deste se nenhuma exceção for lançada. Dessa forma, todas as operações de banco de dados no método CreatePerson se tornam atômicas.

Controlando um UoW de Forma Explicita

Utilizamos o UoW implicitamente como vimos acima. Na maioria dos casos, você não precisa controlar o UoW manualmente.

Existem cenários onde você precisa ter o controle do UoW de forma explicita.

UnitOfWork Attribute

CreatePerson Method
[UnitOfWork]
public void CreatePerson(CreatePersonInput input)
{
    var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
    _personRepository.Insert(person);
    _statisticsRepository.IncrementPeopleCount();
}

Ao utilizar o atributo acima o controle fica ao critério da sua necessidade em utiliza-lo.

IUnitOfWorkManager

Uma segunda abordagem é a utilização da interface IUnitOfWorkManager injetando ela em seu serviço:

MyService.cs
public class MyService
{
    private readonly IUnitOfWorkManager _unitOfWorkManager;
    private readonly IPersonRepository _personRepository;
    private readonly IStatisticsRepository _statisticsRepository;

    public MyService(IUnitOfWorkManager unitOfWorkManager, IPersonRepository personRepository, IStatisticsRepository statisticsRepository)
    {
        _unitOfWorkManager = unitOfWorkManager;
        _personRepository = personRepository;
        _statisticsRepository = statisticsRepository;
    }

    public void CreatePerson(CreatePersonInput input)
    {
        var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };

        using (var unitOfWork = _unitOfWorkManager.Begin())
        {
            _personRepository.Insert(person);
            _statisticsRepository.IncrementPeopleCount();
            unitOfWork.Complete();
        }
    }
}

Você pode injetar e usar o IUnitOfWorkManager como mostrado aqui (Algumas classes base já têm UnitOfWorkManager injetado por padrão: Controladores MVC, serviços de aplicação, serviços de domínio). Nesta abordagem, você deve chamar o método Complete manualmente. Se você não chamar, a transação é revertida e as alterações não são salvas. O método Begin tem sobrecargas para definir o comportamento do uso do UoW.

Você pode obter o UoW corrente em um método através da propriedade IUnitOfWorkManager.Current 

Desabilitando um UoW

Você pode querer desativar o UoW para algum método. Para fazer isso, use propriedade IsDisabled do UnitOfWorkAttribute:

RemoveFriendship Method
[UnitOfWork(IsDisabled = true)]
public virtual void RemoveFriendship(RemoveFriendshipInput input)
{
    _friendshipRepository.Delete(input.Id);
}

Observe que se um método com UoW chamar RemoveFriendship, a ação de desativar o UoW será ignorado se ele usar o mesmo UoW.

Trabalhando com UoW Não Transacional

Um UoW é transacional por padrão. Em alguns casos especiais, a transação pode causar problemas, uma vez que pode bloquear algumas linhas ou tabelas no banco de dados (LOCK). Nestas situações, podemos desabilitar a transação através do atributo UnitOfWork:

GetTasksOutput Method
[UnitOfWork(isTransactional: false)]
public GetTasksOutput GetTasks(GetTasksInput input)
{
    var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State);
    return new GetTasksOutput
	{
    	Tasks = Mapper.Map<List<TaskDto>>(tasks)
	};
}

Se o seu método apenas lê dados ele pode ser seguramente não-transacional.

Se um método que utiliza UoW chama outro método que utiliza UoW, eles compartilham a mesma conexão e transação. O primeiro método gera a conexão e os outros a utilizam.

Definindo um Scope

Para definir o escopo da transação utilize através do .NET TransactionScopeOption em seu atributo UnitOfWorkAttribute ou IUnitOfWorkManager.

Todo método que utilize UoW automaticamente salva as alterações sem precisar que nenhuma chamada seja feita de forma explicita.

Configurações Default para UoW

 Podemos alterar os valores padrão de todos os UoW na configuração de inicialização em nosso módulo. Isso geralmente é feito no método PreInitialize:

SimpleTaskSystemCoreModule.cs
public class SimpleTaskSystemCoreModule : TnfModule
{
    public override void PreInitialize()
    {
        Configuration.UnitOfWork.IsolationLevel = IsolationLevel.ReadCommitted;
        Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30);
    }
}

Para substituir valores default em um determinado UoW utilize o construtor de atributo UnitOfWork ou o método IUnitOfWorkManager.Begin.

Eventos

Uma unidade de trabalho tem eventos concluídos, falhados e descartados. Você pode registrar esses eventos e executar as operações necessárias.

Por exemplo, você pode querer executar algum código quando um UoW é concluído com êxito. Exemplo:

Create Task Method Example
public void CreateTask(CreateTaskInput input)
{
    var task = new Task { Description = input.Description };

    if (input.AssignedPersonId.HasValue)
    {
        task.AssignedPersonId = input.AssignedPersonId.Value;
        _unitOfWorkManager.Current.Completed += (sender, args) => { /* TODO: Send email to assigned person */ };
    }

    _taskRepository.Insert(task);
}
  • Sem rótulos