博客 (112)

ASP.NET 10 网站在 VS 中运行正常,发布到 IIS 后,某个页面打开报 500 错误,在 Action 中整个套上 try-catch 捕获不到错误,怎么才能查看这个异常原因?

最直接的方法是:在 Windows 中打开“事件查看器”,导航到“Windows 日志” -> “应用程序”,查找来源为 IIS AspNetCore Module、.NET Runtime 或 IIS Express AspNetCore Module 的错误事件。

当然你也可以在 IIS 管理器中配置对应网站的错误页,或在 Program.cs 中启用了开发者异常页面,但是非常不建议在开发环境中开启这些。

xoyozo 1 个月前
196

问题复现

使用 .NET Framework 开发的网站项目,用 QQ 浏览器访问无法登录成功,用其它浏览器(如 Edge)没有问题。

只有访问 https 地址时出现问题。


原因

在 HTTPS 协议下,现代浏览器(特别是 QQ 浏览器)会强制执行安全策略。根据规范,当 Cookie 设置了 SameSite=None 时,必须同时设置 Secure 属性,否则浏览器会静默拒绝(丢弃)该 Cookie。


解决方案

方法一:如果您是通过 web.config 配置的,请确保 <system.web> 节点下的 <httpCookies> 设置正确,并且您的 .NET Framework 版本支持这些属性(4.7.2+)。

<system.web>
    <httpCookies sameSite="None" requireSSL="true" />
    <sessionState ... />
</system.web>

方法二:在 Global.asax 文件的 Application_PostAuthenticateRequest 或 Application_EndRequest 事件中,强制为 Cookie 添加 Secure 属性。

protected void Application_PostAuthenticateRequest(object sender, EventArgs e)
{
    // 仅在 HTTPS 环境下处理
    if (Request.IsSecureConnection)
    {
        HttpCookie sessionCookie = Response.Cookies["ASP.NET_SessionId"];
        if (sessionCookie != null)
        {
            // 强制设置 SameSite=None 和 Secure
            sessionCookie.SameSite = SameSiteMode.None;
            sessionCookie.Secure = true; 
            Response.Cookies.Set(sessionCookie);
        }
    }
}


检验

确认 Set-Cookie 的值变为:

ASP.NET_SessionId=...; path=/; HttpOnly; SameSite=None; Secure


xoyozo 2 个月前
296

以下是 Spectre.Console 的核心功能示例,涵盖最常用的输出和交互场景:


1. 基础富文本输出

image.png

using Spectre.Console;

// 使用 Markup 语法(类似 BBCode)
AnsiConsole.Markup("[bold green]成功![/] 文件已保存。\n");
AnsiConsole.Markup("[red]错误:[/] 无法连接到服务器。\n");

// 混合样式
AnsiConsole.Markup("[underline blue]https://example.com[/]\n");

// 自动换行写
AnsiConsole.Write(new Panel("[yellow]警告[/] 磁盘空间不足")
    .Header("系统通知")
    .Border(BoxBorder.Rounded));


2. 表格

image.png

var table = new Table();
table.AddColumn("[u]ID[/]");
table.AddColumn(new TableColumn("[u]名称[/]").Centered());
table.AddColumn("[u]状态[/]");

table.AddRow("1", "订单服务", "[green]运行中[/]");
table.AddRow("2", "支付网关", "[red]离线[/]");
table.AddRow("3", "消息队列", "[yellow]警告[/]");

AnsiConsole.Write(table);


3. 进度条 / 状态指示

image.pngimage.png

// 带进度条的循环任务
await AnsiConsole.Progress()
    .StartAsync(async ctx =>
    {
        var task1 = ctx.AddTask("[green]下载文件[/]", maxValue: 100);
        var task2 = ctx.AddTask("[green]处理数据[/]", maxValue: 100);

        while (!ctx.IsFinished)
        {
            task1.Increment(1.5);
            task2.Increment(0.8);
            await Task.Delay(50);
        }
    });

// 不确定时长的旋转状态
await AnsiConsole.Status()
    .StartAsync("正在连接...", async ctx =>
    {
        await Task.Delay(3000); // 模拟工作
        AnsiConsole.MarkupLine("[green]连接成功![/]");
    });


