.NET Core 3.0

.NET Core 3.0 – Criando tarefas de segundo plano com Worker Service

Quando se pensa em uma aplicação ASP.NET Core, provavelmente irá pensar em uma aplicação MVC, Web API ou Razor. Mas um detalhe pouco conhecido é que também é possível criar tarefas de segundo plano com o ASP.NET Core, através do conceito de generic hosts. Na versão 3.0 do .NET Core, este tipo de aplicação foi definida em um novo template chamado Worker Service, facilitando assim a criação das tarefas de segundo plano e dando mais destaque para este recurso.

O conceito de generic host foi introduzido na versão 2.1 do .NET Core e trata-se de uma aplicação ASP.NET Core que não processa requisições HTTP. O objetivo deste tipo de aplicação é remover o pipeline HTTP enquanto mantém os demais recursos de uma aplicação ASP.NET Core, como configuração, injeção de dependência e logging. Permitindo assim a criação de aplicações que precisam ser executadas em segundo plano, como serviços de mensagens.

Criando uma aplicação Worker Service

Para criar uma aplicação Worker Service é necessário ter instalado o .NET Core 3.0, que no momento da criação deste artigo está no preview 7. Caso esteja utilizando o Visual Studio, este tipo de aplicação está disponível a partir da versão 2019.

Como estou utilizando o Visual Studio Code, vou criar a aplicação pela linha de comando com o código abaixo:

dotnet new worker -n WorkerSample

No Visual Studio 2019 você pode encontrar o template desta aplicação dentro dos subtemplates do ASP.NET Core.

Na classe Program, podemos notar que é criado um servidor web:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<Worker>();
            });
}

Que define um serviço, a classe Worker, que veremos a seguir.

Definindo a tarefa

A classe responsável por executar a tarefa em segundo plano é a classe Worker:

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1000, stoppingToken);
        }
    }
}

Note que o construtor recebe uma instância de Logger. Podemos utilizar esta instância para notificar o usuário do estado do serviço. Já no método ExecuteAsync é onde a tarefa de segundo plano é executada.

Note que ela é executada enquanto o serviço não for cancelado.

Para este artigo iremos modificar apenas o método ExecuteAsync, adicionando informações sobre o estado do serviço:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation("O serviço está iniciando.");

    stoppingToken.Register(() => _logger.LogInformation("Tarefa de segundo plano está parando."));

    while (!stoppingToken.IsCancellationRequested)
    {
        _logger.LogInformation("Executando tarefa: {time}", DateTimeOffset.Now);
        await Task.Delay(10000, stoppingToken);
    }

    _logger.LogInformation("O serviço está parando.");
}

Ao executar a aplicação temos o resultado abaixo:

No momento ela ainda não está sendo executada como serviço, vamos configurá-la para isso.

Configurando a tarefa como serviço para ser executada em segundo plano

Para executar a aplicação como serviço, é necessário adicionar o pacote “Microsoft.Extensions.Hosting.WindowsServices” na aplicação:

dotnet add package Microsoft.Extensions.Hosting.WindowsServices

Ao adicioná-lo podemos chamar o método UseWindowsService():

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseWindowsService()
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();
        });

Este método verifica se a aplicação está executando como serviço e faz as configurações necessárias de acordo deste contexto. Ela também configura a aplicação para utilizar o ServiceBaseLifetime, que auxilia no controle do ciclo de vida da aplicação enquanto ela é executada como serviço. Isso sobrescreve o padrão ConsoleLifetime.

Publicando o serviço

Com o ServiceBaseLifetime configurado, podemos publicar a nossa aplicação:

dotnet publish -c Release

O comando acima irá publicar a aplicação de acordo com o ambiente de desenvolvimento. Você também pode publicar a aplicação para um sistema específico com a opção -r:

dotnet publish -c Release -r <RID>

<RID> significa Runtime Identifier (RID) e na documentação você pode ver as opções disponíveis.

Instalando o serviço no Windows

Com a aplicação publicada, ela pode ser instalada como serviço no Windows com o utilitário sc:

sc create workersample binPath=C:\Programs\Services\WorkerSample.exe

Instalando o serviço no Mac OS X

No caso do Mac OS X, os serviços são conhecimentos como daemon. Para definir o nosso serviço neste sistema como um daemon, é necessário possuir o .NET Core instalado na máquina e definir um script:

#!bin/bash
#Start WorkerSample if not running
if [ “$(ps -ef | grep -v grep | grep WorkerSample | wc -l)” -le 0 ]
then
 dotnet /usr/local/services/WorkerSample.dll
 echo "WorkerSampler Started"
else
 echo "WorkerSample Already Running"
fi

Dê para este script permissão de execução:

chmod +x ~/scripts/startup/startup.sh

Os daemons neste sistema operacional são definidos na pasta /Library/LaunchDaemons/, onde deve ser configurado em um arquivo XML com a extensão .plist (não faz parte do escopo deste artigo explicar as configurações deste arquivo, você pode conhecê-las na documentação do launchd).

No nosso caso, o arquivo terá o conteúdo abaixo:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>EnvironmentVariables</key>
    <dict>
      <key>PATH</key>
      <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:</string>
    </dict>
    <key>Label</key>
    <string>br.com.treinaweb.workersample</string>
    <key>Program</key>
    <string>/Users/admin/scripts/startup/startup.sh</string>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <false/>
    <key>LaunchOnlyOnce</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/tmp/startup.stdout</string>
    <key>StandardErrorPath</key>
    <string>/tmp/startup.stderr</string>
    <key>UserName</key>
    <string>admin</string>
    <key>GroupName</key>
    <string>admin</string>
    <key>InitGroups</key>
    <true/>
  </dict>
</plist>

Com isso, para iniciar o daemon basta carregar o arquivo .plist, que acabamos de definir:

sudo launchctl load -w /Library/LaunchDaemons/br.com.treinaweb.workersample.plist

Instalando o daemon no Linux

Assim como no Mac OS X, no Linux os serviços são conhecidos como daemon e nesta plataforma também é necessário possuir o .NET Core instalado na máquina. Fora isso, cada distribuição pode definir uma forma de criação de daemon, aqui vou explicar a criação utilizando o systemd.

No systemd o serviço é definido em um arquivo .service salvo em /etc/systemd/system e que no exemplo deste artigo conterá o conteúdo o abaixo:

[Unit]
Description=Dotnet Core Demo service

[Service]  
ExecStart=/bin/dotnet/dotnet WorkerSample.dll
WorkingDirectory=/usr/local/services
User=dotnetuser  
Group=dotnetuser  
Restart=on-failure  
SyslogIdentifier=dotnet-sample-service  
PrivateTmp=true  

[Install]  
WantedBy=multi-user.target  

Após isso, basta habilitar o daemon:

systemctl enable worker-sample.service 

Que ele poderá ser iniciado:

systemctl start dotnet-sample-service.service

Obs: Infelizmente no Mac OS X e Linux, a aplicação não sabe quando está sendo parada, algo que ocorre no Windows, quando ela é executada como serviço.

O Worker Service é bom?

O template Worker Service é uma boa adição para o .NET Core, pois isso pode tornar os generic hosts cada vez mais populares. Permitindo que as pessoas consigam utilizar os recursos do ASP.NET Core em uma aplicação console.

E com o tempo, espero que melhore o suporte da aplicação como daemon em ambientes Unix.

Diagnosticando uma aplicação no .NET Core 3.0

Na terceira versão do .NET Core foram introduzidas três ferramentas que permitem analisar o estado de uma aplicação em tempo de execução de forma simples:

  • dotnet-counters;
  • dotnet-trace;
  • dotnet-dump.

Minha aplicação está saudável?

Mesmo que se tenha cuidado e procure criar um bom código, apenas em produção que saberemos exatamente como a aplicação irá se comportar. Às vezes ela pode apresentar algum vazamento de memória ou algum pico inesperado da CPU. Felizmente podemos detectar esses tipos de problemas analisando algumas métricas da aplicação.

