Eseguire la migrazione dalla versione 3.x alla versione 7.0 di Orleans

Orleans 7.0 introduce diverse modifiche utili, tra cui miglioramenti per hosting, serializzazione personalizzata, immutabilità e astrazioni dei grani.

Migrazione

Non è possibile eseguire facilmente la migrazione di applicazioni esistenti che usano promemoria, flussi o persistenza dei grani a Orleans 7.0 a causa di modifiche nel modo in cui Orleans identifica grani e flussi. Prevediamo di offrire in modo incrementale un percorso di migrazione per queste applicazioni.

Le applicazioni che eseguono versioni precedenti di Orleans non possono essere aggiornate senza problemi tramite un aggiornamento in sequenza a Orleans 7.0. Pertanto, è necessario usare una strategia di aggiornamento diversa, ad esempio la distribuzione di un nuovo cluster e la rimozione del cluster precedente. Orleans 7.0 modifica il protocollo di trasmissione in modo incompatibile, il che significa che i cluster non possono contenere una combinazione di host Orleans 7.0 e host che eseguono versioni precedenti di Orleans.

Abbiamo evitato tali modifiche di rilievo per molti anni, anche per le versioni principali, quindi perché ora? I motivi principali sono due: identità e serializzazione. Per quanto riguarda le identità, le identità di grani e flussi sono ora costituite da stringhe, che consentono ai grani di codificare correttamente le informazioni sui tipi generici e ai flussi di eseguire il mapping più facilmente al dominio dell'applicazione. I tipi di grani sono stati identificati in precedenza mediante una struttura di dati complessa che non poteva rappresentare grani generici, con conseguenti casi limite. I flussi erano identificati da uno spazio dei nomi string e da una chiave Guid, difficili da mappare al dominio dell'applicazione per gli sviluppatori, ma efficienti. La serializzazione è ora a tolleranza di versione, ovvero è possibile modificare i tipi in determinati modi compatibili, seguendo un set di regole, ed essere certi di poter aggiornare l'applicazione senza errori di serializzazione. Questo aspetto era particolarmente problematico quando i tipi di applicazione venivano salvati in modo permanente nei flussi o in un archivio di grani. Le sezioni seguenti illustrano in modo più dettagliato le modifiche principali.

Modifiche alla creazione di pacchetti

Se si aggiorna un progetto a Orleans 7.0, sarà necessario eseguire le azioni seguenti:

  • Tutti i client devono fare riferimento a Microsoft.Orleans.Client.
  • Tutti i silos (server) devono fare riferimento a Microsoft.Orleans.Server.
  • Tutti gli altri pacchetti devono fare riferimento a Microsoft.Orleans.Sdk.
  • Rimuovere tutti i riferimenti a Microsoft.Orleans.CodeGenerator.MSBuild e Microsoft.Orleans.OrleansCodeGenerator.Build.
    • Sostituire gli utilizzi di KnownAssembly con GenerateCodeForDeclaringAssemblyAttribute.
    • Il pacchetto Microsoft.Orleans.Sdk fa riferimento al pacchetto di Generatore di origini C# (Microsoft.Orleans.CodeGenerator).
  • Rimuovere tutti i riferimenti a Microsoft.Orleans.OrleansRuntime.
  • Rimuovere le chiamate a ConfigureApplicationParts. Le parti dell'applicazione sono state rimosse. Generatore di origini C# per Orleans viene aggiunto a tutti i pacchetti (inclusi il client e il server) e genererà automaticamente l'equivalente delle parti dell'applicazione.
  • Sostituire i riferimenti a Microsoft.Orleans.OrleansServiceBus con Microsoft.Orleans.Streaming.EventHubs
  • Se si usano promemoria, aggiungere un riferimento a Microsoft.Orleans.Reminders
  • Se si usano flussi, aggiungere un riferimento a Microsoft.Orleans.Streaming

Suggerimento

Tutti gli esempi di Orleans sono stati aggiornati a Orleans 7.0 e possono essere usati come riferimento per le modifiche apportate. Per altre informazioni, vedere il problema di Orleans #8035 in cui sono indicate in dettaglio le modifiche apportate a ogni esempio.

DirettiveOrleansglobal using

