Blazor Server 3: Consultas Dinâmicas no SQL Server

Acesse dinamicamente tabelas de bancos de dados SQL Server através de um frontend em Blazor Server e API .NET CORE 3.1 com funcionalidades de paginação, ordenação e filtros.

Blazor Server: Consultas Dinâmicas no SQL Server

Depois das primeiras aventuras usando Blazor, e após ter escrito os artigos Blazor: O Começo do Fim do Javascript? e Blazor: Muito mais que um WebForms de Luxo, resolvi dar um passo adiante e converter 100% uma aplicação existente.

Imagine uma tela que contém um GRID exibindo registros de uma tabela de bancos de dados SQL SERVER onde seja possível ordernar, paginar e filtrar esses registros.

Pois bem, essa aplicação vem sendo evoluída desde o WebForms, passando pelo MVC, VueJS e chegando agora no Blazor Server.

Blazor Server 3: Consultas Dinâmicas SQL Server

O frontend que utilizei é uma adaptação com bootstrap e layout criado pelo desenvolvedor fullstack Lucas Juliano.

Funcionalidades dessa versão:

– API ASP.NET CORE 3.1;
– Frontend Blazor Server;
– Múltiplas conexões com SQL Server;
– Seleção de qualquer tabela do banco de dados;
– Ordenação de uma coluna;
– Filtro de uma ou mais colunas (tipos de dados numerico ou string);
– Paginação completa;
– Uso do FSL.Framework;
– Desenvolvimento usando Design Patterns;
– Todos os projetos usam as últimas versões de seus frameworks .NET CORE 3.1 e .NET Standard 2.1;


#Blazor Server: FSL.Framework

Algumas informações importantes para quem está chegando agora. Se você é um desenvolvedor .NET mas nunca viu nada de .NET Core, sugiro você ler antes o artigo .NET Core para DesenvolvedoreS .NET.

Benchmark: ASP.NET 4.8 vs ASP.NET CORE 3.1

Para essa solução de consultas dinâmicas SQL Server, usei como base o código fonte do artigo Crie seu Framework em ASP.NET CORE 3 e Blazor.

Depois que conclui esse artigo, evolui o framework FSL.Framework criado com novas funcionalidades e melhorias.


#Blazor Server: Diagrama e .csproj

O diagrama abaixo representa todos os 6 projetos dentro da solution.

As duas aplicações web (em azul) FSL.Database.BlazorSrv a FSL.Database.Api utilizam a biblioteca de negócios FSL.Database.Core que possui funcionalidades e classes genéricas para toda a solução.

E também, essas aplicações usam como base as biobliotecas do FSL.Framework Core, Api e Web.


#Blazor Server: Workflow e endpoints

A aplicação Blazor Server irá consumir uma API feita em ASP.NET CORE 3.1 conforme já mencionado no diagrama de arquitetura.

O código fonte já foi preparado para fazer um publish e hospedar no IIS.

E também, estou utilizando um banco de dados local SQL Server Express para realizar as consultas.

