Primeiros passos com Visual Studio Code e .Net Core

Published on Friday, May 8, 2020

Disponível em 🇧🇷 / Available in 🇺🇸

Já a algum tempo eu queria estudar .Net Core e venho postergando. Confesso que a primeira experiência (a cerca de dois anos) não foi lá das melhores.

First things first

Antes de começar, precisamos das ferramentas que são a IDE Visual Studio Code e o Framework .Net Core (atualmente na versão 3.1).

A escolha do Visual Studio Code para uso com .Net Core foi um pouco difícil... Hoje, praticamente qualquer curso/livro que você encontra sobre o framework, utiliza o Visual Studio Community, que realmente tem seus prós. O Visual Studio Community tem à distância de um click os scaffoldings que no VSCode vai lhe custar escrever os métodos manualmente ou lembrar uma commandline como dotnet aspnet-codegenerator <model> <namespace> <etc>. Mas como tudo na vida, melhor ou pior, isso depende do ponto de vista e, escrever as classes manualmente (ou quase) vai lhe fazer entender melhor a estrutura do framework e depender menos das coisas criadas automaticamente.

Deixando de lado que algumas tarefas "podem" ser ligeiramente mais trabalhosas, o VSCode é uma IDE extremamente mais leve que sua parente, o que permite ter seu ambiente de desenvolvimento funcionando em praticamente apenas um minuto. A comunidade de desenvolvimento (normalmente mais focada em front-end) vem elogiando bastante a ferramenta e eu acho que vale dar o braço a torcer também para o backend.

Então, mãos a massa!

Precisamos do VSCode e do .Net Core instalados para começar. Você pode optar por baixar as ferramentas dos respectivos sites ou poupar algum tempo e instalar tudo via commandline:

Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
choco install vscode -y
choco install dotnetcore -y

Tipos de projeto

Antes de começar o desenvolvimento, dei uma boa lida sobre os tipos de projeto disponíveis utilizando .Net Core. No final das contas, o mesmo projeto com algumas modificações pode rodar em conjunto mvc, razor e uma api, então porque se preocupar com o tipo de projeto? Bom, por alguns motivos:

  • Transformar um projeto iniciado como webapi em razor vai lhe custar algum tempo criando código que vem por padrão na criação do projeto;
  • Se sua especialidade é backend e não front (o que é o meu caso), vem bem a calhar ter um layout padrão pronto;
  • Se você pretende criar uma aplicação modular, criar no mesmo projeto (não na mesma solução) a API e a parte Web pode não ser uma boa prática;

Dito isso, neste post vamos falar sobre 3 tipos de projeto: mvc, webapi e razor. Caso queira consultar, a lista de tipos de projeto completa está disponível neste link e também através da commandline dotnet new --help.

MVC, Razor e API - Principais diferenças

Em suma, páginas Razor lembram muito mais o formato Windows Forms que páginas MVC. Páginas MVC te forçam a utilizar o padrão da arquitetura, o que por algumas vezes pode trazer uma complexidade desnecessária para um projeto, enquanto páginas Razor possuem estrutura mais simples. Ambos, Razor e MVC tem suas vantagens e desvantagem e a decisão vai depender do tipo/tamanho/complexidade do projeto que você quer desenvolver. Se tiver tempo disponível, estude sobre as duas.

Já sobre WebApi, este tipo de projeto utiliza os controllers do MVC - apenas descartando a utilização de models e views. É um abordagem simples, e utiliza o modelo de roteamento e verbos para direcionar as requisições. E aproveitando, páginas Razor utilizam a diretiva @page para tratar o roteamento.

Artigos sobre o assunto:

Criando uma aplicação Razor

