Utilizando o NHibernate em uma aplicação ASP.NET Core

Neste artigo falaremos sobre o NHibernate no ASP.NET Core.

Persistir informações é um requisito básico de quase todos os sistemas. Não importando o tamanho da aplicação, ela necessitará armazenar dados. Às vezes será necessário lidar com apenas um pequeno grupo de tabelas, em outros cenários será necessário trabalhar com um grande e complexo banco de dados.

Quanto mais complexo for o banco, mais robusta deve ser a camada de acesso da aplicação. Felizmente existem frameworks ORM que auxiliam neste processo. No .NET Core, duas soluções se destacam quando se trata de lidar com uma complexa base de dados: Entity Core e NHibernate.

Como já abordei o Entity Core aqui antes, hoje falarei do NHibernate.

NHibernate

O NHibernate é um porte para .NET do framework Hibernate do Java, que é uns dos mais antigos e respeitados ORMs. Assim como o framework que o originou, o NHibernate é um projeto open-source maduro e utilizado em uma infinidade de projetos, principalmente projetos corporativos.

Isso ocorre porque possui uma grande gama de recursos: suporte nativos a vários banco de dados, várias estratégias de geração de ID, cache de segundo nível, entre outras coisas. E por possuir muitos recursos, a sua configuração não é tão simples quando o Entity Core, mas fornece mais opções de customização.

Para conhecê-lo na prática, vamos a um exemplo de CRUD simples.

Criando a aplicação

Neste artigo criarei uma aplicação ASP.NET Core MVC:

dotnet new mvc -n AspNetCoreNHibernate

Nela, adicione o pacote do NHibernate:

dotnet add package NHibernate

Da biblioteca Fluent NHibernate:

dotnet add package FluentNHibernate

E o conector do SQLite (que será o banco deste exemplo):

dotnet add package System.Data.SQLite

Agora podemos começar a configuração do NHibernate.

Criando e mapeando a entidade

No NHibernate é necessário definir a entidade de domínio, uma classe POCO:

public class Product
{
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }
    public virtual int Quantity { get; set; }
    public virtual double Price { get; set; }
}

É importante que todas as propriedades desta entidade sejam virtual.

Para que o NHibernate reconheça a entidade, é necessário mapeá-la. O padrão da biblioteca é um mapeamento por arquivo XML, mas graças ao FluentNHibernate, isso pode ser feito via código:

public class ProductMap: ClassMapping<Product>
{
    public ProductMap()
    {
        Id(x => x.Id, x =>
        {
            x.Generator(Generators.Increment);
            x.Type(NHibernateUtil.Int64);
            x.Column("Id");
        });

        Property(b => b.Name, x =>
        {
            x.Length(520);
            x.Type(NHibernateUtil.String);
            x.NotNullable(true);
        });

        Property(b => b.Quantity, x =>
        {
            x.Type(NHibernateUtil.Int32);
            x.NotNullable(true);
        });

        Property(b => b.Price, x =>
        {
            x.Type(NHibernateUtil.Double);
            x.Scale(2);
            x.Precision(15);
            x.NotNullable(true);
        });

        Table("Products");
    }
}

Com a classe mapeada, podemos registrar o NHibernate nos serviços da nossa aplicação.

Configurando o serviço do NHibernate

Seguindo o padrão do ASP.NET Core, criarei uma extensão para o nosso serviço:

public static class NHibernateExtensions
{
    public static IServiceCollection AddNHibernate(
        this IServiceCollection services, 
        string connectionString)
    {
        var mapper = new ModelMapper();
        mapper.AddMappings(typeof(NHibernateExtensions).Assembly.ExportedTypes);
        HbmMapping entityMapping = mapper.CompileMappingForAllExplicitlyAddedEntities();

        var configuration = new Configuration();
        configuration.DataBaseIntegration(c =>
        {
            c.Dialect<SQLiteDialect>();
            c.ConnectionString = connectionString;
            c.KeywordsAutoImport = Hbm2DDLKeyWords.AutoQuote;
            c.SchemaAction = SchemaAutoAction.Update;
            c.LogFormattedSql = true;
            c.LogSqlInConsole = true;
        });
        configuration.AddMapping(entityMapping);

        var sessionFactory = configuration.BuildSessionFactory();

        services.AddSingleton(sessionFactory);
        services.AddScoped(factory => sessionFactory.OpenSession());

        return services;
    }
}

Note que é indicado que as classes de domínio estão mapeadas em classes da aplicação. Também definimos as configurações de conexão e por fim, é criado a sessão.

C# (C Sharp) Avançado
Curso de C# (C Sharp) Avançado
CONHEÇA O CURSO

Esta sessão é adicionada no escopo, para que a conexão com o banco de dados não fique sempre “aberta”. O banco será acessado por um objeto sessão e ao adicioná-la no escopo, ele só será criado quando for utilizado.

Por fim é necessário chamar o método AddNHibernate em ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddNHibernate(Configuration.GetConnectionString("SQLiteConnection"));
    services.AddControllersWithViews();
}

Com essas configurações já podemos acessar o banco de dados. Mas para este projeto criarei e implementarei o padrão repository.

Criando o repositório da aplicação

O nosso repositório será composto de uma interface:

