VIEApps.Components.WebSockets 10.9.2501.1

dotnet add package VIEApps.Components.WebSockets --version 10.9.2501.1                
NuGet\Install-Package VIEApps.Components.WebSockets -Version 10.9.2501.1                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="VIEApps.Components.WebSockets" Version="10.9.2501.1" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add VIEApps.Components.WebSockets --version 10.9.2501.1                
#r "nuget: VIEApps.Components.WebSockets, 10.9.2501.1"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install VIEApps.Components.WebSockets as a Cake Addin
#addin nuget:?package=VIEApps.Components.WebSockets&version=10.9.2501.1

// Install VIEApps.Components.WebSockets as a Cake Tool
#tool nuget:?package=VIEApps.Components.WebSockets&version=10.9.2501.1                

VIEApps.Components.WebSockets

A concrete implementation of the System.Net.WebSockets.WebSocket abstract class on .NET Standard 2.x/.NET Core 2.x+, that allows you to make WebSocket connections as a client or to respond to WebSocket requests as a server (or wrap existing WebSocket connections of ASP.NET / ASP.NET Core).

NuGet

NuGet

Walking on the ground

The class ManagedWebSocket is an implementation or a wrapper of the System.Net.WebSockets.WebSocket abstract class, that allows you send and receive messages in the same way for both side of client and server role.

Receiving messages:

async Task ReceiveAsync(ManagedWebSocket websocket)
{
	var buffer = new ArraySegment<byte>(new byte[1024]);
	while (true)
	{
		WebSocketReceiveResult result = await websocket.ReceiveAsync(buffer, CancellationToken.None).ConfigureAwait(false);
		switch (result.MessageType)
		{
			case WebSocketMessageType.Close:
				return;
			case WebSocketMessageType.Text:
			case WebSocketMessageType.Binary:
				var value = Encoding.UTF8.GetString(buffer, result.Count);
				Console.WriteLine(value);
				break;
		}
	}
}

Sending messages:

async Task SendAsync(ManagedWebSocket websocket)
{
	var buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes("Hello World"));
	await websocket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false);
} 

Useful properties:

// the identity of the connection
public Guid ID { get; }

// true if the connection was made when connect to a remote endpoint (mean client role)
public bool IsClient { get; }

// original requesting URI of the connection
public Uri RequestUri { get; }

// the time when the connection is established
public DateTime Timestamp { get; }

// the remote endpoint
public EndPoint RemoteEndPoint { get; }

// the local endpoint
public EndPoint LocalEndPoint { get; }

// Extra information
public Dictionary<string, object> Extra { get; }

// Headers information
public Dictionary<string, string> Headers { get; }

Fly on the sky with Event-liked driven

Using the WebSocket class

This is a centralized element for working with both side of client and server role. This class has 04 action properties (event handlers) to take care of all working cases, you just need to assign your code to cover its.

// fire when got any error
Action<ManagedWebSocket, Exception> OnError;

// fire when a connection is established
Action<ManagedWebSocket> OnConnectionEstablished;

// fire when a connection is broken
Action<ManagedWebSocket> OnConnectionBroken;

// fire when a message is received
Action<ManagedWebSocket, WebSocketReceiveResult, byte[]> OnMessageReceived;

Example:

var websocket = new WebSocket
{
	OnError = (webSocket, exception) =>
	{
		// your code to handle error
	},
	OnConnectionEstablished = (webSocket) =>
	{
		// your code to handle established connection
	},
	OnConnectionBroken = (webSocket) =>
	{
		// your code to handle broken connection
	},
	OnMessageReceived = (webSocket, result, data) =>
	{
		// your code to handle received message
	}
};

And this class has some methods for working on both side of client and server role:

void Connect(Uri uri, WebSocketOptions options, Action<ManagedWebSocket> onSuccess, Action<Exception> onFailure);
void StartListen(int port, X509Certificate2 certificate, Action onSuccess, Action<Exception> onFailure, Func<ManagedWebSocket, byte[]> getPingPayload, Func<ManagedWebSocket, byte[], byte[]> getPongPayload, Action<ManagedWebSocket, byte[]> onPong);
void StopListen();

