Condividi tramite


Creare servizi back-end per app native per dispositivi mobili con ASP.NET Core

Di James Montemagno

Le app per dispositivi mobili possono comunicare con i servizi back-end di ASP.NET Core. Per istruzioni sulla connessione di servizi Web locali da simulatori iOS e emulatori Android, vedere Connettersi ai servizi Web locali da emulatori Android e simulatori iOS.

Visualizzare o scaricare codice di esempio di servizi back-end

App nativa di esempio per dispositivi mobili

Questa esercitazione illustra come creare servizi back-end usando ASP.NET Core per supportare app per dispositivi mobili native. Usa un'app.NET MAUI come client nativo. L'esempio include un progetto di servizi API Web di base ASP.NET, che questo articolo illustra come compilare.

To Do Rest in esecuzione su uno smartphone Android

Funzionalità

L'app TodoREST supporta l'inserzione, l'aggiunta, l'eliminazione e l'aggiornamento di elementi todo. Ogni elemento ha un ID, un nome, note e una proprietà che indica se è ancora stata eseguita.

Nell'esempio precedente la visualizzazione principale degli elementi elenca il nome di ogni elemento e indica se è stata eseguita con un segno di spunta.

Toccando l'icona + si passa alla pagina aggiungi elemento:

Finestra di dialogo per l'aggiunta di elementi

Toccando un elemento nella pagina principale, si passa a una pagina di modifica in cui è possibile modificare il nome, le note e le impostazioni eseguite dell'elemento oppure eliminare l'elemento:

Finestra di dialogo di modifica dell'elemento

Per provarlo tu stesso con l'app ASP.NET Core creata nella prossima sezione, se la ospiti online, aggiorna la costante dell'app RestUrl. In caso contrario, l'app comunicherà con l'app ASP.NET Core ospitata localmente nel computer.

Gli emulatori Android non vengono eseguiti nel computer locale e usano un indirizzo IP di loopback (10.0.2.2) per comunicare con il computer locale. Usa la classe DeviceInfo di .NET MAUI per rilevare il sistema operativo su cui è in esecuzione l'applicazione e utilizzare l'URL corretto.

Passare al TodoREST progetto e aprire il Constants.cs file. Il Constants.cs file contiene la configurazione seguente.

namespace TodoREST
{
    public static class Constants
    {
        // URL of REST service
        //public static string RestUrl = "https://dotnetmauitodorest.azurewebsites.net/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 LocalhostUrl = DeviceInfo.Platform == DevicePlatform.Android ? "10.0.2.2" : "localhost";
        public static string Scheme = "https"; // or http
        public static string Port = "5001";
        public static string RestUrl = $"{Scheme}://{LocalhostUrl}:{Port}/api/todoitems/{{0}}";
    }
}

Facoltativamente, è possibile distribuire il servizio Web in un servizio cloud, ad esempio Azure e aggiornare .RestUrl

Creazione del progetto ASP.NET Core

Creare una nuova applicazione Web ASP.NET Core in Visual Studio. Scegliere il modello api Web. Assegnare al progetto il nome TodoAPI.

Finestra di dialogo Nuova applicazione Web ASP.NET Core con il modello di progetto API Web selezionato

L'app deve rispondere a tutte le richieste effettuate tramite HTTPS alla porta 5001.

Nota

Eseguire l'app direttamente, anziché dietro IIS Express. IIS Express ignora le richieste non locali per impostazione predefinita. Esegui dotnet run da un prompt dei comandi oppure seleziona il profilo del nome dell'app dall'elenco a discesa della Destinazione di debug sulla barra degli strumenti di Visual Studio.

Aggiungi una classe modello per rappresentare gli elementi todo. Contrassegnare i campi obbligatori con l'attributo [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; }
    }
}

I metodi API richiedono la definizione per lavorare con i dati. Usare la stessa ITodoRepository interfaccia usata dall'esempio:

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

Per questo esempio, l'implementazione del repository usa solo una raccolta privata di elementi:

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 Code",
                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);
        }
    }
}