Métricas são representações de informações em um intervalo de tempo. No Windows, o .NET Framework gera contadores de performance (perf counters), que fornece algumas métricas das aplicações. Até a versão 2.2 o .NET Core não fornecia nada equivalente, só que na versão 3.0 foi introduzido o EventCounter API.

O EventCounters permite que a aplicação forneça algumas informações sobre o seu estado e através da ferramenta dotnet-counters, essas informações podem ser obtidas em tempo real.

Você pode instalar esta ferramenta como global tool com o comando abaixo:

dotnet tool install --global dotnet-counters --version 1.0.3-preview5.19251.2

E iniciá-la com o comando:

dotnet-counters monitor -p <pid> --refresh-interval 1

Como mostrado abaixo:

Você pode obter mais detalhes desta ferramenta na documentação do repositório dela do Github.

Porque minha aplicação está se comportando desta forma?

Enquanto os contadores de performance ajudam a identificar se a aplicação gerou algum comportamento inesperado, eles não deixam claro o que está errado. Para responder esta questão, podemos coletar informações adicionais com “trace”.

Tracing

Podemos definir “trace” como vestígios que a aplicação gera quando está sendo executada. No sistema isso resulta em registros que descrevem eventos gerados pela aplicação. Esses registros contém o contexto onde os eventos foram gerados, o que nos dá uma ideia melhor sobre o estado do sistema.

Tradicionalmente, o .NET Framework (e frameworks adjacentes, como ASP.NET) gera os “traces” via Event Tracing for Windows (ETW). Já no .NET Core, eles são gerados no ETW no Windows e no LTTng no Linux. No Mac OS X, isso ainda não está disponível.

Na versão 3.0 .NET Core todas as aplicações criam um “pipe” chamado EventPipe que permite a geração dos eventos. Para ter acesso as informações geradas, nesta versão também foi introduzida a ferramenta dotnet-trace.

Assim como a dotnet-counters, esta ferramenta pode ser instalada como uma global tool com o comando abaixo:

dotnet tool install --global dotnet-trace --version 1.0.3-preview5.19251.2

E um “trace” pode ser gerado com o comando:

dotnet-trace collect -p <pid>

Como mostrado abaixo:

No exemplo acima o dotnet-trace é executado com as configurações padrão. Mas você pode obter informações de provedores específicos, como explicado na documentação da ferramenta.

Ao executá-la, será gerado por padrão um arquivo *.netperf, que contém informações dos eventos gerados pela aplicação. Ele pode ser visualizado no perfview caso esteja em um ambiente Windows, ou caso esteja em um ambiente Unix, pode gerar um arquivo .speedscope.json com o atributo --format:

dotnet-trace collect -p <pid> --format "Speedscope"

E visualizá-lo no Speedscope.app:

Porque a minha aplicação “deu pau”?

Em alguns casos, não é possível aferir o que causou o problema da aplicação apenas analisando os eventos gerados, para isso, “process dump” (despejo de processo) pode ser a alternativa.

O “dump” é um registro do estado da memória utilizada pelo processo no momento que ele foi finalizado inesperadamente. Por isso, a análise deste registro é muito utilizada para identificar porque a aplicação foi finalizada ou apresentou algum comportamento inesperado.

Tradicionalmente, se confia no sistema operacional para capturar este registro quando a aplicação é finalizada (e.g. Windows Error Reporting) ou alguma ferramenta como procdump, para captar baseado em um critério.

Só que filtrar apenas as informações pertinentes a uma aplicação .NET Core é um desafio, principalmente em um ambiente Linux. Para ajudar nisso, foi criada a ferramenta dotnet-dump.

Como está em desenvolvimento, ela ainda não é totalmente suportada em todos os sistemas, como mostra a tabela abaixo de recursos:

Windows Mac OS X Linux
Collect ×
Analyze × ×

Assim como as demais, esta ferramenta é uma global tool que pode ser instalada com o comando abaixo:

dotnet tool install --global dotnet-dump --version 1.0.3-preview5.19251.2