Tutti i progetti Orleans fanno riferimento direttamente o indirettamente al pacchetto NuGet Microsoft.Orleans.Sdk. Quando un progetto Orleans è configurato per abilitare usi impliciti (ad esempio <ImplicitUsings>enable</ImplicitUsings>), gli spazi dei nomi Orleans e Orleans.Hosting vengono usati in modo implicito. Ciò significa che il codice dell'app non richiede queste direttive.

Per altre informazioni, vedere ImplicitUsings e dotnet/orleans/src/Orleans.Sdk/build/Microsoft.Orleans.Sdk.targets.

Hosting

Il tipo ClientBuilder è stato sostituito con un metodo di estensione UseOrleansClient in IHostBuilder. Il tipo IHostBuilder proviene dal pacchetto NuGet Microsoft.Extensions.Hosting. Ciò significa che è possibile aggiungere un client Orleans a un host esistente senza dover creare un contenitore di inserimento delle dipendenze separato. Il client si connette al cluster durante l'avvio. Al termine di IHost.StartAsync, il client verrà connesso automaticamente. I servizi aggiunti a IHostBuilder vengono avviati nell'ordine di registrazione, quindi la chiamata a UseOrleansClient prima di chiamare ConfigureWebHostDefaults assicura che Orleans venga avviato prima dell'avvio di ASP.NET Core, ad esempio, consentendo l'accesso immediato al client dall'applicazione ASP.NET Core.

Se si vuole emulare il comportamento di ClientBuilder precedente, è possibile creare un HostBuilder separato e configurarlo con un client Orleans. Per IHostBuilder si può configurare un client Orleans o un silo Orleans. Tutti i silos registrano un'istanza di IGrainFactory e IClusterClient utilizzabile dall'applicazione, quindi la configurazione separata di un client non è necessaria e non è supportata.

Modifica della firma di OnActivateAsync e OnDeactivateAsync

Orleans consente ai grani di eseguire codice durante l'attivazione e la disattivazione. Questa funzionalità può essere usata per eseguire attività quali la lettura dello stato dai messaggi del ciclo di vita di archivi o log. In Orleans 7.0, la firma di questi metodi del ciclo di vita è cambiata:

  • OnActivateAsync() ora accetta un parametro CancellationToken. Quando CancellationToken viene annullato, il processo di attivazione deve essere abbandonato.
  • OnDeactivateAsync() ora accetta un parametro DeactivationReason e un parametro CancellationToken. DeactivationReason indica il motivo per cui l'attivazione viene disattivata. Gli sviluppatori devono usare queste informazioni a scopo di registrazione e diagnostica. Quando CancellationToken viene annullato, il processo di disattivazione deve essere completato tempestivamente. Si noti che poiché qualsiasi host può smettere di funzionare in qualsiasi momento, non è consigliabile fare affidamento su OnDeactivateAsync per eseguire azioni importanti, come salvare in modo permanente lo stato critico.

Si consideri l'esempio seguente di un grano che esegue l'override di questi nuovi metodi:

public sealed class PingGrain : Grain, IPingGrain
{
    private readonly ILogger<PingGrain> _logger;

    public PingGrain(ILogger<PingGrain> logger) =>
        _logger = logger;

    public override Task OnActivateAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("OnActivateAsync()");
        return Task.CompletedTask;
    }

    public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token)
    {
        _logger.LogInformation("OnDeactivateAsync({Reason})", reason);
        return Task.CompletedTask;
    }

    public ValueTask Ping() => ValueTask.CompletedTask;
}

Grani POCO e IGrainBase

I grani in Orleans non devono più ereditare dalla classe di base Grain o da qualsiasi altra classe. Questa funzionalità è nota come grani POCO. Per accedere ai metodi di estensione, ad esempio uno dei seguenti:

Il grano deve implementare IGrainBase o ereditare da Grain. Di seguito è riportato un esempio di implementazione di IGrainBase in una classe di grani:

public sealed class PingGrain : IGrainBase, IPingGrain
{
    public PingGrain(IGrainContext context) => GrainContext = context;

    public IGrainContext GrainContext { get; }

    public ValueTask Ping() => ValueTask.CompletedTask;
}

IGrainBase definisce anche OnActivateAsync e OnDeactivateAsync con implementazioni predefinite, consentendo al grano di partecipare al ciclo di vita, se necessario:

public sealed class PingGrain : IGrainBase, IPingGrain
{
    private readonly ILogger<PingGrain> _logger;

