Criar serviços de back-end para aplicativos móveis nativos com o ASP.NET Core

Por James Montemagno

Os aplicativos móveis podem se comunicar com os serviços de back-end do ASP.NET Core. Para obter instruções sobre como conectar os serviços Web locais dos simuladores do iOS e dos emuladores do Android, confira Conectar-se aos Serviços Web Locais em simuladores do iOS e emuladores do Android.

Exibir ou baixar o código de exemplo dos serviços de back-end

Exemplo do aplicativo móvel nativo

Este tutorial demonstra como criar serviços de back-end usando o ASP.NET Core para dar suporte a aplicativos móveis nativos. Ele usa o aplicativo Xamarin.Forms TodoRest como seu cliente nativo, que inclui clientes nativos separados para Android, iOS e Windows. Você pode seguir o tutorial vinculado para criar o aplicativo nativo (e instalar as ferramentas gratuitas necessárias do Xamarin) e baixar a solução de amostra do Xamarin. A amostra do Xamarin inclui um projeto de serviços da API Web do ASP.NET Core que o aplicativo ASP.NET Core deste artigo substitui (sem nenhuma alteração exigida do cliente).

Aplicativo ToDoRest em execução em um smartphone Android

Recursos

O aplicativoREST Todo dá suporte a listagem, adição, exclusão e atualização de itens de tarefas. Cada item tem uma ID, um Nome, Observações e uma propriedade que indica se ele já foi Concluído.

No exemplo anterior, a exibição principal dos itens lista o nome de cada item e indica se ele está concluído com uma marca de seleção.

Tocar no ícone + abre uma caixa de diálogo de adição de itens:

Caixa de diálogo de adição de itens

Tocar em um item na tela da lista principal abre uma caixa de diálogo de edição, na qual o Nome do item, Observações e configurações de Concluído podem ser modificados, ou o item pode ser excluído:

Caixa de diálogo de edição de itens

Para testá-lo por conta própria no aplicativo ASP.NET Core criado na próxima seção em execução no computador, atualize a constante RestUrl do aplicativo.

Os emuladores de Android não são executados no computador local e usam um IP de loopback (10.0.2.2) para se comunicar com o computador local. Use Xamarin.Essentials DeviceInfo para detectar qual operação o sistema está executando para usar o URL correto.

Navegue até o projeto TodoREST e abra o arquivo Constants.cs. O arquivo Constants.cs contém a configuração a seguir.

using Xamarin.Essentials;
using Xamarin.Forms;

namespace TodoREST
{
    public static class Constants
    {
        // URL of REST service
        //public static string RestUrl = "https://YOURPROJECT.azurewebsites.net:8081/api/todoitems/{0}";

        // URL of REST service (Android does not use localhost)
        // Use http cleartext for local deployment. Change to https for production
        public static string RestUrl = DeviceInfo.Platform == DevicePlatform.Android ? "http://10.0.2.2:5000/api/todoitems/{0}" : "http://localhost:5000/api/todoitems/{0}";
    }
}

Como opção, você pode implantar o serviço Web em um serviço de nuvem, como o Azure, e atualizar o RestUrl.

Criando o projeto ASP.NET Core

Crie um novo aplicativo Web do ASP.NET Core no Visual Studio. Escolha o modelo de API Web. Nomeie o projeto como TodoAPI.

Caixa de diálogo nova do aplicativo Web ASP.NET com modelo de projeto de Web API selecionado

O aplicativo deve responder a todas as solicitações feitas à porta 5000, incluindo o tráfego HTTP em texto simples para nosso cliente móvel. Atualize Startup.cs para que UseHttpsRedirection não seja executado no desenvolvimento:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        // For mobile apps, allow http traffic.
        app.UseHttpsRedirection();
    }

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

Observação

Execute o aplicativo diretamente, em vez de por trás do IIS Express. O IIS Express ignora solicitações não locais por padrão. Execute dotnet run em um prompt de comando ou escolha o perfil de nome do aplicativo no menu suspenso Destino de Depuração na barra de ferramentas do Visual Studio.

Adicione uma classe de modelo para representar itens pendentes. Marque os campos obrigatórios com o atributo [Required]:

using System.ComponentModel.DataAnnotations;