Eu tento normalmente evitar o mouse enquanto programo, então vamos tentar centralizar todo o trabalho no VSCode utilizando commandlines.

  • Com o VSCode aberto, acesse o terminal com o comando CTRL + '
  • No terminal, caso ainda não possua, crie uma pasta para armazenar seus projetos com o comando mkdir ~/Projects
  • Navegue até a pasta onde você armazena de projetos com o comando cd ~/Projects
  • Crie um novo projeto Razor com o comando dotnet new webapp -o RazorProject
  • Carregue a pasta do projeto chamada "RazorProject" com o comando code -r RazorProject ou CRTL + K, CRTL + O (ignore/feche o pop-up de alerta Required assets to build and debug are missing from 'RazorProject'. Add them? por hora)
  • Abra o terminal novamente (CTRL + ') e execute o projeto com o comando dotnet run
  • Leia a mensagem do console, se algo como Now listening on: https://localhost:5001 apareceu, teste o acesso a url https://localhost:5001 no seu navegador, e voilà! Sua aplicação está rodando!

default-razor-page

Bom, quase... Talvez o intelisense ainda não esteja funcionando e nem o debugging, mas já vamos dar uma olhada nesses caveats.

Em ambiente Windows é normal utilizar um backslash "\" enquanto que Linux um slash "/" para o caminho de pastas, mas o exemplo acima vai funcionar da mesma forma em Windows se o seu terminal for um console de Powershell - que é o padrão no VSCode.

Debugging

Quando você carregou o projeto no VSCode, provavelmente um popup com o alerta ⚠ Required assets to build and debug are missing from 'RazorProject'. Add them? apareceu. Isso significa que a pasta .vscode com as configurações de build não existem, mas a pasta é criada automaticamente, quando você executa um F5 ou Run > Start Debugging e seleciona na lista a opção .Net Core. Teclando F5 novamente vai executar a aplicação e você pode tentar debugar a aplicação.

É possível que o Omnisharp ou o ms-dotnettools.csharp, extensões utilizadas para desenvolvimento com .Net Core, falhem em carregar alguma DLL e o path program do projeto, no arquivo /.vscode/launch.json fique mais ou menos assim: ${workspaceFolder}/bin/Debug/insert-target-framework-here/insert-project-name-here.dll. Se você tiver esse problema, substitua no caminho insert-target-framework-here por netcoreapp3.1 e insert-project-name-here por RazorProject.

Se algum erro aparecer relacionado como o MSBuild apontando alguma DLL faltando e você tem outra versão do Visual Studio instalada, veja se na mensagem de erro o path do msbuild aponta para o executável do Visual Studio Community/Professional. É possível corrigir isso de algumas formas e eu não lembro como corrigi o meu... Mas nada q alguns minutos no Google não resolvam.

Se você está comigo até aqui e quer deixar o mouse de lado enquanto programa, siga os seguintes passos:

  • No VSCode, tecle CTRL SHIFT + E, navegue utilizando os direcionais , e (para expandir uma pasta), selecione o arquivo /Pages/Index.cshtml.cs e tecle Enter;
  • Navegue para a linha com o código public void OnGet(), você pode utilizar o comando CTRL + G e digitar o número da linha se quiser;
  • Crie um breakpoint, teclando F9, feito isso volte ao browser com a página https://localhost:5001 e recarregue a mesma;
  • Volte para o VSCode e veja que a IDE está aguardando a continuação, tecle F5 para prosseguir

A princípio, parece trabalho demais lembrar de todos esses comandos para programar e, de fato você pode fazer tudo isso clicando. E você também pode utilizar um alicate para martelar um prego ao invés de um martelo... Essa não é uma verdade universal apesar da brincadeira mas, com o tempo, deixar o mouse de lado te recompensa com uma melhora na produtividade e até com a ergonomia, já que você se movimenta menos não tendo que alcançar o mouse para executar algumas tarefas.

Debugar não tem nenhum segredo, mas lembrar as teclas de debugging vão melhorar um pouquinho a sua produtividade também. Tente utilizar conforme a necessidade, lembrar todos esses comandos de uma vez só pode fazer você criar aversão por utilizá-los e, se gostar muito quem sabe você não migra para o Vim...

Tecla Ação Descrição
F5 Continue / Pause Executa o projeto em Debugging Mode
CTRL + F5 Run Executa o projeto sem Debugging Mode
CTRL SHIFT + F5 Restart Reinicia a execução do projeto
SHIFT + F5 Stop Para a execução do projeto
F10 Step Over Executa a próxima linha de código a partir do ponto de parada atual.
F11 Step Into Executa a próxima linha de código a partir do ponto de parada atual. Se for uma função/método presente no seu código, entra na função e começa a debugar o código da função
SHIFT + F11 Step Out Executa a função atual até o fim, saí da função atual e continua o debugging na próxima linha após termino de execução da função

Bom, já temos o básico para começar e, inspirado num post do Renato Groffe, vamos fazer nossa aplicação consultar a API "Astronomy Picture of the Day" da Nasa, com algumas variações do que ele fez lá para explorarmos o framework, então já faça seu cadastro e pegue sua API Key.

Arquivos de configuração - appsettings.json

Para evitar ter os dados de configuração hardcoded na aplicação, o que é uma péssima prática, vamos armazená-los num arquivo de configuração, que no nosso caso é o appsettings.json, substituto do velho Web.Config e deve estar na raiz do seu projeto após executar o comando dotnet new webapp.

Aqui eu tenho uma crítica. O .Net Core utiliza o padrão de design de software baseado em Dependecy Injection, que permite ter um objeto strong typed como resultado do carregamento das configurações, o que realmente é ótimo, mas em contra partida trouxe uma complexidade desnecessária para recuperar dados de configuração. Talvez essa seja uma tentativa de aplicar o Twelve Factors a risca e forçar o desenvolvedor a utilizar variáveis de ambiente para determinado tipo de dado, mas isso não explica outros problemas com a complexidade desnecessária do framework, como a classe HttpClient só fazer chamadas async, mesmo quando você não precisa usar async. Poxa, extrair dados sem referenciar diretamente o arquivo de configuração poderia ter sido implementado, dado que antes poderiamos fazer algo do tipo string userName = System.Configuration.ConfigurationManager.AppSettings["UserName"];... Vida que segue.

Primeiro, vamos preencher o appsettings.json com os dados para utilizar o endpoint da Nasa. Seu arquivo deve parecer da seguinte forma:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "NasaConfiguration":{
    "Endpoint": "https://api.nasa.gov/planetary/apod",
    "ApiKey": "<your_api_key>"
  }
}

