Condividi tramite


Procedure consigliate per la progettazione di un servizio negoziato

Seguire le indicazioni generali e le restrizioni documentate per le interfacce RPC per StreamJsonRpc.

Inoltre, le linee guida seguenti si applicano ai servizi negoziati.

Firme del metodo

Tutti i metodi devono accettare un CancellationToken parametro come ultimo parametro. Questo parametro in genere non deve essere un parametro facoltativo, quindi è meno probabile che i chiamanti omettano accidentalmente l'argomento. Anche se l'implementazione del metodo dovrebbe essere semplice, fornendo un consente CancellationToken al client di annullare la propria richiesta prima che venga trasmessa al server. Consente inoltre all'implementazione del server di evolversi in qualcosa di più costoso senza dover aggiornare il metodo per aggiungere l'annullamento come opzione in un secondo momento.

È consigliabile evitare più overload dello stesso metodo nell'interfaccia RPC. Anche se la risoluzione dell'overload funziona in genere (e i test devono essere scritti per verificare che lo faccia), si basa sul tentativo di deserializzare gli argomenti in base ai tipi di parametro di ogni overload, determinando la generazione di prime eccezioni di probabilità come parte regolare della selezione di un overload. Poiché si vuole ridurre al minimo il numero di eccezioni di prima probabilità generate nei percorsi di esito positivo, è preferibile avere semplicemente un solo metodo con un nome specificato.

Parametri e tipi restituiti

Tenere presente che tutti gli argomenti e i valori restituiti scambiati tramite RPC sono solo dati. Sono tutti serializzati e inviati in rete. Tutti i metodi definiti in questi tipi di dati operano solo su tale copia locale dei dati e non possono comunicare di nuovo con il servizio RPC che lo ha prodotto. Le uniche eccezioni a questo comportamento di serializzazione sono i tipi esotici per i quali StreamJsonRpc ha un supporto speciale.

Prendere in considerazione l'uso ValueTask<T> di over Task<T> come tipo restituito di metodi perché ValueTask<T> comporta un minor numero di allocazioni. Quando si usa l'varietà non generica (ad esempio, Task e ValueTask) è meno importante, ma ValueTask può comunque essere preferibile. Tenere presente le restrizioni di utilizzo su ValueTask<T> come documentato in tale API. Questo post di blog e il video possono essere utili per decidere anche il tipo da usare.

Tipi di dati personalizzati

Prendere in considerazione la definizione di tutti i tipi di dati non modificabili, che consente una condivisione più sicura dei dati in un processo senza copiare e contribuisce a rafforzare l'idea ai consumer che non possono modificare i dati ricevuti in risposta a una query senza posizionare un'altra RPC.

Definire i tipi di dati come class anziché struct quando si usa ServiceJsonRpcDescriptor.Formatters.UTF8, evitando così il costo del boxing (potenzialmente ripetuto) quando si usa Newtonsoft.Json. La conversione boxing non si verifica quando si usa ServiceJsonRpcDescriptor.Formatters.MessagePack in modo che gli struct siano un'opzione appropriata se si esegue il commit in tale formattatore.

Valutare l'implementazione e l'override IEquatable<T> GetHashCode() di metodi e Equals(Object) sui tipi di dati, che consentono al client di archiviare, confrontare e riutilizzare in modo efficiente i dati ricevuti in base al fatto che corrispondano ai dati ricevuti in un'altra volta.

Usare per supportare la DiscriminatedTypeJsonConverter<TBase> serializzazione di tipi polimorfici tramite JSON.

Raccolte

Usare le interfacce di raccolta readonly nelle firme del metodo RPC (ad esempio, IReadOnlyList<T>) anziché nei tipi concreti (ad esempio, List<T> o T[]), che consente una deserializzazione potenzialmente più efficiente.

Evita IEnumerable<T>. La mancanza di una Count proprietà comporta codice inefficiente e implica una possibile generazione tardiva di dati, che non si applica in uno scenario RPC. Usare IReadOnlyCollection<T> invece per le raccolte non ordinate o IReadOnlyList<T> per le raccolte ordinate.

Considerare IAsyncEnumerable<T>. Qualsiasi altro tipo di raccolta o IEnumerable<T> comporterà l'invio dell'intera raccolta in un unico messaggio. L'uso IAsyncEnumerable<T> di consente un piccolo messaggio iniziale e fornisce al ricevitore il mezzo per ottenere il numero di elementi dalla raccolta desiderato, enumerandolo in modo asincrono. Altre informazioni su questo modello di romanzo.

Schema Observer