4. 交互式提示

image.png

// 确认
if (AnsiConsole.Confirm("是否继续安装?"))
{
    // 执行安装
}

// 文本输入(带验证)
var name = AnsiConsole.Prompt(
    new TextPrompt<string>("请输入用户名:")
        .ValidationErrorMessage("[red]用户名不能为空[/]")
        .Validate(input => !string.IsNullOrWhiteSpace(input)));

// 选择列表
var fruit = AnsiConsole.Prompt(
    new SelectionPrompt<string>()
        .Title("请选择最喜欢的水果")
        .AddChoices(new[] { "苹果", "香蕉", "橙子", "葡萄" }));

AnsiConsole.MarkupLine($"你选择了:[green]{fruit}[/]");

// 多选
var colors = AnsiConsole.Prompt(
    new MultiSelectionPrompt<string>()
        .Title("请选择颜色")
        .AddChoices(new[] { "红色", "绿色", "蓝色", "黄色" }));


5. 树形结构

image.png

var root = new Tree("[yellow]项目结构[/]");
var src = root.AddNode("src");
src.AddNode("Program.cs");
src.AddNode("Services");
src.AddNode("Models");

var tests = root.AddNode("tests");
tests.AddNode("UnitTests");

AnsiConsole.Write(root);


6. 布局 / 网格

image.png

var layout = new Layout("Root")
    .SplitColumns(
        new Layout("Left").Size(30),
        new Layout("Right")
            .SplitRows(
                new Layout("Top"),
                new Layout("Bottom")));

layout["Left"].Update(new Panel("导航栏"));
layout["Top"].Update(new Panel("主内容区"));
layout["Bottom"].Update(new Panel("日志输出"));

AnsiConsole.Write(layout);


7. 实时更新

image.png

var table = new Table().AddColumn("Time").AddColumn("Message");
table.AddRow("10:00", "系统启动");

await AnsiConsole.Live(table)
    .StartAsync(async ctx =>
    {
        for (int i = 1; i <= 5; i++)
        {
            await Task.Delay(1000);
            table.AddRow($"10:0{i}", $"事件 {i}");
            ctx.Refresh(); // 刷新显示
        }
    });


8. 带样式的异常显示

image.png

try
{
    throw new InvalidOperationException("操作失败");
}
catch (Exception ex)
{
    AnsiConsole.WriteException(ex, ExceptionFormats.ShortenPaths);
}


9. 日历

image.png

var calendar = new Calendar(2026, 4);
calendar.AddCalendarEvent(2026, 4, 22); // 标记日期
calendar.HighlightStyle(Style.Parse("yellow bold"));
AnsiConsole.Write(calendar);


10. 条形图 / 柱状图

image.png

AnsiConsole.Write(new BarChart()
    .Width(60)
    .Label("[green bold]项目进度[/]")
    .CenterLabel()
    .AddItem("后端", 80, Color.Green)
    .AddItem("前端", 65, Color.Blue)
    .AddItem("测试", 40, Color.Red));


11. 分解图

image.png

AnsiConsole.Write(new BreakdownChart()
    .Width(60)
    .AddItem("CPU", 45, Color.Red)
    .AddItem("内存", 30, Color.Blue)
    .AddItem("磁盘", 25, Color.Green));


12. 规则线

image.png

AnsiConsole.Write(new Rule("[red]警告区域[/]").RuleStyle("red").LeftJustified());
AnsiConsole.WriteLine("内容");
AnsiConsole.Write(new Rule().RuleStyle("green"));


13. 文本样式

image.png

// 大字标题
AnsiConsole.Write(new FigletText("Hello").Color(Color.Green));
// Emoji
AnsiConsole.Markup(":check_mark_button: 成功 :cross_mark: 失败");


14. 网格

image.png

var grid = new Grid();
grid.AddColumn(new GridColumn().NoWrap().PadRight(4));
grid.AddColumn();

grid.AddRow("[b]名称[/]", "Spectre.Console");
grid.AddRow("[b]版本[/]", "0.49.1");
grid.AddRow("[b]许可[/]", "MIT");

AnsiConsole.Write(grid);






xoyozo 3 个月前
506

