Desenvolvimento - Visual Basic .NET

Entity Framework 4: Repositório Genérico

O Repository Pattern é um padrão conhecido e que consiste em persistir o estado das entidades no banco de dados (Repositório). Como há uma certa similaridade entre as ações das entidades, a ideia é ter um repositório genérico, que sirva como base para todas as entidades da aplicação.

por André Baltieri



1. Introdução

O Repository Pattern é um padrão conhecido e que consiste em persistir o estado das entidades no banco de dados (Repositório).
Como há uma certa similaridade entre as ações das entidades (Por exemplo os métodos CRUD), a ideia é ter um repositório genérico, que sirva como base para todas as entidades da aplicação.
Talvez alguns pontos possam ser melhorados, fiquem à vontade em sugerir melhorias.

2. Domain

Não vou explicar a criação do domínio para não perder tempo, mas é somente a criação das três classes abaixo:

using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
 
namespace SampleApp.Domain.Entities
{
    public class User
    {
        public int Id { get; set; }
 
        [Required(ErrorMessage="Campo obrigatório.")]
        [Display(Name = "Usuário")]
        public string UserName { get; set; }
 
        [Required(ErrorMessage = "Campo obrigatório.")]
        [DataType(DataType.EmailAddress)]
        [Display(Name = "E-mail")]
        public string Email { get; set; }
 
        [Required(ErrorMessage = "Campo obrigatório.")]
        [DataType(DataType.Password)]
        [Display(Name = "Senha")]
        public string Password { get; set; }
 
        [DataType(DataType.Password)]
        [Display(Name = "Confirmar senha")]
        [Compare("Password", ErrorMessage = "As senhas digitadas não conferem.")]
        public string ConfirmPassword { get; set; }
    }

}
Classe User.cs
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
 
namespace SampleApp.Domain.Entities
{
    public class Role
    {
        public int Id { get; set; }
 
        [Required(ErrorMessage = "Campo obrigatório.")]
        [Display(Name = "Perfil")]
        public string Name { get; set; }
 
        [Required(ErrorMessage = "Campo obrigatório.")]
        [Display(Name = "Aplicação")]
        public int ApplicationId { get; set; }
        public virtual Application Application { get; set; }
 
        public virtual ICollection<User> Users { get; set; }
    }
}
Classe Role.cs
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
 
namespace SampleApp.Domain.Entities
{
    public class Application
    {
        public int Id { get; set; }
 
        [Required(ErrorMessage = "Campo obrigatório.")]
        [Display(Name = "Aplicação")]
        public string Name { get; set; }
 
        public virtual ICollection<Role> Roles { get; set; }
    }
}
Classe Application.cs

3. DataContext

O contexto é o responsável por gerenciar os estados das entidades e seus grupos. Desta forma, o contexto fica da seguinte maneira (Por hora):

using System.Data.Entity;
using SampleApp.Domain.Entities;
 
namespace SampleApp.Data.Contexts
{
    public class SampleDataContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<Role> Roles { get; set; }
        public DbSet<Application> Applications { get; set; }
    }
}
Data/Contexts/SampleDataContext.cs

4. Unit of Work

Se utilizarmos o contexto do jeito que ele está, quando precisarmos abrir duas ou mais requisições em um mesmo contexto, teremos problemas. Na aplicação web por exemplo, trabalharemos com o famoso “Session Per Request” onde abrimos uma sessão quando a página inicia e já fechamos a mesma quando a página termina de ser renderizada, mantendo a aplicação sempre desconectada do banco de dados.

Para fazer este gerenciamento utilizaremos o Unit Of Work (Referência 1), que manterá a lista de objetos modificados, excluídos e incluídos (Tracker) na sessão. Com estes itens na sessão, nós decidiremos quando “comitar” estas modificações ou não.

Sua implementação é simples, bastando criar uma interface como na Listagem 1 e implementa-lá no contexto, como na Listagem 2.

namespace SampleApp.Data.Contexts.Interfaces
{
    public interface IUnitOfWork
    {
        void Save();
    }
}
Listagem 1 - Unit of Work
using System.Data.Entity;
using SampleApp.Data.Contexts.Interfaces;
using SampleApp.Domain.Entities;
 
namespace SampleApp.Data.Contexts
{
    public class SampleDataContext : DbContext, IUnitOfWork
    {
        public DbSet<User> Users { get; set; }
        public DbSet<Role> Roles { get; set; }
        public DbSet<Application> Applications { get; set; }
 
        public void Save()
        {
            base.SaveChanges();
        }
    }
}
Listagem 2 - Versão final do contexto

5. Repositório Genérico

O repositório genérico conterá os métodos comuns entre todos os repositórios da aplicação, fique à vontade em incluir outros métodos que julgue comum em sua aplicação na interface e classe.

Deste modo, criaremos a interface determinando quais métodos serão expostos e uma classe que implementará esta interface.

using System.Linq;
 
