ASP .NET Core - Web API com SQL Server em um Contêiner Docker - I


Hoje veremos como criar uma Web API ASP.Net Core acessando o SQL Server e executar a aplicação em um contêiner Docker.

Hoje vamos iniciar uma série de artigos que aborda a utilização da criação de contêineres Docker para aplicações ASP .NET Core e para o Sql Server.

Objetivos e requisitos

Vamos criar uma aplicação ASP .NET Core WEb API para gerenciar informações sobre Produtos como : Id, Name, Description e Price, fazendo um CRUD básico no SQL Server, e vamos conteinerizar esta aplicação API.

Nesta aplicação vamos usar o EF Core, e vamos usar o SQL Server em um contêiner. Assim teremos dois contêineres : a aplicação Web API que vai acessar o contêiner do banco de dados SQL Server.

Os recursos que iremos usar são todos grátis:

A primeira a coisa a fazer é instalar ao .NET Core SDK 5.0 no seu ambiente.

Após a instalação, para verificar o ambiente abra uma janela do PowerShell e digite o comando:  dotnet --version

Com o .NET Core SDK instalado você pode instalar o Visual Studio 2019 Community que é grátis e a seguir instalar o Docker Desktop for Windows que também é grátis e iniciar o desenvolvimento.

Criando a Web API ASP .NET Core

Vamos criar uma solução em branco usando o template Blank Solution com o nome EFCoreDocker:

A seguir no menu File selecione Add-> New Project  e selecione o template ASP .NET Core Web API e clique em Next;

Informe o nome EFCoreSqlServer e clique em Next;

A seguir selecione o Target Framework, Authentication Type e demais configurações conforme mostrada na figura:

Note que não habilitamos o HTTPS para evitar ter que habilitar um certificado no contêiner.

Agora vamos remover os arquivos WeatherForecastController.cs e WeatherForecast.cs do projeto criado.

A seguir inclua no projeto uma referência ao pacote Microsoft.EntityFrameworkCore.SqlServer abrindo a janela Package Manager Console e digitando o comando:

install-package Microsoft.EntityFrameworkCore.SqlServer -Version 5.0.5

Crie uma pasta Entities no projeto e a seguir inclua nesta pasta a classe Product que representa a nossa entidade de domínio:

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Description { get; set; }
    }

Crie uma pasta DataContext e inclua nesta pasta a classe ApplicationDbContext :

using EFCoreSqlServer.Entities;
using Microsoft.EntityFrameworkCore;

namespace EFCoreSqlServer.DataContext
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
            this.Database.EnsureCreated();
        }

        public DbSet<Product> Products { get; set; }
    }
}

Definimos o arquivo de contexto que realiza o mapeamento ORM e permite definir as opções do contexto informando a string de conexão e o provedor do banco de dados.

O método Database.EnsureCreated() garante que o banco de dados para o contexto exista. Se ele existir, nenhuma ação será realizada. Se ele não existir, o banco de dados e todo o seu esquema serão criados.

Configurando o serviço e a string de conexão

Vamos registrar o contexto como um serviço no arquivo Startup e definir o provedor e a string de conexão com o SQL Server que iremos usar.

No método ConfigureServices inclua o código a seguir:

public void ConfigureServices(IServiceCollection services)
 {
            services.AddDbContext<ApplicationDbContext>(options => 
                 options.UseSqlServer(Configuration.GetConnectionString("SqlConnection")));
            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "EFCoreSqlServer", Version = "v1" });
            });
}

Agora abra o arquivo appsettings.json e crie a seção ConnectionStrings definindo a string de conexão com o nome do banco de dados, o usuário e a senha para acessar o SQL Server.

{
  "ConnectionStrings": {
    "SqlConnection": "Server=sqlserver;Database=ProductsDb;User Id=sa;Password=Numsey#2021"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

Criando a API - ProductsController

Agora, Inclua um novo Controller na pasta Controllers chamado ProductsController com o seguinte código:

using EFCoreSqlServer.DataContext;
using EFCoreSqlServer.Entities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace EFCoreSqlServer.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly ApplicationDbContext _productsDbContext;
        public ProductsController(ApplicationDbContext productsDbContext)
        {
            _productsDbContext = productsDbContext;
        }
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Product>>> GetProduct()
        {
            return Ok(await _productsDbContext.Products.ToListAsync());
        }
        [HttpPost]
        public async Task<ActionResult<Product>> Create([FromBody] Product product)
        {
            if (!ModelState.IsValid)
                return BadRequest(ModelState);
            await _productsDbContext.Products.AddAsync(product);
            await _productsDbContext.SaveChangesAsync();
            return Ok(product);
        }
        [HttpPut("{id}")]
        public async Task<ActionResult<Product>> Update(int id, [FromBody] Product productFromJson)
        {
            if (!ModelState.IsValid)
                return BadRequest(ModelState);
            var product = await _productsDbContext.Products.FindAsync(id);
            if (product == null)
            {
                return NotFound();
            }
            product.Name = productFromJson.Name;
            product.Price = productFromJson.Price;
            product.Description = productFromJson.Description;
            await _productsDbContext.SaveChangesAsync();
            return Ok(product);
        }
        [HttpDelete("{id}")]
        public async Task<ActionResult<Product>> Delete(int id)
        {
            var product = await _productsDbContext.Products.FindAsync(id);
            if (product == null)
            {
                return NotFound();
            }
            _productsDbContext.Remove(product);
            await _productsDbContext.SaveChangesAsync();
            return Ok(product);
        }
    }
}

