Blazor -  CRUD básico usando Dapper - I

Hoje veremos como realizar um CRUD básico usando o micro ORM Dapper em uma aplicação Blazor.

Se você esta chegando agora e não sabe o que é o Blazor leia o artigo ASP .NET Core - Iniciando com o Blazor - Macoratti; se você já conhece e quer saber mais pode fazer o curso de Blazor Essencial.  

Hoje teremos uma aplicação bem simples que realiza o CRUD básico usando Dapper em um projeto Blazor Server.

Neste projeto iremos acessar o banco de dados CadastroDB criado no SQL Server usando o script:

USE [master]
GO

CREATE DATABASE [CadastroDB];
GO

Também criamos a tabela Produtos usando o seguinte script SQL :

USE [CadastroDB]
GO
CREATE TABLE [dbo].[Produtos](
	[ProdutoId] [int] IDENTITY(1,1) NOT NULL,
	[Estoque] [int] NOT NULL,
	[Nome] [nvarchar](100) NOT NULL,
	[Preco] [decimal](18, 2) NOT NULL,
	[Descricao] [nvarchar](250) NULL,
	[Imagem] [nvarchar](250) NULL,
 CONSTRAINT [PK_Produtos] PRIMARY KEY CLUSTERED([ProdutoId] ASC);
GO

A seguir temos a estrutura da tabela Produtos no SQL Server :

E os dados que incluimos na tabela para realizar os testes no projeto:

Recursos usados:

Criando o projeto Blazor no VS Community 2019

Abra o VS 2019 Community (versão mínima 16.5) e selecione a opção Create a New Project;

A seguir selecione a opção Blazor app e clique em next;

Informe o nome do projeto :  Blazor_Produtos, a localização e clique em Create;

A seguir teremos uma janela com duas opções :

  1. Blazor Server App
  2. Blazor WebAssembly App

Selecione a primeira opção - Blazor Server  App e clique em Create.

Ajustando o projeto criado

Será criado um projeto padrão que vamos ajustar da seguinte forma:

  • Na pasta Pages vamos remover os arquivos FetchData.razor;
  • Na pasta Pages vamos alterar o nome do arquivo Counter.razor para Sobre.razor;
  • Na pasta Shared vamos remover o arquivo SurveyPrompt.razor;
  • Na pasta Data vamos remover os dois arquivos existentes;
  • No arquivo Startup em ConfigureServices remova a referência ao SurveyPrompt e o namespace relativo à pasta Data;

Na pasta wwwroot vamos criar a pasta images e nesta pasta vamos incluir as imagens que iremos usar em nosso projeto : basicloader.gif, notfound.jpg, loja.jpg e maco1b.jpg;

No arquivo App.razor vamos alterar o código para exibir a imagem notfound.jpg quando uma rota não for encontrada:

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <img src="/images/notfound.jpg" />
        </LayoutView>
    </NotFound>
</Router>

O arquivo NavMenu é onde definimos os links para o menu da aplicação. Vamos alterar o conteúdo deste arquivo conforme abaixo:

<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">Blazor_CalculaIdade</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-calculator" aria-hidden="true"></span>Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="produtos">
                <span class="oi oi-book" aria-hidden="true"></span> Produtos
            </NavLink>
        </li>
    </ul>
</div>
...

Alteramos os links do menu para exibir os links com os textos: Home e Produtos

Vamos agora incluir as referências ao Dapper e ao System.Data.SqlClient no projeto.

Podemos fazer isso usando o Gerenciador de pacotes do NuGet via menu Tools e na guia Browse selecionar o pacote e clicar no botão para instalar:

ou o Console do Gerenciador de Pacotes usando o comando Install-Package <nome_pacote>:

Install-Package System.Data.SqlClient -Version 4.8.1

Criando a camada de acesso aos dados com Dapper

Vamos criar a camada de acesso a dados usando Dapper na pasta Data.

Para isso vamos criar a interface IDapperDAL definindo as operações básicas para obter, incluir, atualizar e executar procedimentos :

using Dapper;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;

namespace BlazorProdutos.Data
{
    public interface IDapperDAL
    {
        DbConnection GetConnection();
        T Get<T>(string sp, DynamicParameters parms, CommandType commandType = CommandType.StoredProcedure);
        List<T> GetAll<T>(string sp, DynamicParameters parms, CommandType commandType = CommandType.StoredProcedure);
        int Execute(string sp, DynamicParameters parms, CommandType commandType = CommandType.StoredProcedure);
        T Insert<T>(string sp, DynamicParameters parms, CommandType commandType = CommandType.StoredProcedure);
        T Update<T>(string sp, DynamicParameters parms, CommandType commandType = CommandType.StoredProcedure);
    }
}

Agora vamos criar a classe DapperDAL que implementa esta interface e define os métodos para realizar as operações de acesso ao banco de dados e realizar o CRUD:

using Dapper;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;

namespace BlazorProdutos.Data
{
    public class DapperDAL : IDapperDAL
    {
        private readonly IConfiguration _config;
        private readonly string _conn;
        public DapperDAL(IConfiguration config)
        {
            _config = config;
            _conn = _config.GetConnectionString("DefaultConnection");
        }

        public DbConnection GetConnection()
        {
            return new SqlConnection(_conn);
        }

        public T Get<T>(string sp, DynamicParameters parms, CommandType commandType = CommandType.StoredProcedure)
        {
            using IDbConnection db = new SqlConnection(_conn);
            return db.Query<T>(sp, parms, commandType: commandType).FirstOrDefault();
        }
        public List<T> GetAll<T>(string sp, DynamicParameters parms, CommandType commandType = CommandType.StoredProcedure)
        {
            using IDbConnection db = new SqlConnection(_conn);
            return db.Query<T>(sp, parms, commandType: commandType).ToList();
        }
        public int Execute(string sp, DynamicParameters parms, CommandType commandType = CommandType.StoredProcedure)
        {
            using IDbConnection db = new SqlConnection(_conn);
            return db.Execute(sp, parms, commandType: commandType);
        }
        public T Insert<T>(string sp, DynamicParameters parms, CommandType commandType = CommandType.StoredProcedure)
        {
            T result;
            using IDbConnection db = new SqlConnection(_conn);
            try
            {
              if (db.State == ConnectionState.Closed)
                    db.Open();
              using var tran = db.BeginTransaction();
              try
              {
                result = db.Query<T>(sp, parms, commandType: commandType, transaction: tran).FirstOrDefault();
                    tran.Commit();
               }
               catch (Exception ex)
               {
                   tran.Rollback();
                   throw ex;
               }
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                if (db.State == ConnectionState.Open)
                    db.Close();
            }
            return result;
        }
        public T Update<T>(string sp, DynamicParameters parms, CommandType commandType = CommandType.StoredProcedure)
        {
            T result;
            using IDbConnection db = new SqlConnection(_conn);
            try
            {
              if (db.State == ConnectionState.Closed)
                 db.Open();
              using var tran = db.BeginTransaction();
              try
              {
                result = db.Query<T>(sp, parms, commandType: commandType, transaction: tran).FirstOrDefault();
                   tran.Commit();
              }
              catch (Exception ex)
              {
                  tran.Rollback();
                  throw ex;
              }
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                if (db.State == ConnectionState.Open)
                    db.Close();
            }
            return result;
        }
        public void Dispose()
        {
            throw new NotImplementedException();
        }
    }
}

Note que fizemos uma implementação genérica que iremos usar quando criamos o serviço para os produtos.

Vamos criar a classe SqlConnectionConfiguration que vai retornar a string de conexão que iremos definir no arquivo appsettings.json :

public class SqlConnectionConfiguration
{
   public SqlConnectionConfiguration(string value) => Value = value;
   public string Value { get; }
}

Criando o modelo de domínio e o serviço

Na pasta Data do projeto vamos criar a classe Produto que representa um produto:

    public class Produto
    {
        public int ProdutoId { get; set; }
        public string Nome { get; set; }
        public string Descricao { get; set; }
        public string Imagem { get; set; }
        public decimal Preco { get; set; }
        public int Estoque { get; set; }
    }

Agora vamos criar, na pasta Data, a interface IProdutoService onde vamos definir o contrato para gerenciar as informações do produto:

using System.Collections.Generic;
using System.Threading.Tasks;
namespace BlazorProdutos.Data
{
    public interface IProdutoService
    {
        Task<int> Create(Produto produto);
        Task<int> Delete(int Id);
        Task<int> Update(Produto produto);
        Task<Produto> GetById(int Id);
        Task<List<Produto>> ListAll();
    }
}

A seguir vamos criar a classe ProdutoService que vai implementar esta interface e usar a nossa camada de acesso a dados para realizar as operações com o banco de dados SQL Server:

using Dapper;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;

namespace BlazorProdutos.Data
{
    public class ProdutoService : IProdutoService
    {
        private readonly IDapperDAL _dapperDal;
        public ProdutoService(IDapperDAL dapperDal)
        {
            this._dapperDal = dapperDal;
        }

        public Task<int> Create(Produto produto)
        {
            var dbPara = new DynamicParameters();
            dbPara.Add("Nome", produto.Nome, DbType.String);
            dbPara.Add("Descricao", produto.Descricao, DbType.String);
            dbPara.Add("Imagem", produto.Imagem, DbType.String);
            dbPara.Add("Preco", produto.Preco, DbType.Decimal);
            dbPara.Add("Estoque", produto.Estoque, DbType.Int32);

            var produtoId = Task.FromResult(_dapperDal.Insert<int>("[dbo].[SP_Novo_Produto]",
                            dbPara,
                            commandType: CommandType.StoredProcedure));

            return produtoId;
        }

        public Task<Produto> GetById(int id)
        {
            var produto = Task.FromResult(_dapperDal.Get<Produto>($"select * from [Produtos] where ProdutoId = {id}", null, commandType: CommandType.Text));
            return produto;
        }

        public Task<int> Delete(int id)
        {
            var deleteProduto = Task.FromResult(_dapperDal.Execute($"Delete [Produtos] where ProdutoId = {id}", null, commandType: CommandType.Text));
            return deleteProduto;
        }

        public Task<List<Produto>> ListAll()
        {
            var produtos = Task.FromResult(_dapperDal.GetAll<Produto>("select * from [Produtos]", null, commandType: CommandType.Text));
            return produtos;
        }

        public Task<int> Update(Produto produto)
        {
            var dbPara = new DynamicParameters();
            dbPara.Add("ProdutoId", produto.ProdutoId);
            dbPara.Add("Nome", produto.Nome, DbType.String);
            dbPara.Add("Descricao", produto.Descricao, DbType.String);
            dbPara.Add("Imagem", produto.Imagem, DbType.String);
            dbPara.Add("Preco", produto.Preco, DbType.Decimal);
            dbPara.Add("Estoque", produto.Estoque, DbType.Int32);

            var updateProduto = Task.FromResult(_dapperDal.Update<int>("[dbo].[SP_Atualiza_Produto]",
                            dbPara,
                            commandType: CommandType.StoredProcedure));
            return updateProduto;
        }
    }
}

Note que neste código estamos implementando a seleção, inclusão, exclusão e atualização de dados da seguinte forma:

  • Seleção de dados =  Usamos uma instrução "Select * from [Produtos]"
  • Exclusão de dados = Usamos uma instrução "Delete [Produtos] where ProdutoId = {id}"
  • Inclusão de dados = Usamos uma stored procedure chamada [SP_Novo_Produto]
  • Atualização de dados = Usamos uma stored procedure chamada [SP_Atualiza_Produto]

Assim teremos que criar no banco de dados as stored procedures usadas.

1- Código para criar a stored Procedure SP_Novo_Produto

Use CadastroDB
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE SP_Novo_Produto
    @Nome NVARCHAR(100),  
    @Descricao NVARCHAR(250),  
    @Imagem NVARCHAR(250),  
    @Preco Decimal(18,2),
    @Estoque INT
AS
BEGIN
	SET NOCOUNT ON;
	 INSERT INTO dbo.Produtos(Nome,Descricao,Imagem,Preco,Estoque)  
     VALUES (@Nome,@Descricao,@Imagem,@Preco,@Estoque)         
END
GO

2- Código para criar a stored Procedure SP_Atualiza_Produto

Use CadastroDB
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE SP_Atualiza_Produto
  @ProdutoId INT,
  @Nome NVARCHAR(100),
  @Descricao NVARCHAR(250),
  @Imagem NVARCHAR(250),
  @Preco Decimal(18,2),
  @Estoque INT
AS
BEGIN
UPDATE dbo.Produtos SET Nome = @Nome, Descricao = @Descricao, Imagem = @Imagem, Preco = 
           @Preco, Estoque = @Estoque Where ProdutoId = @ProdutoId
END
GO

Abaixo vemos o banco de dados e a tabela Produtos e os procedimentos armazenados criados:

Registrando os serviços e definindo a string de conexão

Precisamos agora registrar os serviços criados no método ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
            services.AddRazorPages();
            services.AddServerSideBlazor();

            services.AddScoped<IDapperDAL, DapperDAL>();
            services.AddScoped<IProdutoService, ProdutoService>();

            var sqlConnectionConfiguration = new SqlConnectionConfiguration(Configuration.GetConnectionString("SqlDbContext"));
            services.AddSingleton(sqlConnectionConfiguration);

}

Finalmente vamos definir a string de conexão com o banco de dados no arquivo appsettings.json :

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=Macoratti;Initial Catalog=CadastroDB;Integrated Security=True"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

Atenção essa string de conexão é usada no meu ambiente. A sua string vai ser diferente.

Na segunda parte do artigo iremos criar os componentes para gerenciar e exibir as informações dos produtos.

"Quem tem o Filho tem a vida; quem não tem o Filho de Deus não tem a vida."
1 João 5:12


Referências:


José Carlos Macoratti