Crear servicios back-end para aplicaciones móviles nativas con ASP.NET Core

Por James Montemagno

Las aplicaciones móviles pueden comunicarse con servicios back-end de ASP.NET Core. Para obtener instrucciones sobre cómo conectar servicios web locales desde simuladores de iOS y emuladores de Android, vea Connect to Local Web Services from iOS Simulators and Android Emulators (Conexión a servicios web locales desde simuladores de iOS y emuladores de Android).

Ver o descargar código de ejemplo de servicios back-end

La aplicación móvil nativa de ejemplo

En este tutorial se muestra cómo crear servicios de back-end con ASP.NET Core para admitir aplicaciones móviles nativas. Se utiliza la aplicación ToDoRest de Xamarin.Forms como cliente nativo, que incluye clientes nativos diferentes para Android, iOS y Windows. Puede seguir el tutorial vinculado para crear la aplicación nativa (e instalar las herramientas de Xamarin gratuitas necesarias) y descargar la solución de ejemplo de Xamarin. El ejemplo de Xamarin incluye un proyecto de servicios de ASP.NET Core Web API, que se reemplaza por la aplicación de ASP.NET Core de este artículo (sin necesidad de que el cliente realice cambios).

Aplicación ToDoRest que se ejecuta en un smartphone Android

Características

La aplicación TodoREST permite enumerar, agregar, eliminar y actualizar tareas pendientes. Cada tarea tiene un identificador, un nombre, notas y una propiedad que indica si ya se ha realizado.

En el ejemplo anterior, la vista principal de las tareas indica el nombre de cada tarea e indica si se ha realizado con una marca de verificación.

Al pulsar el icono + se abre un cuadro de diálogo para agregar un elemento:

Cuadro de diálogo para agregar un elemento

Al pulsar un elemento en la pantalla de la lista principal se abre un cuadro de diálogo de edición, donde se puede modificar el nombre del elemento, las notas y la configuración de Done (Listo), o se puede eliminar el elemento:

Cuadro de diálogo de edición del elemento

Para probarlo usted mismo con la aplicación de ASP.NET Core que va a crear en la siguiente sección ejecutándola en su equipo, debe actualizar la constante RestUrl de la aplicación.

Los emuladores de Android no se ejecutan en la máquina local y usan una dirección IP de bucle invertido (10.0.2.2) para comunicarse con la máquina local. Use DeviceInfo de Xamarin.Essentials para detectar qué sistema operativo se está ejecutando y usar la dirección URL correcta.

Vaya al proyecto TodoREST y abra el archivo Constants.cs. El archivo Constants.cs contiene la siguiente configuración:

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}";
    }
}

Opcionalmente, puede implementar el servicio web en un servicio en la nube, como Azure, y actualizar el valor de RestUrl.

Creación del proyecto de ASP.NET Core

Cree una aplicación web de ASP.NET Core en Visual Studio. Elija la plantilla de API web. Dele el nombre TodoAPI al proyecto.

Cuadro de diálogo Nueva aplicación web ASP.NET con la plantilla de proyecto API web seleccionada

La aplicación debe responder a todas las solicitudes realizadas al puerto 5000, incluido el tráfico HTTP no cifrado para nuestro cliente móvil. Actualice Startup.cs para que UseHttpsRedirection no se ejecute durante la fase de desarrollo:

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();
    });
}

Nota:

Ejecute la aplicación directamente, no de detrás de IIS Express. IIS Express ignora las solicitudes no locales de forma predeterminada. Ejecute dotnet run desde un símbolo del sistema o elija el perfil del nombre de aplicación en la lista desplegable Destino de depuración en la barra de herramientas de Visual Studio.

Agregue una clase de modelo para representar las tareas pendientes. Marque los campos obligatorios mediante el 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; }
    }
}

Los métodos de API necesitan alguna manera de trabajar con los datos. Use la misma interfaz de ITodoRepository que usa el ejemplo original de 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);
    }
}

En este ejemplo, la implementación usa solo una colección de elementos privada:

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 la implementación en Startup.cs:

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

Crear el controlador

Agregue un nuevo controlador para el proyecto, ToDoItemsController. Debe heredarse de ControllerBase. Agregue un atributo Route para indicar que el controlador controla las solicitudes realizadas a las rutas de acceso que comiencen con api/todoitems. El token [controller] de la ruta se sustituye por el nombre del controlador (si se omite el sufijo Controller) y es especialmente útil para las rutas globales. Obtenga más información sobre el enrutamiento.

El controlador necesita un ITodoRepository para funcionar. Solicite una instancia de este tipo a través del constructor del controlador. En tiempo de ejecución, esta instancia se proporciona con la compatibilidad del marco con la inserción de dependencias.

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

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

Esta API es compatible con cuatro verbos HTTP diferentes para realizar operaciones CRUD (creación, lectura, actualización, eliminación) en el origen de datos. La más simple de ellas es la operación de lectura, que corresponde a una solicitud HTTP GET.

Prueba de la API mediante curl

Puede probar el método de API mediante una variedad de herramientas. En este tutorial se usan las siguientes herramientas de línea de comandos de código abierto:

  • curl: transfiere datos mediante varios protocolos, como HTTP y HTTPS. curl se usa en este tutorial para llamar a la API mediante métodos HTTP GET, POST, PUT y DELETE.
  • jq: un procesador JSON usado en este tutorial para dar formato a datos JSON para que sea fácil de leer de la respuesta de la API.

Instalación de curl y jq

curl está preinstalado en macOS y se usa directamente dentro de la aplicación Terminal de macOS. Para obtener más información sobre cómo instalar curl, consulte el sitio web oficial de curl.

