Identity -  Crud integrado


  Hoje veremos como fazer o CRUD e uma entidade Aluno integrando a entidade com o Identity.

O cenário é o seguinte :

"Você deseja criar uma aplicação WEB para gerenciar informações de alunos usando a ASP.NET Core , o EF Core e os recursos do Identity de forma integrada."

Assim você vai cadastrar, alterar e excluir alunos em uma tabela Alunos e vai também incluir o aluno nas tabela do Identity.

Criando o projeto Web

No VS 2022 Community crie um novo projeto ASP.NET Core Web App (Model-View-Controller) chamado AlunosWebMvc e a seguir instale os seguintes pacotes:

1. Microsoft.AspNetCore.Identity
2. Microsoft.AspNetCore.Identity.EntityFrameworkCore
3. Microsoft.EntityFrameworkCore.Design
4. Microsoft.EntityFrameworkCore.SqlServer


Instale também a ferramenta EF Core Tools no seu ambiente usando o seguinte comando :
dotnet tool install --global dotnet-ef

No projeto criado inclua uma pasta Entities e nesta pasta crie a classe Aluno que herda de IdentityUser:

public class Aluno : IdentityUser

  [MaxLength(100)]
 
public string? Nome { get; set; }
  [MaxLength(20)]
 
public string? Sexo { get; set; }
 
public int Idade { get; set; }
  [MaxLength(100)]
 
public string? Curso { get; set; }
}

A tabela "AspNetUsers" já é criada automaticamente pelo Identity quando você configura a autenticação em sua aplicação. A ideia de estender a classe "IdentityUser" é adicionar suas próprias propriedades personalizadas à tabela "AspNetUsers", que será usada para armazenar informações do usuário, além das informações padrão fornecidas pelo Identity, como o email, nome de usuário e senha.

A seguir crie uma pasta Context e nesta pasta crie o arquivo AppDbContext:

public class AppDbContext : IdentityDbContext<Aluno>
{
 
// construtor
  
public AppDbContext(DbContextOptions<AppDbContext> options)
   :
base(options){}

   protected override void OnModelCreating(ModelBuilder builder)
   {
   
 base.OnModelCreating(builder);
     builder.Entity<Aluno>()
       .HasKey(u => u.Id);
 
     builder.Entity<Aluno>()
        .ToTable(
"AspNetUsers");
    }
}

Observe que estamos mapeando a tabela Aluno para AspNetUsers do Identity.

Configurando a conexão e o Identity

No arquivo appsetings.json inclua a string de conexão definindo o nome do banco de dados:

{
  "ConnectionStrings"
: {
   
"DefaultConnection": "Server=DESKTOP-DK57UNP\\SQLEXPRESS;Database=AlunosWebDB;Trusted_Connection=True;
      TrustServerCertificate=True;MultipleActiveResultSets=true;"

},
...

A seguir na classe Program do projeto inclua o código a seguir:

...

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

var connection = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connection));

builder.Services.AddIdentity<Aluno, IdentityRole>()
    .AddEntityFrameworkStores<AppDbContext>()
    .AddDefaultTokenProviders();
...

app.UseAuthentication();
app.UseAuthorization();
...
 


Este código é usado para configurar a injeção de dependência do ASP.NET Core para suportar a autenticação de usuários e autorização de acesso. Vamos explicar cada linha em mais detalhes:
  • var connection = builder.Configuration.GetConnectionString("DefaultConnection");:
    A primeira linha obtém a string de conexão do arquivo de configuração appsettings.json para a conexão com o banco de dados.
  • builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(connection));:
    A segunda linha registra o contexto do banco de dados no contêiner de injeção de dependência do ASP.NET Core, usando o provedor de banco de dados SQL Server e a string de conexão obtida anteriormente.
  • builder.Services.AddIdentity<Aluno, IdentityRole>():
    A terceira linha registra o serviço de autenticação e autorização no contêiner de injeção de dependência. O primeiro parâmetro do método AddIdentity especifica a classe que representa um usuário na aplicação (no caso, a classe Aluno), enquanto o segundo parâmetro especifica a classe que representa uma role de autorização (no caso, a classe IdentityRole).
  • .AddEntityFrameworkStores<AppDbContext>():
    A quarta linha configura o Identity para usar o AppDbContext como provedor de armazenamento para os dados de usuário e role.
  • .AddDefaultTokenProviders();:
    A quinta linha adiciona os provedores de token padrão do Identity, que são usados para gerenciar tokens de confirmação de email, senha de redefinição, etc.

Aplicando Migrations

Vamos aplicar o migrations para gerar o banco de dados e as tabelas.

Para isso vamos usar a ferramenta dotnet ef.

1- Para criar o script de migração abra a janela Package Manager Console e execute o comando:

dotnet ef migrations add Inicial --project AlunosWebMvc -s AlunosWebMvc --verbose

2- A seguir , após você verificar o script , execute o seguinte comando para aplicar o script de migração:

dotnet ef database update --project AlunosWebMvc --startup-project AlunosWebMvc --verbose

Com isso teremos o banco de dados e as tabelas geradas com o seguinte relacionamento no SQL Server:

 

A seguir temos o papel de cada tabela gerada:

_EFMigrationsHistory – contém todas as migrações anteriores realizadas.
AspNetRoleClaims – armazena as declarações (claims) por perfis ou roles.
AspNetRoles – armazena todas as roles
AspNetUserClaims – armazena declarações(claims) de usuários.
AspNetUserLogins – armazena o tempo de login dos usuários.
AspNetUserRoles – armazena funções de usuários.
AspNetUsers – armazena todos os usuários.
AspNetUserTokens – armazena tokens de autenticação externos.