No código da nossa API injetamos uma instância do contexto no construtor e assim usaremos essa instância para realizar as operações no SQL Server usando o EF Core.

A seguir criamos os métodos que definem os endpoints da API e realizam um CRUD onde podemos incluir, consultar, excluir e editar produtos.

Criando o Dockerfile e Docker Compose

Vamos agora criar o arquivo Dockerfile no projeto onde teremos os comandos para criar a imagem da nossa aplicação ASP .NET Core.

Após criar a imagem podemos criar o contêiner da aplicação a partir da imagem:

Esta imagem também pode ser publicada em um repositório central(Registry) e pode ser compartilhada em ambiente de desenvolvimento ou produção para utilização:

A seguir iremos criar o arquivo Docker Compose na solução que vai permitir orquestrar os contêineres da aplicação e do banco de dados SQL Server que iremos usar.

Podemos fazer todo esse processo manualmente mas podemos usar a integração do Visual Studio 2019 com o Docker Desktop for Windows e automatizar a maior parte dessa tarefa onde teremos a criação dos arquivos Dockerfile e docker-compose no projeto. A seguir basta somente fazer os ajustes necessários finais.

Vamos usar esta opção clicando com o botão direito do mouse sobre o nome do projeto e  a seguir acionando o menu Add que vai abrir a seguinte janela com as opções:

1- Container Orchestrator Support
2- Docker Support

Vamos clicar na primeira opção pois queremos gerar o Dockerfile e o arquivo docker-compose;

Na próxima janela selecione - Docker Compose - e clique em OK

Na sequência na opção Target OS escolha a opção Linux e clique em OK:

Ao final deste processo veremos na janela Solution Explorer os arquivos Dockerfile, docker-compose e .dockerignore criados:

Abaixo temos o código gerado para o arquivo Dockerfile que vai criar a imagem da aplicação :

FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["EFCoreSqlServer/EFCoreSqlServer.csproj", "EFCoreSqlServer/"]
RUN dotnet restore "EFCoreSqlServer/EFCoreSqlServer.csproj"
COPY . .
WORKDIR "/src/EFCoreSqlServer"
RUN dotnet build "EFCoreSqlServer.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "EFCoreSqlServer.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "EFCoreSqlServer.dll"]

Este arquivo não precisará sofrer nenhuma alteração e vai criar uma imagem para o nosso projeto ASP .NET Core.

A seguir temos o arquivo docker-compose onde incluímos o serviço - sqlserver - com as seguintes definições para criar o contêiner do SQL Server :

version: '3.4'
services:
  EFCoreSqlServer:
    image:  ${DOCKER_REGISTRY}productswebapi
    build:
      context: .
      dockerfile: EFCoreSqlServer/Dockerfile
    depends_on:
      - sqlserver
  sqlserver:
    image: microsoft/mssql-server-linux:2017-latest
    hostname: 'sqlserver'
    environment:
      ACCEPT_EULA: 'Y'
      SA_PASSWORD: "Numsey#2021"
    volumes:
      - c:\dados\volumes\mssql:/var/opt/mssql3
    ports:
      - '11433:1433'    
    expose:
      - 1433

Temos também o arquivo docker-compose.override.yml com o código abaixo:

version: '3.4'
services:
  EFCoreSqlServer:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
    ports:
      - "32033:80"

Neste código estamos mapeando a porta 32033 do host para a porta 80 do contêiner e definindo o ambiente como ambiente de desenvolvimento.

O docker-compose.override.yml é o arquivo de configuração onde você pode substituir as configurações existentes de docker-compose.yml ou até mesmo adicionar serviços completamente novos.

Por padrão, este arquivo não existe e você deve criá-lo manualmente. Como usamos os recursos integrados do VS 2019 ele criou o arquivo automaticamente.

Agora para concluir vamos selecionar o item docker-compose na janela solution explorer e teclar ALT+ENTER.

Isso abrirá a janela de propriedades do docker-compose. Aqui altere a opção Service URL conforme abaixo:

Assim ao iniciar a nossa aplicação a URL irá acionar : http://localhost:XXXX/api/produtos

Agora temos tudo pronto para executar o nosso projeto em um contêiner e usar a API para fazer o CRUD em produtos acessando o SQL Server.

Executando o projeto iremos visualizar a janela Containers no VS 2019 exibindo o contêiner da aplicação e do SQL Server em execução:

Podemos abrir o Dashboard do Docker Desktop no Windows e visualizar os contêineres em execução:

Para acessar a aplicação digite :  localhost:32033/api/products no navegador. Isso irá exibir a seguinte pagina :

Nada será exibido pois não populamos a tabela Products no SQL Server.

Vamos então na próxima parte do artigo usar o Postman para realizar as operações CRUD em nosso Contêiner.

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

"Os que confiam no SENHOR serão como o monte de Sião, que não se abala, mas permanece para sempre."
Salmos 125:1

Referências:


José Carlos Macoratti