Prendere in considerazione l'uso del modello di progettazione observer nell'interfaccia. Si tratta di un modo semplice per il client di sottoscrivere i dati senza i molti insidi che si applicano al modello di eventing tradizionale descritto nella sezione successiva.

Il modello observer può essere semplice come segue:

Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);

I IDisposable tipi e IObserver<T> usati in precedenza sono due dei tipi esotici in StreamJsonRpc, quindi ottengono un comportamento di marshalling speciale anziché essere serializzati come semplici dati.

Eventi

Gli eventi possono essere problematici rispetto a RPC per diversi motivi e si consiglia invece il modello di osservatore descritto in precedenza.

Tenere presente che il servizio non ha visibilità sul numero di gestori eventi collegati dal client quando il servizio e il client si trovano in processi separati. JsonRpc collega sempre esattamente un gestore responsabile della propagazione dell'evento al client. Il client può avere zero o più gestori collegati sul lato lontano.

La maggior parte dei client RPC non avrà gestori eventi cablati quando sono connessi per la prima volta. Evitare di generare il primo evento fino a quando il client ha richiamato un metodo "Subscribe*" sull'interfaccia per indicare l'interesse e l'idoneità a ricevere eventi.

Se l'evento indica un delta nello stato (ad esempio, un nuovo elemento aggiunto a una raccolta), è consigliabile generare tutti gli eventi passati o descrivere tutti i dati correnti come se fosse nuovo nell'argomento evento quando un client sottoscrive per aiutarli a "sincronizzarsi" con codice di gestione degli eventi.

Prendere in considerazione l'accettazione di argomenti aggiuntivi nel metodo "Subscribe*" indicato in precedenza se il client potrebbe voler esprimere interesse in un subset di dati o notifiche, per ridurre il traffico di rete e la CPU necessari per inoltrare queste notifiche.

Si consiglia di non offrire un metodo che restituisca il valore corrente se si espone anche un evento per ricevere notifiche di modifica o sconsigliare attivamente ai client di usarlo in combinazione con l'evento. Un client che sottoscrive un evento per i dati e chiama un metodo per ottenere il valore corrente è in competizione con le modifiche apportate a tale valore e manca un evento di modifica o non sa come riconciliare un evento di modifica in un thread con il valore ottenuto in un altro thread. Questo problema è generale per qualsiasi interfaccia, non solo quando è su RPC.

Convenzioni di denominazione

  • Usare il Service suffisso nelle interfacce RPC e un prefisso semplice I .
  • Non usare il Service suffisso per le classi nell'SDK. Il wrapper RPC o la libreria deve usare un nome che descrive esattamente le operazioni che esegue, evitando il termine "servizio".
  • Evitare il termine "remoto" nei nomi di interfaccia o membri. Ricordare che i servizi negoziati si applicano idealmente tanto negli scenari locali quanto quelli remoti.

Problemi di compatibilità delle versioni

Si vuole che qualsiasi servizio broker specificato esposto ad altre estensioni o esposto su Live Share sia compatibile con le versioni precedenti e precedenti, il che significa che un client potrebbe essere precedente o più recente del servizio e che la funzionalità deve essere approssimativamente uguale a quella delle due versioni applicabili.

