...
Bloco de código | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
using Tnf.Reflection.Extensions; using Tnf.App.EntityFrameworkCore; using Tnf.Architecture.Domain; using Tnf.Modules; [DependsOn( typeof(DomainModule), typeof(TnfAppEntityFrameworkCoreModule))] public class EntityFrameworkModule : TnfModule { public override void Initialize() { IocManager.RegisterAssemblyByConvention(typeof(EntityFrameworkModule).GetAssembly()Assembly); } } |
Podemos perceber em nosso módulo definido acima no atributo "DependsOn" fazendo referencia ao TnfAppEntityFrameworkCoreModule. Essa dependência comporta toda a estrutura necessária para a utilização do Entity Framework Core.
...
Bloco de código | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Tnf.Architecture.Dto;
using Tnf.AutoMapper;
using Tnf.Domain.Entities;
[AutoMap(typeof(CountryDto))]
[Table("Countries")]
public class Country : Entity
{
public const int MaxNameLength = 256;
[Required]
[MaxLength(MaxNameLength)]
public string Name { get; set; }
public Country()
{
}
public Country(int id, string name)
{
Id = id;
Name = name;
}
} |
...
Bloco de código | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
public interface ICountryRepository : IRepository<Country> { } |
Essa interface está dentro do projeto de Domain para que ele não precise conhecer o projeto de Arquitetura.
Como instalamos o pacote Tnf.App.EntityFrameworkCore e configuramos a dependência do módulo TnfAppEntityFrameworkCoreModule, ao utilizar o tipo ICountryRepository para a entidade "Country", nosso mecanismo de injeção de dependência está injetando o repositório padrão TnfRepositoryBase com AppEfCoreRepositoryBase com suporte a métodos para realizar operações de CRUD e queries mais complexas.
Caso você precise de uma especialização para seu repositório com métodos que você queira definir, você pode utilizar a classe abstrata TnfRepositoryBase<TEntity, TPrimaryKey> e fazer a criação de seu repositório informando a entidade e qual é o tipo de sua chave primariafazer override dos métodos do repositório ou implementar de acordo com a sua necessidade:
Bloco de código | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
public class CustomCountryRepository : TnfRepositoryBase<CountryAppEfCoreRepositoryBase<ArchitectureDbContext, Country, int>, ICountryRepository { public ProfessionalRepository(IDbContextProvider<ArchitectureDbContext> dbContextProvider) : base(dbContextProvider) { } public override voidProfessional DeleteGet(Countrydecimal entityid) { throw new NotImplementedException(); } public override void Delete(int id) { throw new NotImplementedException(); } public override IQueryable<Country> GetAll() { throw new NotImplementedException(); } public override Country Insert(Country entity) { throw new NotImplementedException(); } public override Country Update(Country entity) { throw new NotImplementedException(); } }return base.Get(id); } } |
Em muitos cenários estamos reescrevendo uma funcionalidade de um sistema legado e nos deparamos com chaves primarias compostas. Nesses casos precisamos especializar nosso repositório.
Abaixo temos um exemplo de uma tabela "Professional" representada pela classe ProfessionalPoco (PKs: ProfessionalId, Code):
Bloco de código | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
using System;
using System.Collections.Generic;
using Tnf.Architecture.Dto.Registration;
using Tnf.AutoMapper;
using Tnf.Domain.Entities;
namespace Tnf.Architecture.EntityFrameworkCore.Entities
{
public class ProfessionalPoco : Entity
{
public ProfessionalPoco()
{
}
public decimal ProfessionalId { get; set; }
public string Name { get; set; }
public Guid Code { get; set; }
public string Address { get; set; }
public string AddressNumber { get; set; }
public string AddressComplement { get; set; }
public string ZipCode { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public List<ProfessionalSpecialtiesPoco> ProfessionalSpecialties { get; set; }
}
} |
Configuração do contexto:
Bloco de código | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
using Microsoft.EntityFrameworkCore;
using Tnf.EntityFrameworkCore;
using Tnf.Architecture.EntityFrameworkCore.Entities;
namespace Tnf.Architecture.EntityFrameworkCore
{
public class LegacyDbContext : TnfDbContext
{
public DbSet<ProfessionalPoco> Professionals { get; set; }
public DbSet<ProfessionalSpecialtiesPoco> ProfessionalSpecialties { get; set; }
public DbSet<SpecialtyPoco> Specialties { get; set; }
public LegacyDbContext(DbContextOptions<LegacyDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ProfessionalPoco>(m =>
{
// PKs
m.Ignore(i => i.Id);
m.HasKey(i => new { i.ProfessionalId, i.Code });
// Configure PKs auto generated
m.Property(i => i.Code).ValueGeneratedOnAdd().HasColumnName("SYS009_PROFESSIONAL_CODE");
// Hack pois o método ValueGeneratedOnAdd do EF Core ainda não aceita propriedades decimal
m.Property(i => i.ProfessionalId).HasDefaultValueSql("SELECT ISNULL(MAX(SYS009_PROFESSIONAL_ID), 1) FROM SYS009_PROFESSIONAL").HasColumnName("SYS009_PROFESSIONAL_ID");
m.Property(p => p.Name).HasColumnName("SYS009_NAME").HasMaxLength(50).IsRequired();
m.Property(p => p.Address).HasColumnName("SYS009_ADDRESS").HasMaxLength(50).IsRequired();
m.Property(p => p.AddressComplement).HasColumnName("SYS009_ADDRESS_COMPLEMENT").HasMaxLength(100).IsRequired();
m.Property(p => p.AddressNumber).HasColumnName("SYS009_ADDRESS_NUMBER").HasMaxLength(9).IsRequired();
m.Property(p => p.Email).HasColumnName("SYS009_EMAIL").HasMaxLength(50).IsRequired();
m.Property(p => p.Email).HasColumnName("SYS009_EMAIL").HasMaxLength(50).IsRequired();
m.Property(p => p.Phone).HasColumnName("SYS009_PHONE").HasMaxLength(50).IsRequired();
m.Property(p => p.ZipCode).HasColumnName("SYS009_ZIP_CODE").HasMaxLength(15).IsRequired();
m.ToTable("SYS009_PROFESSIONAL");
});
modelBuilder.Entity<SpecialtyPoco>(m =>
{
// PKs
m.HasKey(i => i.Id);
// Configure PKs auto generated
m.Property(i => i.Id).ValueGeneratedOnAdd().HasColumnName("SYS011_SPECIALTIES_ID");
m.Property(p => p.Description).HasColumnName("SYS011_SPECIALTIES_DESCRIPTION").HasMaxLength(100).IsRequired();
m.ToTable("SYS011_SPECIALTIES");
});
modelBuilder.Entity<ProfessionalSpecialtiesPoco>(m =>
{
// PKs
m.Ignore(i => i.Id);
m.HasKey(i => new { i.ProfessionalId, i.Code, i.SpecialtyId });
m.Property(i => i.ProfessionalId).HasColumnName("SYS009_PROFESSIONAL_ID");
m.Property(i => i.Code).HasColumnName("SYS009_PROFESSIONAL_CODE");
m.Property(i => i.SpecialtyId).HasColumnName("SYS011_SPECIALTIES_ID");
m.HasOne(o => o.Professional)
.WithMany(w => w.ProfessionalSpecialties)
.HasPrincipalKey(k => new { k.ProfessionalId, k.Code })
.HasForeignKey(k => new { k.ProfessionalId, k.Code });
m.HasOne(o => o.Specialty)
.WithMany(w => w.ProfessionalSpecialties)
.HasPrincipalKey(k => k.Id)
.HasForeignKey(k => k.SpecialtyId);
m.ToTable("SYS010_PROFESSIONAL_SPECIALTIES");
});
}
}
}
|
Note que no contexto acima temos a configuração de nossa entidade Professional (SYS009_PROFESSIONAL) com um relacionamento de N para N (SYS010_PROFESSIONAL_SPECIALTIES) com a entidade Specialty (SYS011_SPECIALTIES) que representa as especialidades de um profissional.
Como o repositório por default exige uma chave primaria, nossa entidade comporta apenas uma chave. Dessa forma devemos ignora-la na hora de representar a tabela em nosso contexto utilizando a configuração m.Ignore(i => i.Id).
Definimos nossa chave composta através do método de extensão chamado HasKey passando os múltiplos valores que irão constitui-la.
Como em muitos casos temos que criar chaves auto incremento podemos utilizar o método chamado ValueGeneratedOnAdd que irá configurar a chave como Identity.
Obs: Existem alguns tipos de dados ainda no Entity Framework Core que não estão funcionando com valores auto incremento como decimal. Para esses valores podemos contornar o problema usando o método HasDefaultValueSql, onde é passada uma instrução SQL que irá recuperar o valor do campo na hora de usar o seu auto incremento (Esse problema já está previsto para ser resolvido em futuras releases).
Note que além das chaves auto incremento configuramos também nomes das colunas, relacionamentos N para N e nomes das tabelas.
Abaixo temos a reescrita de nosso repositório com métodos que atendem o cenários com chaves primarias compostas:
Bloco de código | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
using System;
using System.Collections.Generic;
using Tnf.App.Dto.Request;
using Tnf.Architecture.Common.ValueObjects;
using Tnf.Architecture.Domain.Registration;
using Tnf.Domain.Repositories;
namespace Tnf.Architecture.Domain.Interfaces.Repositories
{
public interface IProfessionalRepository : IRepository
{
Professional GetProfessional(IRequestDto<ComposeKey<Guid, decimal>> requestDto);
ComposeKey<Guid, decimal> CreateProfessional(Professional entity);
void UpdateProfessional(Professional entity);
bool DeleteProfessional(ComposeKey<Guid, decimal> keys);
void AddOrRemoveSpecialties(ComposeKey<Guid, decimal> keys, IList<Specialty> specialties);
bool ExistsProfessional(ComposeKey<Guid, decimal> keys);
}
}
|
Bloco de código | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Transactions;
using Tnf.App.AutoMapper;
using Tnf.App.Dto.Request;
using Tnf.App.EntityFrameworkCore;
using Tnf.App.EntityFrameworkCore.Repositories;
using Tnf.Architecture.Common.ValueObjects;
using Tnf.Architecture.Domain.Interfaces.Repositories;
using Tnf.Architecture.Domain.Registration;
using Tnf.Architecture.Dto.Registration;
using Tnf.Architecture.EntityFrameworkCore.Contexts;
using Tnf.Architecture.EntityFrameworkCore.Entities;
using Tnf.Domain.Uow;
using Tnf.EntityFrameworkCore;
namespace Tnf.Architecture.EntityFrameworkCore.Repositories
{
public class ProfessionalRepository : AppEfCoreRepositoryBase<LegacyDbContext, ProfessionalPoco>, IProfessionalRepository
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
public ProfessionalRepository(IDbContextProvider<LegacyDbContext> dbContextProvider, IUnitOfWorkManager unitOfWorkManager)
: base(dbContextProvider)
{
_unitOfWorkManager = unitOfWorkManager;
}
public bool DeleteProfessional(ComposeKey<Guid, decimal> keys)
{
var dbEntity = Context.Professionals
.Include(i => i.ProfessionalSpecialties)
.SingleOrDefault(s => s.ProfessionalId == keys.SecundaryKey && s.Code == keys.PrimaryKey);
if (dbEntity == null)
return false;
dbEntity.ProfessionalSpecialties.ForEach(w => Context.ProfessionalSpecialties.Remove(w));
Context.Professionals.Remove(dbEntity);
return true;
}
private ProfessionalPoco GetProfessionalPoco(IRequestDto<ComposeKey<Guid, decimal>> requestDto)
{
var dbEntity = Context.Professionals
.IncludeByRequestDto(requestDto)
.Where(w => w.ProfessionalId == requestDto.GetId().SecundaryKey && w.Code == requestDto.GetId().PrimaryKey)
.SelectFieldsByRequestDto(requestDto)
.SingleOrDefault();
return dbEntity;
}
public Professional GetProfessional(IRequestDto<ComposeKey<Guid, decimal>> requestDto)
{
var dbEntity = GetProfessionalPoco(requestDto);
if (dbEntity == null)
return null;
var dto = dbEntity.MapTo<Professional>();
return dto;
}
public ComposeKey<Guid, decimal> CreateProfessional(Professional entity)
{
var dbEntity = entity.MapTo<ProfessionalPoco>();
dbEntity.ProfessionalId = GetNextKeyProfessional();
Context.Professionals.Add(dbEntity);
Context.SaveChanges();
return new ComposeKey<Guid, decimal>(dbEntity.Code, dbEntity.ProfessionalId);
}
private decimal GetNextKeyProfessional()
{
using (var uow = _unitOfWorkManager.Begin(TransactionScopeOption.RequiresNew))
{
var lastKey = Context.Professionals.Select(s => s.ProfessionalId).DefaultIfEmpty(0).Max();
return (lastKey + 1);
}
}
public void UpdateProfessional(Professional entity)
{
var mappedEntity = GetProfessionalPoco(new RequestDto<ComposeKey<Guid, decimal>>(new ComposeKey<Guid, decimal>(entity.Code, entity.ProfessionalId)));
entity.MapTo(mappedEntity);
Context.Professionals.Update(mappedEntity);
Context.SaveChanges();
}
public void AddOrRemoveSpecialties(ComposeKey<Guid, decimal> keys, IList<Specialty> specialties)
{
var request = new RequestDto<ComposeKey<Guid, decimal>>(keys) { Expand = new ProfessionalDto().Expandables[0] };
var dbProfessional = GetProfessionalPoco(request);
if (dbProfessional == null)
return;
var idsToAdd = specialties.Select(s => s.Id).ToArray();
if (dbProfessional.ProfessionalSpecialties == null)
dbProfessional.ProfessionalSpecialties = new List<ProfessionalSpecialtiesPoco>();
dbProfessional.ProfessionalSpecialties.RemoveAll(w => !idsToAdd.Contains(w.SpecialtyId));
foreach (var specialty in specialties)
{
var dbProfessionalSpecialties = dbProfessional.ProfessionalSpecialties
.FirstOrDefault(s => s.SpecialtyId == specialty.Id);
if (dbProfessionalSpecialties == null)
{
dbProfessional.ProfessionalSpecialties.Add(new ProfessionalSpecialtiesPoco
{
ProfessionalId = dbProfessional.ProfessionalId,
Code = dbProfessional.Code,
SpecialtyId = specialty.Id
});
}
}
}
public bool ExistsProfessional(ComposeKey<Guid, decimal> keys)
{
var dbEntity = Context.Professionals
.SingleOrDefault(s => s.ProfessionalId == keys.SecundaryKey && s.Code == keys.PrimaryKey);
return dbEntity != null;
}
}
}
|
Este exemplo com umas estrutura legada esta disponível em nosso github: https://github.com/totvsnetcore/tnf-samples/tree/master/TnfSample-Architecture