nanoFramework.WebServer 1.2.56

前缀保留
dotnet add package nanoFramework.WebServer --version 1.2.56                
NuGet\Install-Package nanoFramework.WebServer -Version 1.2.56                
此命令旨在在 Visual Studio 的包管理器控制台中用于,因为它使用了 NuGet 模块的 Install-Package 版本。
<PackageReference Include="nanoFramework.WebServer" Version="1.2.56" />                
对于支持 PackageReference 的项目,将此 XML 节点复制到项目文件中以引用此包。
paket add nanoFramework.WebServer --version 1.2.56                
#r "nuget: nanoFramework.WebServer, 1.2.56"                
#r 指令可用于 F# Interactive 和多语言笔记本。将此内容复制到交互式工具或脚本的源代码中,以引用包。
// Install nanoFramework.WebServer as a Cake Addin
#addin nuget:?package=nanoFramework.WebServer&version=1.2.56

// Install nanoFramework.WebServer as a Cake Tool
#tool nuget:?package=nanoFramework.WebServer&version=1.2.56                

Quality Gate Status Reliability Rating NuGet #yourfirstpr Discord

nanoFramework logo


欢迎使用 .NET nanoFramework WebServer 代码库

构建状态

组件 构建状态 NuGet 包
nanoFramework.WebServer Build Status NuGet
nanoFramework.WebServer.FileSystem Build Status NuGet

.NET nanoFramework WebServer

此库由 Laurent Ellerbach 编写,他慷慨地将其提供给 .NET nanoFramework 项目。

这是一个简单的 nanoFramework WebServer。功能包括

  • 处理多线程请求
  • 使用 nanoFramework.WebServer.FileSystem NuGet 从任何存储空间中提供服务。需要一个支持存储(具有 System.IO.FileSystem 功能)的目标设备。
  • 处理 URL 中的参数
  • 可行同时运行多个 WebServer
  • 支持 GET/PUT 和其他任何命令
  • 支持任何类型的头部
  • 支持 POST 中的内容
  • 反射以方便使用控制器和路由概念
  • 辅助函数以直接返回错误代码,方便 REST API 使用
  • 支持 HTTPS
  • URL 解码/编码

限制

  • 不支持请求或响应流中的任何 zip 文件

用法

您只需要为查询指定端口和超时时间,并添加一个当请求到来时进行事件处理的操作。用这种方法,每次接收到请求时都会触发一个事件。

using (WebServer server = new WebServer(80, HttpProtocol.Http)
{
    // Add a handler for commands that are received by the server.
    server.CommandReceived += ServerCommandReceived;

    // Start the server.
    server.Start();

    Thread.Sleep(Timeout.Infinite);
}

您还可以传递一个控制器,您可以在其中对路由和方法使用装饰。

using (WebServer server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(ControllerPerson), typeof(ControllerTest) }))
{
    // Start the server.
    server.Start();

    Thread.Sleep(Timeout.Infinite);
}

在这种情况下,您正在传递两个具有公开方法的类,这些方法将在找到路由时被调用。

使用前面的例子,一个非常简单直接测试控制器看起来是这样的

public class ControllerTest
{
    [Route("test"), Route("Test2"), Route("tEst42"), Route("TEST")]
    [CaseSensitive]
    [Method("GET")]
    public void RoutePostTest(WebServerEventArgs e)
    {
        string route = $"The route asked is {e.Context.Request.RawUrl.TrimStart('/').Split('/')[0]}";
        e.Context.Response.ContentType = "text/plain";
        WebServer.OutPutStream(e.Context.Response, route);
    }

    [Route("test/any")]
    public void RouteAnyTest(WebServerEventArgs e)
    {
        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
    }
}

在这个例子中,当请求的URL是 testTest2tEst42TEST 时,将调用 RoutePostTest,URL可以带参数,且方法是GET。请注意,Test 不会调用该函数,也不会调用 test/

RouteAnyTest 在URL是 test/any 且不区分方法时会被调用。

更高级的例子是通过简单的REST API获取人员列表并添加人员。请查看示例

[!Important]

默认情况下,路由不区分大小写,并且属性必须全部小写。如果您想使用区分大小写的路由,如前面的例子所示,请使用属性 CaseSensitive。同样,您必须按照您想要的方式响应路由来编写路由。