Após publicação da API (endereço local http://localhost/fsl-database-api/), é possível chamá-la através do Postman.

A API possui três endpoints POST que retornam todas as tabelas de um banco de dados, todas as colunas de uma determinada tabela e todos os registros de uma tabela.

Endpoint api/database/tables:

{
    "DataSource" : ".\\SQLEXPRESS2008R2",
    "DatabaseName" : "consulta_cep",
    "User" : "sa",
    "Password" : "1234567890",
    "TableName" : "",
    "OrderBy" : "",
    "OrderTypeStr" : "Asc",
    "PerPage" : "10",
    "Columns":[]
}

Esse endpoint irá retornar uma lista de nomes de tabelas.

Endpoint api/database/columns:

{
    "DataSource" : ".\\SQLEXPRESS2008R2",
    "DatabaseName" : "consulta_cep",
    "User" : "sa",
    "Password" : "1234567890",
    "TableName" : "tb_cep", // here
    "OrderBy" : "",
    "OrderTypeStr" : "Asc",
    "PerPage" : "10"
}

Esse endpoint irá retornar uma lista de colunas, com propriedades nome da coluna e tipo de dados.

Endpoint api/database/data:

{
    "DataSource" : ".\\SQLEXPRESS2008R2",
    "DatabaseName" : "consulta_cep",
    "User" : "sa",
    "Password" : "1234567890",
    "TableName" : "tb_cep", // here
    "OrderBy" : "cod_cep", // here
    "OrderTypeStr" : "Asc", //here
    "PerPage" : "10" // here
}

Por fim o endpoint de dados, que retornará todos os registros da tabela tb_cep, ordenados pela coluna cod_cep, da página 1 com 10 registros por página.

Com a API funcionando, vamos ver agora o frontend no Blazor.


#Blazor Server: fonte da API

Depois de testado a API, vamos dar uma olhada em seu código fonte.

Para essa solução, só estão sendo utilizados as classes DatabaseController e DatabaseQuerySqlRepository.

Os três endpoints mencionados anteriormente e testados no Postman são:

    [Route("api/database")]
    [ApiController]
    public sealed class DatabaseController : ControllerBase
    {
        private readonly IDatabaseQueryRepository _databaseQueryRepository;

        public DatabaseController(
            IDatabaseQueryRepository databaseQueryRepository)
        {
            _databaseQueryRepository = databaseQueryRepository;
        }

        [HttpPost("tables")]
        public async Task<IActionResult> PostTablesAsync(
            [FromBody] Core.Models.DatabaseRequest request)
        {
            var data = await _databaseQueryRepository.GetAllTables(request);

            return Ok(data.ToResult());
        }

        [HttpPost("columns")]
        public async Task<IActionResult> PostColumnsAsync(
            [FromBody] Core.Models.DatabaseRequest request)
        {
            var data = await _databaseQueryRepository.GetAllColumnsFromTable(
                request, 
                request.TableName);

            return Ok(data.ToResult());
        }

        [HttpPost("data")]
        public async Task<IActionResult> PostDataAsync(
            [FromBody] Core.Models.DatabaseRequest request)
        {
            var data = await _databaseQueryRepository.GetDataAsync(request);
            
            return Ok(data.ToResult());
        }
    }

A classe DatabaseQuerySqlRepository implementa a inteface IDatabaseQueryRepository, retorna as informações de um banco de dados SQL Server através de parâmetros de entrada definidos na classe DatabaseRequest.

Destaque para o método que monta SELECT com paginação no SQL Server:

        private string BuildQuery(
            DatabaseRequest request)
        {
            var page = (request.Page <= 0) ? 1 : request.Page;
            var rows = (request.Rows <= 0) ? 10 : request.Rows;
            var select = BuildSelect(request);
            var where = BuildWhere(request);

            var sql = $@"
                    DECLARE @p_num_page AS INT
                    DECLARE @p_page_size AS INT
                    SET @p_num_page = {page}
                    SET @p_page_size = {rows}
                    SET @p_num_page = ((@p_num_page * @p_page_size) - @p_page_size) + 1
                    SET @p_page_size = @p_num_page + @p_page_size - 1
                    BEGIN WITH [query] AS (SELECT ROW_NUMBER() OVER (ORDER BY {request.OrderBy} {request.OrderType.ToString().ToUpper()}) AS [row_num], {select} FROM [{request.DatabaseName}].[dbo].[{request.TableName}] WITH (NOLOCK) {where}) SELECT * FROM [query] CROSS JOIN (SELECT COUNT(*) AS [total_records] FROM [query]) AS [counters] WHERE ([row_num] BETWEEN @p_num_page AND @p_page_size) END ";

            return sql;
        }

Você pode acessar todo o código fonte completo diretamente no meu github.


#Blazor Server: Frontend

Além dos componentes básicos utilizados em Blazor: Muito mais que um WebForms de Luxo, criei mais algums como Paging.razor, PageItem.razor e FieldContainer.razor.

O PageItem.razor é responsável pelos botões de paginação First, Next, Previous e Last:

@inherits PageItemComponent

<li class="page-item @IsDisabled(IsTrue)">
    <a class="page-link" 
       href="#" 
       tabindex="-1" 
       aria-disabled="true" 
       @onclick="OnPagingAsync">@Title</a>
</li>
    public class PageItemComponent : ComponentBase
    {
        [Parameter]
        public string Title { get; set; }

        [Parameter]
        public bool IsTrue { get; set; }

        [Parameter]
        public int RequestedPage { get; set; }

        [Parameter]
        public EventCallback<int> OnClickAsync { get; set; }

        protected async Task OnPagingAsync(
            EventArgs e)
        {
            await OnClickAsync.InvokeAsync(RequestedPage);
        }

        protected string IsDisabled(
            bool isOk)
        {
            return !isOk ? "disabled" : "";
        }
    }

O Paging.razor é responsável pela exibição dos botões de paginação, labels de quantidade de registros e página atual:

@inherits PagingComponent

@{ 
    var pages = Count > 0 ? Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(TotalRecords) / Convert.ToDecimal(Rows))) : 0;
    var firstPage = Page > 1;
    var nextPage = Page < pages;
    var previousPage = firstPage;
    var lastPage = nextPage;
}