    public PingGrain(IGrainContext context, ILogger<PingGrain> logger)
    {
        _logger = logger;
        GrainContext = context;
    }

    public IGrainContext GrainContext { get; }

    public Task OnActivateAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("OnActivateAsync()");
        return Task.CompletedTask;
    }

    public Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token)
    {
        _logger.LogInformation("OnDeactivateAsync({Reason})", reason);
        return Task.CompletedTask;
    }

    public ValueTask Ping() => ValueTask.CompletedTask;
}

Serializzazione

La modifica più complessa in Orleans 7.0 è l'introduzione del serializzatore a tolleranza di versione. Questa modifica è stata apportata perché le applicazioni tendono a evolversi e questo ha portato a un problema significativo per gli sviluppatori, poiché il serializzatore precedente non tollerava l'aggiunta di proprietà ai tipi esistenti. D'altra parte, il serializzatore era flessibile, consentendo agli sviluppatori di rappresentare la maggior parte dei tipi .NET senza modifiche, incluse funzionalità come generics, polimorfismo e rilevamento dei riferimenti. La sostituzione era attesa da tempo, ma gli utenti hanno ancora bisogno della rappresentazione ad alta fedeltà dei loro tipi. Pertanto, in Orleans 7.0 è stato introdotto un serializzatore sostitutivo che supporta la rappresentazione ad alta fedeltà dei tipi .NET, consentendo al tempo stesso anche l'evoluzione dei tipi. Il nuovo serializzatore è molto più efficiente del serializzatore precedente, con una velocità effettiva end-to-end fino al 170% superiore.

Per altre informazioni, vedere gli articoli seguenti in relazione a Orleans 7.0:

Identità dei grani

Ogni grano ha un'identità univoca costituita dal tipo di grano e dalla relativa chiave. Le versioni precedenti di Orleans utilizzavano un tipo composto per i GrainId per supportare le chiavi per i tipi:

Ciò comporta una certa complessità quando si tratta di gestire le chiavi dei grani. Le identità dei grani sono costituite da due componenti: un tipo e una chiave. Il componente del tipo in precedenza era costituito da un codice di tipo numerico, una categoria e 3 byte di informazioni sul tipo generico.

Le identità dei grani hanno ora il formato type/key, in cui sia type che key sono stringhe. L'interfaccia per le chiavi dei grani più comunemente usata è IGrainWithStringKey. Questo semplifica notevolmente il funzionamento dell'identità dei grani e migliora il supporto per i tipi di grani generici.

Le interfacce per i grani, inoltre, sono ora rappresentate usando un nome leggibile, anziché una combinazione di codice hash e rappresentazione stringa di qualsiasi parametro di tipo generico.

Il nuovo sistema è più personalizzabile e queste personalizzazioni possono essere gestite con gli attributi.

  • GrainTypeAttribute(String) per una class di grano specifica la parte Tipo del relativo ID del grano.
  • DefaultGrainTypeAttribute(String) per una interface di grano specifica la parte Tipo del grano che IGrainFactory deve risolvere per impostazione predefinita quando si ottiene un riferimento al grano. Ad esempio, quando si chiama IGrainFactory.GetGrain<IMyGrain>("my-key"), la factory del grano restituirà un riferimento a "my-type/my-key" del grano se per IMyGrain viene specificato l'attributo precedente.
  • GrainInterfaceTypeAttribute(String) consente di eseguire l'override del nome dell'interfaccia. Se si specifica un nome in modo esplicito usando questo meccanismo, è possibile rinominare il tipo di interfaccia senza interrompere la compatibilità con i riferimenti ai grani esistenti. Si noti che l'interfaccia deve avere anche AliasAttribute in questo caso, poiché la relativa identità può essere serializzata. Per altre informazioni sulla specifica di un alias di tipo, vedere la sezione sulla serializzazione.

Come accennato in precedenza, l'override dei nomi di interfaccia e di classe di grani predefiniti per i tipi consente di rinominare i tipi sottostanti senza interrompere la compatibilità con le distribuzioni esistenti.

Identità dei flussi

Nella prima versione dei flussi di Orleans, i flussi potevano essere identificati solo usando un Guid. Si trattava di un meccanismo efficiente in termini di allocazione di memoria, ma era difficile per gli utenti creare identità dei flussi significative e spesso era necessario ricorrere alla codifica o ai riferimenti indiretti per determinare l'identità del flusso appropriata per uno scopo specifico.