WebSocket client

Use the Connect method to connect to a remote endpoint

WebSocket server

Use the StartListen method to start the listener to listen incoming connection requests.

Use the StopListen method to stop the listener.

WebSocket server with Secure WebSockets (wss://)

Enabling secure connections requires two things:

  • Pointing certificate to an x509 certificate that containing a public and private key.
  • Using the scheme wss instead of ws (or https instead of http) on all clients
var websocket = new WebSocket
{
	Certificate = new X509Certificate2("my-certificate.pfx")
	// Certificate = new X509Certificate2("my-certificate.pfx", "cert-password", X509KeyStorageFlags.UserKeySet)
};
websocket.StartListen();

Want to have a free SSL certificate? Take a look at Let's Encrypt.

Special: A simple tool named win-acme will help your IIS works with Let's Encrypt very well.

SubProtocol Negotiation

To enable negotiation of subprotocols, specify the supported protocols on SupportedSubProtocols property. The negotiated subprotocol will be available on the socket's SubProtocol.

If no supported subprotocols are found on the client request (Sec-WebSocket-Protocol), the listener will raises the SubProtocolNegotiationFailedException exception.

var websocket = new WebSocket
{
	SupportedSubProtocols = new[] { "messenger", "chat" }
};
websocket.StartListen();

Nagle's Algorithm

The Nagle's Algorithm is disabled by default (to send a message immediately). If you want to enable the Nagle's Algorithm, set NoDelay to false

var websocket = new WebSocket
{
	NoDelay = false
};
websocket.StartListen();

Wrap an existing WebSocket connection of ASP.NET / ASP.NET Core

When integrate this component with your app that hosted by ASP.NET / ASP.NET Core, you might want to use the WebSocket connections of ASP.NET / ASP.NET Core directly, then the method WrapAsync is here to help. This method will return a task that run a process for receiving messages from this WebSocket connection.

Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary<string, string> headers, Action<ManagedWebSocket> onSuccess);

And might be you need an extension method to wrap an existing WebSocket connection, then take a look at some lines of code below:

ASP.NET

public static Task WrapAsync(this net.vieapps.Components.WebSockets.WebSocket websocket, AspNetWebSocketContext context)
{
	var serviceProvider = (IServiceProvider)HttpContext.Current;
	var httpWorker = serviceProvider?.GetService<HttpWorkerRequest>();
	var remoteAddress = httpWorker == null ? context.UserHostAddress : httpWorker.GetRemoteAddress();
	var remotePort = httpWorker == null ? 0 : httpWorker.GetRemotePort();
	var remoteEndpoint = IPAddress.TryParse(remoteAddress, out IPAddress ipAddress)
		? new IPEndPoint(ipAddress, remotePort > 0 ? remotePort : context.RequestUri.Port) as EndPoint
		: new DnsEndPoint(context.UserHostName, remotePort > 0 ? remotePort : context.RequestUri.Port) as EndPoint;
	var localAddress = httpWorker == null ? context.RequestUri.Host : httpWorker.GetLocalAddress();
	var localPort = httpWorker == null ? 0 : httpWorker.GetLocalPort();
	var localEndpoint = IPAddress.TryParse(localAddress, out ipAddress)
		? new IPEndPoint(ipAddress, localPort > 0 ? localPort : context.RequestUri.Port) as EndPoint
		: new DnsEndPoint(context.RequestUri.Host, localPort > 0 ? localPort : context.RequestUri.Port) as EndPoint;
	return websocket.WrapAsync(context.WebSocket, context.RequestUri, remoteEndpoint, localEndpoint);
}

ASP.NET Core