今天发现在 .NET Framework 和 .NET 9 中使用 System.Uri.EscapeDataString() 方法对字符串进行编码,会产生不同的结果。

譬如“(”符号,前者视其为非保留字符,不进行转义,后者视为保留字符,转义为“%28”。

原因是 .NET Framework 4.8 主要遵循 RFC 2396,而 .NET 9 遵循 RFC 3986。


在跨平台签名验证场景中,对 URL 编码的一致性要求极高,任何细微差别都会导致签名校验失败。

以下是以 RFC 3986 标准为核心、优先使用各平台内置的高一致性方案。


对于 .NET 9,直接使用 Uri.EscapeDataString()。

string encodedData = System.Uri.EscapeDataString(dataToEncode);


对于 .NET Framework,以下是一个遵循 RFC 3986 严格标准的自定义编码方法示例。

static string Rfc3986EscapeDataString(string input)
{
    // 定义 RFC 3986 中明确的未保留字符集(不编码)
    var unreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";

    var result = new StringBuilder();
    byte[] data = Encoding.UTF8.GetBytes(input); // 统一转换为 UTF-8 字节

    foreach (byte b in data)
    {
        char currentChar = (char)b;
        // 如果是未保留字符,直接输出
        if (unreservedChars.IndexOf(currentChar) != -1)
        {
            result.Append(currentChar);
        }
        else
        {
            // 否则,进行百分号编码(%XX,大写)
            result.Append('%').Append(b.ToString("X2"));
        }
    }
    return result.ToString();
}


对于 PHP,直接使用内置的 rawurlencode() 函数。这个函数的设计初衷就是严格遵循 RFC 3986 标准。

$encoded_data = rawurlencode($data_to_encode);


对于 JavaScript,encodeURIComponent 函数严格遵循 RFC 3986 标准。

let encodedData = encodeURIComponent(dataToEncode);


重要提示:无论使用哪种语言,务必在编码前明确指定字符串使用 UTF-8 编码。编码不一致是导致乱码和签名失败最常见的原因之一 。

xoyozo 8 个月前
1,155

今天遇到一个向基于 .NET Framework 框架开发的 Web 网站 POST 数据响应 405 Method Not Allowed 的问题。

服务端接口地址是 https://***/api/abc/Default.aspx,

若请求 https://***/api/abc/Default.aspx 或 https://***/api/abc/ 则正常 ,

若请求 https://***/api/abc 就会出现这个问题。

原因可能是服务器可能返回 301 重定向到 https://***/api/abc/Default.aspx,

导致最终接口接受到的请求方法不是 POST 。

xoyozo 9 个月前
1,017

个人整理,仅供参考。具体规格请以官方发布为准。


X1.pngx1p.pngx3p.png
名称X1X1 ProX3 Pro 日照金山(1母1子套装)
上市时间2025/42025/42025/11
RAM512 MB512 MB
ROM128 MB128 MB
频段2.4 GHz 速率为 688 Mbps

5 GHz 速率为 2882 Mbps

2.4 GHz 速率为 688 Mbps

5 GHz 速率为 2882 Mbps

2.4 GHz

5 GHz

天线

5 根全向高增益天线

5 根定向智能天线

1 根星闪天线

5 根全向高增益天线

5 根定向智能天线

1根 星闪天线


星闪网关支持支持
蓝牙网关支持支持
接口

2.5GE * 2

1GE * 2

2.5GE * 4
Wi-FiWi-Fi 7+Wi-Fi 7+Wi-Fi 7+
适用面积90-120㎡90-120㎡主路由覆盖90㎡内,每增加一个子路由可扩展约30㎡
价格



xoyozo 9 个月前
1,187

使用 Linq 语法调用数据库时,需要包含导航属性(外键表数据)会用到 Include 方法,但是如果引用的程序集搞错了,就不会有数据输出,应该:

using Microsoft.EntityFrameworkCore;

而不是

using System.Data.Entity;

xoyozo 9 个月前
542

个人整理,仅供参考。具体规格请以官方发布为准。

名称 BE10000 BE7000 BE6500 Pro BE6500 BE3600 Pro 套装 BE3600 Pro 网线版 BE10000 Pro BE7200 Pro
型号

主:RN04

子:RN01

主:RP01

子:RP03

上市时间 2022.10 2023.5 2023.10 2024.8 2024.10 2025.5 2025.9 2026.5
处理器 Qualcomm 四核 A73 2.2GHz Qualcomm 四核 A73 1.5GHz Qualcomm 四核 A53 1.5GHz Qualcomm 四核 A53 1.1GHz

主/子:

高通 IPQ5312 四核 1.1GHz

主/子:

Qualcomm Dragonwing N7

Qualcomm A7 四核 1.8GHz Qualcomm 四核 A55 1.8GHz
内存 2GB 1GB 1GB 512MB

主/子:512MB

一说子是 128MB

主:512MB

子:256MB

2GB 1G DRAM + 512MB Flash
频段 2.4GHz、5.2GHz、5.8GHz 2.4GHz、5GHz 2.4GHz、5GHz 2.4GHz、5GHz 2.4GHz、5GHz 2.4GHz、5GHz 2.4GHz、5.2GHz、5.8GHz 2.4GHz、5GHz
组网 混合 Mesh 混合 Mesh 混合 Mesh 混合 Mesh 混合 Mesh AC + AP AI Mesh AI Mesh
天线 12根高增益天线 + 12路信号放大器 + NFC内置天线 7根外置高增益WiFi天线 + 1根内置高增益WiFi天线 + NFC内置天线 6根高增益WiFi内置天线 + 1根蓝牙内置天线 + 1根NFC内置天线 6根外置高增益Wi-Fi天线 主/子:4根内置天线

主:无

子:2根内置双频天线

12根高增益天线 + 12路信号放大器 8根高增益天线 + 8路信号放大器
中枢网关 不支持 不支持 支持 不支持

主:支持

子:不支持

主:支持

子:不支持

支持 支持
蓝牙网关 不支持 不支持

蓝牙 Mesh 1.0 100 台 + 蓝牙 100 台

升级固件后支持蓝牙 Mesh 2.0

蓝牙 Mesh 1.0

蓝牙 Mesh 1.0 200 台 + 蓝牙 100 台

主:不支持

子:蓝牙 Mesh 2.0

蓝牙 Mesh 2.0 200 台 + 蓝牙 100 台

蓝牙 Mesh 2.0 200 台 + 蓝牙 100 台
散热 主动散热 自然散热 自然散热 自然散热 自然散热

主:自然散热

子:主动散热

主动散热 自然散热
接口

4×2.5G

1×10G

1×10G SFP+

1×USB 3.0

4×2.5G

1×USB 3.0

4×2.5G 4×2.5G

主/子:

1×2.5G

3×1G

主:5×2.5G

子:2×2.5G

4×2.5G

2×10G

1×M.2

1×USB 3.0

4×2.5G
Wi-Fi Wi-Fi 7 Wi-Fi 7 Wi-Fi 7 Wi-Fi 7 Wi-Fi 7

主:无

子:Wi-Fi 7

Wi-Fi 7 Wi-Fi 7
MLO 多链路聚合

双频 4.3Gbps

5 GHz 和 5 GHz-Game

双频 4.3Gbps

5 GHz 和 5 GHz-Game

双频 3.6Gbps

2.4 GHz 和 5 GHz

双频 3.57Gbps

2.4 GHz 和 5 GHz

双频 3.57Gbps

2.4 GHz 和 5 GHz

双频

2.4 GHz 和 5 GHz

双频 4.3Gbps

5 GHz 和 5 GHz-Game

双频 3.6Gbps

2.4 GHz 和 5 GHz

价格
最新价格 最新价格 最新价格 最新价格 最新价格 最新价格 最新价格 最新价格
  • 表格于 2025 年 10 月整理更新。

  • 如果只考虑支持蓝牙 Mesh 2.0,那么有 BE3600 Pro 网线版 和 BE10000 Pro 可选,搭配其它 Mesh 路由器实现全屋 Wi-Fi 7 覆盖,搭配其它中枢网关或从网关设备实现全屋蓝牙 Mesh 2.0 覆盖。

  • 如果考虑用 Xiaomi 中枢网关 来部署独立的中枢架构,那么选择路由器就没有限制了。

  • 名称中带有“全屋”字样的通常以子母套装形式出售,子母路由配置通常不同。购买两台一模一样的普通 BE 路由器,就相当于组建了一套“不分子母”的 Mesh 套装。

  • 名称中带有“Pro”字样的通常具备中枢网关功能。

xoyozo 9 个月前
6,198

在 Linux 上运行 .NET 网站,通过

HttpContext.Connection.RemoteIpAddress

获取客户端的 IP 地址,结果是

::ffff:127.0.0.1

解决方法:

打开 Program.cs 文件,在 var app = builder.Build(); 之前(尽量往前)添加以下代码:

if (OperatingSystem.IsLinux())
{
    builder.Services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.ForwardedHeaders = ForwardedHeaders.XForwardedFor
                                    | ForwardedHeaders.XForwardedProto
                                    | ForwardedHeaders.XForwardedHost;

        // 清除 KnownNetworks 和 KnownProxies,表示信任来自本机的代理(如 Nginx)
        options.KnownNetworks.Clear();
        options.KnownProxies.Clear();
    });

    Console.WriteLine("ForwardedHeaders enabled (Running on Linux)");
}