<nav class="pt-4">
    <div>
        <strong>Current Page:</strong>
        <span class="badge badge-secondary">@(Page)</span>
        |
        <strong>Pages: </strong>
        <span class="badge badge-secondary">@(pages)</span>
        |
        <strong>Records:</strong>
        <span class="badge badge-secondary">@(TotalRecords)</span>
    </div>
    <ul class="pagination justify-content-end" style="margin-top:-30px">
        <PageItem Title="First" 
                  IsTrue="firstPage" 
                  RequestedPage="1" 
                  OnClickAsync="OnClickAsync"></PageItem>

        <PageItem Title="Previous" 
                  IsTrue="previousPage" 
                  RequestedPage="Page - 1" 
                  OnClickAsync="OnClickAsync"></PageItem>

        <PageItem Title="Next" 
                  IsTrue="nextPage" 
                  RequestedPage="Page + 1" 
                  OnClickAsync="OnClickAsync"></PageItem>

        <PageItem Title="Last" 
                  IsTrue="lastPage" 
                  RequestedPage="pages" 
                  OnClickAsync="OnClickAsync"></PageItem>
    </ul>
</nav>
    public class PagingComponent : ComponentBase
    {
        [Parameter]
        public int Page { get; set; }

        [Parameter]
        public int TotalRecords { get; set; }

        [Parameter]
        public int Rows { get; set; }

        [Parameter]
        public int Count { get; set; }

        [Parameter]
        public EventCallback<int> OnClickAsync { get; set; }
    }

O FieldContainer.razor é responsável pela exibição dos campos de parametrização do banco de dados:

@inherits FieldContainerComponent

<div class="col-md-@Cols mb-@Cols">
    <label class="badge badge-pill badge-dark">@Title</label>
        <div class="input-group">
            <CascadingValue Value=this>
                @ChildContent
            </CascadingValue>
    </div>
</div>
    public class FieldContainerComponent : ComponentBase
    {
        [Parameter]
        public RenderFragment ChildContent { get; set; }

        [Parameter]
        public string Title { get; set; }

        [Parameter]
        public int Cols { get; set; }
    }

E o mais importante deles, o Database.razor que é o frontend completo que engloba todos os componente anteriores.

FieldContainer.razor usado em Database.razor:

