Show / Hide Table of Contents

Migration from 3.x

4.x introduces a number of breaking changes in the following libraries: DotNext, DotNext.Threading, DotNext.Net.Cluster, DotNext.AspNetCore.Cluster.

Core Library

WaitAsync and ContinueWithTimeout methods

Both methods have been removed from the library because they can be replaced with WaitAsync method from .NET 6 standard library.

Box<T>

DotNext.Runtime.Box<T> value type is replaced by DotNext.Runtime.Reference<T> value type. A new type allows to hold the reference to the various memory locations: array element, static of instance field, boxed value type. As a result, you need to use the factory method to make a reference to the boxed value:

using DotNext.Runtime;

object boxed = 25;
Reference<int> referenceToBoxedValue = Reference.Unbox<int>(boxed);
Console.WriteLine(referenceToBoxedValue.Target);

IO

SequenceBinaryReader has been renamed to SequenceReader.

IAsyncBinaryReader and IAsyncBinaryWriter

IAsyncBinaryWriter prior to 3.x had a set of methods (WriteGuidAsync, WriteDateTimeAsync) for encoding various value types as a string using the specified encoding. Starting from .NET 6, there is a special public interface ISpanFormattable that is implemented by all formattable data types in .NET Base Class Library. It's reasonable to offer a single method that can work with any type implementing the necessary interface instead of multiple methods for each formattable data type.

using System.Text;
using DotNext.IO;

IAsyncBinaryWriter writer;
await writer.WriteFormattableAsync<int>(42, LengthFormat.Plain, Encoding.UTF8, "X"); // encode int as as set of bytes using UTF-8 encoding

The same change was applied to IAsyncBinaryReader interface. All methods for parsing various value types have been replaced with ParseAsync method. It accepts the delegate responsible for parsing of the value from the sequence of characters represented by ReadOnlySpan<char> data type:

using System.Text;
using DotNext.IO;

IAsyncBinaryReader reader;
var i = await reader.ParseAsync<int>(static (chars, provider) => int.Parse(chars, provider: provider), Encoding.UTF8);

Threading

QueuedSynchronizer and Synchronizer classes as well as ISynchronizer interface are merged into single QueuedSynchronizer class. Lock acquisition methods now return ValueTask or ValueTask<T> value type instead of Task class. If you still need a Task then use AsTask() instance method.

AsyncReaderWriterLock

Presence of upgradeable read lock was a huge mistake in architecture. It allows to upgrade the read lock to the write lock by concurrent flow, e.g.:

using DotNext.Threading;

using var rwLock = new AsyncReaderWriterLock();

// async flow #1
await rwLock.EnterUpgradeableReadLockAsync();

// async flow #2
await rwLock.EnterWriteLockAsync(); // this flow can upgrade the existing read lock

To avoid that, starting from 4.0 version of the library you need to upgrade the lock manually:

using DotNext.Threading;

using var rwLock = new AsyncReaderWriterLock();
await rwLock.EnterReadLockAsync();
await rwLock.UpgradeToWriteLockAsync();
rwLock.DowngradeFromWriteLock();
rwLock.Release();

UpgradeToWriteLockAsync should be called by the same flow after the invocation of EnterReadLockAsync method.

ExitReadLock and ExitWriteLock methods replaced with a single Release method.

AsyncTrigger

AsyncTrigger divided to two classes: generic AsyncTrigger<T> class and non-generic AsyncTrigger class. For producer-consumer scenario without coordinated state you need to use non-generic version. If you have coordinated state then use generic version. Read more here for detailed information about the differences.

Cluster Programming Suite

DotNext.Net.Cluster and DotNext.AspNetCore.Cluster have breaking changes in Raft implementation.

ICluster interface

All custom delegate types for Cluster events have been replaced with delegate types from .NET Base Class Library. Members property is replaced by IPeerMesh.Peers property.

IExpandableCluster interface

The interface is completely replaced with IPeerMesh interface that has more reusable design.

IP safelist

allowedNetworks configuration parameter is no longer available. It's easily to implement it by the user through ASP.NET Core Middleware and amazing IPNetwork library. Also, there is perfect article about how to organize IP safelist.

Interpreter Framework