E o dump pode ser criado com o comando abaixo:

sudo dotnet-dump collect -p <pid>

No Linux, ele pode ser analisado com o comando abaixo:

dotnet dump analyze <dump-name>

Você pode ver mais detalhes desta ferramenta na documentação do repositório dela do Github.

Concluindo

Assim como a versão 3.0 do .NET Core, no momento essas ferramentas estão em preview, mas já mostram um grande potencial. Quando saíram do preview, devem se tornar ferramentas essenciais de qualquer desenvolvedor .NET Core.

ASP.NET Core: Compreendendo AddMvc(), AddMvcCore(), AddControllers(), AddControllersWithViews() e AddRazorPages()

Neste artigo veremos as diferenças entres os métodos AddMvc(), AddMvcCore(), AddControllers(), AddControllersWithViews() e AddRazorPages() e quando utilizar cada um.

Tratando-se de uma reescrita do ASP.NET, a sua versão Core sempre foi pensada como um framework modular. Isso significa que as aplicações podem (e devem) adicionar apenas os recursos que utilizarão, o que irá otimizar a sua execução.

Infelizmente geralmente isso não é seguido, uma prova é a pouca adoção do método AddMvcCore().

AddMvcCore

Caso seja um iniciante no ASP.NET Core, talvez nunca tenha ouvido falar no método AddMvcCore(), mesmo na documentação oficial não é muito citado. Ele é um método de extensão de IServiceCollection, que carrega apenas os recursos básicos de uma aplicação ASP.NET:

  • Controllers: reconhece os controllers da aplicação;
  • Rotas: adiciona as rotas padrão;
  • CoreServiecs: adiciona os recursos básicos do ASP.NET.

Por adicionar os recursos básicos, qualquer outro recurso precisa ser adicionado “na mão”.

Por exemplo, crie uma aplicação web vazia e nela adicione um controller qualquer, com um método para a solicitação POST:

[Route("api/[controller]")]
public class HomeController : Controller
{
    [HttpPost]
        public ActionResult Post([FromBody] Usuario user)
    {
        if(ModelState.IsValid)
            return Ok(user);
        else
            return BadRequest(ModelState);
    }
}

E um model com ao menos um campo obrigatório:

public class Usuario
{
    public int Id { get; set; }
    public string Nome { get; set; }
    [Required]
    public string User { get; set; }
    [Required]
    public string Senha { get; set; }
}

Caso no método ConfigureServices seja adicionado apenas o método AddMvcCore():

public void ConfigureServices(IServiceCollection services) => services.AddMvcCore().AddNewtonsoftJson();

Obs.: Estou usando o .NET Core 3.0 Preview 4 e nesta versão as classes do Json.NET foram removidas da biblioteca padrão, assim, é necessário adicioná-las com o pacote Microsoft.AspNetCore.Mvc.NewtonsoftJson e o método AddNewtonsoftJson() utilizado acima.

O ASP.NET não irá validar os campos obrigatórios na solicitação POST:

Para que isso ocorra é necessário adicionar as Data Annotations:

public void ConfigureServices(IServiceCollection services) => services.AddMvcCore().AddNewtonsoftJson().AddDataAnnotations();

Com isso, a validação funcionará:

Claro que este nosso exemplo é simples, mas imagine ter que especificar autorização (a anotação [Authorize] ), CORS, etc. Por causa disso, geralmente utiliza-se o método AddMvc().

AddMvc

Assim como o AddMvcCore(), o AddMvc() é um método de extensão de IServiceCollection que carrega praticamente todos os recursos necessários de uma aplicação ASP.NET, como:

  • MvcCore: as dependências mínimas para executar uma aplicação ASP.NET Core MVC. Reconhece os controllers, realiza os bindings, adiciona das rotas padrão e os recursos básicos do ASP.NET;
  • ApiExplorer: Habilita as API Help pages;
  • Authorization: Habilita autorização;
  • FormatterMappings: Traduz a extensão de um arquivo para content-type;
  • Views: Habilita as Views;
  • RazorViewEngine: Habilita o Razor nas Views (páginas salvas com a extensão cshtml);
  • RazorPages: Habilita as Razor Pages (um tipo de aplicação do ASP.NET Core);
  • TagHelper: Adiciona as tags helpers do Razor;
  • Data Annotations: Habilita validação dos model por data annotations;
  • Json Formatters: Adiciona parsers de JSON;
  • CORS: Habilita o Cross-origin resource sharing (CORS).

