Xamarin Form & HttpClient

Avendo un controller MVC e un'app basata su Xamarin Forms come faccio chiamate Get e Post per leggere e inviare dati?

Avendo un controller MVC e un'app basata su Xamarin Forms come faccio chiamate Get e Post per leggere e inviare dati?

Tempo fa ho creato una piccola classe base "ApiServiceBase" che contiene i metodi generici per eseguire le chiamate "Get" e "Post" verso il mio servizio REST.

La classe ApiService che estende la "ApiServiceBase" contiene i metodi specifici.

public class ApiService : ApiServiceBase
    {
        public async Task<TestClassDto> TestGet()
        {
            return await Get<TestClassDto>("TestGet");
        }

        public async Task<TestClassDto> TestPost(TestPostClassDto obj)
        {
            return await Post<TestClassDto, TestPostClassDto>("TestPost", obj);
        }

    }

public abstract class ApiServiceBase
    {
        private string BaseAddress = "https://192.168.100.112:7001/api/";
        private JsonSerializer _serializer = new JsonSerializer();
        HttpClient _httpClient;

        protected async Task<T> Get<T>(string methodName)
        {
            try
            {
                var response = await GetHttpClient().GetAsync($"{BaseAddress}{methodName}");
                response.EnsureSuccessStatusCode();

                using (var stream = await response.Content.ReadAsStreamAsync())
                using (var reader = new StreamReader(stream))
                using (var json = new JsonTextReader(reader))
                {
                    return _serializer.Deserialize<T>(json);
                }
            }
            catch (Exception ex)
            {
                return default(T);
            }
        }

        protected async Task<TRes> Post<TRes, TParam>(string methodName, TParam obj)
        {
            try
            {
                string jsonObj = JsonConvert.SerializeObject(obj);
                HttpContent httpContent = new StringContent(jsonObj, Encoding.UTF8);
                httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
                var response = await GetHttpClient().PostAsync($"{BaseAddress}{methodName}", httpContent);
                response.EnsureSuccessStatusCode();

                using (var stream = await response.Content.ReadAsStreamAsync())
                using (var reader = new StreamReader(stream))
                using (var json = new JsonTextReader(reader))
                {
                    return _serializer.Deserialize<TRes>(json);
                }
            }
            catch (Exception ex)
            {
                return default(TRes);
            }
        }

        private HttpClient GetHttpClient()
        {
            if (_httpClient != null)
                return _httpClient;
#if DEBUG
            var handler = new HttpClientHandler();
            handler.ServerCertificateCustomValidationCallback = (message, certificate, chain, sslPolicyErrors) => true;
            _httpClient = new HttpClient(handler);
#else
            _httpClient = new HttpClient();
#endif

            return _httpClient;
        }
    }

Il controller è il seguente:

[Route("api")]
    public class ApiController : Controller
    {
        [HttpGet]
        [Route("TestGet")]
        public async Task<JsonResult> TestGet()
        {
            return new JsonResult(new TestClassDto { Field1 = "f1" });
        }

        [HttpPost]
        [Route("TestPost")]
        public async Task<JsonResult> TestPost([FromBody]TestPostClassDto obj)
        {
            return new JsonResult(new TestClassDto { Field1 = "f1" + obj.Req });
        }

    }

Utilizzo:

public async Task Initialize()
{
    var get = await ApiService.TestGet();
    var post = await ApiService.TestPost(new TestPostClassDto { Req = "aa" });
}

Per completezza mi dilungo sull'architettura della "solution" e su alcuni punti fondamentali.

Raggiungere il servizio REST dall'emulatore Android

Prima di tutto, se il servizio REST parte su localhost, l'emulatore Android non potrà raggiungerlo. Per ovviare è necessario che i metodi del controller siano raggiungibili dall'IP della macchina. Saranno necessarie 2 operazioni.

Modifica del file "launchSettings.json"

Solitamente appeno creo un'applicazione web apro il file "launchSettings.json" e elimino la sezione relativa ad IIS. Abitudine.

Nel nostro caso deve essere modificato il valore della chiave "applicationUrl". Nel mio caso il risultato è il seguente:

{
  "profiles": {
    "MobileAppTemplate2021.BackOffice": {
      "commandName": "Project",
      "dotnetRunMessages": "true",
      "launchBrowser": true,
      "applicationUrl": "https://192.168.100.112:7001;http://192.168.100.112:7000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Modifica della configurazione di avvio "in debug"

Altra configurazione da impostare è in Proprietà del progetto che contiene il controller. 

Nella scheda "Debug" è necessario impostare "AppUrl" in modo coerenete con il tuo IP. Quindi nel mio caso ho sostituito "localhost" con "192.168.100.112".

Architettura

L'oggetto "TestClassDto" è l'oggetto POCO che ho utilizzato per l'invio e il ritorno dei dati. Per evitare di creare lo stesso oggetto sia nel progetto del servizio REST (.NET Core) che in quello Xamarin, ho creato un assembly  basato sul Framework 4.7.2 che è referenziato da entrambi i progetti (Client Xamarin e Server .NET Core).

Per utilizzare il servizio ApiService  all'interno di tutti i ViewModel, ho creato una variabile nel BaseViewModel:

public ApiService ApiService => DependencyService.Get<ApiService>();

Naturalmente è necessario registrare la dipendenza nel file App.xaml.cs:

DependencyService.Register<ApiService>();

Per chiudere cito uno dei problemi più frequenti quando si fa una richiesta http in un metodo asincrono.

La soluzione è:

Non dimenticare gli await!

Il problema è che se il metodo che esegue la chiamata termina prima della chiamata stessa, l'HttpClient chiude la connessione. Molto probabilmente non sarà sollevata nessuna eccezione. Nella migliore delle ipotesi si vedrà nella finestra di output di visual studio un'eccezione java praticamente incomprensibile. Il caso peggiore è se la chiamata avviene nel costruttore del viewmodel!

Al più utilizza Task.Run().