Não esqueça de substituir <your_api_key> pela sua chave de API!

Com isso, precisamos extrair os dados da API do arquivo appsettings.json e fazer uma chamada HTTP para o endpoint, o que pode ser feito de algumas formas, dependendo do refinamento que você quer para seu código.

Via Dependency Injection (ou DI)

Você pode recuperar os dados passando um parâmetro IConfiguration config no construtor IndexModel da sua página Index.cshtml.cs, o que vai produzir uma variável de saída "https://api.nasa.gov/planetary/apod". A classe deve ficar mais ou menos assim (este é só o código dentro de public class IndexModel : PageModel):

private readonly ILogger<IndexModel> _logger;
private readonly IConfiguration _config;
public IndexModel(ILogger<IndexModel> logger, IConfiguration config)
{
    _config = config;
    _logger = logger;
}

public void OnGet()
{
    var endPoint = _config.GetSection("NasaConfiguration:EndPoint");
}

O código acima faz com que o resultado seja apenas uma saída de texto criada a partir do conteúdo preenchido no arquivo de configuração.

Já para que possamos utilizar melhor a capacidade do IConfiguration, é possível extrair os dados baseado num modelo com atributos pré definidos. Esta é a abordagem que vou utilizar, pois apesar de ambas terem o mesmo efeito como produto final, deixar os atributos visíveis ao recuperar dados de configuração evita que precisemos abrir o arquivo appsettings.json para confirmar o nome das variaveis quando precisarmos utiliza-las.

