QPANC - Parte 4 - ASP.NET - Entity Framework e ASP.NET Core Identity
Tags: quasar, postgres, aspnetcore, docker
Published: 2020-04-19 23:36:22

> **QPANC** são as iniciais de **Q**uasar **P**ostgreSQL **A**SP **N**ET **C**ore.

  * [Source](https://github.com/TobyMosque/qpanc)
  * [Introdução](https://dev.to/quasar-pt-br/arquiteturando-uma-aplicacao-com-quasar-postgresql-asp-net-core-e-docker-introducao-o7o)
  * [Parte I - ASP.NET - Inicializando os Projetos](https://dev.to/quasar-pt-br/qpanc-parte-1-asp-net-inicializando-os-projetos-lkm)
  * [Parte 2 - PostgreSQL](https://dev.to/quasar-pt-br/qpanc-parte-2-postgresql-51pb)
  * [Parte 3 - ASP.NET - Registrando Serviços e Lendo Variáveis de Ambiente](https://dev.to/quasar-pt-br/qpanc-parte-3-asp-net-registrando-servicos-e-lendo-variaveis-de-ambiente-3do2)
  * [Parte 4 - ASP.NET - Entity Framework e ASP.NET Core Identity](https://dev.to/quasar-pt-br/qpanc-parte-4-asp-net-entity-framework-e-asp-net-core-identity-40f9)
  * [Parte 5 - ASP.NET - Documentação Interativa com Swagger](https://dev.to/quasar-pt-br/qpanc-parte-5-asp-net-documentacao-interativa-com-swagger-4n0o)
  * [Parte 6 - ASP.NET - Regionalização](https://dev.to/quasar-pt-br/qpanc-parte-6-asp-net-regionalizacao-13p5)
  * [Parte 7 - ASP.NET - Autenticação e Autorização](https://dev.to/quasar-pt-br/qpanc-parte-7-asp-net-autenticacao-e-autorizacao-bpd)
  * [Parte 8 - ASP.NET - CORS](https://dev.to/quasar-pt-br/qpanc-parte-8-asp-net-cors-52dn)
  * [Parte 9 - Quasar - Criação e Configuração do Projeto](https://dev.to/quasar-pt-br/qpanc-parte-9-quasar-criacao-e-configuracao-do-projeto-5hbi)
  * [Parte 10 - Quasar - Configurações e Customizações](https://dev.to/quasar-pt-br/qpanc-parte-10-quasar-configuracoes-e-customizacoes-2i86)
  * [Parte 11 - Quasar - Componentes - Diferença entre SPA e SSR](https://dev.to/quasar-pt-br/qpanc-parte-11-quasar-componentes-diferenca-entre-spa-e-ssr-1a3c)
  * [Parte 12 - Quasar - Serviços](https://dev.to/quasar-pt-br/qpanc-parte-12-quasar-servicos-4e61)
  * [Parte 13 - Quasar - Regionalização e Stores](https://dev.to/quasar-pt-br/qpanc-parte-13-quasar-regionalizacao-e-stores-1kep)
  * [Parte 14 - Quasar - Consumindo a API](https://dev.to/quasar-pt-br/qpanc-parte-14-quasar-consumindo-a-api-fn6)
  * [Parte 15 - Quasar - Login](https://dev.to/quasar-pt-br/qpanc-parte-15-quasar-login-jc6)
  * [Parte 16 - Quasar - Áreas Protegidas](https://dev.to/quasar-pt-br/qpanc-parte-16-quasar-areas-protegidas-3jkn)
  * [Parte 17 - Quasar - Registro](https://dev.to/quasar-pt-br/qpanc-parte-17-quasar-registro-3b8c)
  * [Parte 18 - Docker - Maquina Virtual Linux](https://dev.to/quasar-pt-br/qpanc-parte-18-docker-maquina-virtual-linux-g5c)
  * [Parte 19 - Docker - Registro e Build](https://dev.to/quasar-pt-br/qpanc-parte-19-docker-registro-e-build-31i4)
  * [Parte 20 - Docker - Traefik e Publicação](https://dev.to/quasar-pt-br/qpanc-parte-20-docker-traefik-e-publicacao-3g3d)
  * [Demo Online](https://qpanc.tobiasmesquita.dev)

##7 Entity Framework Core & Identity

###7.1 Definindo os Serviços.

Antes de começamos a modelar o novo banco de dados, será necessario adicionar duas interfaces ao projeto `QPANC.Services.Abstract`, serão elas `ILoggedUser` e `IConnectionStrings`.

A `ILoggedUser` irá expor o Id do Usuário Logado:

**QPANC.Services.Abstract/ILoggedUser.cs**
```cs
using System;

namespace QPANC.Services.Abstract
{
    public interface ILoggedUser
    {
        Guid? SessionId { get; }
        Guid? UserId { get; }
    }
}
```

A `IConnectionStrings` as strings de conexão com os Bancos de Dados:

**QPANC.Services.Abstract/IConnectionStrings.cs**
```cs
namespace QPANC.Services.Abstract
{
    public interface IConnectionStrings
    {
        string DefaultConnection { get; }
    }
}
```

Por hora, não será necessário se preocupar com a sua respectiva implementação

###7.2 Instalando as dependências.

Abra o terminal e navegue até a pasta do Projeto `QPANC.Domain`, então execute os seguintes comandos.:

```
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package EntityFrameworkCore.Triggers
```

###7.3 Propriedades comuns à todas as entidades

Agora precisamos criar uma interface que definir as propriedades que serão utilizadas por todas as nossas entidades, para tal, iremos criar uma pasta chamada `Interfaces` e dentro dentro criar a interface `IEntity`

**QPANC.Domain/Interfaces/IEntity.cs**
```cs
using System;

namespace QPANC.Domain.Interfaces
{
    public interface IEntity
    {
        bool IsDeleted { get; set; }
        DateTimeOffset CreatedAt { get; set; }
        DateTimeOffset? UpsertedAt { get; set; }
        DateTimeOffset? DeletedAt { get; set; }
        Guid? UserSessionId { get; set; }
    }
}
```

###7.4 Entidades necessárias para a autenticação/autorização

Para que possamos injetar novas propriedades nas classes usadas pelo Identity, teremos que criar classes derivadas destas classes, para tal, crie uma pasta chamada `Identity` e adicione as seguintes classes:

**QPANC.Domain/Identity/Role.cs**
```cs
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace QPANC.Domain.Identity
{
    public class Role : IdentityRole<Guid>, Interfaces.IEntity
    {
        public Role() { }
        public Role(string roleName) : base(roleName) { }

        #region IEntity interface
        public bool IsDeleted { get; set; }
        public DateTimeOffset CreatedAt { get; set; }
        public DateTimeOffset? DeletedAt { get; set; }
        public DateTimeOffset? UpsertedAt { get; set; }
        public Guid? UserSessionId { get; set; }
        #endregion

        [InverseProperty(nameof(UserRole.Role))]
        public ICollection<UserRole> Users { get; set; }

        [InverseProperty(nameof(RoleClaim.Role))]
        public ICollection<RoleClaim> Claims { get; set; }
    }
}
```
**QPANC.Domain/Identity/RoleClaim.cs**
```cs
using Microsoft.AspNetCore.Identity;
using System;
using System.ComponentModel.DataAnnotations.Schema;

namespace QPANC.Domain.Identity
{
    public class RoleClaim : IdentityRoleClaim<Guid>, Interfaces.IEntity
    {
        #region IEntity interface
        public bool IsDeleted { get; set; }
        public DateTimeOffset CreatedAt { get; set; }
        public DateTimeOffset? DeletedAt { get; set; }
        public DateTimeOffset? UpsertedAt { get; set; }
        public Guid? UserSessionId { get; set; }
        #endregion

        [ForeignKey(nameof(RoleClaim.RoleId))]
        public Role Role { get; set; }
    }
}
```
**QPANC.Domain/Identity/Session.cs**
```cs
using Microsoft.AspNetCore.Identity;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace QPANC.Domain.Identity
{
    public class Session : Interfaces.IEntity
    {
        [Key]
        public Guid SessionId { get; set; }

        public Guid UserId { get; set; }

        public DateTimeOffset ExpireAt { get; set; }

        #region IEntity interface
        public bool IsDeleted { get; set; }
        public DateTimeOffset CreatedAt { get; set; }
        public DateTimeOffset? DeletedAt { get; set; }
        public DateTimeOffset? UpsertedAt { get; set; }
        public Guid? UserSessionId { get; set; }
        #endregion

        [ForeignKey(nameof(Session.UserId))]
        public User User { get; set; }
    }
}
```
**QPANC.Domain/Identity/User.cs**
```cs
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace QPANC.Domain.Identity
{
    public class User : IdentityUser<Guid>, Interfaces.IEntity
    {
        public User() { }
        public User(string userName) : base(userName) { }

        public string FirstName { get; set; }
        public string LastName { get; set; }

        #region IEntity interface
        public bool IsDeleted { get; set; }
        public DateTimeOffset CreatedAt { get; set; }
        public DateTimeOffset? DeletedAt { get; set; }
        public DateTimeOffset? UpsertedAt { get; set; }
        public Guid? UserSessionId { get; set; }
        #endregion

        [InverseProperty(nameof(Session.User))]
        public ICollection<Session> Sessions { get; set; }

        [InverseProperty(nameof(UserRole.User))]
        public ICollection<UserRole> Roles { get; set; }

        [InverseProperty(nameof(UserLogin.User))]
        public ICollection<UserLogin> Logins { get; set; }

        [InverseProperty(nameof(UserClaim.User))]
        public ICollection<UserClaim> Claims { get; set; }

        [InverseProperty(nameof(UserToken.User))]
        public ICollection<UserToken> Tokens { get; set; }
    }
}
```
**QPANC.Domain/Identity/UserLogin.cs**
```cs
using Microsoft.AspNetCore.Identity;
using System;
using System.ComponentModel.DataAnnotations.Schema;

namespace QPANC.Domain.Identity
{
    public class UserLogin : IdentityUserLogin<Guid>, Interfaces.IEntity
    {
        #region IEntity interface
        public bool IsDeleted { get; set; }
        public DateTimeOffset CreatedAt { get; set; }
        public DateTimeOffset? DeletedAt { get; set; }
        public DateTimeOffset? UpsertedAt { get; set; }
        public Guid? UserSessionId { get; set; }
        #endregion

        [ForeignKey(nameof(UserLogin.UserId))]
        public User User { get; set; }
    }
}
```
**QPANC.Domain/Identity/UserRole.cs**
```cs
using Microsoft.AspNetCore.Identity;
using System;
using System.ComponentModel.DataAnnotations.Schema;

namespace QPANC.Domain.Identity
{
    public class UserRole : IdentityUserRole<Guid>, Interfaces.IEntity
    {
        #region IEntity interface
        public bool IsDeleted { get; set; }
        public DateTimeOffset CreatedAt { get; set; }
        public DateTimeOffset? DeletedAt { get; set; }
        public DateTimeOffset? UpsertedAt { get; set; }
        public Guid? UserSessionId { get; set; }
        #endregion

        [ForeignKey(nameof(UserRole.UserId))]
        public User User { get; set; }

        [ForeignKey(nameof(UserRole.RoleId))]
        public Role Role { get; set; }
    }
}
```
**QPANC.Domain/Identity/UserClaim.cs**
```cs
using Microsoft.AspNetCore.Identity;
using System;
using System.ComponentModel.DataAnnotations.Schema;

namespace QPANC.Domain.Identity
{
    public class UserClaim : IdentityUserClaim<Guid>, Interfaces.IEntity
    {
        #region IEntity interface
        public bool IsDeleted { get; set; }
        public DateTimeOffset CreatedAt { get; set; }
        public DateTimeOffset? DeletedAt { get; set; }
        public DateTimeOffset? UpsertedAt { get; set; }
        public Guid? UserSessionId { get; set; }
        #endregion

        [ForeignKey(nameof(UserClaim.UserId))]
        public User User { get; set; }
    }
}
```
**QPANC.Domain/Identity/UserToken.cs**
```cs
using Microsoft.AspNetCore.Identity;
using System;
using System.ComponentModel.DataAnnotations.Schema;

namespace QPANC.Domain.Identity
{
    public class UserToken : IdentityUserToken<Guid>, Interfaces.IEntity
    {
        #region IEntity interface
        public bool IsDeleted { get; set; }
        public DateTimeOffset CreatedAt { get; set; }
        public DateTimeOffset? DeletedAt { get; set; }
        public DateTimeOffset? UpsertedAt { get; set; }
        public Guid? UserSessionId { get; set; }
        #endregion

        [ForeignKey(nameof(UserToken.UserId))]
        public User User { get; set; }
    }
}
```

O motivo para estamos sobrescrevendo as classes do Identity, é para que possamos incrementar elas com propriedades personalizadas, por exemplo, o `IdentityUser` não possui as propriedades `FirstName` e `LastName`.

O `Identity` não possui uma classe Session, mas iremos precisar utilizar esta entidade para determinar que tokens foram criados pela aplicação e estão ativos.

###7.5 Configurando as Entidades

Por mais que possamos configurar todas as entidades no `OnModelCreating` do `DbContext`, o `DbContext` pode acabar se tornando um monólito com centenas de linhas.

Para evitar que isto ocorra, iremos criar um `IEntityTypeConfiguration` para cada entidade que precise ser configura, ou que necessite de um processo `seed` à ser executado durante o `migrations`.

**QPANC.Domain/Configuration/Role.cs**
```cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace QPANC.Domain.Configuration
{
    public class Role : IEntityTypeConfiguration<Identity.Role>
    {
        public void Configure(EntityTypeBuilder<Identity.Role> entity)
        {
            entity.HasMany(x => x.Users)
                .WithOne(x => x.Role)
                .HasForeignKey(x => x.RoleId)
                .HasPrincipalKey(x => x.Id)
                .OnDelete(DeleteBehavior.Cascade);

            entity.HasMany(x => x.Claims)
                .WithOne(x => x.Role)
                .HasForeignKey(x => x.RoleId)
                .HasPrincipalKey(x => x.Id)
                .OnDelete(DeleteBehavior.Cascade);
        }
    }
}
```
**QPANC.Domain/Configuration/User.cs**
```cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace QPANC.Domain.Configuration
{
    public class User : IEntityTypeConfiguration<Identity.Role>
    {
        public void Configure(EntityTypeBuilder<Identity.Role> entity)
        {
            entity.HasMany(x => x.Users)
                .WithOne(x => x.Role)
                .HasForeignKey(x => x.RoleId)
                .HasPrincipalKey(x => x.Id)
                .OnDelete(DeleteBehavior.Cascade);

            entity.HasMany(x => x.Claims)
                .WithOne(x => x.Role)
                .HasForeignKey(x => x.RoleId)
                .HasPrincipalKey(x => x.Id)
                .OnDelete(DeleteBehavior.Cascade);
        }
    }
}
```

####7.5.1 Seed

Antes que me pergunte, caso precise inserir algum dado nas tabelas durante a criação delas, pode usar o método `entity.HasData`, como por exemplo:

```cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace QPANC.Domain.Configuration
{
    public class Sample : IEntityTypeConfiguration<Model.Sample>
    {
        public void Configure(EntityTypeBuilder<Model.Sample> entity)
        {
            /*...*/
            this.Seed(entity);
        }

        public void Seed(EntityTypeBuilder<Model.Sample> entity)
        {
            entity.HasData(
                new Model.Sample { Id = 1, Description = "Sample 01" },
                new Model.Sample { Id = 2, Description = "Sample 02" },
                new Model.Sample { Id = 3, Description = "Sample 03" }
            );
        }
    }
}
```

####7.5.2 Reuso e Limitações.

Note que, todas as nossas classes herdão de `Interfaces.IEntity`, e algumas propriedades definidas no `IEntity` precisam ser mapeadas, desta forma teríamos quer criar uma classe abstrata que configure as propriedades definidas em ``Interfaces.IEntity``, algo como:

```cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace QPANC.Domain.Configuration
{
    public abstract class EntityConfiguration<T> : IEntityTypeConfiguration<T> where T : class, Interfaces.IEntity
    {
        public void Configure(EntityTypeBuilder<T> entity)
        {
            entity.HasQueryFilter(x => !x.IsDeleted);
            entity.Property(x => x.DeletedAt).HasColumnType("timestamp with time zone");
            entity.Property(x => x.CreatedAt).HasColumnType("timestamp with time zone");
            entity.Property(x => x.UpsertedAt).HasColumnType("timestamp with time zone");
        }
    }
}
```
Então, criar uma classe de configuração para todas as classes que implementam o `Interfaces.IEntity`, mesmo que esta classe não precise de nenhuma atenção adicional.
```cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace QPANC.Domain.Configuration
{
    public class Sample : EntityConfiguration<Model.Sample>
    {
        public void Configure(EntityTypeBuilder<Model.Sample> entity)
        {
            base.Configure(entity);
        }
    }
}
```

Mesmo assim, ainda enfrentaríamos a limitação de não podemos repetir a estrategias acima para múltiplas interfaces, já que o C# não suporta herança múltipla.

###7.6 Extensões

Para contornar a limitação acima citada, podemos criar uma extensão que irá configurar as classes, pelo menos no tocante as suas Interfaces, então crie a pasta `Extensions` e adicione a classe estática `ModelBuilderExtensions`

**QPANC.Domain/Extensions/ModelBuilderExtensions.cs**
```cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System;
using System.Linq;
using System.Reflection;

namespace QPANC.Domain.Extensions
{
    public static class ModelBuilderExtensions
    {
        public static void Entities<T>(this ModelBuilder builder, DbContext instance, string methodName)
        {
            var method = instance.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
            var typesStatus = builder.Model.GetEntityTypes().Where(type => typeof(T).IsAssignableFrom(type.ClrType));
            foreach (var type in typesStatus)
            {
                var builderType = typeof(EntityTypeBuilder<>).MakeGenericType(type.ClrType);
                var buildMethod = method.MakeGenericMethod(type.ClrType);
                var buildAction = typeof(Action<>).MakeGenericType(builderType);
                var buildDelegate = Delegate.CreateDelegate(buildAction, instance, buildMethod);
                var buildEntity = typeof(ModelBuilder).GetMethods()
                    .Single(m => m.Name == "Entity" && m.GetGenericArguments().Any() && m.GetParameters().Any())
                    .MakeGenericMethod(type.ClrType);

                buildEntity.Invoke(builder, new[] { buildDelegate });
            }
        }
    }
}
```

###7.7 DbContext

Agora que já definimos a estrutura dos serviços, modelamos as entidades e implementamos as extensões necessárias, podemos criar o nosso DbContext, que na pratica irá modelar o Banco de Dados e permitir que acessemos o mesmo.

Crie a classe `QpancContext` na raiz do projeto `QPANC.Domain`

```cs
using EntityFrameworkCore.Triggers;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using QPANC.Domain.Extensions;
using QPANC.Domain.Interfaces;
using QPANC.Services.Abstract;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace QPANC.Domain
{
    public class QpancContext : IdentityDbContext<Identity.User, Identity.Role, Guid, Identity.UserClaim, Identity.UserRole, Identity.UserLogin, Identity.RoleClaim, Identity.UserToken>
    {
        private IConnectionStrings _connStrings;
        private ILoggedUser _loggedUser;

        public QpancContext(IConnectionStrings connStrings, ILoggedUser loggedUser)
        {
            this._connStrings = connStrings;
            this._loggedUser = loggedUser;
        }

        public QpancContext(DbContextOptions<QpancContext> options, IConnectionStrings connStrings, ILoggedUser loggedUser) : base(options)
        {
            this._connStrings = connStrings;
            this._loggedUser = loggedUser;
        }

        public DbSet<Identity.Session> Sessions { get; set; }

        #region EntityFrameworkCore.Triggers extensions
        public override Int32 SaveChanges()
        {
            return this.SaveChangesWithTriggers(base.SaveChanges, acceptAllChangesOnSuccess: true);
        }
        public override Int32 SaveChanges(Boolean acceptAllChangesOnSuccess)
        {
            return this.SaveChangesWithTriggers(base.SaveChanges, acceptAllChangesOnSuccess);
        }
        public override Task<Int32> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            return this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, acceptAllChangesOnSuccess: true, cancellationToken: cancellationToken);
        }
        public override Task<Int32> SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
        {
            return this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, acceptAllChangesOnSuccess, cancellationToken);
        }
        #endregion

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.ApplyConfiguration(new Configuration.User());
            modelBuilder.ApplyConfiguration(new Configuration.Role());

            modelBuilder.Entities<IEntity>(this, nameof(this.ModelEntity));
        }

        private void ModelEntity<TEntity>(EntityTypeBuilder<TEntity> entity) where TEntity : class, IEntity
        {
            var keys = entity.Metadata.FindPrimaryKey().Properties.Select(p => p.Name).ToList();
            keys.Insert(0, "IsDeleted");
            entity.HasIndex(keys.ToArray()).IsUnique();

            entity.HasQueryFilter(x => !x.IsDeleted);
            entity.Property(x => x.DeletedAt).HasColumnType("timestamp with time zone");
            entity.Property(x => x.CreatedAt).HasColumnType("timestamp with time zone");
            entity.Property(x => x.UpsertedAt).HasColumnType("timestamp with time zone");

            Triggers<TEntity>.Inserting += entry =>
            {
                var context = entry.Context as QpancContext;
                entry.Entity.UserSessionId = context._loggedUser.SessionId;
                entry.Entity.CreatedAt = DateTimeOffset.Now;
                entry.Entity.UpsertedAt = DateTimeOffset.Now;
                entry.Entity.IsDeleted = false;
            };

            Triggers<TEntity>.Updating += entry =>
            {
                var context = entry.Context as QpancContext;
                entry.Entity.UserSessionId = context._loggedUser.SessionId;
                entry.Entity.UpsertedAt = DateTimeOffset.Now;
            };

            Triggers<TEntity>.Deleting += entry =>
            {
                var context = entry.Context as QpancContext;
                entry.Cancel = true;
                entry.Entity.UserSessionId = context._loggedUser.SessionId;
                entry.Entity.DeletedAt = DateTimeOffset.Now;
                entry.Entity.IsDeleted = true;
            };
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseNpgsql(this._connStrings.DefaultConnection, options => {
                    options.MigrationsAssembly("QPANC.Domain");
                });
            }
        }
    }
}

```

Estamos utilizando o `EntityFrameworkCore.Triggers` para criar `Triggers` em nosso código, desta forma, toda vez que uma entidade for criada, atualiza ou apagada, as propriedades `IsDeleted`, `CreatedAt`, `UpsertedAt`, `DeletedAt`, `UserSessionId` serão atualizadas de acordo.

Outro aspecto importante, é que adicionamos um Filtro Global, para que todas as consultas do sistema tragam apenas as entidades que não foram apagadas (usando Soft Delete).

####7.7.1 Sugestão para Otimização

Alternativamente, ao invés de criar um índice usando a coluna `IsDeleted` e a respectiva Chave Primaria, podemos particionar as tabelas pela coluna `IsDeleted`, desta forma, como todas as consultas estão filtrando por `IsDeleted = false`, então a partição que contem os arquivos consultados será ignorada.

####7.7.2 Sistema com Multiplos Tenants

Caso o sistema possua múltiplos tenants, devemos incluir o TenantId na interface `ILoggedUser` e `IEntity` e modificar o filtro global para:

```js
var keys = entity.Metadata.FindPrimaryKey().Properties.Select(p => p.Name).ToList();
keys.Insert(0, "TenantId");
keys.Insert(0, "IsDeleted");
entity.HasIndex(keys.ToArray()).IsUnique();

entity.HasQueryFilter(x => !x.IsDeleted && x.TenantId == this._loggedUser.TenantId);
```

Neste caso, aplicar o particionamento das tabelas pelo `IsDeleted` e então pelo `TenantId` será bastante benéfico, porém, sempre que um novo Tenant for criado, terá de configurar as partições para o seu `TenantId`.

###7.8 Project ScreenShot

![Project ScreenShot](https://dev-to-uploads.s3.amazonaws.com/i/4s5ucju0xniutjljcztt.png)

##8 DbContext e Migrations

Antes de continuamos, precisamos implementar a interface `ILoggedUser`, já que o `QpancContext` depende dela. crie à classe `LoggedUser` no projeto.

**QPANC.Services/LoggedUser.cs**
```cs
using QPANC.Services.Abstract;
using System;

namespace QPANC.Services
{
    public class LoggedUser : ILoggedUser
    {
        public Guid? SessionId { get; } = default;
        public Guid? UserId { get; } = default;
    }
}
```

Agora iremos registrar o serviço `LoggedUser` no `Startup` do `QPANC.Api`, assim como o `QpancContext` e demais serviços relacionados.

Note que a implementação atual do `LoggedUser` não nos é útil, iremos alterar à sua implementação após configurar o processo de Autenticação e Autorização.

**QPANC.Api/Startup.cs**
```cs
...
using EntityFrameworkCore.Triggers;
using Microsoft.AspNetCore.Identity;
using QPANC.Domain;
using QPANC.Domain.Identity;

namespace QPANC.Api
{
    public class Startup
    {
        ...
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddAppSettings();
            services.AddScoped<ILoggedUser, LoggedUser>();
            services.AddDbContext<QpancContext>();
            services.AddIdentity<User, Role>()
               .AddEntityFrameworkStores<QpancContext>()
               .AddDefaultTokenProviders();
            services.AddTriggers();
        }
        ...
    }
}
```

Para podemos criar os arquivos para o Migrations, precisamos adicionar o seguinte pacote ao projeto `QPANC.Api`

```
cd QPANC.Api
dotnet add package Microsoft.EntityFrameworkCore.Design
```

Agora adicione a string de conexão ao arquivo `appsettings.Development.json`

```json
{
  ...
  "DEFAULT_CONNECTION": "Server=qpanc.database;Port=5432;Database=postgres;User Id=postgres;Password=keepitsupersecret;"
}
```

Feito isto, abra o terminal na pasta do projeto `QPANC.Api`, então execute o seguinte comando.:

```
cd QPANC.Api
dotnet ef migrations add -s "../QPANC.Api" -p "../QPANC.Domain" -c "QpancContext" Initial
```

Se tudo ocorreu como esperado, deve ter surgido uma pasta `Migrations` no projeto `QPANC.Domain`, assim como um arquivo `${timestamp}_Initial.cs`.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/hvdg12g8thq9l0c1vxub.png)

##9 Realizando o seed do banco e dados

> Note que, nem todo o processo de `seed` precisa ser implementado como um serviço, apenas aqueles que dependem de algum outro serviço, ou que precisem ser executados de forma rotineira.

Agora que o banco de dados está estruturado, precisamos inserir os registros necessários para que a aplicação funcione, mas antes, iremos escrever um serviço para gerar as Guid sequenciais, que serão utilizados por toda a aplicação, o motivo para criamos um serviço para isto, é para que seja fácil alternar entre as diferentes estrategias (PostgreSQL ou SqlServer).

instale o pacote `RT.Comb` no projeto `./QPANC.Services`:
```
cd QPANC.Services
dotnet add package RT.Comb
```

então crie a seguinte interface e a sua respectiva implementação:

**./QPANC.Services.Abstract/ISGuid.cs**
```cs
using System;

namespace QPANC.Services.Abstract
{
    public interface ISGuid
    {
        Guid NewGuid();
    }
}
```
**./QPANC.Services/Guid.cs**
```cs
using QPANC.Services.Abstract;
using System;

namespace QPANC.Services
{
    public class SGuid : ISGuid
    {
        public Guid NewGuid()
        {
            return RT.Comb.Provider.PostgreSql.Create();
        }
    }
}
```
**./QPANC.Services.Abstract/ISeed.cs**
```cs
namespace QPANC.Services.Abstract
{
    public interface ISeeder
    {
        Task Execute();
    }
}
```
**./QPANC.Services/Seed.cs**
```cs
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using QPANC.Domain;
using QPANC.Domain.Identity;
using QPANC.Services.Abstract;
using System.Threading.Tasks;

namespace QPANC.Services
{
    public class Seeder : ISeeder
    {
        private ISGuid _sGuid;
        private QpancContext _context;
        private UserManager<User> _userManager;
        private RoleManager<Role> _roleManager;
        public Seed(ISGuid sGuid, QpancContext context, UserManager<User> userManager, RoleManager<Role> roleManager)
        {
            this._sGuid = sGuid;
            this._context = context;
            this._userManager = userManager;
            this._roleManager = roleManager;
        }

        public async Task Execute()
        {
            await this._context.Database.MigrateAsync();
            await this.CreateRolesAndDevUser();
        }

        private async Task CreateRolesAndDevUser()
        {
            var roleNames = new string[] { "User", "Manager", "Admin", "Developer" };
            foreach (var roleName in roleNames)
            {
                var roleExists = await this._roleManager.RoleExistsAsync(roleName.ToUpperInvariant());
                if (!roleExists)
                {
                    var role = new Role(roleName)
                    {
                        Id = this._sGuid.NewGuid()
                    };
                    await this._roleManager.CreateAsync(role);
                }
            }

            var developer = "developer@qpanc.app";
            var user = await this._userManager.FindByNameAsync(developer);
            if (user == default)
            {
                user = new User(developer)
                {
                    Id = this._sGuid.NewGuid(),
                    Email = developer,
                    EmailConfirmed = true
                };
                await this._userManager.CreateAsync(user, "KeepItSuperSecret$512");
            }

            roleNames = new string[] { "Developer" };
            foreach (var roleName in roleNames)
            {
                var inInRole = await this._userManager.IsInRoleAsync(user, roleName);
                if (!inInRole)
                {
                    await this._userManager.AddToRoleAsync(user, roleName);
                }
            }
        }
    }
}
```

Agora, precisamos registrar os serviços e chamar o serviço responsável pelo seed, abra o arquivo `./QPANC.Api/Startup.cs` e adicione as seguintes linhas.:

```cs
namespace QPANC.Api
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<ISGuid, SGuid>();
            services.AddScoped<ISeeder, Seeder>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISeeder seeder)
        {
            seeder.Execute().GetAwaiter().GetResult();
        }
    }
}
```

Agora só nos resta iniciar a aplicação, e verificar se o usuário e as roles foram inseridas.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/nn6uxr12tnzgj59ci2wq.png)