DEV Community

Tobias Mesquita for Quasar Framework Brasil

Posted on • Updated on

QPANC - Parte 4 - ASP.NET - Entity Framework e ASP.NET Core Identity

QPANC são as iniciais de Quasar PostgreSQL ASP NET Core.

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

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

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

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

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

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

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

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

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

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

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

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

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

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:

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:

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.

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

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

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:

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

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

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

...
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

{
  ...
  "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

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

using System;

namespace QPANC.Services.Abstract
{
    public interface ISGuid
    {
        Guid NewGuid();
    }
}

./QPANC.Services/Guid.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

namespace QPANC.Services.Abstract
{
    public interface ISeeder
    {
        Task Execute();
    }
}

./QPANC.Services/Seed.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.:

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

Discussion (2)

Collapse
w3web profile image
Marcelo Gondim

7.2 > a pasta correta é QPANC.Domain
7.4 > falta a class UserClaim
; a mais na linha public bool IsDeleted { get; set; }; dos arquivos QPANC.Domain/Identity/Session.cs e QPANC.Domain/Identity/User.cs
8 > o dotnet-ef não faz parte do dotnet por padrao. para instalar tem que executar dotnet tool install --global dotnet-ef

Collapse
tobymosque profile image
Tobias Mesquita Author

Obrigado por mencionar, tenho o ef instalado à tanto tempo, que esqueci deste detalhe.