namespace SampleApp.Data.Repositories.Interfaces
{
    public interface IBaseRepository<T> where T : class
    {
        T Find(int id);
        IQueryable<T> List();
        void Add(T item);
        void Remove(T item);
        void Edit(T item);
    }
}
Data/Repositories/Interfaces/IBaseRepository.cs
using System;
using System.Data;
using System.Linq;
using SampleApp.Data.Contexts;
using SampleApp.Data.Contexts.Interfaces;
using SampleApp.Data.Repositories.Interfaces;
 
namespace SampleApp.Data.Repositories
{
    public class BaseRepository<T> 
        : IDisposable, IBaseRepository<T> where T : class
    {
        private SampleDataContext _context;
 
        #region Ctor
        public BaseRepository(IUnitOfWork unitOfWork)
        {
            if (unitOfWork == null)
                throw new ArgumentNullException("unitOfWork");
 
            _context = unitOfWork as SampleDataContext;
        }
        #endregion
 
        public T Find(int id)
        {
            return _context.Set<T>().Find(id);
        }
 
        public IQueryable<T> List()
        {
            return _context.Set<T>();
        }
 
        public void Add(T item)
        {
            _context.Set<T>().Add(item);
        }
 
        public void Remove(T item)
        {
            _context.Set<T>().Remove(item);
        }
 
        public void Edit(T item)
        {
            _context.Entry(item).State = EntityState.Modified;
        }
 
        public void Dispose()
        {
            _context.Dispose();
        }
    }
}
Data/Repositories/BaseRepository.cs
A única observação é que o repositório recebe em seu construtor o “Unit of Work” que se refere a sessão atual onde as entidades serão gerenciadas.

6. Outros repositórios

Com o repositório genérico criado, vamos criar os outros repositórios, e para cada repositório sua interface, para que possam ser injetadas futuramente.

Aqui listarei somente o repositório da classe Role, para ver o resto consulte o código disponibilizado na sessão 10 deste artigo.

using System;
using SampleApp.Domain.Entities;
 
namespace SampleApp.Data.Repositories.Interfaces
{
    public interface IRoleRepository : IBaseRepository<Role>, IDisposable
    {
    }
}
Data/Repositories/Interfaces/IRoleRepository.cs
using SampleApp.Data.Contexts;
using SampleApp.Data.Contexts.Interfaces;
using SampleApp.Data.Repositories.Interfaces;
using SampleApp.Domain.Entities;
 
namespace SampleApp.Data.Repositories
{
    public class RoleRepository: BaseRepository<Role>, IRoleRepository
    {
        IUnitOfWork unitOfWork = new SampleDataContext();
 
        public RoleRepository(IUnitOfWork unitOfWork) 
            : base(unitOfWork)
        {
            
        }
    }
}
Data/Repositories/RoleRepository.cs
Como existem várias formas de gerenciar contexto com Unit of Work, aqui eu instancio ele na classe internamente, mas ele poderia ser recebido pelo construtor e passado adiante.

7. Camada de serviços

Com os repositórios criados, é hora de ver como utiliza-los, e para este caso utilizarei a camada de serviços.

A camada de serviços acumula toda burocracia que ficaria nos controllers, e como a ideia é ter os controllers limpos, podemos trazer o código para cá para ajudar.

Assim como os repositórios tem suas operações em comum, os serviços também tem.

Nota: Para utilizar o ModelStateDictionary é preciso adicionar referência ao namespace System.Web.Mvc.

using System.Linq;
 
namespace SampleApp.Service.Interfaces
{
    public interface IBaseService<T> where T : class
    {
        T Find(int id);
        IQueryable<T> List();
        void Add(T item);
        void Remove(T item);
        void Edit(T item);
    }
}
IBaseService.cs
using System.Linq;
using SampleApp.Data.Contexts;
using SampleApp.Data.Contexts.Interfaces;
using SampleApp.Data.Repositories;
using SampleApp.Data.Repositories.Interfaces;
using SampleApp.Service.Interfaces;
 
namespace SampleApp.Service
{
    public class BaseService<T> : IBaseService<T> where T : class
    {
        IUnitOfWork unitOfWork = new SampleDataContext();
        IBaseRepository<T> _repository;
 
        public BaseService()
        {
            _repository = new BaseRepository<T>(unitOfWork);
        }
 
        public T Find(int id)
        {
            return _repository.Find(id);
        }
 
        public IQueryable<T> List()
        {
            return _repository.List();
        }
 
        public void Add(T item)
        {           
            _repository.Add(item);
            unitOfWork.Save();
        }
 
        public void Remove(T item)
        {
            _repository.Remove(item);
            unitOfWork.Save();
        }
 
        public void Edit(T item)
        {
            _repository.Edit(item);
            unitOfWork.Save();
        }
    
        public void Dispose()
        {
            _repository.Dispose();
        }
    }
}
BaseService.cs
Com o serviço base criado, basta criar os outros serviços herdando deste serviço base e implementar os métodos adicionais necessários.

