Comunicazione app / server - versioning, serializzazione JSON vs. binario

1

Mi scuso in anticipo per la lunghezza di questa domanda; richiede qualche spiegazione. Cercherò di renderlo il più chiaro possibile.

Sto sviluppando e gestendo un'app mobile (Xamarin, F #) che comunica con un'API Web (ASP.NET Core, F #). L'API lato server esiste solo per comunicare con l'app e eseguire operazioni di database e altre richieste HTTP in base alle esigenze. Il modello di dominio, per così dire, è più o meno esattamente lo stesso nell'app e nell'API. Ad esempio, sia l'app che l'API hanno un concetto di CardId con le stesse nozioni di ciò che costituisce esattamente un ID di carta valido.

Poiché le app non possono essere aggiornate istantaneamente, devo eseguire la versione dell'API in modo che gli utenti di versioni di app obsolete possano ancora utilizzare il servizio. Attualmente l'app e il server comunicano serializzando / deserializzando semplici tipi di DTO (inviati come payload JSON su HTTP) definiti in un assembly comune a cui fanno riferimento sia l'app che l'API. Ad esempio, un ID della carta è rappresentato come una semplice stringa. I tipi in questo assembly condiviso sono versionati e l'app e l'API convertono tra questi tipi e i tipi corrispondenti che usano internamente.

  • Dal lato app, l'app converte tra le rappresentazioni interne (definite negli assembly dell'app) e l'ultima versione di questi tipi (poiché l'app deve solo funzionare con una singola versione API).

  • Dal punto di vista dell'API è simile, ma l'API contiene convertitori per tutte le versioni dei tipi condivisi, poiché deve supportare anche le versioni precedenti.

Tutto ciò significa che qualsiasi concetto / tipo condiviso tra l'app e il server richiede almeno 3 tipi (per una singola versione): uno nell'app, uno nell'API e uno nella assembly condiviso da serializzare / deserializzare - così come 2-4 convertitori (1-2 ciascuno nell'API / API, a seconda che sia necessario ricevere / inviare i tipi o entrambi). Questo è molto robusto poiché disaccoppia la rappresentazione interna (e la logica che lavora su di essi) dalla comunicazione tra app e server, ma è molto dannoso. Inoltre, mi porta spesso a interrompere la logica dall'app: ad esempio, l'ID di una scheda nell'app viene modellato come una semplice stringa e l'API esegue la convalida e può restituire un codice di errore che indica "ID carta non valido".