Configurare l'implementazione in Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddSingleton<TodoAPI.Interfaces.ITodoRepository, TodoAPI.Services.TodoRepository>();
builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Creazione del controller

Aggiungere un nuovo controller al progetto, TodoItemsController. Deve ereditare da ControllerBase. Aggiungere un Route attributo per indicare che il controller gestisce le richieste effettuate ai percorsi che iniziano con api/todoitems. Il token [controller] nella route viene sostituito dal nome del controller (omettendo il suffisso Controller) ed è particolarmente utile per le route globali. Altre informazioni sul routing.

Per funzionare, il controller richiede un ITodoRepository. Richiedere un'istanza di questo tipo tramite il costruttore del controller. In fase di esecuzione, questa istanza viene fornita usando il supporto del framework per l'iniezione di dipendenze.

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

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

Questa API supporta quattro verbi HTTP diversi per eseguire operazioni CRUD (Create, Read, Update, Delete - Creazione, Lettura, Aggiornamento, Eliminazione) nell'origine dati. Il più semplice è l'operazione di lettura, che corrisponde a una richiesta HTTP GET .

Testare l'API usando curl

È possibile testare il metodo API usando un'ampia gamma di strumenti. Per questa esercitazione vengono usati gli strumenti da riga di comando open source seguenti:

  • curl: trasferisce i dati usando vari protocolli, tra cui HTTP e HTTPS. Curl viene usato in questa esercitazione per chiamare l'API usando i metodi GETHTTP , POST, PUTe DELETE.
  • jq: processore JSON usato in questa esercitazione per formattare i dati JSON in modo che sia facile leggere dalla risposta dell'API.

Installare curl e jq

curl è preinstallato in macOS e viene usato direttamente all'interno dell'applicazione terminale macOS. Per altre informazioni sull'installazione di curl, vedere il sito Web ufficiale curl.

Jq può essere installato da Homebrew dal terminale:

Installare Homebrew, se non è già installato, con il comando seguente:

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

Seguire le istruzioni presentate dal programma di installazione.

Installare jq usando Homebrew con il comando seguente:

brew install jq

Per altre informazioni sull'installazione di Homebrew e jq, vedere Homebrew e jq.

Lettura di elementi

La richiesta di un elenco di elementi viene eseguita con una richiesta GET al metodo List. L'attributo [HttpGet] del metodo List indica che questa azione deve gestire solo le richieste GET. La percorso per questa azione è il percorso specificato sul controller. Non è necessariamente necessario usare il nome dell'azione come parte del percorso. È sufficiente garantire che ogni azione disponga di una route univoca e non ambigua. Gli attributi di routing possono essere applicati sia a livello di controller che di metodo per definire route specifiche.

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

Nel terminale chiamare il comando curl seguente:

curl -v -X GET 'https://localhost:5001/api/todoitems/' | jq

Il comando curl precedente include i componenti seguenti:

  • -v: attiva la modalità verbosa, fornendo informazioni dettagliate sulla risposta HTTP ed è utile per testare e risolvere i problemi delle API.
  • -X GET: specifica l'uso del metodo HTTP GET per la richiesta. Anche se curl spesso può dedurre il metodo HTTP previsto, questa opzione lo rende esplicito.
  • 'https://localhost:5001/api/todoitems/': è l'URL di destinazione della richiesta. In questa istanza si tratta di un REST endpoint API.
  • | jq: questo segmento non è correlato direttamente a curl. La pipe | è un operatore della shell che accetta l'output dal comando a sinistra e lo indirizza al comando a destra. jq è un processore JSON della riga di comando. Anche se non è necessario, jq rende i dati JSON restituiti più facili da leggere.

Il List metodo restituisce un codice di risposta 200 OK e tutti gli elementi Todo, serializzati come 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 Code",
    "done": false
  },
  {
    "id": "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
    "name": "Publish apps",
    "notes": "All app stores",
    "done": false
  }
]

Creazione di elementi