In Orleans 7.0 i flussi vengono ora identificati usando stringhe. Lo structOrleans.Runtime.StreamId contiene tre proprietà: StreamId.Namespace, StreamId.Key e StreamId.FullKey. Questi valori delle proprietà sono stringhe UTF-8 codificate. Ad esempio: StreamId.Create(String, String).

Sostituzione di SimpleMessageStreams con BroadcastChannel

SimpleMessageStreams (detto anche SMS) è stato rimosso nella versione 7.0. SMS aveva la stessa interfaccia di Orleans.Providers.Streams.PersistentStreams, ma il suo comportamento era molto diverso, poiché si basava su chiamate dirette da grano a grano. Per evitare confusione, SMS è stato rimosso ed è stata introdotta una nuova sostituzione chiamata Orleans.BroadcastChannel.

BroadcastChannel supporta solo le sottoscrizioni implicite e può essere una sostituzione diretta in questo caso. Se sono necessarie sottoscrizioni esplicite o se è necessario usare l'interfaccia PersistentStream (ad esempio, si usa SMS nei test mentre si usa EventHub in produzione), MemoryStream è il candidato migliore.

BroadcastChannel avrà gli stessi comportamenti di SMS, mentre MemoryStream si comporterà come altri provider di flussi. Si consideri l'esempio di utilizzo del canale broadcast seguente:

// Configuration
builder.AddBroadcastChannel(
    "my-provider",
    options => options.FireAndForgetDelivery = false);

// Publishing
var grainKey = Guid.NewGuid().ToString("N");
var channelId = ChannelId.Create("some-namespace", grainKey);
var stream = provider.GetChannelWriter<int>(channelId);

await stream.Publish(1);
await stream.Publish(2);
await stream.Publish(3);

// Simple implicit subscriber example
[ImplicitChannelSubscription]
public sealed class SimpleSubscriberGrain : Grain, ISubscriberGrain, IOnBroadcastChannelSubscribed
{
    // Called when a subscription is added to the grain
    public Task OnSubscribed(IBroadcastChannelSubscription streamSubscription)
    {
        streamSubscription.Attach<int>(
          item => OnPublished(streamSubscription.ChannelId, item),
          ex => OnError(streamSubscription.ChannelId, ex));

        return Task.CompletedTask;

        // Called when an item is published to the channel
        static Task OnPublished(ChannelId id, int item)
        {
            // Do something
            return Task.CompletedTask;
        }

        // Called when an error occurs
        static Task OnError(ChannelId id, Exception ex)
        {
            // Do something
            return Task.CompletedTask;
        }
    }
}

La migrazione a MemoryStream sarà più semplice, poiché deve essere modificata solo la configurazione. Si consideri la configurazione di MemoryStream seguente:

builder.AddMemoryStreams<DefaultMemoryMessageBodySerializer>(
    "in-mem-provider",
    _ =>
    {
        // Number of pulling agent to start.
        // DO NOT CHANGE this value once deployed, if you do rolling deployment
        _.ConfigurePartitioning(partitionCount: 8);
    });

OpenTelemetry

Il sistema di telemetria è stato aggiornato in Orleans 7.0 e il sistema precedente è stato rimosso a favore di API .NET standardizzate, ad esempio Metriche di .NET per le metriche e ActivitySource per la traccia.

Nell'ambito di questa novità, i pacchetti Microsoft.Orleans.TelemetryConsumers.* esistenti sono stati rimossi. Stiamo valutando l'introduzione di un nuovo set di pacchetti per semplificare il processo di integrazione delle metriche generate da Orleans nella soluzione di monitoraggio preferita. Come sempre, feedback e contributi sono i benvenuti.

Lo strumento dotnet-counters offre funzionalità di monitoraggio delle prestazioni per il monitoraggio ad hoc dello stato e l'analisi delle prestazioni di primo livello. Per i contatori di Orleans, è possibile usare lo strumento dotnet-counters per monitorarli:

dotnet counters monitor -n MyApp --counters Microsoft.Orleans

Analogamente, le metriche OpenTelemetry possono aggiungere i contatori Microsoft.Orleans, come illustrato nel codice seguente:

builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddPrometheusExporter()
        .AddMeter("Microsoft.Orleans"));