Ora, quello che voglio fare è semplicemente condividere gli oggetti tra client e server. Se potessero in qualche modo condividere la memoria (assurdo, ma spiega il mio punto), ciò semplificherebbe molto le cose. Poiché sia il client che il server utilizzano la stessa piattaforma / lingua (.NET / F #), e questo è improbabile che cambi mai (in tal caso, si potrebbe sempre tornare alla soluzione attuale), ho preso in considerazione il crollo di tutte e tre le versioni di un digita (app, API e condivisa) in un singolo tipo (sempre versione, ovviamente) e semplicemente serializza / deserializza. Ad esempio, utilizzando una sorta di serializzazione binaria (non ho esperienza con questo).

Ciò garantisce che il server e l'app parlino realmente la stessa lingua e che i concetti di parità siano trattati allo stesso modo. Ad esempio, posso condividere oggetti complessi con garanzie su alcuni invarianti per costruzione (come il CardID sopra menzionato) e sapere che tutto l'invariante vale ancora una volta che l'ho deserializzato dall'altra parte, perché è lo stesso identico oggetto. Potrei quindi anche sbarazzarmi di diversi controlli lato server ed errori come "ID carta non valido" perché l'app non potrebbe mai costruire e inviare un ID carta non valido in primo luogo.

Significa anche che in tutta la mia app e API, ci sarebbero riferimenti a una versione specifica di oggetti, ma non penso che sarebbe peggio mantenere la soluzione corrente.

Tuttavia, non riesco a scuotere la sensazione che mi sarei in qualche modo sparato al piede, o che il controllo delle versioni sarebbe più difficile e fragile, anche se non riesco a individuare il motivo per cui mi sento così (forse si sente solo più robusto con oggetti dedicati che uso per la serializzazione / deserializzazione e conversione da / verso l'interno, ma mi sto ancora affidando alla serializzazione / deserializzazione di questi oggetti DTO, quindi perché non andare fino in fondo?). La domanda è quindi:

Alla luce della spiegazione di cui sopra, si condividono gli oggetti tra client e server utilizzando, ad es. la serializzazione binaria è una buona opzione, o ho perso alcuni chiari problemi con questo approccio? Ho forse perso un'altra opzione migliore interamente?

    
posta cmeeren 07.12.2017 - 11:37
fonte

2 risposte

2

La serializzazione binaria può essere molto fragile. Userò solo la serializzazione binaria in casi molto speciali in cui è necessaria l'economia che tale serializzazione fornirebbe (ad esempio i trasferimenti di file), non è necessario passare attraverso i firewall, si ha il controllo completo su entrambe le estremità della comunicazione e i dati il formato cambierà raramente.

Invio di JSON arbitrario sul filo; utilizzando Newtonsoft.JSON per deserializzare su dynamic , un dizionario di coppie chiave / valore o ExpandoObject ; e quindi mappare quei campi arbitrari a una rappresentazione concreta o utilizzarli direttamente; è una cosa perfettamente ragionevole da fare. Potenzialmente, è possibile includere alcuni metadati nella parte superiore del JSON che suggeriranno quale tipo di dati vengono trasmessi.

Potresti anche sperimentare qualcosa come i Protocol Buffers.

Se vuoi mantenere la tipizzazione statica, condividere le classi DTO tra il client e il server è inevitabile, a meno che tu non voglia scrivere un metodo factory che produca l'oggetto corretto.

    
risposta data 07.12.2017 - 20:44
fonte
0

Sono un po 'confuso dal tuo setup, lasciami il mio solito approccio:

  • Modelli anemici condivisi
  • Servizi logici client - esegui logica lato client
  • Client con versione - serializzato / deserializzato su JSON (o alternativo) e invia al server
  • Server con versione - deserializza su Modello e chiama il livello di servizio
  • Livello di servizio: esegue la logica aziendale, restituisce il modello

Tecnicamente ovviamente tutto è in versione. ma per la corrispondenza di client e server è davvero solo la comunicazione che conta.

Quando hai più di una versione live, hai diversi endpoint per ogni versione. (di cui il client con versione conosce e tratta automaticamente)

Ora la differenza chiave qui sembra essere che ho una logica diversa sul client e sul server. Tendo ad usare l'approccio ADM e codifico questa logica come servizi, ma potresti anche metterlo negli oggetti.

Non ho bisogno di condividere alcuna logica tra il server e il client perché fanno cose diverse. Se ho qualche logica condivisa, diciamo la convalida della carta di credito, la metterei in un servizio / dll separato e condividerla. Inseriscilo in altri servizi come richiesto.

Ora potrei prendere i dati serializzati e metterli direttamente in un oggetto / servizio condiviso, ma in realtà non salverei alcun codice.

Invece di de / serializzare nel client e nel server, avrei quel codice nel costruttore di oggetti. Mappatura a membri privati dell'oggetto anziché a membri pubblici del DTO.

Questo accoppierà strettamente la mia logica di de / serialization alle mie classi di business logic e tutto il salvataggio di Im è un codice di definizione di classe.

La mia sensazione è che la complessità della versione provenga dall'esecuzione di un codice server in grado di far fronte a più versioni. Ti suggerirei di dividerlo, creando una distribuzione del server per versione, in modo che il codice necessario riguardi solo la versione. Puoi rimuovere le vecchie versioni dal momento che l'aggiornamento del tuo client avviene lentamente.

Poi invece di più convertitori, hai solo più versioni dello stesso convertitore e la tua base di codice e il test sono notevolmente semplificati.

JSON vs Binary vs XML o qualcos'altro non ha importanza. .Net è gestito dalla memoria, quindi (per quanto ne so!) Non c'è modo di prendere un flusso binario, scriverlo direttamente in memoria e quindi chiamare quella memoria come se fosse una classe già istanziata.

Non puoi ignorare il costruttore e solo dire, quella cosa che hai appena scaricato, è un MyObject. Quindi, qualunque cosa tu faccia, devi sempre prendere i dati in entrata e dire che quel bit è una stringa e io la sto assegnando a MyObject.Id o qualsiasi altra cosa

    
risposta data 07.12.2017 - 12:40
fonte

Leggi altre domande sui tag