Para criar o arquivo de modelo NasaConfiguration.cs, basta no terminal (CTRL + ') executar o comando code NasaConfiguration.cs. Aqui, podemos escrever o arquivo do zero nomeando o namespace e a class, ou se você não curte um mínimo já pré-definido num novo arquivo de classe, há a opção de instalar uma extensão como a jchannon.csharpextensions. No meu caso, por hora vou seguir com o setup mais limpo possível.

//NasaConfiguration.cs
namespace RazorProject {
    public class NasaConfiguration {
        public string Endpoint { get; set; }
        public string ApiKey { get; set; }
    }
}

E para nosso Index, vamos utilizar o construtor novamente com DI, mas desta vez vamos atribuir os dadosa nossa variável que possui o modelo do arquivo de configuração. Isso pode ser feito tanto com config.GetSection("NasaConfiguration").Get<NasaConfiguration>(); quanto com config.GetSection("NasaConfiguration").Bind(<input_variable>);, sendo que no segundo caso é necessário que a variável utilizada como input_variable seja previamente instânciada. Veremos num exemplo adiante.

//Index.cshtml.cs
public class IndexModel : PageModel
{
    private readonly NasaConfiguration _nasa;
    private readonly ILogger<IndexModel> _logger;

    public IndexModel(ILogger<IndexModel> logger, IConfiguration config)
    {
        _nasa = config.GetSection("NasaConfiguration").Get<NasaConfiguration>();
        _logger = logger;
    }

    public void OnGet()
    {
        string endPoint = _nasa.Endpoint;
        string apiKey = _nasa.ApiKey;
    }
}

Este modo de uso cobre essencialmente o que precisamos e você pode pular para a proxima parte se não tiver interesse nos demais modos de carregar dados do arquivo de configuração.

Via ConfigurationBuilder

Com o exemplo anterior, utilizar Dependency Injection não parece tão ruim. O problema fica evidente mesmo quando precisamos usar uma configuração numa classe que passa por algumas funções até chegar no ponto que é feito o Injection. Imagine a seguinte chamada das funções:

IndexModel(IConfiguration config) > 
    Nasa.GetItem(IConfiguration config, "PlanetName") > 
    NasaCustomQuery(IConfiguration config, $"Select Name, Location from NasaSQL where Name ='{planetName}'") >
    NasaWebRequest(IConfiguration config, string params)

Claro que para uma chamada simples como é o da API da Nasa, esse exemplo não requer tanta complexidade, mas vão haver casos onde você vai ter uma complexidade parecida e, trafegar os dados recuperados na primeira função durante 4 chamadas é muita repetição de código desnecessária. Se o dado só precisa ser utilizado na função NasaWebRequest podemos criar uma função auxiliar para carregar uma instância do IConfiguration pode ser a melhor opção:

// configPath = "NasaConfiguration:Endpoint"
public string RetrieveConfiguration (string configPath){ 
    var configurationBuilder = new ConfigurationBuilder();
    var path = Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json");
    configurationBuilder.AddJsonFile(path, false);
    var root = configurationBuilder.Build();

    return root.GetSection(configPath).Value;
}

E utilizando a classe ConfigurationBuilder é possível também carregar as configurações através de um modelo, da mesma forma que no exemplo utilizando DI:

var configurationBuilder = new ConfigurationBuilder();
var path = Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json");
configurationBuilder.AddJsonFile(path, false);
var config = configurationBuilder.Build();

var nasa = new NasaConfiguration();
config.GetSection("NasaConfiguration").Bind(nasa);

string endPoint = _nasa.Endpoint;
string apiKey = _nasa.ApiKey;

Via IOptions

O uso de IOptions demanda configurar o Startup.cs e carregar o objeto utilizando dependency injection no arquivo onde você for utilizar as informações, da mesma forma que o IConfiguration:

//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.Configure<NasaConfiguration>(options => Configuration.GetSection("NasaConfiguration").Bind(options));
}
//Index.cshtml.cs
private readonly IOptions<NasaConfiguration> _options;
private readonly ILogger<IndexModel> _logger;