Por adicionar vários recursos geralmente opta-se por definir apenas o método AddMvc(), em detrimento do AddMvcCore().

Ao fazer esta mudança na nossa aplicação:

public void ConfigureServices(IServiceCollection services) => services.AddMvc().AddNewtonsoftJson();

Não é necessário especificar o Data Annotations, para que a validação seja realizada:

Mas caso esteja desenvolvendo uma aplicação Web API, o método AddMvc() irá carregar recursos que não serão utilizados. Nesta situação, o ideal seria optar pelo AddMvcCore(), mas se notar, até a versão 2.2 do ASP.NET a aplicação Web API criada pelo template padrão, também faz uso do método AddMvc().

Para alterar este comportamento, na versão 3.0 (que no momento da publicação deste artigo está em preview 4) foram adicionados os métodos AddControllers(), AddControllersWithViews() e AddRazorPages().

AddControllers

Podemos dizer que o método AddControllers() é uma evolução do AddMvcCore(), já que foi criado visando preencher a lacuna que este método não conseguiu. Para isso, como o seu nome sugere, ele carrega recursos relacionados aos controllers:

  • Controllers
  • Model Binding;
  • ApiExplorer;
  • Authorization;
  • CORS;
  • Data Annotations;
  • FormatterMappings.

Desta forma, se a aplicação não possuir uma interface, como a do nosso exemplo, que é uma Web API, pode-se optar por ele:

public void ConfigureServices(IServiceCollection services) => services.AddControllers().AddNewtonsoftJson();

Que todos os recursos relacionados a este tipo de aplicação serão carregados.

AddControllersWithViews

O método AddMvc() não foi descontinuado na versão 3.0, mas foi adicionando o método AddControllersWithViews() que carrega praticamente os mesmos recursos:

  • MvcCore;
  • ApiExplorer;
  • Authorization;
  • FormatterMappings;
  • Views;
  • RazorViewEngine;
  • TagHelper;
  • Data Annotations;
  • Json Formatters;
  • CORS.

O único que foi removido é o Razor Pages. Desta forma, uma aplicação ASP.NET MVC que não faça uso do Razor Pages deve optar por este método.

Para exemplificá-lo, é necessário adicionar uma view na aplicação:

@{
    ViewData["Title"] = "Home Page";
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"]</title>
</head>
<body>
    <p>Aplicativo de exemplo!</p>
</body>
</html>

Definir uma action no controller:

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

E definir as rotas no método Configure:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});

Com isso, a view será mostrada:

AddRazorPages

Razor Pages é um tipo de aplicação que permite adicionar códigos C# e HTML em um mesmo arquivo ou em arquivos separados, de uma forma muito similar ao antigo Web Forms. Por ser um aspecto de uma aplicação ASP.NET MVC, o AddRazorPages() carrega quase os mesmos recursos do método AddControllersWithViews():

  • RazorPages;
  • MvcCore;
  • Authorization;
  • Views;
  • RazorViewEngine;
  • TagHelper;
  • Data Annotations;
  • Json Formatters.

Os recursos que não são carregados por padrão são:

  • ApiExplorer;
  • FormatterMappings;
  • CORS.

Como o nome sugere este método deve ser utilizado em aplicações Razor Pages.

Mas caso a sua aplicação faça uso do Razor Pages e do ASP.NET MVC padrão, ele pode ser utilizado em conjunto com AddControllers() ou AddControllersWithViews():

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddControllersWithViews();
}

Que irá gerar o mesmo resultado do uso do AddMvc().

Conclusão