<div class="form-row pb-3 mt-2">
    <FieldContainer Cols="2" Title="Data Source">
        <TextBox Id="tbxDataSource"
                 @bind-Text="@Request.DataSource"
                 TextValueChanged="ConectionChangedAsync"></TextBox>
    </FieldContainer>
    <FieldContainer Cols="2" Title="Initial Catalog">
        <TextBox Id="tbxInitialCatalog"
                 @bind-Text="@Request.DatabaseName"
                 TextValueChanged="ConectionChangedAsync"></TextBox>
    </FieldContainer>
    <FieldContainer Cols="2" Title="User">
        <TextBox Id="tbxUser"
                 @bind-Text="@Request.User"
                 TextValueChanged="ConectionChangedAsync"></TextBox>
    </FieldContainer>
    <FieldContainer Cols="2" Title="Password">
        <TextBox Id="tbxPassword"
                 @bind-Text="@Request.Password"
                 TextMode="TextBoxMode.Password"
                 TextValueChanged="ConectionChangedAsync"></TextBox>
    </FieldContainer>
    <FieldContainer Cols="3" Title="Table Name">
        <DropDownList Id="ddlTables"
                      DataSource="@Tables"
                      SelectedValue="@Request.TableName"
                      SelectedValueChanged="@OnTableChangedAsync" />
    </FieldContainer>
    <FieldContainer Cols="1" Title="Per Page">
        <TextBox Id="tbxPerPage"
                 @bind-Text="@Request.PerPage"
                 TextMode="TextBoxMode.Number"
                 TextValueChanged="ValueChangedAsync"></TextBox>
    </FieldContainer>
</div>

Você pode acessar todo o código fonte completo de Database.razor diretamente no meu github.

Por fim, tudo isso não seria possível se não configurássemos o Startup.cs para usar o FSL.Framework e todas as injeções de dependência.

        public void ConfigureServices(
            IServiceCollection services)
        {
            services
                .AddBlazorFslFramework(Configuration) // here
                .Config(opt => // here
                {
                    opt.AddDefaultConfiguration();
                    opt.AddConfiguration<MyConfiguration>();
                    opt.AddFactoryService<DefaultFactoryService>();
                    opt.AddTransient<IApiClientProvider, HttpClientApiClientProvider>();
                    opt.AddSingleton<IApiClientService, DatabaseApiClientService>();
                    opt.AddSingleton<IDatabaseQueryRepository, ApiDatabaseQueryRepository>();
                });
        }

       public void Configure(
            IApplicationBuilder app, 
            IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseBlazorFslFramework(); // here
        }


#Considerações finais

A aplicação web Blazor Server usa o arquivo AppSettings.JSON para guardar a URL da API que traz os dados do banco SQL Server.

Para o componente Database funcionar é necessário colocar o namespace FSL.DatabaseQuery.BlazorSrv.Components no arquivo _Imports.razor.

Eu utilizei o template padrão do Visual Studio Preview para criar a aplicação Blazor Server, porém limpeialgumas coisas como por exemplo arquivo Site.css.

Essa solução pode te dar um norte (e muitas outras ideias) caso queira programar em .NET CORE e/ou Blazor.

Obrigado e até a próxima 🙂

Obs.: artigo publicado e não revisado.

Artigos sobre Blazor e ASP.NET CORE:

Crie seu Framework em ASP.NET CORE 3 e Blazor
Cookies: Identity no Blazor e ASP.NET CORE 3.0
Blazor: Muito mais que um WebForms de Luxo
Benchmark: ASP.NET 4.8 vs ASP.NET CORE 3.0
AppSettings: 6 Formas de Ler o Config no ASP.NET CORE 3.0
JWT: Customizando o Identity no ASP.NET CORE 3.0
Crie um Gerenciador de Arquivos do Zero em .NET Core e VueJS
IIS: Como Hospedar Aplicação .NET Core em 10 Passos
.NET Core para Desenvolvedores .NET
Blazor: O Começo do Fim do Javascript?

Faça download completo do código fonte no github.
Sobre o Autor:
Trabalha como arquiteto de soluções e desenvolvedor, tem mais de 18 anos de experiência em desenvolvimento de software em diversas plataformas sendo mais de 16 anos somente para o mercado de seguros.