public IndexModel(ILogger<IndexModel> logger, IOptions<NasaConfiguration> options)
{
    _options = options;
    _logger = logger;
}

public void OnGet()
{
    var options = _options.Value;
    string endPoint = options.Endpoint;
    string apiKey = options.ApiKey;
}

Particularmente, eu acho desnecessário utilizar IOptions que, apesar de possuir algumas diferenças em relação ao IConfiguration (como por exemplo, detectar mudanças no arquivo de configuração e recarregar as mesmas), acaba fazendo o mesmo papel que o IConfiguration.

Ufa! Depois de tudo isso só para explicar os arquivos de configuração, hora de voltar ao projeto.

Consumindo a API Picture of the day

Como este post já está bem extenso, vamos consumir a API a forma mais simples possível para que possamos ter um resultado da aplicação em funcionamento.

Numa situação normal (o que é o caso da API da Nasa), os dados retornados possuem nomes de atributos bem definidos e não dinâmicos, o que permite criar um modelo da mesma forma que fizemos com nosso arquivo de configuração. Contudo, eu vou abordar aqui uma forma mais dinâmica onde não vamos utilizar um modelo e vamos expor todos os dados retornados da API. Você pode checar abaixo um exemplo de resultado da API ou neste link: https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY

{
  "date": "yyyy-MM-dd",
  "explanation": "",
  "hdurl": "https://apod.nasa.gov/apod/image/yyyy/file.jpg",
  "media_type": "image",
  "service_version": "v1",
  "title": "Mystic Mountain Monster being Destroyed",
  "url":  "https://apod.nasa.gov/apod/image/yyyy/file.jpg"
}

Aqui, precisamos de uma lib para tratar o conteúdo em Json retornado pela api. Execute no terminal dotnet add package NewtonSoft.Json. Terminada a importação, configurar a URL para onde precisamos enviar nossas requisições, no arquivo /Pages/Index.cshtml.cs, adicionando o seguinte código no método OnGet():

string endPoint = _nasa.Endpoint;
string apiKey = _nasa.ApiKey;

//uri = "https://api.nasa.gov/planetary/apod?api_key=<your_api_key>&date=2019-08-01"
var uri = new Uri(
    QueryHelpers.AddQueryString(endPoint,
        new Dictionary<string, string>(){
            {"api_key", apiKey},
            {"date", DateTime.Now.AddDays(-new Random().Next(365)).ToString("yyyy-MM-dd")}
        }
    )
);

Aqui -new Random().Next(365)).ToString("yyyy-MM-dd"), estamos gerando um número negativo, randômico, entre 0 e 365 e subtraíndo este dos dias a contar da data atual e formatando este dado para o padrão aceito pela API, então na nossa página iremos exibir a foto de um dia aleatório de até um ano atrás. Utilizar o método QueryHelpers.AddQueryString é opcional, mas deixa o código mais organizado para trabalhar com parâmetros no .Net.

Para utilizar o QueryHelpers, é necessário utilizar o namespace Microsoft.AspNetCore.WebUtilities. Para facilitar a importação, quando digitar QueryHelpers, caso uma mensagem de erro O nome "QueryHelpers" não existe no contexto atual [RazorProject]csharp(CS0103) apareça, tecle CTRL + . e selecione a opção using Microsoft.AspNetCore.WebUtilities;.

Aqui vale uma dica: Evite copiar código.

Copiando código e colando no seu projeto, a única coisa que você está exercitando é sua habilidade de copiar e colar. Escrever o código, mesmo que exatamente igual ao que você está consultando aqui ou em outro momento no Google/StackOverflow vai exercitar sua memória e, provávelmente na próxima vez que você precisar usar essa informação, não vai ter que consultar externamente. Talvez essa seja a informação mais importante de todo o post.