Nos exemplos abaixo, criei um método Validate(T item) para exemplificar a criação de métodos adicionais. Na maioria das vezes este método verifica se o item já está cadastrado.

using System;
using SampleApp.Domain.Entities;
 
namespace SampleApp.Service.Interfaces
{
    public interface IRoleService : IBaseService<Role>, IDisposable
    {
        bool Validate(Role role);
    }
}
IRoleService.cs
using System.Linq;
using System.Web.Mvc;
using SampleApp.Domain.Entities;
using SampleApp.Service.Interfaces;
 
namespace SampleApp.Service
{
    public class RoleService : BaseService<Role>, IRoleService
    {
        private ModelStateDictionary _modelState;
 
        public RoleService(ModelStateDictionary modelState)
        {
            _modelState = modelState;
        }
 
        public bool Validate(Role item)
        {
            if (item.Id == 0)
            {
                if (this.List().Where(c => c.Name == item.Name).Count() > 0)
                    _modelState.AddModelError("Name", "Perfil já cadastrado");
            }
 
            return _modelState.IsValid;
        }
    }
}
RoleService.cs
Os demais serviços podem ser vistos no código fonte, que pode ser baixado no fim do artigos.

8. Integrando com ASP.NET MVC

A integração com o ASP.NET MVC é tranquila, abaixo está o código do controller RoleController.cs. Escolhi este controller pois o Role pertence à uma Application, e sendo assim, será necessário ter um DropDownList para escolher a aplicação, na criação do perfil.

Os outros controllers estão no source.

using System.Data.Entity;
using System.Linq;
using System.Web.Mvc;
using SampleApp.Domain.Entities;
using SampleApp.Service;
using SampleApp.Service.Interfaces;
 
namespace SampleApp.Web.Controllers
{
    public class RoleController : Controller
    {
        IRoleService _service;
        IApplicationService _applicationService;
 
        public RoleController()
        {
            _service = new RoleService(this.ModelState);
            _applicationService = new ApplicationService(this.ModelState);
        }
 
        #region Actions
        public ActionResult Index()
        {
            return View(
                _service.List().Include(r => r.Application));
        }
 
        public ViewResult Details(int id)
        {
            return View(_service.List()
                .Include(r => r.Application)
                .Where(r => r.Id == id).First());
        }
 
        public ActionResult Create()
        {
            ViewBag.ApplicationId = 
                new SelectList(_applicationService.List(), "Id", "Name");
            return View();
        }
 
        [HttpPost]
        public ActionResult Create(Role role)
        {
            if (_service.Validate(role))
            {
                _service.Add(role);
                return RedirectToAction("Index");
            }
 
            ViewBag.ApplicationId = 
                new SelectList(_applicationService.List(), "Id", "Name");
            return View(role);
        }
 
        public ActionResult Edit(int id)
        {
            var role = _service.Find(id);
            ViewBag.ApplicationId = new SelectList(
                _applicationService.List(), "Id", "Name", role.ApplicationId);
            return View(_service.Find(id));
        }
 
        [HttpPost]
        public ActionResult Edit(Role role)
        {
            if (_service.Validate(role))
            {
                _service.Edit(role);
                return RedirectToAction("Index");
            }
 
            ViewBag.ApplicationId = new SelectList(
                _applicationService.List(), "Id", "Name", role.ApplicationId);
            return View(role);
        }
        #endregion
 
        protected override void Dispose(bool disposing)
        {
            _service.Dispose();
            base.Dispose(disposing);
        }
 
    }

}
RoleController.cs

9. Conclusão

OK, eu concordo que escrevi um bocado de código, mas que isto irá salvar muitas linhas de código posteriormente.

Generalizando os repositórios e os serviços, economizamos boa parte do tempo de desenvolvimento e teste, já que a maioria das entidades possuem métodos CRUD e para muitas os métodos CRUD são tudo que elas possuem.

10. Source

EF 4.1 Generic Repository http://files.insidedotnet.com.br/EF4.1.Generic.Repository.zip

11. Referências

1. Using Repository and Unit of Work patterns with Entity Framework 4.0

http://blogs.msdn.com/b/adonet/archive/2009/06/16/using-repository-and-unit-of-work-patterns-with-entity-framework-4-0.aspx

2. ASP.NET MVC-Validação de dados [Camada de Serviço]

http://weblogs.asp.net/andrebaltieri/archive/2011/01/08/asp-net-mvc-valida-231-227-o-de-dados-camada-de-servi-231-o.aspx

André Baltieri

André Baltieri - Trabalha com desenvolvimento de aplicações web a mais de 7 anos, e com ASP.NET desde 2003. É líder da comunidade Inside .NET (http://www.insidedotnet.com.br/) e do projeto Learn MVC .NET (http://learn-mvc.net/). Bacharelando em Sistemas de Informação, atualmente trabalha com desenvolvimento e suporte de aplicações web em ASP.NET/C# em projetos internacionais e ministra treinamentos e consultorias sobre a plataforma .NET.