一个简单的GPIO控制器REST API

您将在简单的 GPIO控制器示例 REST API中找到。控制器不区分大小写,工作方式如下

  • 打开2号引脚作为输出:http://你的链接/open/2/output
  • 打开4号引脚作为输入:http://你的链接/open/4/input
  • 将2号引脚写入高电平:http://你的链接/write/2/high
    • 您可以使用high或1,它们具有相同的效果,并将引脚放置在高电平位置
    • 您可以使用low或0,它们具有相同的效果,并将引脚放置在低电平位置
  • 读取4号引脚:http://你的链接/read/4,您将得到纯文本形式的 highlow,取决于状态

控制器上的身份验证

控制器支持身份验证。目前控制器上只有三种身份验证类型已实施

  • 基本身份验证:遵循HTTP标准的经典用户和密码。用法:
    • [Authentication("Basic")] 将使用Web服务器的默认凭证
    • [Authentication("Basic:myuser mypassword")] 将使用myuser作为用户名,使用mypassword作为密码。注意:用户名不能包含空格。
  • 在标题中添加ApiKey:在标题中添加带有关键API的ApiKey。用法:
    • [Authentication("ApiKey")] 将使用Web服务器的默认凭证
    • [Authentication("ApiKey:akey")] 将使用akey作为ApiKey。
  • 无:无需身份验证。用法:
    • [Authentication("None")] 将使用Web服务器的默认凭证

Authentication属性适用于公开类和公开方法。

至于控制器中的其余部分,您可以为它们添加属性来定义或覆盖它们。以下示例显示了可以做什么

[Authentication("Basic")]
class ControllerAuth
{
    [Route("authbasic")]
    public void Basic(WebServerEventArgs e)
    {
        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
    }

    [Route("authbasicspecial")]
    [Authentication("Basic:user2 password")]
    public void Special(WebServerEventArgs e)
    {
        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
    }

    [Authentication("ApiKey:superKey1234")]
    [Route("authapi")]
    public void Key(WebServerEventArgs e)
    {
        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
    }

    [Route("authnone")]
    [Authentication("None")]
    public void None(WebServerEventArgs e)
    {
        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
    }

    [Authentication("ApiKey")]
    [Route("authdefaultapi")]
    public void DefaultApi(WebServerEventArgs e)
    {
        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
    }
}

并且您可以向服务器传递默认凭证

using (WebServer server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(ControllerPerson), typeof(ControllerTest), typeof(ControllerAuth) }))
{
    // To test authentication with various scenarios
    server.ApiKey = "ATopSecretAPIKey1234";
    server.Credential = new NetworkCredential("topuser", "topPassword");

    // Start the server.
    server.Start();

    Thread.Sleep(Timeout.Infinite);
}

使用前面的示例发生以下情况:

  • 默认情况下,所有控制器,即使未指定任何内容,也将使用控件凭证。在我们的案例中,使用默认用户(topuser)和密码(topPassword)的基本身份验证将被使用。
    • 当通过浏览器调用http://你的链接/authbasic时,将提示您输入用户名和密码,使用默认的用户topuser和密码topPassword以获取访问权限
    • 当调用http://你的链接/authnone时,因为没有身份验证被覆盖为无,所以不会提示您。
    • 当调用 http://yoururl/authbasicspecial 时,用户和密码与默认的 user2 和密码不同,这里正确的用户密码对是 user2 和 password
  • 如果您在控制器中定义了特定的用户和密码,例如 [Authentication("Basic:myuser mypassword")],那么所有控制器都将使用 myuser 和 mypassword 作为默认密码
  • 当调用 http://yoururl/authapi 时,您必须传递名为 ApiKey(区分大小写)的头部,其值应为 superKey1234 以获得授权,这将覆盖默认的 Basic 认证
  • 当调用 http://yoururl/authdefaultapi 时,将使用默认密钥 ATopSecretAPIKey1234,因此您需要在请求头部中传递它

总的来说,这是一个展示如何使用认证的示例,它被定义为允许灵活性

通过事件管理传入的查询

基本用法如下