namespace TodoAPI.Models
{
    public class TodoItem
    {
        [Required]
        public string ID { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Notes { get; set; }

        public bool Done { get; set; }
    }
}

Os métodos da API exigem alguma maneira de trabalhar com dados. Use a mesma interface ITodoRepository nos usos de exemplo originais do Xamarin:

using System.Collections.Generic;
using TodoAPI.Models;

namespace TodoAPI.Interfaces
{
    public interface ITodoRepository
    {
        bool DoesItemExist(string id);
        IEnumerable<TodoItem> All { get; }
        TodoItem Find(string id);
        void Insert(TodoItem item);
        void Update(TodoItem item);
        void Delete(string id);
    }
}

Para esta amostra, a implementação apenas usa uma coleção particular de itens:

using System.Collections.Generic;
using System.Linq;
using TodoAPI.Interfaces;
using TodoAPI.Models;

namespace TodoAPI.Services
{
    public class TodoRepository : ITodoRepository
    {
        private List<TodoItem> _todoList;

        public TodoRepository()
        {
            InitializeData();
        }

        public IEnumerable<TodoItem> All
        {
            get { return _todoList; }
        }

        public bool DoesItemExist(string id)
        {
            return _todoList.Any(item => item.ID == id);
        }

        public TodoItem Find(string id)
        {
            return _todoList.FirstOrDefault(item => item.ID == id);
        }

        public void Insert(TodoItem item)
        {
            _todoList.Add(item);
        }

        public void Update(TodoItem item)
        {
            var todoItem = this.Find(item.ID);
            var index = _todoList.IndexOf(todoItem);
            _todoList.RemoveAt(index);
            _todoList.Insert(index, item);
        }

        public void Delete(string id)
        {
            _todoList.Remove(this.Find(id));
        }

        private void InitializeData()
        {
            _todoList = new List<TodoItem>();

            var todoItem1 = new TodoItem
            {
                ID = "6bb8a868-dba1-4f1a-93b7-24ebce87e243",
                Name = "Learn app development",
                Notes = "Take Microsoft Learn Courses",
                Done = true
            };

            var todoItem2 = new TodoItem
            {
                ID = "b94afb54-a1cb-4313-8af3-b7511551b33b",
                Name = "Develop apps",
                Notes = "Use Visual Studio and Visual Studio for Mac",
                Done = false
            };

            var todoItem3 = new TodoItem
            {
                ID = "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
                Name = "Publish apps",
                Notes = "All app stores",
                Done = false,
            };

            _todoList.Add(todoItem1);
            _todoList.Add(todoItem2);
            _todoList.Add(todoItem3);
        }
    }
}

Configure a implementação em Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<ITodoRepository, TodoRepository>();
    services.AddControllers();
}

Criando o controlador

Adicione um novo controlador ao projeto, TodoItemsController. Ele deve herdar de ControllerBase. Adicione um atributo Route para indicar que o controlador lida com solicitações feitas para caminhos que começam com api/todoitems. O token [controller] na rota é substituído pelo nome do controlador (com a omissão do sufixo Controller) e é especialmente útil para rotas globais. Saiba mais sobre o roteamento.

O controlador requer um ITodoRepository para a função; solicite uma instância desse tipo usando o construtor do controlador. No runtime, essa instância é fornecida usando o suporte da estrutura para injeção de dependências.

[ApiController]
[Route("api/[controller]")]
public class TodoItemsController : ControllerBase
{
    private readonly ITodoRepository _todoRepository;

    public TodoItemsController(ITodoRepository todoRepository)
    {
        _todoRepository = todoRepository;
    }

Essa API é compatível com quatro verbos HTTP diferentes para executar operações CRUD (Criar, Ler, Atualizar, Excluir) na fonte de dados. A mais simples delas é a operação de Leitura, que corresponde a uma solicitação HTTP GET.

Testar a API usando curl

Você pode testar o método da API usando uma variedade de ferramentas. Para este tutorial, são usadas as seguintes ferramentas de linha de comando de código aberto:

  • curl: transfere dados usando vários protocolos, inclusive HTTP e HTTPS. O curl é usado neste tutorial para chamar a API usando os métodos HTTP GET, POST, PUT e DELETE.
  • jq: um processador JSON usado neste tutorial para formatar os dados JSON de modo que sejam fáceis de ler na resposta da API.

Instalar o curl e o jq

O curl vem pré-instalado no macOS e é usado diretamente no aplicativo Terminal do macOS. Para obter mais informações sobre a instalação do curl, consulte o site Oficial do curl.

O jq pode ser instalado a partir do Homebrew no terminal:

Instale o Homebrew, se ainda não estiver instalado, com o seguinte comando:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Siga as instruções apresentadas pelo instalador.

Instale o jq usando o Homebrew com o seguinte comando:

brew install jq

Para obter mais informações sobre a instalação do Homebrew e do jq, consulte Homebrew e jq.

Lendo itens

A solicitação de uma lista de itens é feita com uma solicitação GET ao método List. O atributo [HttpGet] no método List indica que esta ação só deve lidar com as solicitações GET. A rota para esta ação é a rota especificada no controlador. Você não precisa necessariamente usar o nome da ação como parte da rota. Você precisa garantir que cada ação tem uma rota exclusiva e não ambígua. Os atributos de roteamento podem ser aplicados nos níveis de método e controlador para criar rotas específicas.

[HttpGet]
public IActionResult List()
{
    return Ok(_todoRepository.All);
}

No terminal, chame o seguinte comando curl:

curl -v -X GET 'http://localhost:5000/api/todoitems/' | jq

O comando curl anterior inclui os seguintes componentes:

  • -v: ativa o modo detalhado, fornecendo informações detalhadas sobre a resposta HTTP e é útil para testes de API e solução de problemas.
  • -X GET: especifica o uso do método HTTP GET para a solicitação. Embora o curl possa frequentemente inferir o método HTTP pretendido, essa opção o torna explícito.
  • 'http://localhost:5000/api/todoitems/': esse é o URL de destino da solicitação. Neste caso, é um ponto de extremidade da API REST.
  • | jq: esse segmento não está relacionado diretamente ao curl. O pipe | é um operador do shell que pega a saída do comando à sua esquerda e a "canaliza" para o comando à sua direita. jq é um processador de linha de comando JSON. Embora não seja necessário, jq facilita a leitura dos dados JSON retornados.

O método List retorna um código de resposta 200 OK e todos os itens Todo, serializados como JSON:

[
  {
    "id": "6bb8a868-dba1-4f1a-93b7-24ebce87e243",
    "name": "Learn app development",
    "notes": "Take Microsoft Learn Courses",
    "done": true
  },
  {
    "id": "b94afb54-a1cb-4313-8af3-b7511551b33b",
    "name": "Develop apps",
    "notes": "Use Visual Studio and Visual Studio for Mac",
    "done": false
  },
  {
    "id": "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
    "name": "Publish apps",
    "notes": "All app stores",
    "done": false
  }
]

Criando itens

Por convenção, a criação de novos itens de dados é mapeada para o verbo HTTP POST. O método Create tem um atributo [HttpPost] aplicado a ele e aceita uma instância TodoItem. Como o argumento item é passado no corpo do POST, esse parâmetro especifica o atributo [FromBody].

Dentro do método, o item é verificado quanto à validade e existência anterior no armazenamento de dados e, se nenhum problema ocorrer, ele será adicionado usando o repositório. A verificação de ModelState.IsValid executa a validação do modelo e deve ser feita em todos os métodos de API que aceitam a entrada do usuário.

[HttpPost]
public IActionResult Create([FromBody]TodoItem item)
{
    try
    {
        if (item == null || !ModelState.IsValid)
        {
            return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
        }
        bool itemExists = _todoRepository.DoesItemExist(item.ID);
        if (itemExists)
        {
            return StatusCode(StatusCodes.Status409Conflict, ErrorCode.TodoItemIDInUse.ToString());
        }
        _todoRepository.Insert(item);
    }
    catch (Exception)
    {
        return BadRequest(ErrorCode.CouldNotCreateItem.ToString());
    }
    return Ok(item);
}

A amostra usa uma enum que contém códigos de erro que são passados para o cliente móvel:

public enum ErrorCode
{
    TodoItemNameAndNotesRequired,
    TodoItemIDInUse,
    RecordNotFound,
    CouldNotCreateItem,
    CouldNotUpdateItem,
    CouldNotDeleteItem
}

No terminal, teste a adição de novos itens chamando o seguinte comando curl usando o verbo POST e fornecendo o novo objeto no formato JSON no Corpo da solicitação.

curl -v -X POST 'http://localhost:5000/api/todoitems/' \
--header 'Content-Type: application/json' \
--data '{
  "id": "6bb8b868-dba1-4f1a-93b7-24ebce87e243",
  "name": "A Test Item",
  "notes": "asdf",
  "done": false
}' | jq

