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).
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. Assim, você pode usar a conexão com segurança neste método. 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, uma unidade de trabalho é tida como atômica. o TNF faz tudo isso automaticamente.
Se uma um método que utiliza o UoW chama outro método que também a utiliza, ambos usam mesma conexão e transação. Neste cenário o primeiro método fará o gerenciamento da conexão e da transação.
Alguns métodos utilizam UoW por default:
Vamos utilizar como exemplo o serviço de aplicação abaixo:
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 de pessoas IPersonRepository e incrementando a contagem total de pessoas usando o repositório de estatísticas 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 é a unidade de trabalho 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 do método 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.
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] 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.
Uma segunda abordagem é a utilização da interface IUnitOfWorkManager, injetando ela em seu serviço:
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
Você pode querer desativar o UoW para algum método. Para fazer isso, use propriedade IsDisabled do UnitOfWorkAttribute:
[UnitOfWork(IsDisabled = true)] public virtual void RemoveFriendship(RemoveFriendshipInput input) { _friendshipRepository.Delete(input.Id); }
Em algumas situações você pode precisar desabilita-lo: utilizando com um escopo limitado (UnitOfWorkScope). 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.
Um UoW é transacional como 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:
[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) }; }
Observe que estruturas de ORM (como NHibernate e EntityFramework) salva internamente as alterações em um único comando. Suponha que você atualizou algumas Entidades com UoW não-transacional. Mesmo nesta situação, todas as atualizações são executadas no final do UoW com um único comando de banco de dados. Mas se você executar uma consulta SQL diretamente, ela será executada imediatamente e não revertida se sua UoW não for transacional. Há uma restrição para UoWs não-transacionais. Se você já estiver em um escopo de UoW transacional, a configuração isTransactional para false é ignorada.
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.
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.
Podemos alterar os valores padrão de todos os UoW na configuração de inicialização em nosso modulo. Isso geralmente é feito no método PreInitialize:
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.
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:
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); }