private static void ServerCommandReceived(object source, WebServerEventArgs e)
{
    var url = e.Context.Request.RawUrl;
    Debug.WriteLine($"Command received: {url}, Method: {e.Context.Request.HttpMethod}");

    if (url.ToLower() == "/sayhello")
    {
        // This is simple raw text returned
        WebServer.OutPutStream(e.Context.Response, "It's working, url is empty, this is just raw text, /sayhello is just returning a raw text");
    }
    else
    {
        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
    }
}

您可以进行更复杂的场景,例如 返回一个完整的 HTML 页面

WebServer.OutPutStream(e.Context.Response, "<html><head>" +
    "<title>Hi from nanoFramework Server</title></head><body>You want me to say hello in a real HTML page!<br/><a href='/useinternal'>Generate an internal text.txt file</a><br />" +
    "<a href='/Text.txt'>Download the Text.txt file</a><br>" +
    "Try this url with parameters: <a href='/param.htm?param1=42&second=24&NAme=Ellerbach'>/param.htm?param1=42&second=24&NAme=Ellerbach</a></body></html>");

并且可以从 URL 获取参数,例如从之前的 param.html 页面中的链接示例

if (url.ToLower().IndexOf("/param.htm") == 0)
{
    // Test with parameters
    var parameters = WebServer.decryptParam(url);
    string toOutput = "<html><head>" +
        "<title>Hi from nanoFramework Server</title></head><body>Here are the parameters of this URL: <br />";
    foreach (var par in parameters)
    {
        toOutput += $"Parameter name: {par.Name}, Value: {par.Value}<br />";
    }
    toOutput += "</body></html>";
    WebServer.OutPutStream(e.Context.Response, toOutput);
}

并且服务静态文件

// E = USB storage
// D = SD Card
// I = Internal storage
// Adjust this based on your configuration
const string DirectoryPath = "I:\\";
string[] _listFiles;

// Gets the list of all files in a specific directory
// See the MountExample for more details if you need to mount an SD card and adjust here
// https://github.com/nanoframework/Samples/blob/main/samples/System.IO.FileSystem/MountExample/Program.cs
_listFiles = Directory.GetFiles(DirectoryPath);
// Remove the root directory
for (int i = 0; i < _listFiles.Length; i++)
{
    _listFiles[i] = _listFiles[i].Substring(DirectoryPath.Length);
}

var fileName = url.Substring(1);
// Note that the file name is case sensitive
// Very simple example serving a static file on an SD card                   
foreach (var file in _listFiles)
{
    if (file == fileName)
    {
        WebServer.SendFileOverHTTP(e.Context.Response, DirectoryPath + file);
        return;
    }
}

WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);

[!Important]

服务文件需要 nanoFramework.WebServer.FileSystem nuget AND 设备支持存储,因此需要 System.IO.FileSystem

此外,也支持 REST API,这里有一个全面的示例

if (url.ToLower().IndexOf("/api/") == 0)
{
    string ret = $"Your request type is: {e.Context.Request.HttpMethod}\r\n";
    ret += $"The request URL is: {e.Context.Request.RawUrl}\r\n";
    var parameters = WebServer.DecodeParam(e.Context.Request.RawUrl);
    if (parameters != null)
    {
        ret += "List of url parameters:\r\n";
        foreach (var param in parameters)
        {
            ret += $"  Parameter name: {param.Name}, value: {param.Value}\r\n";
        }
    }

    if (e.Context.Request.Headers != null)
    {
        ret += $"Number of headers: {e.Context.Request.Headers.Count}\r\n";
    }
    else
    {
        ret += "There is no header in this request\r\n";
    }

    foreach (var head in e.Context.Request.Headers?.AllKeys)
    {
        ret += $"  Header name: {head}, Values:";
        var vals = e.Context.Request.Headers.GetValues(head);
        foreach (var val in vals)
        {
            ret += $"{val} ";
        }

        ret += "\r\n";
    }

    if (e.Context.Request.ContentLength64 > 0)
    {

        ret += $"Size of content: {e.Context.Request.ContentLength64}\r\n";
        byte[] buff = new byte[e.Context.Request.ContentLength64];
        e.Context.Request.InputStream.Read(buff, 0, buff.Length);
        ret += $"Hex string representation:\r\n";
        for (int i = 0; i < buff.Length; i++)
        {
            ret += buff[i].ToString("X") + " ";
        }

    }

    WebServer.OutPutStream(e.Context.Response, ret);
}

