QPANC são as iniciais de Quasar PostgreSQL ASP NET Core.
- Source
- Introdução
- Parte I - ASP.NET - Inicializando os Projetos
- Parte 2 - PostgreSQL
- Parte 3 - ASP.NET - Registrando Serviços e Lendo Variáveis de Ambiente
- Parte 4 - ASP.NET - Entity Framework e ASP.NET Core Identity
- Parte 5 - ASP.NET - Documentação Interativa com Swagger
- Parte 6 - ASP.NET - Regionalização
- Parte 7 - ASP.NET - Autenticação e Autorização
- Parte 8 - ASP.NET - CORS
- Parte 9 - Quasar - Criação e Configuração do Projeto
- Parte 10 - Quasar - Configurações e Customizações
- Parte 11 - Quasar - Componentes - Diferença entre SPA e SSR
- Parte 12 - Quasar - Serviços
- Parte 13 - Quasar - Regionalização e Stores
- Parte 14 - Quasar - Consumindo a API
- Parte 15 - Quasar - Login
- Parte 16 - Quasar - Áreas Protegidas
- Parte 17 - Quasar - Registro
- Parte 18 - Docker - Maquina Virtual Linux
- Parte 19 - Docker - Registro e Build
- Parte 20 - Docker - Traefik e Publicação
- Demo Online
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
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
.
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.
Top comments (2)
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
Obrigado por mencionar, tenho o ef instalado à tanto tempo, que esqueci deste detalhe.