Árvore de páginas

Versões comparadas

Chave

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

...

Para isso através do Visual Studio 2017 vamos criar nosso projeto acessando: File -> New -> Project -> .NET Standard -> Class Library.

Com o projeto criado vamos adicionar o seguinte pacote via nuget:  Tnf.App.EntityFrameworkCore disponível em nosso package source: em https://www.myget.org/F/tnf/api/v3/index.json

Podemos agora começar a criar nosso contexto e entidades de nosso banco de dados.

Para isso vamos criar o módulo que fará a configuração do uso do TNF para Entity Framework Core.

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.

...

Agora podemos definir nossa entidade que representará a tabela "Countries" em nosso exemplo:

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;
	}
}

...

Note que temos uma herança para a classe Entity do TNF. Por padrão essa herança define que a entidade tenha uma propriedade Id (int) como chave primária.

Para alterar o tipo da chave primaria a ser usada informe através do parâmetro genérico qual tipo será usado. Exemplo: public class Country : Entity<Guid>

O atributo presente no exemplo AutoMap  AutoMapAttribute é um wrapper do TNF para uso do framework AutoMapper que fará o mapeamento automático da nossa tabela para o seu Dto. 

Definindo um contexto

Para trabalhar usando contextos do Entity Framework Core devemos definir a classe que fará o acesso e configuração de nossas entidades.

Para isso o próximo Próximo passo é definir o DbContext fazendo a herança do TnfDbContext:

Bloco de código
languagec#
firstline1
titleSampleDbContext
linenumberstrue
using Microsoft.EntityFrameworkCore;
using Tnf.Architecture.EntityFrameworkCore.Entities;
using Tnf.EntityFrameworkCore;
 
public class SampleDbContext : TnfDbContext
{
	public DbSet<Country> Countries { get; set; }

	public ArchitectureDbContext(DbContextOptions<SampleDbContext> options)
		: base(options)
	{
	}
}

Com nosso contexto criado podemos agora utilizar o Entity Framework Core utilizando o contexto criado.

Criando um Repositório

Para trabalhar com repositórios no TNF você precisa definir um TnfDbContext necessariamente. Quando o repositório for criado ele utilizará o TnfDbContext definido para criá-lo.

Vamos criar um repositório para a entidade criada anteriormente em nosso exemplo:

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 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 fazer override dos métodos do repositório ou implementar de acordo com a sua necessidade:

Bloco de código
languagec#
firstline1
titleCustomCountryRepository.cs
linenumberstrue
public class CustomCountryRepository : AppEfCoreRepositoryBase<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