这个 API 示例很简单,但如您所知,您可以选择做什么。

当您获得 URL 时,您可以检查是否调用了特定的控制器。您还有参数和内容有效载荷!

以下是一个调用示例的结果

result

还有更多!查看完整的示例了解有关此 WebServer 的更多信息!

使用 HTTPS

您需要生成证书和密钥

X509Certificate _myWebServerCertificate509 = new X509Certificate2(_myWebServerCrt, _myWebServerPrivateKey, "1234");

// X509 RSA key PEM format 2048 bytes
        // generate with openssl:
        // > openssl req -newkey rsa:2048 -nodes -keyout selfcert.key -x509 -days 365 -out selfcert.crt
        // and paste selfcert.crt content below:
        private const string _myWebServerCrt =
@"-----BEGIN CERTIFICATE-----
MORETEXT
-----END CERTIFICATE-----";

        // this one is generated with the command below. We need a password.
        // > openssl rsa -des3 -in selfcert.key -out selfcertenc.key
        // the one below was encoded with '1234' as the password.
        private const string _myWebServerPrivateKey =
@"-----BEGIN RSA PRIVATE KEY-----
MORETEXTANDENCRYPTED
-----END RSA PRIVATE KEY-----";

using (WebServer server = new WebServer(443, HttpProtocol.Https)
{
    // Add a handler for commands that are received by the server.
    server.CommandReceived += ServerCommandReceived;
    server.HttpsCert = _myWebServerCertificate509;

    server.SslProtocols = System.Net.Security.SslProtocols.Tls | System.Net.Security.SslProtocols.Tls11 | System.Net.Security.SslProtocols.Tls12;
    // Start the server.
    server.Start();

    Thread.Sleep(Timeout.Infinite);
}

[!重要] 由于上述证书不是由证书颁发机构签发的,因此它不会被认可为有效证书。如果您想通过浏览器访问 nanoFramework 设备,例如,您必须将 CRT 文件添加为可信对象。在 Windows 上,您只需双击 CRT 文件,然后单击“安装证书...”。

当然,您可以使用前面定义的路径。两种方法都有效,即使路由或具有控制器概念的路径。

WebServer 状态

可以通过订阅事件来获取 WebServer 的状态。这可以很有用,例如重新启动服务器,设置重试机制或类似机制。

server.WebServerStatusChanged += WebServerStatusChanged;

private static void WebServerStatusChanged(object obj, WebServerStatusEventArgs e)
{
    // Do whatever you need like restarting the server
    Debug.WriteLine($"The web server is now {(e.Status == WebServerStatus.Running ? "running" : "stopped" )}");
}

端到端测试

在 WebServerE2ETests 中有一个名为 nanoFramework WebServer E2E Tests.postman_collection.json 的 Postman 测试集合,它应该在真实世界场景中测试 WebServer。使用方法简单

  • 将 json 文件导入 Postman
  • 将 WebServerE2ETests 部署到您的设备上 - 复制 IP 地址
  • base_url 变量设置为与您的设备 IP 地址匹配
  • 选择要测试的请求或运行整个集合并检查测试结果。

反馈和文档

有关文档、提供反馈、问题以及了解如何贡献,请参阅 Home 仓库

加入我们的 Discord 社区 这里

鸣谢

此项目的贡献者名单可以在 CONTRIBUTORS 中找到。

许可

nanoFramework WebServer 库 licensed under the MIT license

行为准则

此项目已采用贡献者公约中定义的行为准则,以阐明我们在社区中期望的行为。有关更多信息,请参阅 .NET Foundation Code of Conduct

.NET Foundation

此项目由 .NET Foundation 支持。

产品 兼容和额外的目标框架版本。
.NET 框架 net 兼容。
兼容的目标框架
包含的目标框架(在包中)
了解有关 目标框架.NET Standard 的更多信息。

NuGet 包

此包没有被任何 NuGet 包使用。

GitHub 仓库 (1)

显示依赖 nanoFramework.WebServer 的最受欢迎的前 1 个 GitHub 仓库

仓库 星标
nanoframework/Samples
🍬 nanoFramework 团队用于测试、概念证明和其他探索性活动的代码示例
版本 下载 最后更新
1.2.56 63 7/30/2024
1.2.55 103 7/24/2024
1.2.52 241 6/3/2024
1.2.48 148 5/17/2024
1.2.45 90 5/13/2024
1.2.43 112 5/10/2024
1.2.40 206 4/12/2024
1.2.38 105 4/9/2024
1.2.36 95 4/8/2024
1.2.34 112 4/5/2024
1.2.32 103 4/3/2024
1.2.30 88 4/3/2024
1.2.27 304 2/14/2024
1.2.25 96 2/12/2024
1.2.23 192 1/26/2024
1.2.21 87 1/26/2024
1.2.19 82 1/26/2024
1.2.17 118 1/24/2024
1.2.14 423 11/17/2023
1.2.12 133 11/10/2023
1.2.9 115 11/9/2023
1.2.7 136 11/8/2023
1.2.6 106 11/8/2023
1.2.3 223 10/27/2023
1.2.1 138 10/25/2023
1.1.79 193 10/10/2023
1.1.77 178 10/4/2023
1.1.75 401 8/8/2023
1.1.73 233 7/27/2023
1.1.71 119 7/27/2023
1.1.65 652 2/17/2023
1.1.63 361 1/24/2023
1.1.61 258 1/24/2023
1.1.59 293 1/24/2023
1.1.56 380 12/30/2022
1.1.54 304 12/28/2022
1.1.51 316 12/27/2022
1.1.47 685 10/26/2022
1.1.44 378 10/25/2022
1.1.41 365 10/24/2022
1.1.39 400 10/23/2022
1.1.36 400 10/10/2022
1.1.32 402 10/8/2022
1.1.29 446 9/22/2022
1.1.27 413 9/22/2022
1.1.25 426 9/22/2022
1.1.23 483 9/16/2022
1.1.21 458 9/15/2022
1.1.19 500 8/29/2022
1.1.17 525 8/6/2022
1.1.14 423 8/4/2022
1.1.12 389 8/3/2022
1.1.10 421 8/3/2022
1.1.8 389 8/3/2022
1.1.6 583 6/13/2022
1.1.4 452 6/8/2022
1.1.2 406 6/8/2022
1.1.1 450 5/30/2022
1.0.0 676 3/30/2022
1.0.0-preview.260 138 3/29/2022
1.0.0-preview.258 122 3/28/2022
1.0.0-preview.256 122 3/28/2022
1.0.0-preview.254 128 3/28/2022
1.0.0-preview.252 115 3/28/2022
1.0.0-preview.250 123 3/28/2022
1.0.0-preview.248 139 3/17/2022
1.0.0-preview.246 127 3/14/2022
1.0.0-preview.244 120 3/14/2022
1.0.0-preview.242 120 3/14/2022
1.0.0-preview.240 118 3/14/2022
1.0.0-preview.238 127 3/8/2022
1.0.0-preview.236 126 3/8/2022
1.0.0-preview.234 118 3/4/2022
1.0.0-preview.232 115 3/3/2022
1.0.0-preview.230 127 3/2/2022
1.0.0-preview.228 125 2/28/2022
1.0.0-preview.226 159 2/24/2022
1.0.0-preview.222 136 2/17/2022
1.0.0-preview.220 132 2/17/2022
1.0.0-preview.218 163 2/6/2022
1.0.0-preview.216 122 2/4/2022
1.0.0-preview.214 141 2/4/2022
1.0.0-preview.212 149 1/28/2022
1.0.0-preview.210 142 1/28/2022
1.0.0-preview.208 138 1/28/2022
1.0.0-preview.206 136 1/25/2022
1.0.0-preview.204 133 1/21/2022
1.0.0-preview.202 126 1/21/2022
1.0.0-preview.200 133 1/21/2022
1.0.0-preview.198 133 1/21/2022
1.0.0-preview.196 135 1/21/2022
1.0.0-preview.194 146 1/13/2022
1.0.0-preview.192 142 1/12/2022
1.0.0-preview.190 137 1/12/2022
1.0.0-preview.188 131 1/11/2022
1.0.0-preview.186 140 1/11/2022
1.0.0-preview.183 150 1/6/2022
1.0.0-preview.181 139 1/5/2022
1.0.0-preview.180 146 1/3/2022
1.0.0-preview.179 137 1/3/2022
1.0.0-preview.178 136 1/3/2022
1.0.0-preview.177 139 12/30/2021
1.0.0-preview.176 143 12/28/2021
1.0.0-preview.174 183 12/3/2021
1.0.0-preview.172 155 12/3/2021
1.0.0-preview.170 144 12/3/2021
1.0.0-preview.168 145 12/3/2021
1.0.0-preview.166 145 12/3/2021
1.0.0-preview.164 148 12/2/2021
1.0.0-preview.162 147 12/2/2021
1.0.0-preview.160 144 12/2/2021
1.0.0-preview.158 144 12/2/2021
1.0.0-preview.156 147 12/2/2021
1.0.0-preview.154 142 12/2/2021
1.0.0-preview.152 152 12/1/2021
1.0.0-preview.150 139 12/1/2021
1.0.0-preview.148 149 12/1/2021
1.0.0-preview.145 194 11/11/2021
1.0.0-preview.143 189 10/22/2021
1.0.0-preview.141 176 10/18/2021
1.0.0-preview.138 198 10/18/2021
1.0.0-preview.136 283 7/17/2021
1.0.0-preview.134 150 7/16/2021
1.0.0-preview.132 161 7/16/2021
1.0.0-preview.130 168 7/15/2021
1.0.0-preview.128 176 7/14/2021
1.0.0-preview.126 261 6/19/2021
1.0.0-preview.124 257 6/19/2021
1.0.0-preview.122 157 6/17/2021
1.0.0-preview.119 160 6/7/2021
1.0.0-preview.117 154 6/7/2021
1.0.0-preview.115 194 6/7/2021
1.0.0-preview.113 193 6/7/2021
1.0.0-preview.111 208 6/6/2021
1.0.0-preview.109 905 6/5/2021
1.0.0-preview.107 168 6/3/2021
1.0.0-preview.105 156 6/2/2021
1.0.0-preview.103 158 6/2/2021
1.0.0-preview.101 169 6/1/2021
1.0.0-preview.99 186 6/1/2021
1.0.0-preview.96 184 6/1/2021
1.0.0-preview.94 191 5/31/2021
1.0.0-preview.92 199 5/30/2021
1.0.0-preview.90 166 5/27/2021
1.0.0-preview.88 171 5/26/2021
1.0.0-preview.86 282 5/23/2021
1.0.0-preview.84 187 5/22/2021
1.0.0-preview.82 225 5/21/2021
1.0.0-preview.80 178 5/19/2021
1.0.0-preview.78 165 5/19/2021
1.0.0-preview.76 187 5/19/2021
1.0.0-preview.71 170 5/15/2021
1.0.0-preview.69 144 5/14/2021
1.0.0-preview.66 168 5/13/2021
1.0.0-preview.64 178 5/11/2021
1.0.0-preview.62 165 5/11/2021
1.0.0-preview.59 226 5/6/2021
1.0.0-preview.57 153 5/5/2021
1.0.0-preview.51 174 4/12/2021
1.0.0-preview.49 170 4/12/2021
1.0.0-preview.47 183 4/10/2021
1.0.0-preview.44 180 4/6/2021
1.0.0-preview.41 159 4/5/2021
1.0.0-preview.32 197 3/21/2021
1.0.0-preview.30 216 3/20/2021
1.0.0-preview.28 203 3/19/2021
1.0.0-preview.26 190 3/18/2021
1.0.0-preview.24 152 3/17/2021
1.0.0-preview.22 163 3/17/2021
1.0.0-preview.20 186 3/5/2021
1.0.0-preview.18 162 3/2/2021
1.0.0-preview.15 407 1/19/2021
1.0.0-preview.13 175 1/19/2021
1.0.0-preview.11 233 1/7/2021
1.0.0-preview.10 196 12/22/2020
1.0.0-preview.6 262 12/1/2020
1.0.0-preview.3 263 11/6/2020