Por ser um framework modular, o ideal é que a aplicação ASP.NET carregue apenas os recursos que necessita. Comparando os métodos AddMvc(), AddMvcCore(), AddControllers(), AddControllersWithViews() e AddRazorPages(); e vendo quais recursos cada um carrega, fica claro a função de cada um e quando utilizá-los de acordo com cada tipo de aplicação.

Assim, procure sempre utilizar o método que for mais adequado ao tipo de aplicação que está desenvolvendo, mesmo se o template padrão do ASP.NET sugerir outro método.

.NET Core 3.0 – Criando componentes no Razor Components

Em um artigo passado, falei sobre o Razor Components. Este novo tipo de aplicação, presente no .NET Core 3.0. Como dito anteriormente, o Razor Components é uma evolução do Blazor. Uma forma de criar componentes web no C#. Uma analogia seria com os componentes de frameworks JavaScript, como React e Angular.

Assim como nesses frameworks, os componentes do Razor encapsulam toda a lógica. Podendo ser adicionados em qualquer aplicação ASP.NET.

Para compreender isso, vamos colocar a mão na massa.

Este artigo está sendo escrito com base nas definições do NET Core 3 Preview 3, então no futuro algumas informações não podem ser compatíveis com a versão mais atual do .NET Core. E os exemplos demostrados aqui, requerem esta versão do framework ou uma superior.

Estrutura de um componente Razor

Pelo terminal, é possível criar uma aplicação Razor Components com o comando abaixo:

dotnet new razorcomponents -n RazorApp

Na aplicação criada, você notará uma pasta chamada Components e dentro dela localizará arquivos com a extensão *.razor:

Esses arquivos são os componentes do Razor. Ao verificar o código de um componente, teremos:

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" onclick="@IncrementCount">Click me</button>

@functions {
    int currentCount = 0;

    void IncrementCount()
    {
        currentCount++;
    }
}

O início (@page "/counter") define a URL do componente. Dentro dela há códigos HTML e um bloco com a anotação @functions:

@functions {
    int currentCount = 0;

    void IncrementCount()
    {
        currentCount++;
    }
}

Como o nome indica, no bloco desta anotação são definidas funções invocadas pelos componentes. Elas podem ser referenciadas no HTML do arquivo através da arroba (@) como no trecho abaixo:

<button class="btn btn-primary" onclick="@IncrementCount">Click me</button>

Qualquer código definido neste ponto pode ser referenciado no HTML com a arroba, como é o caso da variável currentCount:

<p>Current count: @currentCount</p>

Ao executar a aplicação (dotnet run), podemos acessar o componente em /counter e visualizar o seu comportamento:

Criando um componente/página Razor

Agora que conhecemos um pouco da estrutura de um componente Razor vamos criar o nosso. Este primeiro componente será uma página, então dentro da pasta Components/Pages, adicione um arquivo chamado Posts.razor e nele adicione o código abaixo:

@page "/posts"
@using RazorApp.Services
@inject BlogService blogService

<h1>Posts</h1>

@if (posts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    @foreach (var post in posts)
    {
        <div class="card" style="width: 36rem;">
            <div class="card-body">
                <h1 class="card-title">@post.Title</h1>
                <p class="card-text">@post.Content</p>
            </div>
        </div>
    }
}

@functions {
    Post[] posts;

    protected override async Task OnInitAsync()
    {
        posts = await blogService.GetPostsAsync();
    }
}

Note que nesta página estamos definindo a listagem de posts:

@foreach (var post in posts)
{
    <div class="card" style="width: 36rem;">
        <div class="card-body">
            <h1 class="card-title">@post.Title</h1>
            <p class="card-text">@post.Content</p>
        </div>
    </div>
}

Dados que virão de um service (BlogService), chamado na criação da página:

protected override async Task OnInitAsync()
{
    posts = await blogService.GetPostsAsync();
}

Assim, é necessário definir este service:

namespace RazorApp.Services
{
    public class BlogService 
    {
        public Task<Post[]> GetPostsAsync()
        {
            return Task.FromResult<Post[]>(new Post[]
            {
                new Post { Title = "Post 1", Content = "Conteúdo do Post 1" },
                new Post { Title = "Post 2", Content = "Conteúdo do Post 2" },
                new Post { Title = "Post 3", Content = "Conteúdo do Post 3" },
                new Post { Title = "Post 4", Content = "Conteúdo do Post 4" }
            });
        }
    }
}

Que contém apenas dados de exemplo. Em uma aplicação real, esta informação poderia vir do banco e/ou uma API.

Também será necessário definir o model Post:

public class Post 
{
    public string Title { get; set; }
    public string Content { get; set; }
}

“Injetar” o service na classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    //Código omitido

    services.AddSingleton<BlogService>();
}

Por fim, no arquivo NavMenu.razor (presente na pasta Components/Shared), adicione um link para a página que acabamos de definir:

<li class="nav-item px-3">
    <NavLink class="nav-link" href="posts">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Posts
    </NavLink>
</li>

Ao acessar a aplicação, teremos acesso a esta página:

Simples, não é?

Criando um componente reutilizável

Agora que vimos como criar uma página, veremos como criar um componente reutilizável, que poderá ser adicionado em outras páginas e/ou componentes da aplicação.

Assim como o anterior, este componente será criado dentro da pasta Components/Pages, agora com o nome de LikeButton.razor e nele adicione o código abaixo:

<button onclick="@Liked" 
        class="btn @( likedCount == 0 ? "btn-outline-secondary" : "btn-outline-danger" )">
    <i class="far fa-heart"></i>
    @likedCount
</button>

@functions {
    [Parameter] int InitialLikedCount { get; set; }

    int likedCount = 0;

    protected override void OnInit()
    {
        likedCount = InitialLikedCount;
    }

    void Liked()
    {
        likedCount++;
    }
}

Neste componente estamos definindo um botão:

<button onclick="@Liked" 
        class="btn @( likedCount == 0 ? "btn-outline-secondary" : "btn-outline-danger" )">
    <i class="far fa-heart"></i>
    @likedCount
</button>

Este botão contém o ícone de coração do font-awesome, então é necessário adicionar a referência dela na página Index.cshml (localizada em Pages). No bloco @functions, é definido um parâmetro:

[Parameter] int InitialLikedCount { get; set; }

Que será atribuído a variável likedCount na inicialização da página:

protected override void OnInit()
{
    likedCount = InitialLikedCount;
}

Como é possível notar, ao definir [Parameter] , indicamos que o valor da variável irá como um atributo do componente.

Já no método Liked a variável likedCount é incrementada:

void Liked()
{
    likedCount++;
}

Para utilizar este componente, basta defini-lo em outra página como uma tag:

<LikedButton>

Faça isso no componente que criamos anteriormente:

@foreach (var post in posts)
{
    <div class="card" style="width: 36rem;">
        <div class="card-body">
            <h1 class="card-title">@post.Title</h1>
            <p class="card-text">@post.Content</p>
        </div>
        <div class="card-footer">
            <LikeButton />
        </div>
    </div>
}

Inicialmente não estamos passando o parâmetro para o componente, mesmo assim ele funciona:

Caso seja passado o parâmetro:

@foreach (var post in posts)
{
    <div class="card" style="width: 36rem;">
        <div class="card-body">
            <h1 class="card-title">@post.Title</h1>
            <p class="card-text">@post.Content</p>
        </div>
        <div class="card-footer">
            <LikeButton InitialLikedCount="5" />
        </div>
    </div>
}

O componente será inicializado com o valor informado:

Conclusão

Este é um exemplo simples de criação e reutilização de componentes, mas nele é possível notar o poder deste recurso. Tenho certeza que não irá demorar muito para termos componentes para ecossistemas específicos, como Bootstrap, Material, etc.

.NET Core 3.0 – Razor Components

No final do mês de janeiro, foi lançado o segundo preview do .NET Core 3.0. Nele foi introduzido o Razor Components, conhecido anteriormente como Blazor server-side.