然后在 app.UseRouting(); 之前添加以下代码:

if (OperatingSystem.IsLinux())
{
    app.UseForwardedHeaders();
    Console.WriteLine("UseForwardedHeaders() applied.");
}

其中,OperatingSystem.IsLinux() 用于判断只在 Linux 环境中生效,你可以视自身情况作判断。

xoyozo 11 个月前
4,526

AutoUpdater.NET 是一个开源库,专为 .NET 桌面应用程序设计,支持 Windows Forms 和 WPF 应用。它通过从服务器获取 XML 文件来检测新版本信息,当发现新版本时向用户显示更新对话框。

相对于 ClickOnce,AutoUpdater.NET 的配置更简单一些。

首先通过 NuGet 包管理器安装 Autoupdater.NET.Official,然后在应用程序入口点添加以下代码:

AutoUpdater.Start("http://yourserver.com/path/to/update.xml");

XML 文件结构如下:

<?xml version="1.0" encoding="UTF-8"?>
<item>
  <version>2.0.0.0</version> <!--必填:最新版本号-->
  <url>http://yourserver.com/path/to/updatefile.zip</url> <!--必填:更新文件下载地址-->
  <changelog>http://yourserver.com/path/to/changelog.txt</changelog> <!--可选:更新日志-->
  <mandatory>False</mandatory> <!--可选:是否强制更新-->