public static async Task WrapAsync(this net.vieapps.Components.WebSockets.WebSocket websocket, HttpContext context)
{
	if (context.WebSockets.IsWebSocketRequest)
	{
		var webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
		var requestUri = new Uri($"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.PathBase}{context.Request.QueryString}");
		var remoteEndPoint = new IPEndPoint(context.Connection.RemoteIpAddress, context.Connection.RemotePort);
		var localEndPoint = new IPEndPoint(context.Connection.LocalIpAddress, context.Connection.LocalPort);
		await websocket.WrapAsync(webSocket, requestUri, remoteEndPoint, localEndPoint).ConfigureAwait(false);
	}
}

While working with ASP.NET Core, we think that you need a middle-ware to handle all request of WebSocket connections, just look like this:

public class WebSocketMiddleware
{
	readonly RequestDelegate _next;
	net.vieapps.Components.WebSockets.WebSocket _websocket;

	public WebSocketMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
	{
		var logger = loggerFactory.CreateLogger<WebSocketMiddleware>();
		this._websocket = new net.vieapps.Components.WebSockets.WebSocket(loggerFactory)
		{
			OnError = (websocket, exception) =>
			{
				logger.LogError(exception, $"Got an error: {websocket?.ID} @ {websocket?.RemoteEndPoint} => {exception.Message}");
			},
			OnConnectionEstablished = (websocket) =>
			{
				logger.LogDebug($"Connection is established: {websocket.ID} @ {websocket.RemoteEndPoint}");
			},
			OnConnectionBroken = (websocket) =>
			{
				logger.LogDebug($"Connection is broken: {websocket.ID} @ {websocket.RemoteEndPoint}");
			},
			OnMessageReceived = (websocket, result, data) =>
			{
				var message = result.MessageType == System.Net.WebSockets.WebSocketMessageType.Text ? data.GetString() : "(binary message)";
				logger.LogDebug($"Got a message: {websocket.ID} @ {websocket.RemoteEndPoint} => {message}");
			}
		};
		this._next = next;
	}

	public async Task Invoke(HttpContext context)
	{
		await this._websocket.WrapAsync(context).ConfigureAwait(false);
		await this._next.Invoke(context).ConfigureAwait(false);
	}
}

And remember to tell APS.NET Core uses your middleware (at Configure method of Startup.cs)

app.UseWebSockets();
app.UseMiddleware<WebSocketMiddleware>();

Receiving and Sending messages:

Messages are received automatically via parallel tasks, and you only need to assign OnMessageReceived event for handling its.

Sending messages are the same as ManagedWebSocket, with a little different: the first argument - you need to specify a WebSocket connection (by an identity) for sending your messages.

Task SendAsync(Guid id, ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken);
Task SendAsync(Guid id, string message, bool endOfMessage, CancellationToken cancellationToken);
Task SendAsync(Guid id, byte[] message, bool endOfMessage, CancellationToken cancellationToken);
Task SendAsync(Func<ManagedWebSocket, bool> predicate, ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken);
Task SendAsync(Func<ManagedWebSocket, bool> predicate, string message, bool endOfMessage, CancellationToken cancellationToken);
Task SendAsync(Func<ManagedWebSocket, bool> predicate, byte[] message, bool endOfMessage, CancellationToken cancellationToken);

Connection management

Take a look at some methods GetWebSocket... to work with all connections.

ManagedWebSocket GetWebSocket(Guid id);
IEnumerable<ManagedWebSocket> GetWebSockets(Func<ManagedWebSocket, bool> predicate);
bool CloseWebSocket(Guid id, WebSocketCloseStatus closeStatus, string closeStatusDescription);
bool CloseWebSocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus, string closeStatusDescription);

Others

The important things

  • 16K is default size of the protocol buffer for receiving messages (its large enough for most case because we are usually use WebSocket to send/receive small data). If you want to change (to receive large messages), then set a new value for the static property named ReceiveBufferSize of the WebSocket class.
  • Some portion of codes are reference from NinjaSource WebSocket.