jq se puede instalar desde Homebrew en el terminal:

Instale Homebrew, si aún no está instalado, con el siguiente comando:

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

Siga las instrucciones presentadas por el instalador.

Instale jq mediante Homebrew con el siguiente comando:

brew install jq

Para obtener más información sobre la instalación de Homebrew y jq, consulte Homebrew y jq.

Leer elementos

La solicitud de una lista de elementos se realiza con una solicitud GET al método List. El atributo [HttpGet] en el método List indica que esta acción solo debe controlar las solicitudes GET. La ruta de esta acción es la ruta especificada en el controlador. No es necesario usar el nombre de acción como parte de la ruta. Solo debe asegurarse de que cada acción tiene una ruta única e inequívoca. El enrutamiento de atributos se puede aplicar tanto a los niveles de controlador como de método para crear rutas específicas.

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

En el terminal, llame al siguiente comando curl:

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

El comando curl anterior incluye los siguientes componentes:

  • -v: activa el modo detallado, proporciona información detallada sobre la respuesta HTTP y es útil para las pruebas de API y la solución de problemas.
  • -X GET: especifica el uso del método HTTP GET para la solicitud. Aunque curl a menudo puede deducir el método HTTP previsto, esta opción lo hace explícito.
  • 'http://localhost:5000/api/todoitems/': esta es la dirección URL de destino de la solicitud. En esta instancia, es un punto de conexión de API de REST.
  • | jq: este segmento no está relacionado directamente con curl. La canalización | es un operador de shell que toma la salida del comando a la izquierda y “lo canaliza” al comando a la derecha. jq es un procesador JSON de línea de comandos. Aunque no es necesario, jq facilita la lectura de los datos JSON devueltos.

El método List devuelve un código de respuesta 200 OK y todas las tareas pendientes, serializadas 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
  }
]

Crear elementos

Por convención, la creación de elementos de datos se asigna al verbo HTTP POST. El método Create tiene un atributo [HttpPost] aplicado y acepta una instancia TodoItem. Puesto que el argumento item se pasa en el cuerpo de la solicitud POST, este parámetro especifica el atributo [FromBody].

Dentro del método, se comprueba la validez del elemento y si existió anteriormente en el almacén de datos y, si no hay problemas, se agrega mediante el repositorio. Al comprobar ModelState.IsValid se realiza una validación de modelos, y debe realizarse en cada método de API que acepte datos proporcionados por usuario.

[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);
}

El ejemplo usa un elemento enum que contiene códigos de error que se pasan al cliente móvil:

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

En el terminal, pruebe a agregar nuevos elementos llamando al siguiente comando curl mediante el verbo POST y proporcionando el nuevo objeto en formato JSON en el cuerpo de la solicitud.

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

El comando curl anterior incluye las siguientes opciones:

  • --header 'Content-Type: application/json': establece el encabezado Content-Type en application/json, lo que indica que el cuerpo de la solicitud contiene datos JSON.
  • --data '{...}': envía los datos especificados en el cuerpo de la solicitud.

El método devuelve el elemento recién creado en la respuesta.

Actualizar elementos

La modificación de registros se realiza mediante solicitudes HTTP PUT. Aparte de este cambio, el método Edit es casi idéntico a Create. Si no se encuentra el registro, la acción Edit devuelve una respuesta 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 probar con curl, cambie el verbo a PUT. Especifique los datos actualizados del objeto en el cuerpo de la solicitud.

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 devuelve una respuesta NoContent (204) cuando se realiza correctamente, para mantener la coherencia con la API existente.

Eliminar elementos

La eliminación de registros se consigue mediante solicitudes DELETE al servicio y pasando el identificador del elemento que va a eliminar. Al igual que con las actualizaciones, las solicitudes para elementos que no existen reciben respuestas NotFound. De lo contrario, una solicitud correcta devuelve una respuesta 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();
}

Pruebe con curl cambiando el verbo HTTP a DELETE y anexando el identificador del objeto de datos que se va a eliminar al final de la dirección URL. No se requiere nada en el cuerpo de la solicitud.

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

Prevención del exceso de publicación

Actualmente, la aplicación de ejemplo expone todo el objeto TodoItem. Las aplicaciones de producción suelen limitar los datos que se escriben y se devuelven mediante un subconjunto del modelo. Hay varias razones para ello y la seguridad es una de las principales. El subconjunto de un modelo se suele conocer como un objeto de transferencia de datos (DTO), modelo de entrada o modelo de vista. En este artículo, se usa DTO.

Se puede usar un DTO para:

  • Evitar el exceso de publicación.
  • Ocultar las propiedades que los clientes no deben ver.
  • Omitir algunas propiedades para reducir el tamaño de la carga.
  • Acoplar los gráficos de objetos que contienen objetos anidados. Los gráficos de objetos acoplados pueden ser más cómodos para los clientes.

Para mostrar el enfoque de DTO, consulte Prevención del exceso de publicación.

Convenciones comunes de Web API

Al desarrollar los servicios back-end de la aplicación, necesitará acceder a un conjunto coherente de convenciones o directivas para controlar cuestiones transversales. Por ejemplo, en el servicio mostrado anteriormente, las solicitudes de registros específicos que no se encontraron recibieron una respuesta NotFound, en lugar de una respuesta BadRequest. De forma similar, los comandos realizados a este servicio que pasaron en tipos enlazados a un modelo siempre se comprobaron como ModelState.IsValid y devolvieron una BadRequest para los tipos de modelos no válidos.

Después de identificar una directiva común para las API, normalmente puede encapsularla en un filtro. Obtenga más información sobre cómo encapsular directivas de API comunes en aplicaciones de ASP.NET Core MVC.

Recursos adicionales