DotNext.Runtime.Serialization.IFormatter<T> interface has been replaced with DotNext.Runtime.Serialization.ISerializable<TSelf> interface so now the type itself must control its serialization/deserialization logic. ISerializable<TSelf> interface is more reusable across various scenarios and not tightly coupled with Interpreter Framework anymore.

The next thing is CommandAttribute<T>. Thanks to C# 10, we can use generic attributes. Therefore, you don't need to annotate command DTO with this attribute. Instead, you need to apply the attribute for each command type on the class that derives from CommandInterpreter class:

using DotNext.Net.Cluster.Consensus.Raft.Commands;

[Command<BinaryOperationCommand>(BinaryOperationCommand.Id)]
[Command<UnaryOperationCommand>(UnaryOperationCommand.Id)]
[Command<SnapshotCommand>(SnapshotCommand.Id)]
sealed class CustomInterpreter : CommandInterpreter
{
}

Typed Messages

MessageHandler uses the same concept of generic attributes as Interpreter Framework. All typed messages must be registered with generic MessageAttribute<T> attribute instead of annotating individual types:

using DotNext.Net.Cluster.Messaging;

[Message<AddMessage>(AddMessage.Name)]
[Message<SubtractMessage>(SubtractMessage.Name)]
[Message<ResultMessage>(ResultMessage.Name)]
public class TestMessageHandler : MessageHandler
{
}

Each typed message must implement DotNext.Runtime.Serialization.ISerializable<TSelf> interface to provide serialization/deserialization logic.

MessageClient is no longer use MessageAttribute<T> for registering message types. Instead, message types can be registered using RegisterMessage method as follows:

using DotNext.Net.Cluster.Messaging;

var typedClient = new MessagingClient(client)
    .RegisterMessage<AddMessage>(AddMessage.Name)
    .RegisterMessage<SubtractMessage>(SubtractMessage.Name)
    .RegisterMessage<ResultMessage>(ResultMessage.Name);

Configuration of cluster members

members configuration property is no longer available. The way of configuring, adding and removing of cluster members is changed completely. It happened because Raft provides a mechanism of cluster configuration management on top of itself without involving external discovery. Raft-native cluster membership protocol allows to avoid some critical issues associated with the external discovery mechanism. One of them - a probability of having two leaders in the same cluster during modification of the cluster configuration.

A new cluster configuration management model introduces DotNext.Net.Cluster.Consensus.Raft.Membership.IClusterConfigurationStorage interface. Currently, there are two storages supported:

  • In-memory configuration suitable for testing purposes
  • Persistent configuration suitable for production use

Raft-native membership protocol offers the easy and safe way of adding and removing new cluster members. All we need is to provide a guarantee that only one member can be removed or added at a time in the same cluster. When the modified configuration will be committed by the majority of nodes, you can perform the next change.

Therefore, IClusterConfigurationStorage actually holds the two configurations:

  • Active configuration which is applied and commited by the majority of nodes
  • Proposed configuration which reflects the requested change (added or removed member) and must be replicated and committed by the majority of nodes.

IClusterConfigurationStorage<TAddress> generic interface allows to add or remove members dynamically and wait until the proposed configuration turned into active configuration.

This mechanism for membership management dictates how the node must be bootstrapped:

  • coldStart=true (Cold Start mode) configuration property means that you want to start a first node in the cluster. It means that the node adds itself to the cluster configuration as proposed change and wait for other nodes
  • coldStart=false (Announcement mode) configuration property means that the node must be added to existing cluster.

Any change in configuration must be processed by the leader node as well as any other write operation.

IRaftHttpCluster offers high-level API for managing cluster members:

  • AddMemberAsync that allows to announce and warmup the node
  • RemoveMemberAsync that allows to remove the node from the cluster

These methods are callable on leader node only. It's possible to automate announcement of a new node. ClusterMemberAnnouncer<TAddress> delegate instance can be registered in DI container and then called automatically when the node started in Announcement mode. In case of HTTP, the announcer can utilize redirection to leader capability and send the announcement to the leader node. Then, the announcement endpoint on the leader node can call AddMemberAsync method to propose and commit a new cluster configuration. Read this for more information.

Persistent WAL

PersistentState class exists but cannot be inherited directly anymore. Now this is a base type for disk-based and memory-based state machine. Derive from MemoryBasedStateMachine class to migrate existing code.

  • Edit this page
☀
☾
In this article
Back to top
Supported by the .NET Foundation
☀
☾