Per abilitare la traccia distribuita, si configura OpenTelemetry come illustrato nel codice seguente:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.SetResourceBuilder(ResourceBuilder.CreateDefault()
            .AddService(serviceName: "ExampleService", serviceVersion: "1.0"));

        tracing.AddAspNetCoreInstrumentation();
        tracing.AddSource("Microsoft.Orleans.Runtime");
        tracing.AddSource("Microsoft.Orleans.Application");

        tracing.AddZipkinExporter(options =>
        {
            options.Endpoint = new Uri("http://localhost:9411/api/v2/spans");
        });
    });

Nel codice precedente, OpenTelemetry è configurato per il monitoraggio di:

  • Microsoft.Orleans.Runtime
  • Microsoft.Orleans.Application

Per propagare l'attività, chiamare AddActivityPropagation:

builder.Host.UseOrleans((_, clientBuilder) =>
{
    clientBuilder.AddActivityPropagation();
});

Refactoring delle funzionalità dal pacchetto principale in pacchetti separati

In Orleans 7.0 abbiamo fatto uno sforzo per eseguire il refactoring delle estensioni in pacchetti separati che non si basano su Orleans.Core. Per l'esattezza, Orleans.Streaming, Orleans.Reminders e Orleans.Transactions sono stati separati dal pacchetto principale. Ciò significa che per questi pacchetti si paga solo quando vengono usati e non c'è codice nel core di Orleans dedicato a queste funzionalità. Questo riduce la superficie dell'API principale e le dimensioni dell'assembly, semplifica il core e migliora le prestazioni. Per quanto riguarda le prestazioni, le transazioni in Orleans in precedenza richiedevano codice da eseguire per tutti i metodi per coordinare le potenziali transazioni. È stato ora spostato per ciascun metodo.

Si tratta di una modifica che causa un'interruzione della compilazione. Potrebbe esserci ancora del codice che interagisce con i promemoria o i flussi con chiamate nei metodi definiti in precedenza nella classe di base Grain, ma che ora sono metodi di estensione. Tali chiamate che non specificano this (ad esempio GetReminders) dovranno essere aggiornate per includere this (ad esempio this.GetReminders()) perché i metodi di estensione devono essere qualificati. Se non si aggiornano queste chiamate si verifica un errore di compilazione e la modifica del codice richiesta potrebbe non essere ovvia se non si sa cosa è cambiato.

Client delle transazioni

Orleans 7.0 introduce la nuova astrazione Orleans.ITransactionClient per coordinare le transazioni. In precedenza, le transazioni potevano essere coordinate solo da grani. Con ITransactionClient, disponibile tramite l'inserimento delle dipendenze, i client possono coordinare le transazioni anche senza bisogno di un grano intermedio. L'esempio seguente ritira i crediti da un conto e li deposita in un'altro all'interno di una singola transazione. Questo codice può essere chiamato dall'interno di un grano o da un client esterno che ha recuperato ITransactionClient dal contenitore di inserimento delle dipendenze.

await transactionClient.RunTransaction(
  TransactionOption.Create,
  () => Task.WhenAll(from.Withdraw(100), to.Deposit(100)));

Per le transazioni coordinate dal client, il client deve aggiungere i servizi richiesti durante la fase di configurazione:

clientBuilder.UseTransactions();

L'esempio BankAccount offre una dimostrazione dell'utilizzo di ITransactionClient. Per altre informazioni, vedere Transazioni di Orleans.

Reentrancy della catena di chiamate

I grani sono a thread singolo ed elaborano le richieste una alla volta, dall'inizio al completamento, per impostazione predefinita. In altre parole, i grani non sono rientranti per impostazione predefinita. L'aggiunta di ReentrantAttribute a una classe di grani consente l'elaborazione simultanea di più richieste, in modalità di interleaving, pur rimanendo a thread singolo. Ciò può essere utile per i grani che non contengono uno stato interno o che eseguono molte operazioni asincrone, ad esempio l'emissione di chiamate HTTP o la scrittura in un database. Occorre prestare particolare attenzione in caso di interleaving delle richieste: è possibile che lo stato di un grano osservato prima di un'istruzione await sia cambiato nel momento in cui l'operazione asincrona viene completata e il metodo riprende l'esecuzione.