Per convenzione, la creazione di nuovi elementi di dati viene mappata al verbo HTTP POST . Al Create metodo è applicato un [HttpPost] attributo e accetta un'istanza TodoItem . Poiché l'argomento item viene passato nel corpo di POST, questo parametro specifica l'attributo [FromBody] .

All'interno del metodo, l'elemento viene verificato per validità ed esistenza precedente nell'archivio dati. Se non si verificano problemi, l'elemento viene aggiunto tramite il repository. La verifica ModelState.IsValid esegue la convalida del modello e deve essere eseguita in ogni metodo dell'API che accetta input utente.

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

L'esempio usa un oggetto enum contenente i codici di errore passati al client mobile:

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

Nel terminale testare l'aggiunta di nuovi elementi chiamando il comando curl seguente usando il POST verbo e specificando il nuovo oggetto in formato JSON nel corpo della richiesta.

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

Il comando curl precedente include le opzioni seguenti:

  • --header 'Content-Type: application/json': imposta l'intestazione Content-Type su application/json, che indica che il corpo della richiesta contiene dati JSON.
  • --data '{...}': invia i dati specificati nel corpo della richiesta.

Il metodo restituisce l'elemento appena creato nella risposta.

Aggiornamento degli elementi

La modifica dei record viene ottenuta usando le richieste HTTP PUT . Fatta eccezione per questa modifica, il metodo Edit è quasi identico a Create. Se il record non viene trovato, l'azione Edit restituisce una NotFound risposta (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();
}

Per eseguire il test con curl, modificare il verbo in PUT. Specificare i dati dell'oggetto aggiornati nel Corpo della richiesta.

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

Se va a buon fine, questo metodo restituisce una risposta NoContent (204) per coerenza con l'API preesistente.

Eliminazione di elementi

L'eliminazione dei record viene eseguita effettuando DELETE richieste al servizio e passando l'ID dell'elemento da eliminare. Così come per gli aggiornamenti, le richieste di oggetti che non esistono ricevono NotFound risposte. In caso contrario, una richiesta con esito positivo restituisce una NoContent risposta (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();
}

Testare con curl modificando il verbo HTTP in DELETE e aggiungendo l'ID dell'oggetto dati da eliminare alla fine dell'URL. Non è necessario alcun elemento nel corpo della richiesta.

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

Impedire le pubblicazioni eccessive

Attualmente l'app di esempio espone l'intero TodoItem oggetto. Le app di produzione limitano in genere i dati di input e restituiti usando un subset del modello. Esistono diversi motivi alla base di questa situazione e la sicurezza è una delle principali. Il subset di un modello viene in genere definito DTO (Data Transfer Object), modello di input o modello di visualizzazione. DTO viene usato in questo articolo.

Un DTO può essere usato per:

  • Prevenire la pubblicazione eccessiva.
  • Nascondi le caratteristiche che i clienti non devono visualizzare.
  • Omettere alcune proprietà per ridurre le dimensioni del payload.
  • Appiattire gli oggetti grafici che contengono oggetti annidati. Gli oggetti grafici appiattiti possono essere più pratici per i client.

Per illustrare l'approccio DTO, consulta Impedire l'invio eccessivo

Convenzioni comuni dell'API Web

Man mano che si sviluppano i servizi back-end per l'app, è consigliabile creare un set coerente di convenzioni o criteri per la gestione delle problematiche trasversali. Nel servizio illustrato in precedenza, ad esempio, le richieste di record specifici che non sono state trovate hanno ricevuto una NotFound risposta, anziché una BadRequest risposta. Analogamente, i comandi inoltrati a questo servizio che trasmettono tipi associati al modello verificano sempre ModelState.IsValid e restituiscono BadRequest per i tipi di modello non validi.

Dopo aver identificato criteri comuni per le API è in genere possibile incapsulare tali criteri in un filtro. Altre informazioni su come incapsulare criteri comuni per le API nelle applicazioni ASP.NET Core MVC.

Vedere anche