</item>

Windows Forms 在 Program.cs 文件的 Main() 方法中,WPF 在 App.xaml.cs 的 OnStartup() 中添加:

AutoUpdater.Start("http://yourserver.com/path/to/update.xml");
AutoUpdater.CheckForUpdateEvent += (e) =>
{
    if (e.Error != null)
    {
        MessageBox.Show($"检查版本更新失败:{e.Error.Message}");
    }
    if (e.IsUpdateAvailable)
    {
        if (e.Mandatory.Value)
        {
            // 强制更新
            AutoUpdater.DownloadUpdate(e);
        }
        else
        {
            // 可选更新
            if (MessageBox.Show("发现新版本,是否立即更新?", "更新提示", MessageBoxButtons.YesNo, MessageBoxIcon.Information) == DialogResult.Yes)
            {
                AutoUpdater.DownloadUpdate(e);
            }
        }
    }
};

这样就能简单实现打开应用时判断是否有更新。具体用法参:GitHub

对于控制台应用程序,AutoUpdater.NET 并不直接支持。可以使用 GeneralUpdate 等轻量级自动更新库。

其它:

  • 若希望通过标准安装流程(如添加到开始菜单),优先选择 ClickOnce,适合长期维护的内部工具。

  • 若追求快速集成和无感更新,AutoUpdater.NET 更灵活。

  • 无论哪种方案,务必对程序进行代码签名(如购买企业证书或生成测试证书),否则系统可能拦截安装。(你要允许来自未知发布者的此应用对你的设备进行更改吗)

  • 可以购买“OV 代码签名”证书或“EV 代码签名”证书,注意:“代码签名证书”与“域名证书”互不通用。

xoyozo 11 个月前
7,591