public interface IRepository<T>
{
    Task Add(T item);
    Task Remove(long id);
    Task Update(T item);
    Task<T> FindByID(long id);
    IEnumerable<T> FindAll();
}

E sua implementação:

public class ProductRepository : IRepository<Product>
{
    private ISession _session;
    public ProductRepository(ISession session) => _session = session;
    public async Task Add(Product item)
    {
        ITransaction transaction = null;
        try
        {
            transaction = _session.BeginTransaction();
            await _session.SaveAsync(item);
            await transaction.CommitAsync();
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex);
            await transaction?.RollbackAsync();
        }
        finally
        {
            transaction?.Dispose();
        }
    }

    public IEnumerable<Product> FindAll() => 
                    _session.Query<Product>().ToList();

    public async Task<Product> FindByID(long id) => 
                    await _session.GetAsync<Product>(id);

    public async Task Remove(long id)
    {
        ITransaction transaction = null;
        try
        {
            transaction = _session.BeginTransaction();
            var item = await _session.GetAsync<Product>(id);
            await _session.DeleteAsync(item);
            await transaction.CommitAsync();
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex);
            await transaction?.RollbackAsync();
        }
        finally
        {
            transaction?.Dispose();
        }
    }

    public async Task Update(Product item)
    {
        ITransaction transaction = null;
        try
        {
            transaction = _session.BeginTransaction();
            await _session.UpdateAsync(item);
            await transaction.CommitAsync();
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex);
            await transaction?.RollbackAsync();
        }
        finally
        {
            transaction?.Dispose();
        }
    }
}

Note que nas operações que realizam alterações no banco, é criada uma transação:

transaction = _session.BeginTransaction();
await _session.SaveAsync(item);
await transaction.CommitAsync();

Isso não é necessário, mas é recomendado que seja implementado. Assim, caso ocorra um erro durante a alteração, é possível voltar o banco para o estado anterior a transação:

await transaction?.RollbackAsync();

Para finalizarmos, é necessário criar o controller.

Criando controller da aplicação

O nosso controller será um controller CRUD padrão:

public class ProductController : Controller
{
    private readonly ProductRepository productRepository;

    public ProductController(NHibernate.ISession session) => 
                        productRepository = new ProductRepository(session);

    // GET: Products
    public ActionResult Index()
    {
        return View(productRepository.FindAll().ToList());
    }

    // GET: Products/Details/5
    public async Task<ActionResult> Details(long? id)
    {
        if (id == null)
        {
            return StatusCode(StatusCodes.Status404NotFound);
        }
        Product product = await productRepository.FindByID(id.Value);
        if (product == null)
        {
            return StatusCode(StatusCodes.Status404NotFound);
        }
        return View(product);
    }

    // GET: Products/Create
    public ActionResult Create()
    {
        return View();
    }

    // POST: Products/Create
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Create(
                    [Bind("Id,Name,Quantity,Price")]
                    Product product)
    {
        if (ModelState.IsValid)
        {
            await productRepository.Add(product);
            return RedirectToAction("Index");
        }

        return View(product);
    }

    // GET: Products/Edit/5
    public async Task<ActionResult> Edit(long? id)
    {
        if (id == null)
        {
            return StatusCode(StatusCodes.Status400BadRequest);
        }
        Product product = await productRepository.FindByID(id.Value);
        if (product == null)
        {
            return StatusCode(StatusCodes.Status404NotFound);
        }
        return View(product);
    }

    // POST: Products/Edit/5
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Edit(
                [Bind("Id,Name,Quantity,Price")]
                Product product)
    {
        if (ModelState.IsValid)
        {
            await productRepository.Update(product);
            return RedirectToAction("Index");
        }
        return View(product);
    }

    // GET: Products/Delete/5
    public async Task<ActionResult> Delete(long? id)
    {
        if (id == null)
        {
            return StatusCode(StatusCodes.Status400BadRequest);
        }
        Product product = await productRepository.FindByID(id.Value);
        if (product == null)
        {
            return StatusCode(StatusCodes.Status404NotFound);
        }
        return View(product);
    }

    // POST: Products/Delete/5
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> DeleteConfirmed(long id)
    {
        await productRepository.Remove(id);
        return RedirectToAction("Index");
    }
}

Antes de ver o sistema funcionando é necessário criar as views para as actions acima. Você pode vê-la no repositório do projeto.

Com isso, ao executá-lo, veremos que está tudo certo:

Página index mostrando a listagem de produtos

Conclusão

O NHibernate é uma ótima solução para aplicações que precisam lidar com bancos de dados complexos ou que não sejam suportado pelo Entity Core. Com vários anos de estrada é um framework popular e estável, que pode ser utilizado em qualquer projeto sem medo.

Então quando for criar o seu projeto, não deixe de dar uma olhada neste framework.

Você pode ver o código completo do projeto no meu Github.

Deixe seu comentário

Instrutor, nerd, cinéfilo e desenvolvedor nas horas vagas. Graduado em Ciências da Computação pela Universidade Metodista de São Paulo.

© 2004 - 2019 TreinaWeb Tecnologia LTDA - CNPJ: 06.156.637/0001-58 Av. Paulista, 1765, Conj 71 e 72 - Bela Vista - São Paulo - SP - 01311-200