Árvore de páginas

Versões comparadas

Chave

  • Esta linha foi adicionada.
  • Esta linha foi removida.
  • A formatação mudou.

...

Bloco de código
languagec#
firstline1
titleEntityFrameworkCoreModule.cs
linenumberstrue
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
languagec#
firstline1
titleCountry.cs
linenumberstrue
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
languagec#
firstline1
titleICountryRepository.cs
linenumberstrue
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 EfCoreRepositoryBasepadrão AppEfCoreRepositoryBase com suporte a métodos para realizar operações de CRUD e queries mais complexas.

...

Bloco de código
languagec#
firstline1
titleCustomCountryRepository.cs
linenumberstrue
public class CustomCountryRepository : EfCoreRepositoryBase<ArchitectureDbContextAppEfCoreRepositoryBase<ArchitectureDbContext, Country, int>, ICountryRepository
{
	public ProfessionalRepository(IDbContextProvider<ArchitectureDbContext> dbContextProvider) 
		: base(dbContextProvider)
	{
	}
	
	public override Professional Get(decimal id)
	{
    	return base.Get(id);
	}
}

Mapeando uma Estrutura Legada

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
languagec#
firstline1
titleProfessionalPoco.cs
linenumberstrue
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
languagec#
firstline1
titleLegacyDbContext.cs
linenumberstrue
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
languagec#
firstline1
titleIProfessionalRepository.cs
linenumberstrue
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
languagec#
firstline1
titleProfessionalRepository.cs
linenumberstrue
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