Blazor  -  Acesso ao PostgreSQL com filtro de dados


  Hoje veremos como acessar o PostgreSql e implementar um filtro de dados básico.

Existem muitos componentes prontos que podemos usar para implementar um filtro de dados e neste artigo veremos uma implementação básica.


No projeto exemplo vamos usar o EF Core Code-First para acessar e criar uma base de dados no PostgreSQL e a seguir vamos popular o banco com dados iniciais e a seguir vamos implementar uma busca parcial.

Criando o projeto no VS 2022

Abra o VS 2022 e clique em New Project e a seguir selecione o template Blazor Server App e informe o nome Blazor_Search :

A seguir defina as configurações conforme abaixo e clique em Create:

Vamos limpar o projeto removendo os componentes Counter.razor, FetchData.razor, SurveyPrompt.razor e o conteúdo da pasta Data do projeto.

Temos que incluir no projeto os seguintes pacotes usando o comando : install-package <nome>

  1. Npgsql.EntityFrameworkCore.PostgreSQL
  2. Microsoft.EntityFrameworkCore.Design

O primeiro permite acessar o banco PostgreSQL e o seguinte compartilha componentes com a ferramenta que realiza o Migrations.

Agora vamos verificar se temos a ferramenta EF Core Tools instalada usando o comando em um terminal: dotnet ef

Se a ferramenta não estiver instalada o comando para instalar é o seguinte:  
dotnet
tool install --global dotnet-ef

Com a ferramenta instalada podemos prosseguir.

No arquivo Index.razor vamos definir o código abaixo que vai ser a página de apresentação da aplicação e que vai exibir apenas uma imagem:

@page "/"

<PageTitle>Index</PageTitle>

<h1>Search Example</h1>

 

No arquivo NavMenu.razor vamos ajustar o código para exibir dois itens no menu: Home e Products

..
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>

        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="products">
                <span class="oi oi-plus" aria-hidden="true"></span> Products
            </NavLink>

        </div>
    </nav>
</div>
..

Vamos incluir na pasta wwwroot do projeto a imagem Spinner-2.gif que iremos usar para indicar uma operação em andamento.

Na pasta Data do projeto vamos criar a classe  Product com o código:   

public class Product

{

  public int Id { get; set; }

  [MaxLength(100)]
 
public string? Name { get; set; }

 
public decimal Price { get; set; }
 
public int Stock { get; set; }

}

A classe Product define 4 propriedades que iremos preencher com dados iniciais para exibir no projeto.

Vamos criar a classe de contexto AppDbContext que herda de DbContext e define o mapeamento ORM entre a entidade Product e a tabela Products e onde usamos o método HasData() para preencher a tabela Products com dados iniciais:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
                                       :base(options)
    {}
    public DbSet<Product>? Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>().HasData(
            new Product
            {
                Id = 1,
                Name = "Caderno",
                Price = 7.45m,
                Stock = 10
            },
            new Product
            {
                Id = 2,
                Name = "Borracha",
                Price = 3.55m,
                Stock = 12
            },
            new Product
            {
                Id = 3,
                Name = "Estojo",
                Price = 6.99m,
                Stock = 9
            },
            new Product
            {
                Id = 4,
                Name = "Lapis",
                Price = 2.35m,
                Stock = 30
            },
            new Product
            {
                Id = 5,
                Name = "Caneta",
                Price = 5.75m,
                Stock = 20
            },
            new Product
            {
                Id = 6,
                Name = "Compasso",
                Price = 8.15m,
                Stock = 13
            },
            new Product
            {
                Id = 7,
                Name = "Caderno Brochura",
                Price = 9.33m,
                Stock = 10
            },
            new Product
            {
                Id = 8,
                Name = "Transferidor",
                Price = 5.22m,
                Stock = 10
            }
        );
    }
}

Vamos incluir no arquivo appsettings.json a string de conexão para acessar o banco de dados PostgreSql:

{
  "ConnectionStrings": {
    "Defaultconnection": "User ID=postgres;Password=***;Host=localhost;Port=5432;Database=CadastroDB;Pooling=true;"
  },
...
 

Definimos o banco de dados CadastroDB, o usuário e a senha para acessar o banco de dados.

No arquivo _Imports.razor vamos definir o código que será usado pelos componentes:

@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Blazor_Search
@using Blazor_Search.Shared
@using Blazor_Search.Data

Vamos registrar o contexto do EF Core definido no arquivo AppDbContext no container DI incluindo na classe Program o código abaixo:

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

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(connectionString));

...

Podemos aplicar o Migrations usando os comandos abaixo na janela do Package Manager Console:

- dotnet ef migrations add Inicial
- dotnet database update

O primeiro criar o arquivo de script com o código para criar as tabelas e popular as tabelas com os dados. O segundo aplicar esse script no banco de dados.

Podemos conferir os dados no PostgreSQL abrindo o pgAdmin:

Vemos a tabela Products do banco de dados CadastroDB preenchida com os dados definidos no arquivo de contexto.

Criando o Serviço para acessar os dados

Vamos criar na pasta Data o serviço que vai permitir obter os dados no PostgreSQL. Para isso vamos criar a interface IProductService e classe ProductService que implementa esta interface :

1- IProductService

public interface IProductService
{
    Task<IEnumerable<Product>> GetAllProducts();
    Task<IEnumerable<Product>> GetProductByName(string name);
}

2- ProductService

public class ProductService : IProductService
{
    protected readonly AppDbContext _context;

    public ProductService(AppDbContext context)
    {
        _context = context;
    }
    public async Task<IEnumerable<Product>> GetAllProducts()
    {
        try
        {
            var result = await _context.Products.ToListAsync();
            return result;
        }
        catch(Exception)
        {
            throw;
        }
    }
    public async Task<IEnumerable<Product>> GetProductByName(string name)
    {
        var result = await _context.Products.Where(x => x.Name.Contains(name)).ToListAsync();
        return result;
    }
}

Neste serviço implementamos dois métodos :

1- GetAllProducts() que retorna uma lista com todos os produtos

2- GetProductByName(string name) que retorna uma lista de objetos que atendem ao critério de conter parte do critério de filtro definido em name;

Criando o componente para exibir os produtos

Na pasta Shared vamos criar dois componentes de suporte que iremos usar no projeto:

1- DisplayError.razor - Exibe mensagens de erros

<h3 class="text-danger"><b>@MensagemErro</b></h3>

@code {
    [Parameter]
    public string? MensagemErro { get; set; }
}

 

2- DisplaySpinner.razor - Exibe a imagem do spinner


 <
img src="/Spinner-2.gif" />
 

Agora na pasta Pages vamos criar o componente ListProducts.razor :

@page "/products"
@inject IProductService productService

<h3>Products</h3>

<hr />
    <td><input type="text" placeholder="Product name" @bind="@produto.Name" /></td>
    <button class="btn-primary" @onclick="FilterProduct">
        Search
    </button>
<hr />

@if (Products == null && ErrorMessage == null)
{
    <DisplaySpinner/>
}
else if (ErrorMessage != null)
{
    <DisplayError MensagemErro="@ErrorMessage"></DisplayError>
}
else
{
    <table class="table">
        <tr>
            <th>Id</th>
            <th>Name</th>
            <th>Price</th>
            <th>Stock</th>
        </tr>

        @foreach (var item in @Products)
        {
            <tr>
                <td>@item.Id</td>
                <td>@item.Name</td>
                <td>@item.Price</td>
                <td>@item.Stock</td>
            </tr>
        }
    </table>

}

@code {
        IEnumerable<Product> Products { get; set; }
        public string ErrorMessage { get; set; }
   

        Product produto = new Product();
        public string productName { get; set; }

        protected async void FilterProduct()
        {
            productName = produto.Name;
            Products = await productService.GetProductByName(productName);
            StateHasChanged();
        }

        protected async override Task OnInitializedAsync()
        {
            try
            {
                  Products = await productService.GetAllProducts();
            }
            catch (Exception ex)
            {
                ErrorMessage = ex.Message;
            }
        }
}

Vamos entender o código:

No inicio estamos injetando uma dependência IProductService, que nos permitirá usar a instância do serviço e acessar os dados;

Criamos o botão com o texto para o usuário submeter o filtro usando o evento onclick e chamando o método FilterProduct.

Estamos usando os componentes DisplaySpinner e DisplayError para exibir o spinner e algum erro que ocorra.

Montamos uma tabela HTML para exibir os dados dos produtos que estão sendo obtidos no método OnInitializedAsync.

No método FilterProduct usamos o método GetProductByName invocado a partir do serviço para filtrar os dados. Neste método estamos usando o método StateHasChanged() para indicar que o estado do componente foi alterado e assim atualizar a interface do usuário.

Agora é só alegria...

Executando o projeto teremos o seguinte resultado:

Pegue o projeto aqui:  Blazor_Search.zip (sem as referências)

"E se alguém ouvir as minhas palavras, e não crer, eu não o julgo; porque eu vim, não para julgar o mundo, mas para salvar o mundo."
João 12:47

Referências:


José Carlos Macoratti