Prima di tutto, esaminiamo la terminologia delle modifiche che causano un'interruzione:

  • Modifica binaria che causa un'interruzione binaria: una modifica dell'API che provocherebbe l'errore di associazione di altro codice gestito rispetto a una versione precedente dell'assembly a quella nuova. Alcuni esempi:

    • Modifica della firma di un membro pubblico esistente.
    • Ridenominazione di un membro pubblico.
    • Rimozione di un tipo pubblico.
    • Aggiunta di un membro astratto a un tipo o a qualsiasi membro a un'interfaccia.

    Tuttavia, le modifiche seguenti non sono di rilievo binarie:

    • Aggiunta di un membro non astratto a una classe o a uno struct.
    • Aggiunta di un'implementazione completa (non astratta) dell'interfaccia a un tipo esistente.
  • Modifica che causa un'interruzione del protocollo: modifica alla forma serializzata di alcuni tipi di dati o chiamata al metodo RPC in modo che la parte remota non possa deserializzare ed elaborarla correttamente. Alcuni esempi:

    • Aggiunta di parametri obbligatori a un metodo RPC.
    • La rimozione di un membro da un tipo di dati garantito in precedenza non null.
    • Aggiunta di un requisito che deve essere effettuata una chiamata al metodo prima di altre operazioni preesistenti.
    • Aggiunta, rimozione o modifica di un attributo in un campo o proprietà che controlla il nome serializzato dei dati in tale membro.
    • (MessagePack): modifica della proprietà o KeyAttribute dell'intero DataMemberAttribute.Order di un membro esistente.

    Di seguito, tuttavia, non vengono apportate modifiche che causano un'interruzione del protocollo:

    • Aggiunta di un membro facoltativo a un tipo di dati.
    • Aggiunta di membri alle interfacce RPC.
    • Aggiunta di parametri facoltativi ai metodi esistenti.
    • Modifica di un tipo di parametro che rappresenta un numero intero o float a uno con lunghezza o precisione maggiore, int ad esempio su long o float su double.
    • Ridenominazione di un parametro. Questa tecnicamente causa un'interruzione per i client che usano argomenti denominati JSON-RPC, ma i client che usano gli ServiceJsonRpcDescriptor argomenti posizionali per impostazione predefinita e non sono interessati da una modifica del nome di parametro. Ciò non ha nulla a che fare con se il codice sorgente client usa la sintassi degli argomenti denominati, a cui un parametro rinomina sarebbe una modifica che causa un'interruzione di origine.
  • Modifica di rilievo comportamentale: modifica all'implementazione di un servizio negoziato che aggiunge o modifica il comportamento in modo che i client meno recenti potrebbero non funzionare correttamente. Alcuni esempi:

    • Non inizializzare più un membro di un tipo di dati che è stato sempre inizializzato in precedenza.
    • Generazione di un'eccezione in una condizione che in precedenza poteva essere completata correttamente.
    • Restituzione di un errore con un codice di errore diverso rispetto a quello restituito in precedenza.

    Tuttavia, le seguenti non sono modifiche di rilievo comportamentali:

    • Generazione di un nuovo tipo di eccezione (perché tutte le eccezioni vengono incluse in RemoteInvocationException ogni caso).

Quando sono necessarie modifiche che causano un'interruzione, possono essere apportate in modo sicuro registrando e offrendo un nuovo moniker del servizio. Questo moniker può condividere lo stesso nome, ma con un numero di versione superiore. L'interfaccia RPC originale potrebbe essere riutilizzabile se non è presente alcuna modifica di rilievo binaria. In caso contrario, definire una nuova interfaccia per la nuova versione del servizio. Evitare di interrompere i vecchi client continuando a registrare, proffer e supportare anche la versione precedente.

Si vuole evitare tutte queste modifiche di rilievo, ad eccezione dell'aggiunta di membri alle interfacce RPC.

Aggiunta di membri alle interfacce RPC

Non aggiungere membri a un'interfaccia di callback client RPC, poiché molti client possono implementare tale interfaccia e aggiungere membri generano l'eccezione CLR TypeLoadException quando tali tipi vengono caricati ma non implementano i nuovi membri dell'interfaccia. Se è necessario aggiungere membri da richiamare su una destinazione di callback client RPC, definire una nuova interfaccia (che può derivare dall'originale) e quindi seguire il processo standard per il proffering del servizio negoziato con un numero di versione incrementato e offrire un descrittore con il tipo di interfaccia client aggiornato specificato.

È possibile aggiungere membri alle interfacce RPC che definiscono un servizio negoziato. Questa non è una modifica che causa un'interruzione del protocollo ed è solo una modifica di rilievo binaria a quelle che implementano il servizio, ma presumibilmente si sta aggiornando il servizio per implementare anche il nuovo membro. Poiché le linee guida sono che nessuno deve implementare l'interfaccia RPC ad eccezione del servizio negoziato stesso (e i test devono usare framework fittizi), l'aggiunta di un membro a un'interfaccia RPC non deve interrompere nessuno.

Questi nuovi membri devono avere commenti della documentazione xml che identificano la versione del servizio aggiunta per la prima volta a tale membro. Se un client più recente chiama il metodo su un servizio meno recente che non implementa il metodo , tale client può intercettare RemoteMethodNotFoundException. Ma il client può (e probabilmente dovrebbe) prevedere l'errore ed evitare la chiamata in primo luogo. Le procedure consigliate per l'aggiunta di membri ai servizi esistenti includono:

  • Se si tratta della prima modifica all'interno di una versione del servizio: aggiornare la versione secondaria nel moniker del servizio quando si aggiunge il membro e si dichiara il nuovo descrittore.
  • Aggiornare il servizio per registrare e aggiornare la nuova versione oltre alla versione precedente.
  • Se si dispone di un client del servizio negoziato, aggiornare il client per richiedere la versione più recente e il fallback per richiedere la versione precedente se quello più recente torna come Null.