Logging

  • Can be any provider that supports extension of Microsoft.Extensions.Logging (via dependency injection).
  • Set the log's level to Trace to see all processing logs

Our prefers:

Namespaces

using net.vieapps.Components.Utility;
using net.vieapps.Components.WebSockets;

A very simple stress test

Environment

  • 01 server with Windows 2012 R2 x64 on Intel Xeon E3-1220 v3 3.1GHz - 8GB RAM
  • 05 clients with Windows 10 x64 and Ubuntu Linux 16.04 x64

The scenario

  • Clients (05 stations) made 20,000 concurrent connections to the server, all connections are secured (use Let's Encrypt SSL certificate)
  • Clients send 02 messages per second to server (means server receives 40,000 messages/second) - size of 01 message: 1024 bytes (1K)
  • Server sends 01 message to all connections (20,000 messages) each 10 minutes - size of 01 message: 1024 bytes (1K)

The results

  • Server is still alive after 01 week (60 * 24 * 7 = 10,080 minutes)
  • No dropped connection
  • No hang
  • Used memory: 1.3 GB - 1.7 GB
  • CPU usages: 3% - 15% while receiving messages, 18% - 35% while sending messages

Performance Tuning

While working directly with this component, performance is not your problem, but when you wrap WebSocket connections of ASP.NET or ASP.NET Core (with IIS Integration), may be you reach max 5,000 concurrent connections (because IIS allows 5,000 CCU by default).

ASP.NET and IIS scale very well, but you'll need to change a few settings to set up your server for lots of concurrent connections, as opposed to lots of requests per second.

IIS Configuration

Max concurrent requests per application

Increase the number of concurrent requests IIS will serve at once:

  • Open an administrator command prompt at %windir%\System32\inetsrv
  • Run the command below to update the appConcurrentRequestLimit attribute to a suitable number (5000 is the default in IIS7+)

Example:

appcmd.exe set config /section:system.webserver/serverRuntime /appConcurrentRequestLimit:100000

ASP.NET Configuration

Maximum Concurrent Requests Per CPU

By default ASP.NET 4.0 sets the maximum concurrent connections to 5000 per CPU. If you need more concurrent connections then you need to increase the maxConcurrentRequestsPerCPU setting.

Open %windir%\Microsoft.NET\Framework\v4.0.30319\aspnet.config (Framework64 for 64 bit processes)

Copy from the sample below (ensure case is correct!)

Example:

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
	<runtime>
		<legacyUnhandledExceptionPolicy enabled="false" />
		<legacyImpersonationPolicy enabled="true"/>
		<alwaysFlowImpersonationPolicy enabled="false"/>
		<SymbolReadingPolicy enabled="1" />
		<shadowCopyVerifyByTimestamp enabled="true"/>
	</runtime>
	<startup useLegacyV2RuntimeActivationPolicy="true" />
	<system.web>
		<applicationPool maxConcurrentRequestsPerCPU="20000" />
	</system.web>
</configuration>
Request Queue Limit

When the total amount of connections exceed the maxConcurrentRequestsPerCPU setting (i.e. maxConcurrentRequestsPerCPU * number of logical processors), ASP.NET will start throttling requests using a queue. To control the size of the queue, you can tweak the requestQueueLimit.

  • Open %windir%\Microsoft.NET\Framework\v4.0.30319\Config\machine.config (Framework64 for 64 bit processes)
  • Locate the processModel element
  • Set the autoConfig attribute to false and the requestQueueLimit attribute to a suitable number

Example:

<processModel autoConfig="false" requestQueueLimit="250000" />
Performance Counters

The following performance counters may be useful to watch while conducting concurrency testing and adjusting the settings detailed above:

Memory

  • .NET CLR Memory# bytes in all Heaps (for w3wp)

ASP.NET

  • ASP.NET\Requests Current
  • ASP.NET\Queued
  • ASP.NET\Rejected

CPU

  • Processor Information\Processor Time

TCP/IP

  • TCPv6\Connections Established
  • TCPv4\Connections Established

Web Service

  • Web Service\Current Connections
  • Web Service\Maximum Connections

Threading

  • .NET CLR LocksAndThreads\ # of current logical Threads
  • .NET CLR LocksAndThreads\ # of current physical Threads
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (4)

Showing the top 4 NuGet packages that depend on VIEApps.Components.WebSockets:

Package Downloads
VIEApps.Components.Utility.AspNetCore

The general purpose components for developing apps with ASP.NET Core

VIEApps.Services.Base

The base of all microservices in the VIEApps NGX

VIEApps.Components.Utility.AspNet

The general purpose library for developing apps with ASP.NET

AltV.Net.NetworkingEntity

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
10.9.2501.1 88 12/31/2024
10.9.2412.1 178 12/9/2024
10.9.2411.1 192 11/18/2024
10.8.2410.1 223 10/2/2024
10.8.2408.1 176 8/1/2024
10.8.2407.1 338 6/22/2024
10.8.2406.2 232 5/30/2024
10.8.2406.1 151 5/30/2024
10.8.2404.1 232 5/9/2024
10.8.2312.1 1,266 12/22/2023
10.8.2311.1 418 11/16/2023
10.8.2310.2 316 10/25/2023
10.8.2310.1 416 9/30/2023
10.8.2309.1 285 9/8/2023
10.8.2308.1 342 8/29/2023
10.7.2307.1 436 7/6/2023
10.7.2306.1 393 6/6/2023
10.7.2305.1 264 5/17/2023
10.7.2303.2 373 3/17/2023
10.7.2303.1 437 3/2/2023
10.7.2302.1 696 1/31/2023
10.7.2301.1 779 1/2/2023
10.7.2212.1 1,724 11/30/2022
10.7.2211.1 903 11/27/2022
10.6.2211.1 940 11/9/2022
10.5.2211.1 974 11/5/2022
10.5.2209.1 2,585 9/20/2022
10.5.2207.1 3,901 7/18/2022
10.5.2205.6 2,605 5/6/2022
10.5.2205.3 1,294 5/5/2022
10.5.2205.1 1,938 4/30/2022
10.5.2204.2 1,385 4/12/2022
10.5.2204.1 1,402 4/10/2022 10.5.2204.1 is deprecated because it has critical bugs.
10.5.2203.5 5,762 3/16/2022
10.5.2203.4 1,615 3/14/2022
10.5.2203.3 1,815 3/10/2022
10.5.2203.2 1,627 3/10/2022
10.5.2203.1 1,700 3/5/2022
10.5.2201.1 2,606 1/3/2022
10.4.2112.1 1,341 12/3/2021
10.4.2111.1 1,416 11/2/2021
10.4.2110.3 1,281 10/13/2021
10.4.2110.2 1,354 9/30/2021
10.4.2110.1 1,489 9/30/2021 10.4.2110.1 is deprecated because it has critical bugs.
10.4.2109.7 1,178 9/22/2021
10.4.2109.6 1,347 9/20/2021
10.4.2109.5 1,256 9/17/2021
10.4.2109.3 1,923 9/16/2021 10.4.2109.3 is deprecated because it has critical bugs.
10.4.2109.2 1,468 9/13/2021 10.4.2109.2 is deprecated because it has critical bugs.
10.4.2109.1 1,276 9/3/2021
10.4.2108.3 1,397 8/2/2021
10.4.2108.2 1,603 8/1/2021
10.4.2108.1 1,403 7/26/2021
10.4.2107.2 1,356 7/17/2021
10.4.2107.1 1,512 7/1/2021
10.4.2106.2 2,124 6/8/2021
10.4.2106.1 1,238 6/6/2021
10.4.2105.1 1,251 5/3/2021
10.4.2104.1 2,073 4/4/2021
10.4.2103.1 1,378 3/9/2021
10.4.2102.1 1,330 2/1/2021
10.4.2101.1 1,405 12/31/2020
10.3.2012.5 1,840 12/23/2020
10.3.2012.3 2,424 12/17/2020
10.3.2012.2 1,652 12/16/2020
10.3.2012.1 1,603 12/3/2020
10.3.2011.2 1,910 11/15/2020
10.3.2011.1 1,927 11/12/2020
10.3.2010.1 1,541 10/24/2020
10.3.2009.3 1,294 9/9/2020
10.3.2009.2 1,249 9/3/2020
10.3.2009.1 1,307 9/2/2020
10.3.2008.3 1,334 8/21/2020
10.3.2008.2 1,443 8/12/2020
10.3.2008.1 1,589 8/3/2020
10.3.2007.2 1,913 7/15/2020
10.3.2007.1 2,012 6/30/2020
10.3.2006.2 1,941 6/10/2020
10.3.2006.1 1,971 6/2/2020
10.3.2005.2 2,046 5/20/2020
10.3.2005.1 2,098 5/13/2020
10.3.2004.24 1,976 4/24/2020
10.3.2004.1 1,970 3/31/2020
10.3.2003.4 2,108 3/7/2020
10.3.2003.3 1,936 3/5/2020
10.3.2003.2 7,098 3/3/2020
10.3.2002.5 5,118 2/20/2020
10.3.2002.4 2,161 2/19/2020
10.3.2002.3 2,260 2/11/2020
10.3.2002.2 2,096 2/3/2020
10.3.2002.1 2,080 2/2/2020
10.3.2001.4 2,165 1/19/2020
10.3.2001.3 2,045 1/17/2020
10.3.2001.2 3,080 1/6/2020
10.3.2001.1 857 1/2/2020
10.3.1912.3 1,111 12/6/2019
10.3.1912.2 899 12/4/2019
10.3.1912.1 875 12/1/2019
10.3.1911.1 3,538 10/28/2019
10.3.1910.4 2,269 10/17/2019
10.3.1910.3 2,120 10/10/2019
10.3.1910.2 2,088 10/1/2019
10.3.1910.1 1,967 9/26/2019
10.3.1908.1-preview 689 8/3/2019
10.3.1907.1-preview 430 7/2/2019
10.3.1906.5-preview 628 6/21/2019
10.3.1906.4-preview 549 6/20/2019
10.3.1906.3-preview 580 6/18/2019
10.3.1906.2-preview 518 6/17/2019
10.3.1906.1-preview 564 6/17/2019
10.2.1906.2 1,884 6/12/2019
10.2.1906.1 3,014 5/22/2019
10.2.1905.2 1,722 5/7/2019
10.2.1905.1 1,006 5/1/2019
10.2.1904.3 1,032 4/11/2019
10.2.1904.1 1,090 4/1/2019
10.2.1903.3 1,101 3/6/2019
10.2.1903.2 1,084 3/2/2019
10.2.1903.1 1,056 3/1/2019
10.2.1902.2 1,095 2/22/2019
10.2.1902.1 1,176 1/31/2019
10.2.1901.1 1,446 1/8/2019
10.2.1812.2 1,150 12/4/2018
10.2.1812.1 1,206 12/2/2018
10.2.1811.2 1,177 11/23/2018
10.2.1811.1 1,181 11/12/2018
10.2.1810.1 1,211 10/8/2018
10.2.6.1809 1,225 9/10/2018
10.2.5.1808 1,285 7/24/2018
10.2.4.1806 1,597 5/31/2018
10.2.3.1806 1,496 5/28/2018
10.2.2.1806 1,524 5/17/2018
10.2.1.1805 1,453 5/7/2018
10.2.0.1805 1,661 5/6/2018
10.1.19.1805 1,548 5/1/2018
10.1.18.1805 1,238 4/27/2018
10.1.17.1804 1,162 4/11/2018
10.1.15.1804 1,180 4/4/2018

Add support of .NET 9