Criando o controlador e as views

Vamos criar na pasta Controllers o controlador AlunosController com o código abaixo:

public class AlunosController : Controller
{
    private readonly AppDbContext _context;

    public AlunosController(AppDbContext context)
    {
        _context = context;
    }

    public IActionResult Index()
    {
        var alunos = _context.Users.ToList();
        return View(alunos);
    }

    public IActionResult Create()
    {
        return View();
    }

    [HttpPost]
    public async Task<IActionResult> Create(Aluno aluno)
    {
        if (ModelState.IsValid)
        {
            _context.Users.Add(aluno);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        return View(aluno);
    }

    public async Task<IActionResult> Edit(string id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var aluno = await _context.Users.FindAsync(id);
        if (aluno == null)
        {
            return NotFound();
        }
        return View(aluno);
    }

    [HttpPost]
    public async Task<IActionResult> Edit(string id, Aluno aluno)
    {
        if (id != aluno.Id)
        {
            return NotFound();
        }

        if (ModelState.IsValid)
        {
            try
            {
                _context.Update(aluno);
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!AlunoExists(aluno.Id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }
            return RedirectToAction(nameof(Index));
        }
        return View(aluno);
    }

    public async Task<IActionResult> Details(string id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var aluno = await _context.Users.FindAsync(id);
        if (aluno == null)
        {
            return NotFound();
        }

        return View(aluno);
    }

    public async Task<IActionResult> Delete(string id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var aluno = await _context.Users.FirstOrDefaultAsync(m => m.Id == id);
        if (aluno == null)
        {
            return NotFound();
        }

        return View(aluno);
    }

    [HttpPost, ActionName("Delete")]
    public async Task<IActionResult> DeleteConfirmed(string id)
    {
        var aluno = await _context.Users.FindAsync(id);
        _context.Users.Remove(aluno);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }

    private bool AlunoExists(string id)
    {
        return _context.Users.Any(e => e.Id == id);
    }
}

Agora para cada método Action basta criar uma view correspondente.  Vou mostrar o código da view Index que apresenta uma visão dos alunos cadastrados:

@model IEnumerable<Aluno>

<h2>Alunos</h2>

<p>
    <a asp-action="Create">Novo Aluno</a>
</p>

<table class="table">
    <thead>
        <tr>
            <th>@Html.DisplayNameFor(model => model.Nome)</th>
            <th>@Html.DisplayNameFor(model => model.Sexo)</th>
            <th>@Html.DisplayNameFor(model => model.Idade)</th>
            <th>@Html.DisplayNameFor(model => model.Curso)</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>@Html.DisplayFor(modelItem => item.Nome)</td>
                <td>@Html.DisplayFor(modelItem => item.Sexo)</td>
                <td>@Html.DisplayFor(modelItem => item.Idade)</td>
                <td>@Html.DisplayFor(modelItem => item.Curso)</td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Editar</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Excluir</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Detalhes</a>
                </td>
            </tr>
        }
    </tbody>
 </table>

As demais views você pode obter no código do projeto.

Executando o projeto teremos o seguinte resultado:

Pode ocorrer na edição dos dados a exceção : DbUpdateConcurrencyException : database operation expected to affect 1 row(s) but actually affected 0 row(s)

O problema provavelmente ocorre devido à otimização de concorrência do Entity Framework Core e do Identity Framework. Quando você edita os dados de um usuário usando o Identity Framework, o Entity Framework Core atualiza automaticamente o valor do ConcurrencyStamp na tabela AspNetUsers para garantir que as alterações sejam sincronizadas corretamente. Isso é necessário para impedir que um usuário edite os dados de outro usuário enquanto ambos estiverem editando os mesmos dados.

Quando você tenta salvar as alterações usando o método SaveChangesAsync, o Entity Framework Core verifica o valor do ConcurrencyStamp para garantir que o registro ainda não tenha sido alterado por outro usuário. Se o valor do ConcurrencyStamp no banco de dados for diferente do valor que você está tentando salvar, o Entity Framework Core lançará uma exceção de concorrência (ConcurrencyException).

Para resolver esse problema, você pode recarregar os dados do usuário do banco de dados antes de aplicar as alterações e salvar as alterações em uma transação separada. Isso garantirá que o ConcurrencyStamp seja atualizado corretamente e que as alterações sejam salvas sem erros de concorrência.

Adotei uma solução mais simples só para permitir a execução:

[HttpPost]
    public async Task<IActionResult> Edit(string id, Aluno aluno)
    {
        if (id != aluno.Id)
        {
            return NotFound();
        }

        var alunoDb = await _context.Users.FindAsync(id);
        if (alunoDb == null)
        {
            return NotFound();
        }

        alunoDb.Curso = aluno.Curso;
        alunoDb.Idade = aluno.Idade;
        alunoDb.Sexo = aluno.Sexo;
        alunoDb.Nome = aluno.Nome;

        if (ModelState.IsValid)
        {
            try
            {
                _context.Entry(alunoDb).State = EntityState.Modified;
                _context.Users.Update(alunoDb);
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!AlunoExists(alunoDb.Id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }
            return RedirectToAction(nameof(Index));
        }
        return View(aluno);
    }

Pegue o projeto aqui :   AlunosWebMvc.zip ...

"Todavia para nós há um só Deus, o Pai, de quem é tudo e para quem nós vivemos; e um só Senhor, Jesus Cristo, pelo qual são todas as coisas, e nós por ele."
Coríntios 8:6

Referências:


José Carlos Macoratti