Após gerar a URL, vamos fazer uma chamada HTTP, utilizando o seguinte código (ainda em /Pages/Index.cshtml.cs, no método OnGet():):

using (var client = new HttpClient()){
    var response = client.GetAsync(uri).Result;
    response.EnsureSuccessStatusCode();
    TempData["NasaData"] = JsonConvert.DeserializeObject(
        response.Content.ReadAsStringAsync().Result
    );
}

Da mesma forma que com o QueryHelpers, é preciso uma biblioteca para utilizar o método JsonConvert, mas a essa altura, você já sabe como fazer isso.

Aqui, é importante notar a variável TempData. Essa variável é storage container e seus dados estarão disponíveis mesmo fora de nosso arquivo /Pages/Index.cshtml.cs, vamos utilizar essa capacidade mais a seguir. Se quiser saber mais sobre TempData: https://www.learnrazorpages.com/razor-pages/tempdata

No fim, nosso arquivo /Pages/Index.cshtml.cs ficou assim:

//Pages/Index.cshtml.cs:
using System;
using System.Collections.Generic;
using System.Net.Http;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace RazorProject.Pages
{
    public partial class IndexModel : PageModel
    {
        private readonly NasaConfiguration _nasa;
        private readonly ILogger<IndexModel> _logger;
        public IndexModel(ILogger<IndexModel> logger, IConfiguration config)
        {
            _nasa = config.GetSection("NasaConfiguration").Get<NasaConfiguration>();
            _logger = logger;
        }

        public void OnGet()
        {
            string endPoint = _nasa.Endpoint;
            string apiKey = _nasa.ApiKey;

            var uri = new Uri(
                QueryHelpers.AddQueryString(endPoint,
                    new Dictionary<string, string>(){
                        {"api_key", apiKey},
                        {"date", DateTime.Now.AddDays(-new Random().Next(365)).ToString("yyyy-MM-dd")}
                    }
                )
            );

            using (var client = new HttpClient())
            {
                var response = client.GetAsync(uri).Result;
                response.EnsureSuccessStatusCode();
                TempData["NasaData"] = JsonConvert.DeserializeObject(
                    response.Content.ReadAsStringAsync().Result
                );
            }
        }
    }
}

Neste momento, caso você execute a aplicação, o request na API já deve funcionar e você pode colocar alguns pontos de parada no código para entender melhor o funcionamento. E se tudo deu certo até aqui, podemos seguir para a ultima parte: Exibir os dados resultados da consulta na API.

// /Pages/Index.cshtml
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
    string url = ((dynamic)TempData["NasaData"])["url"];
    var date = (DateTime)(((dynamic)TempData["NasaData"])["date"]);
}

<div class="text-center">
    <h1 class="display-4">This was Nasa Picture of the day, of date @date.ToShortDateString()</h1>
    @foreach (dynamic item in (dynamic)TempData["NasaData"])
    {
        <p><b>@item.Name</b> : @item.Value</p>   
    }
    <img src="@url" />
</div>

No exemplo acima, estamos convertendo implicitamente a variavel TempData para dynamic e utilizando os dados retornados pela API. Esta parte é bem auto-explicativa e, repare que além dos atributos url e date, estamos deixando que todos os atributos coletados sejam exibidos, enquanto percorremos os atributos da variável (dynamic)TempData["NasaData"].

Tecle F5 e, se tudo funcionou, uma pagina semelhante a abaixo deve ser o resultado do projeto:

picture-of-the-day

Por enquanto é isso, num próximo post devemos falar sobre testes.

Link do projeto no Github: https://github.com/daniloalsilva/NasaApod

Fontes:

https://weblog.west-wind.com/posts/2017/dec/12/easy-configuration-binding-in-aspnet-core-revisited https://docs.microsoft.com/pt-br/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-3.1&tabs=visual-studio https://softchris.github.io/pages/dotnet-core.html

comments powered by Disqus