O comando curl anterior inclui as seguintes opções:

  • --header 'Content-Type: application/json': define o cabeçalho Content-Type como application/json, indicando que o corpo da solicitação contém dados JSON.
  • --data '{...}': envia os dados especificados no corpo da solicitação.

O método retorna o item recém-criado na resposta.

Atualizando itens

A modificação de registros é feita por meio de solicitações HTTP PUT. Além desta mudança, o método Edit é quase idêntico ao Create. Se o registro não for encontrado, a ação Edit retorna uma resposta NotFound (404).

[HttpPut]
public IActionResult Edit([FromBody] TodoItem item)
{
    try
    {
        if (item == null || !ModelState.IsValid)
        {
            return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
        }
        var existingItem = _todoRepository.Find(item.ID);
        if (existingItem == null)
        {
            return NotFound(ErrorCode.RecordNotFound.ToString());
        }
        _todoRepository.Update(item);
    }
    catch (Exception)
    {
        return BadRequest(ErrorCode.CouldNotUpdateItem.ToString());
    }
    return NoContent();
}

Para testar com curl, altere o verbo para PUT. Especifique os dados do objeto atualizado no corpo da solicitação.

curl -v -X PUT 'http://localhost:5000/api/todoitems/' \
--header 'Content-Type: application/json' \
--data '{
  "id": "6bb8b868-dba1-4f1a-93b7-24ebce87e243",
  "name": "A Test Item",
  "notes": "asdf",
  "done": true
}' | jq

Este método retornará uma resposta NoContent (204) quando obtiver êxito, para manter a consistência com a API já existente.

Excluindo itens

A exclusão de registros é realizada por meio de solicitações DELETE ao serviço, passando a ID do item a ser excluído. Assim como nas atualizações, as solicitações de itens que não existem recebem respostas NotFound. Caso contrário, uma solicitação bem-sucedida retornará uma resposta NoContent (204).

[HttpDelete("{id}")]
public IActionResult Delete(string id)
{
    try
    {
        var item = _todoRepository.Find(id);
        if (item == null)
        {
            return NotFound(ErrorCode.RecordNotFound.ToString());
        }
        _todoRepository.Delete(id);
    }
    catch (Exception)
    {
        return BadRequest(ErrorCode.CouldNotDeleteItem.ToString());
    }
    return NoContent();
}

Teste com o curl alterando o verbo HTTP para DELETE e acrescentando a ID do objeto de dados a ser excluído no final do URL. Nada é necessário no corpo da solicitação.

curl -v -X DELETE 'http://localhost:5000/api/todoitems/6bb8b868-dba1-4f1a-93b7-24ebce87e243'

Impedir o excesso de postagem

Atualmente, o aplicativo de exemplo expõe todo o objeto TodoItem. Os aplicativos de produção normalmente limitam os dados alimentados e retornados pelo uso de um subconjunto do modelo. Há várias razões por trás disso, e a segurança é uma das principais. O subconjunto de um modelo geralmente é chamado de DTO (Objeto de Transferência de Dados), modelo de entrada ou modelo de exibição. O DTO é usado neste artigo.

Um DTO pode ser usado para:

  • Impedir o excesso de postagem.
  • Oculte propriedades que os clientes não deveriam visualizar.
  • Omita algumas propriedades para reduzir o tamanho do conteúdo.
  • Nivelar gráficos de objetos que contenham objetos aninhados. Os grafos de objeto nivelados podem ser mais convenientes para os clientes.

Para demonstrar a abordagem de DTO, consulte Impedir o excesso de postagem

Convenções de Web API comuns

Ao desenvolver os serviços de back-end para o seu aplicativo, você deverá criar um conjunto consistente de convenções ou políticas para lidar com questões transversais. Por exemplo, no serviço mostrado anteriormente, as solicitações de registros específicos que não foram encontrados receberam uma resposta NotFound, em vez de uma resposta BadRequest. Da mesma forma, os comandos feitos para esse serviço que passaram tipos associados a um modelo sempre verificaram ModelState.IsValid e retornaram um BadRequest para tipos de modelo inválidos.

Depois de identificar uma diretiva comum para suas APIs, você geralmente pode encapsulá-la em um filtro. Saiba mais sobre como encapsular políticas comuns da API em aplicativos ASP.NET Core MVC.

Recursos adicionais