Ad esempio, il grano seguente rappresenta un contatore. È stato contrassegnato come Reentrant, consentendo l'interleaving di più chiamate. Il metodo Increment() deve incrementare il contatore interno e restituire il valore osservato. Tuttavia, poiché il corpo del metodo Increment() osserva lo stato del grano prima di un punto await e lo aggiorna in seguito, è possibile che più esecuzioni interleaving di Increment() possano restituire un _value minore del numero totale di chiamate Increment() ricevute. Si tratta di un errore introdotto dall'uso improprio della reentrancy.

La rimozione di ReentrantAttribute è sufficiente per risolvere il problema.

[Reentrant]
public sealed class CounterGrain : Grain, ICounterGrain
{
    int _value;
    
    /// <summary>
    /// Increments the grain's value and returns the previous value.
    /// </summary>
    public Task<int> Increment()
    {
        // Do not copy this code, it contains an error.
        var currentVal = _value;
        await Task.Delay(TimeSpan.FromMilliseconds(1_000));
        _value = currentVal + 1;
        return currentValue;
    }
}

Per evitare tali errori, per impostazione predefinita i grani non sono rientranti. Lo svantaggio è la velocità effettiva ridotta per grani che eseguono operazioni asincrone nell'implementazione, poiché non è possibile elaborare altre richieste mentre il grano è in attesa del completamento di un'operazione asincrona. Per ovviare a questo problema, Orleans offre diverse opzioni per consentire la reentrancy in determinati casi:

  • Per un'intera classe: l'inserimento di ReentrantAttribute per il grano consente l'interleaving di qualsiasi richiesta con qualsiasi altra richiesta.
  • Per un subset di metodi: l'inserimento di AlwaysInterleaveAttribute per il metodo dell'interfaccia del grano consente l'interleaving delle richieste di tale metodo con qualsiasi altra richiesta e l'interleaving delle richieste a tale metodo da parte di qualsiasi altra richiesta.
  • Per un subset di metodi: l'inserimento di ReadOnlyAttribute per il metodo dell'interfaccia del grano consente l'interleaving delle richieste di tale metodo con qualsiasi altra richiesta ReadOnly e l'interleaving delle richieste a tale metodo da parte di qualsiasi altra richiesta ReadOnly. In questo senso, si tratta di una forma più limitata di AlwaysInterleave.
  • Per qualsiasi richiesta all'interno di una catena di chiamate: RequestContext.AllowCallChainReentrancy() e <xref:Orleans.Runtime.RequestContext.SuppressCallChainReentrancy?displayProperty=nameWithType consentono di accettare esplicitamente e rifiutare esplicitamente la reentrancy delle richieste downstream nel grano. Entrambe le chiamate restituiscono un valore che deve essere eliminato all'uscita dalla richiesta. Di conseguenza, l'utilizzo corretto è il seguente:
public Task<int> OuterCall(IMyGrain other)
{
    // Allow call-chain reentrancy for this grain, for the duration of the method.
    using var _ = RequestContext.AllowCallChainReentrancy();
    await other.CallMeBack(this.AsReference<IMyGrain>());
}

public Task CallMeBack(IMyGrain grain)
{
    // Because OuterCall allowed reentrancy back into that grain, this method 
    // will be able to call grain.InnerCall() without deadlocking.
    await grain.InnerCall();
}

public Task InnerCall() => Task.CompletedTask;

Occorre il consenso esplicito per la reentrancy della catena di chiamate per ogni grano e per ogni catena di chiamate. Si considerino ad esempio due grani, il grano A e il grano B. Se il grano A abilita la reentrancy della catena di chiamate prima della chiamata del grano B, il grano B può richiamare il grano A in tale chiamata. Tuttavia, il grano A non può richiamare il grano B se anche il grano B non ha abilitato la reentrancy della catena di chiamate. Il meccanismo si applica a ogni grano e a ogni catena di chiamate.

I grani possono anche usare using var _ = RequestContext.SuppressCallChainReentrancy() per evitare che le informazioni di reentrancy della catena di chiamate fluiscano verso il basso in una catena di chiamate. In questo modo si impedisce la reentrancy delle chiamate successive.

Script di migrazione ADO.NET

Per garantire la compatibilità delle versioni successive con clustering, persistenza e promemoria di Orleans che si basano su ADO.NET, sarà necessario lo script di migrazione SQL appropriato:

Selezionare i file per il database usato e applicarli in ordine.