Para os que não conhecem, o Blazor é um projeto experimental da Microsoft que tem por objetivo levar uma aplicação .NET ao navegador utilizando WebAssembly. Pense em uma aplicação Angular ou React, mas utilizando C# e Razor.

No momento o Blazor trata-se apenas de um experimento, mas a equipe do ASP .NET pretende adicionar na versão final do .NET Core 3.0 o Razor Components.

Então vamos conhecê-lo.

Razor Components

O desenvolvimento web tem evoluído de várias maneiras ao longo dos últimos anos. Hoje há uma infinidade de frameworks front-end e N formas de se criar aplicações web. Mesmo assim, ainda há desafios na criação de uma aplicação web moderna.

Com o objetivo de sanar alguns desses desafios que se iniciou o “experimento” Blazor e dele, agora temos o Razor Components.

Funcionamento

Diferente do Blazor, no Razor Components o código C# não é executado no navegador. Ele é processado no servidor, que se comunica com a interface através do SignalR:

Na prática, ao acessar uma aplicação criada com o Razor Components, ela se comportará como uma SPA (Single Page Application). Será baixado um arquivo index.html e um arquivo Javascript chamado blazor.server.js. Em seguida a aplicação estabelecerá uma conexão com o servidor através do SignalR.

No servidor, o componente da página será processado gerando o seu HTML. Este é comparado “instantaneamente” com a versão presente no client. Caso haja alguma diferença, ela é comparada, as alterações são enviadas para o client, o navegador “desempacota” o pacote e aplica as alterações no DOM do client.

Quando há alguma interação com a tela (como o clique de um botão), o evento é comparado e enviado para o servidor pela mesma conexão. Onde ele é processado e retorna o resultado, com as alterações que devem ser aplicadas ao DOM.

Vantagens

Há várias vantagens em utilizar o Razor Components:

  • .NET Core APIs: Por executar no servidor, a aplicação pode fazer uso das várias APIs disponíveis no .NET Core. Além de reaproveitar códigos de outras aplicações C#, se for necessário. Sendo possível até utilizar os componentes do Razor em aplicações ASP.NET MVC (no momento o inverso ainda não é possível);
  • Aparência de SPA: Como disse antes, uma aplicação Razor Components se comporta como uma aplicação SPA, mesmo que não seja desenvolvida como tradicionalmente este tipo de aplicação é criada;
  • Estabilidade e performance: A equipe do .NET Core procura sempre melhorar a performance do framework. Então ao utilizá-lo, já há a garantia que a aplicação será performática, estável e segura;
  • Rapidez no carregamento: Comparado com o Blazor, a aplicação do Razor Components envia para o client apenas dois arquivos leves, que são baixados rapidamente;
  • Suporte de vários navegadores: A principal desvantagem do Blazor é o suporte apenas dos navegadores que suportam o WebAssembly. Já o Razor Components é suportado praticamente por todos os navegadores, incluindo navegadores móveis.

Desvantagens

Infelizmente não existe almoço grátis, o Razor Components também possui algumas desvantagens:

  • Online only: Como o código C# é executado no servidor, a aplicação necessita sempre se comunicar com ele quando ocorrer qualquer tipo de interação na página. Assim, não é possível gerar uma versão offline da aplicação;
  • Latência: Mesmo o SignalR sendo muito eficiente, ainda pode haver alguns engasgos na comunicação do client com o servidor. Principalmente porque qualquer interação do usuário com a aplicação precisa ser enviada para o servidor, processada e um DOM resultante retornado;
  • Persistência do estado da aplicação: No momento se a comunicação com o servidor for perdida, o estado da aplicação também será perdido. E por enquanto não há muitas soluções para este cenário.

Encerrando

Vou finalizar este artigo por aqui. Agora que você conhece um pouco do Razor Components, no próximo vou mostrar um pouco de código. Veremos como criar este tipo de aplicação e a criação de alguns componentes.

Até a próxima.

JUNTE-SE A MAIS DE 150.000 PROGRAMADORES