如何使用 PostMan 进行并发测试?_postman 并发测试-CSDN博客

mikel阅读(25)

来源: 如何使用 PostMan 进行并发测试?_postman 并发测试-CSDN博客

01、POST篇
要在Postman中对POST请求进行压力测试,可以按照以下步骤进行操作:

打开Postman应用程序并创建一个新的请求集合(Collection)。

在请求集合中创建一个新的请求,并选择HTTP方法为POST。

在请求URL字段中输入要测试的目标URL地址。

在请求主体(Body)部分中输入POST请求的有效载荷(Payload)数据。

配置请求头(Headers)和其他必要的参数。

在Postman界面的右上角,找到“Runner”按钮并点击它。

进入运行器(Runner)界面,选择之前创建的请求集合,并配置运行参数。

可以设置运行器的迭代次数、并发请求数量、延迟时间等选项,根据需要进行调整。

点击“Start Run”按钮开始运行压力测试。

运行结束后,可以查看每个请求的响应结果和性能指标,如响应时间、吞吐量等。

请注意

进行压力测试时要确保目标服务器能够承受相应的负载。此外,压力测试可能会对目标服务器产生一定的负担,请谨慎操作,避免对生产环境或敏感系统造成不良影响。

流程

 

 

 

 

id 和 procureId 均是唯一的,不能插入重复,因此要在Pre-req中设置变量

现在我也找了很多测试的朋友,做了一个分享技术的交流群,共享了很多我们收集的技术文档和视频教程。
如果你不想再体验自学时找不到资源,没人解答问题,坚持几天便放弃的感受
可以加入我们一起交流。而且还有很多在自动化,性能,安全,测试开发等等方面有一定建树的技术大牛
分享他们的经验,还会分享很多直播讲座和技术沙龙
可以免费学习!划重点!开源的!!!
qq群号:691998057【暗号:csdn999】
AI构建项目

Pre-req 脚本 js

在这里(View -> Show Postman Console)可以看到log的内容

 

生成的id: a54dc69c8ba94dffb2a0813fcf88c069

生成的procureId: P20231201132925285867

并发操作步骤

 

结果

 

02、GET篇
添加环境变量

 

 

引用变量

 

下面是配套资料,对于做【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!

 

最后: 可以在公众号:自动化测试老司机 ! 免费领取一份216页软件测试工程师面试宝典文档资料。以及相对应的视频学习教程免费分享!,其中包括了有基础知识、Linux必备、Shell、互联网程序原理、MySQL数据库、抓包工具专题、接口测试工具、测试进阶-Python编程、Web自动化测试、APP自动化测试、接口自动化测试、测试高级持续集成、测试架构开发测试框架、性能测试、安全测试等。

如果我的博客对你有帮助、如果你喜欢我的博客内容,请 “点赞” “评论” “收藏” 一键三连哦!
————————————————
版权声明:本文为CSDN博主「自动化测试老司机」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_47485438/article/details/138800928

AI面试官:Asp.Net 中使用Log4Net (一) - Ysの陈憨憨 - 博客园

mikel阅读(67)

log4net.config”

来源: AI面试官:Asp.Net 中使用Log4Net (一) – Ysの陈憨憨 – 博客园

1. 先新建一个ASP.NET Core空项目

 

 

1. 什么是log4net?它的作用是什么?

解答:log4net是一个开源的日志记录框架,用于在.NET应用程序中记录日志信息。它可以帮助开发人员将不同级别的日志信息输出到不同的目标(如文件、数据库、控制台等),以便进行应用程序的调试、监控和错误追踪。

案例和代码:假设有一个.NET Core Web API应用程序,我们可以通过集成log4net来记录API请求和异常信息,并将日志信息输出到文件中。

// 首先,通过NuGet安装log4net包

// 在Startup.cs中添加log4net配置
public void ConfigureLogging(IServiceCollection services)
{
    services.AddLogging(builder =>
    {
        builder.AddLog4Net(); // 添加log4net
    });
}

2. log4net日志级别有哪些?如何设置日志级别?

解答:log4net定义了几个日志级别,包括DebugINFOWARNERRORFATAL。可以通过在配置文件或代码中设置<root>或特定<logger>节点的level属性来指定日志级别。

案例和代码:在log4net的配置文件中设置日志级别,例如输出INFO级别及以上的日志信息:

<log4net>
    <root>
        <level value="INFO" />
        <appender-ref ref="FileAppender" />
    </root>
    <!-- 其他appender配置 -->
</log4net>

3. 如何在.NET Core应用程序中使用log4net记录日志?

解答:在.NET Core应用程序中使用log4net需要使用第三方库log4net.Extensions.Logging来进行集成。通过添加log4net的配置,然后在代码中使用ILogger接口来记录日志。

案例和代码:在.NET Core控制台应用程序中使用log4net记录日志:

class Program
{
    private static readonly ILog log = LogManager.GetLogger(typeof(Program));

    static void Main(string[] args)
    {
        // 初始化log4net
        var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
        XmlConfigurator.Configure(logRepository, new FileInfo("log4net.config"));

        log.Info("Application started");

        try
        {
            // 业务逻辑代码
            log.Debug("Some debug information");
        }
        catch (Exception ex)
        {
            log.Error("An error occurred", ex);
        }

        log.Info("Application ended");
    }
}

4. log4net如何输出日志到文件?

解答:可以使用RollingFileAppenderFileAppender来将日志输出到文件。FileAppender每次启动时创建一个新的日志文件,而RollingFileAppender会根据配置的文件大小或日期来生成不同的日志文件。

案例和代码:在log4net的配置文件中添加FileAppender,将日志输出到文件:

<log4net>
    <appender name="FileAppender" type="log4net.Appender.FileAppender">
        <file value="logs/application.log" />
        <appendToFile value="true" />
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="%date %level %logger - %message%newline" />
        </layout>
    </appender>

    <root>
        <level value="INFO" />
        <appender-ref ref="FileAppender" />
    </root>
</log4net>

5. log4net如何将日志信息输出到数据库?

解答:可以使用AdoNetAppender将日志信息输出到数据库。需要配置数据库连接字符串、日志表的结构和相应的SQL语句。

案例和代码:在log4net的配置文件中添加AdoNetAppender,将日志输出到数据库:

<log4net>
    <appender name="AdoNetAppender" type="log4net.Appender.AdoNetAppender">
        <!-- 数据库连接字符串 -->
        <connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
        <connectionString value="Data Source=ServerName;Initial Catalog=DatabaseName;Integrated Security=True;" />

        <!-- 日志表的结构 -->
        <commandText value="INSERT INTO LogTable (Date, Level, Logger, Message) VALUES (@log_date, @log_level, @logger, @message)" />
        <parameter>
            <parameterName value="@log_date" />
            <dbType value="DateTime" />
            <layout type="log4net.Layout.RawTimeStampLayout" />
        </parameter>
        <parameter>
            <parameterName value="@log_level" />
            <dbType value="String" />
            <size value="50" />
            <layout type="log4net.Layout.PatternLayout">
                <conversionPattern value="%level" />
            </layout>
        </parameter>
        <parameter>
            <parameterName value="@logger" />
            <dbType value="String" />
            <size value="255" />
            <layout type="log4net.Layout.PatternLayout">
                <conversionPattern value="%logger" />
            </layout>
        </parameter>
        <parameter>
            <parameterName value="@message" />
            <dbType value="String" />
            <size value="4000" />
            <layout type="log4net.Layout.PatternLayout">
                <conversionPattern value="%message" />
            </layout>
        </parameter>
    </appender>

    <root>
        <level value="INFO" />
        <appender-ref ref="AdoNetAppender" />
    </root>
</log4net>

windows下PC端小程序抓包--Fiddler&Charles-腾讯云开发者社区-腾讯云

mikel阅读(195)

来源: windows下PC端小程序抓包–Fiddler&Charles-腾讯云开发者社区-腾讯云

【背景说明】

当压测小程序没有原码和小程序开发者权限时,我们压测脚本中的header和入参需要通过抓包工具进行抓包,本文讲解在windows下通过pc端微信小程序抓包拿取需要的参数,如需通过手机端抓包和工具下载安装,请参考以下文章:

Fiddler工具下载和安装:https://cloud.tencent.com/developer/article/1810615

Charles工具下载和安装:https://cloud.tencent.com/developer/article/1833591

【操作说明】

Fiddler抓包:

  1. 打开Fiddler工具,开启抓包

2、打开微信pc端小程序

发现fiddler请求数据很多,这时我们可以通过请求过滤来过滤掉没用的请求,操作如下:

3、查看抓到的请求,拿去需要的header和入参信息

4、拿取到需要的参数后复制粘贴到压测工具中即可

Charles抓包:

1、打开安装好的Charles工具,开启抓包

2、这里着重说下windows下证书的安装(比较坑的地方)

点击Help -> SSL Proxying -> Install Charies Root Certificate

这里一定要选择第二个安装地址,将证书安装到受信任的根证书下,不要使用第一个自动选择证书存储

3、证书安装完成后重新请求小程序,抓取到的内容如图,这里我们使用的是Structure模式,根据URL来查看

4、把抓包拿取到的数据放入压测工具中即可

【总结】

总的来说通过抓包工具实现header和入参的拿取还是很简单的,需要注意的就是证书安装问题

CLodop打印控件谷歌浏览器新版本禁用跨域访问解决方法

mikel阅读(116)

最近项目的CLodop打印控制在谷歌浏览器自动升级到V140新版本后,打印提示禁止跨域访问本地的CLodop服务的端口的问题,过去老版本还可以用禁用跨域检测来解决Lodop谷歌浏览器无法打印,解决谷歌浏览器最新chrome94版本CORS跨域问题 – 知乎》,现在没有这个设置项了

网上提供了三种方法,分别是设置调试模式

Chrome 临时关闭 CORS跨域报错

在快捷方式的属性 后面添加

–disable-web-security –user-data-dir=C:\Users\你的用户名\Desktop\Google Chrome Dev

另一个就是在服务器上配置Ngnix代理方式:

nginx

# 在xxxx.gnway.vip的Nginx配置中添加
location /lodop/ {
proxy_pass https://localhost.lodop.net:8443/;
proxy_ssl_verify off;
proxy_set_header Host localhost.lodop.net;
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "*";
}

第三个方案是将项目改成https的访问,我看了下CLodop的打印控件服务它开通了https本地访问地址

我将调取Lodop的脚本链接地址改成了https也不行还是提示跨域错误

于是想到既然不允许跨域,我在服务器安装Lodop打印控件,然后运行后,通过域名解析到打印控件的端口18000,然后同域名下不就可以访问了吗,说干就干

首先服务器安装打印控件

运行打印控件后,查看端口设置

在域名服务器上坐下映射解析

访问一下打印控件的脚本文件,可以正常访问,那应该就没有问题了

下面修改下加载打印脚本的地址

测试下项目的打印功能,可以正常加载Lodop打印控件的脚本了,打印正常了,跨域问题其实没有根本解决,只是折中了下改成了同域名下,就不算跨域了。

C# 任务队列还在轮询?300%性能提升的智能调度方案来了!

mikel阅读(138)

来源: C# 任务队列还在轮询?300%性能提升的智能调度方案来了!

前言

后端系统开发中,异步任务处理几乎是绕不开的环节。然而,传统的ConcurrentQueue<T> + 轮询方案存在诸多弊端,如CPU空转严重、内存占用高、响应延迟大等,严重影响系统性能。

对于.NET开发而言,实现一个高效的任务处理机制至关重要,它能让系统性能得到显著提升。本文将介绍如何用C#完成一个真正高效的任务处理器,告别性能瓶颈。

系统效果

图片

传统方案的三大问题

问题一:CPU空转浪费

传统轮询方式如同不停转圈的陀螺,即便没有任务,也会不断检查队列状态,造成CPU资源的严重浪费。

问题二:响应延迟

100ms的轮询间隔在高并发场景下,延迟会被无限放大,直接影响用户体验。

问题三:扩展性差

单线程处理限制了并发能力,面对突发流量,系统极易成为性能瓶颈。

解决方案

WinForm可视化任务处理器

为解决上述问题,我们将构建一个完整的WinForm任务处理器,其核心设计思路如下:

  • 可视化监控:实时呈现队列状态、处理进度、CPU使用率,让开发者对系统运行情况一目了然。

  • 异常处理:模拟真实场景的错误恢复机制,增强系统的稳定性和可靠性。

  • 优雅停机:确保任务处理的安全性,避免因突然停止导致的任务丢失或数据损坏。

代码实现

主窗体实现

主窗体代码负责处理用户交互和界面更新,通过事件绑定与任务处理器进行通信。

例如,在启动按钮点击事件中,启动任务处理器并更新日志信息;

在添加任务按钮点击事件中,创建新任务并加入队列。

以下是部分关键代码:

namespace AppTaskProcessorDemo
{  
    publicpartialclassForm1 : Form
    {  
        privatereadonly TraditionalTaskProcessor _taskProcessor;  
        privatereadonly System.Windows.Forms.Timer _uiTimer;  
        privateint _taskCounter = 1;  
        public Form1()
        {  
            InitializeComponent();  
            _taskProcessor = new TraditionalTaskProcessor();  

            // 定时器用于更新UI显示  
            _uiTimer = new System.Windows.Forms.Timer();  
            _uiTimer.Interval = 100;  
            _uiTimer.Tick += UpdateUI;  
            _uiTimer.Start();  

            // 绑定事件  
            _taskProcessor.TaskProcessed += OnTaskProcessed;  
            _taskProcessor.ProcessorStatusChanged += OnProcessorStatusChanged;  
            _taskProcessor.ErrorOccurred += OnErrorOccurred;  
        }  

        private void btnStart_Click(object sender, EventArgs e)
        {  
            btnStart.Enabled = false;  
            btnStop.Enabled = true;  
            btnAddTask.Enabled = true;  

            _ = _taskProcessor.StartProcessing();  
            LogMessage("任务处理器已启动");  
        }  

        private void btnStop_Click(object sender, EventArgs e)
        {  
            _taskProcessor.Stop();  
            btnStart.Enabled = true;  
            btnStop.Enabled = false;  
            btnAddTask.Enabled = false;  
            LogMessage("任务处理器已停止");  
        }  

        private void btnAddTask_Click(object sender, EventArgs e)
        {  
            var taskItem = new TaskItem  
            {  
                Id = _taskCounter++,  
                Name = $"任务-{_taskCounter - 1}",  
                Data = $"这是第{_taskCounter - 1}个任务的数据",  
                CreatedTime = DateTime.Now  
            };  

            _taskProcessor.EnqueueTask(taskItem);  
            LogMessage($"已添加任务: {taskItem.Name}");  
        }  

        private void btnClear_Click(object sender, EventArgs e)
        {  
            txtLog.Clear();  
        }  

        private void OnTaskProcessed(object sender, TaskProcessedEventArgs e)
        {  
            if (InvokeRequired)  
            {  
                Invoke(new Action(() => OnTaskProcessed(sender, e)));  
                return;  
            }  

            LogMessage($"✅ 任务完成: {e.Task.Name} (耗时: {e.ProcessTime}ms)");  
        }  

        private void OnProcessorStatusChanged(object sender, ProcessorStatusEventArgs e)
        {  
            if (InvokeRequired)  
            {  
                Invoke(new Action(() => OnProcessorStatusChanged(sender, e)));  
                return;  
            }  

            lblStatusValue.Text = e.IsRunning ? "运行中" : "已停止";  
            lblStatusValue.ForeColor = e.IsRunning ? System.Drawing.Color.Green : System.Drawing.Color.Red;  
        }  

        private void OnErrorOccurred(object sender, TaskErrorEventArgs e)
        {  
            if (InvokeRequired)  
            {  
                Invoke(new Action(() => OnErrorOccurred(sender, e)));  
                return;  
            }  

            LogMessage($"❌ 错误: 任务 {e.Task.Name} - {e.Exception.Message}");  
        }  

        private void UpdateUI(object sender, EventArgs e)
        {  
            lblQueueCountValue.Text = _taskProcessor.QueueCount.ToString();  
            lblProcessedCountValue.Text = _taskProcessor.ProcessedCount.ToString();  
            lblErrorCountValue.Text = _taskProcessor.ErrorCount.ToString();  

            // 更新CPU使用率(模拟)  
            lblCpuUsageValue.Text = $"{_taskProcessor.CpuUsagePercentage:F1}%";  
        }  

        private void LogMessage(string message)
        {  
            if (InvokeRequired)  
            {  
                Invoke(new Action(() => LogMessage(message)));  
                return;  
            }  

            string logEntry = $"[{DateTime.Now:HH:mm:ss}] {message}";  
            txtLog.AppendText(logEntry + Environment.NewLine);  
            txtLog.SelectionStart = txtLog.Text.Length;  
            txtLog.ScrollToCaret();  
        }  

        protected override void OnFormClosing(FormClosingEventArgs e)
        {  
            _taskProcessor?.Stop();  
            _uiTimer?.Stop();  
            base.OnFormClosing(e);  
        }  
    }  
}  

核心处理器实现

核心处理器负责任务的获取、处理和状态管理。它使用ConcurrentQueue<TaskItem>存储任务,通过循环检查队列状态来处理任务,并在处理过程中处理异常和更新任务计数。

以下是部分关键代码:

public classTraditionalTaskProcessor
{  
    privatereadonly ConcurrentQueue<TaskItem> _queue = new();  
    privatereadonly CancellationTokenSource _cts = new();  
    privatevolatilebool _isRunning = false;  
    privateint _processedCount = 0;  
    privateint _errorCount = 0;  

    public async Task StartProcessing()
    {  
        if (_isRunning) return;  

        _isRunning = true;  
        OnProcessorStatusChanged(true);  

        await Task.Run(async () =>  
        {  
            while (_isRunning && !_cts.Token.IsCancellationRequested)  
            {  
                if (_queue.TryDequeue(outvar task))  
                {  
                    try
                    {  
                        var sw = Stopwatch.StartNew();  
                        await ProcessTask(task);  
                        sw.Stop();  

                        Interlocked.Increment(ref _processedCount);  
                        OnTaskProcessed(task, sw.ElapsedMilliseconds);  
                    }  
                    catch (Exception ex)  
                    {  
                        Interlocked.Increment(ref _errorCount);  
                        OnErrorOccurred(task, ex);  
                    }  
                }  
                else
                {  
                    // ⚠️ 关键问题:CPU空转  
                    await Task.Delay(100, _cts.Token);  
                }  
            }  
        }, _cts.Token);  
    }  

    private async Task ProcessTask(TaskItem task)
    {  
        // 模拟不同复杂度的任务处理  
        var processingTime = new Random().Next(500, 2000);  
        await Task.Delay(processingTime);  

        // 模拟偶发异常  
        if (new Random().Next(1, 20) == 1)  
        {  
            thrownew InvalidOperationException($"任务 {task.Name} 处理失败");  
        }  
    }  
}

UI设计器

UI设计器代码定义了窗体的布局和控件属性,包括控制面板、状态监控面板和日志面板等。

通过设置控件的属性,如文本框的背景色、字体等,提升了界面的美观性和可读性。

应用场景

1、数据处理场景

日志分析系统:处理海量日志文件,通过异步任务处理器提高处理效率,减少系统响应时间。

报表生成:异步生成复杂统计报表,避免阻塞主线程,提升用户体验。

数据同步:定时同步不同系统间的数据,确保数据的一致性和及时性。

2、通知系统场景

邮件队列:批量发送营销邮件,通过任务处理器控制发送速度,避免邮件服务器压力过大。

消息推送:移动端消息推送队列,确保消息能够及时、准确地推送给用户。

短信服务:验证码和通知短信发送,提高短信发送的可靠性和效率。

常见问题

问题1:UI线程安全

在更新UI时,若直接跨线程操作UI控件,会引发异常。正确的做法是使用Invoke方法将操作委托给UI线程执行,确保线程安全。

例如:

// ❌ 错误做法  
private void OnTaskProcessed(TaskProcessedEventArgs e)
{  
    txtLog.AppendText($"任务完成: {e.Task.Name}"); // 跨线程操作异常  
}  

// ✅ 正确做法  
private void OnTaskProcessed(TaskProcessedEventArgs e)
{  
    if (InvokeRequired)  
    {  
        Invoke(new Action(() => OnTaskProcessed(e)));  
        return;  
    }  
    txtLog.AppendText($"任务完成: {e.Task.Name}");  
}

问题2:资源释放

在窗体关闭时,应确保任务处理器停止运行,定时器停止工作,避免资源泄漏。

例如:

protected override void OnFormClosing(FormClosingEventArgs e)  
{  
    _taskProcessor?.Stop(); // 确保优雅停机  
    _uiTimer?.Stop();       // 停止定时器  
    base.OnFormClosing(e);  
}

问题3:异常处理

在处理任务时,应区分不同类型的异常,对正常取消操作不记录为错误,对真实业务异常进行详细记录。

例如:

// ✅ 完善的异常处理  
try
{  
    await ProcessTask(task);  
}  
catch (OperationCanceledException)  
{  
    // 正常取消操作,不记录为错误  
    return;  
}  
catch (Exception ex)  
{  
    // 记录真实业务异常  
    LogError(ex, task);  
}

性能测试数据

经过实际测试,传统轮询方案与优化后方案在各项指标上存在显著差异:

指标
传统轮询
优化后方案
CPU占用率
15 – 25%
2 – 5%
内存使用
120MB
45MB
响应延迟
50 – 150ms
1 – 5ms
并发处理
单线程
多线程池

说明

“好的架构不是设计出来的,而是演进出来的”:从轮询到事件驱动,每一步优化都是为了更好的用户体验。

“性能优化的本质是资源的合理分配”:CPU不应该浪费在无意义的空转上。

“可视化是调试的最佳伙伴”:眼见为实的监控面板让问题无所遁形。

高级进阶方向

Channel模式:探索.NET 中的高性能队列解决方案,进一步提升任务处理效率。

Producer – Consumer模式:研究更高效的生产者消费者实现,优化任务的生产和消费流程。

背压机制:引入背压机制,防止队列溢出,确保系统的稳定性。

分布式队列:集成Redis、RabbitMQ等中间件,实现分布式任务队列,处理大规模任务。

总结

通过这个实战项目,我们直观地看到了传统任务队列方案的局限性。可视化的监控界面让我们能够实时观察系统的运行状态,为性能调优和问题排查提供支持。

回顾三个核心要点:

传统轮询方案存在CPU空转、响应延迟、扩展性差的问题

通过事件驱动和可视化监控可以显著提升开发效率

异常处理和优雅停机是生产环境的必备特性

这只是任务队列优化之旅的第一步。在后续文章中,我们将为大家带来更高效的Channel模式和分布式队列解决方案,帮助大家开发更强大的系统。

《手把手教你用 .NET Core 搭建高并发、可扩展的 CQRS 与 DDD 架构》

mikel阅读(112)

来源: 《手把手教你用 .NET Core 搭建高并发、可扩展的 CQRS 与 DDD 架构》

趣事:

最近有人留言,说讲解一下CQRS和DDD架构,其实DDD有分开讲,在历史文章中有体现,比较好理解。

这几天单独重新回顾一次。

直接开干!

01

第一章:DDD 核心概念与 .NET Core 中的分层架构

1.1 什么是 DDD?

领域驱动设计(Domain-Driven Design)是一种软件开发方法论,强调以业务领域为核心,通过与领域专家的深度合作,构建出能够准确反映业务逻辑的软件模型。其核心思想是将复杂的业务逻辑集中在“领域层”,并通过清晰的边界(如聚合、限界上下文)来管理复杂性。

DDD 适用于业务复杂、规则多变、核心竞争力在于业务逻辑的系统,例如金融、电商、ERP、医疗等系统。

02

1.2 DDD 的核心概念

在 .NET Core 项目中,我们通常会将 DDD 分为以下几个核心概念:

  • 实体(Entity)
    :具有唯一标识的对象,其状态会随时间变化。例如 Order(订单)、Customer(客户)。
  • 值对象(Value Object)
    :没有唯一标识,通过属性值来定义的对象。例如 Address(地址)、Money(金额)。
  • 聚合(Aggregate)
    :一组相关对象的集合,由一个**聚合根(Aggregate Root)**统一管理。聚合根负责维护聚合内部的一致性。例如 Order 是聚合根,包含 OrderItem 等子实体。
  • 领域服务(Domain Service)
    :当某个操作不属于任何实体或值对象时,使用领域服务。它封装了跨多个实体的业务逻辑。
  • 仓储(Repository)
    :提供对聚合的持久化访问,屏蔽底层数据访问细节。在 .NET Core 中,通常通过接口定义仓储,由 Entity Framework Core 实现。
  • 领域事件(Domain Event)
    :表示领域中发生的重要事件,用于解耦和通知。例如 OrderPlacedEvent
  • 限界上下文(Bounded Context)
    :一个明确的业务边界,在此边界内,术语、模型和规则具有一致性。在微服务架构中,一个限界上下文通常对应一个微服务。

03

1.3 .NET Core 中的典型分层架构

在 .NET Core 项目中,我们通常采用六边形架构洋葱架构来实现 DDD。以下是常见的分层结构:

MyApp.Solution├── MyApp.Domain         // 领域层:实体、值对象、聚合、领域服务、仓储接口├── MyApp.Application    // 应用层:应用服务、DTO、CQRS 命令/查询、中介处理├── MyApp.Infrastructure // 基础设施层:EF Core 实现仓储、事件总线、外部服务调用├── MyApp.WebApi         // 表现层:ASP.NET Core Web API,接收请求,返回响应└── MyApp.UnitTests      // 单元测试
  • Domain 层
    :纯业务逻辑,不依赖任何外部框架或基础设施。
  • Application 层
    :协调领域逻辑与基础设施,定义用例。
  • Infrastructure 层
    :实现持久化、消息队列、缓存等。
  • WebApi 层
    :处理 HTTP 请求,调用应用服务。

关键点:依赖关系只能从外向内,即 WebApi → Application → DomainInfrastructure 实现 Domain 的接口。

04

1.4 实战示例:订单聚合

我们以电商系统中的 Order 聚合为例:

// Domain/Entities/Order.cspublic class Order : AggregateRoot{    public Guid CustomerId { getprivate set; }    public decimal TotalAmount { getprivate set; }    public OrderStatus Status { getprivate set; }    private readonly List<OrderItem> _items = new();    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();    // 工厂方法    public static Order Create(Guid customerId, List<OrderItemDto> items)    {        var order = new Order        {            Id = Guid.NewGuid(),            CustomerId = customerId,            Status = OrderStatus.Pending        };        foreach (var item in items)        {            order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);        }        // 添加领域事件        order.AddDomainEvent(new OrderCreatedEvent(order.Id));
        return order;    }    private void AddItem(Guid productId, int quantity, decimal unitPrice)    {        // 业务规则校验        if (quantity <= 0throw new DomainException("数量必须大于0");        var item = new OrderItem(productId, quantity, unitPrice);        _items.Add(item);        TotalAmount += item.Amount;    }}
// Domain/Entities/OrderItem.cspublic class OrderItem : Entity{    public Guid ProductId { getprivate set; }    public int Quantity { getprivate set; }    public decimal UnitPrice { getprivate set; }    public decimal Amount => Quantity * UnitPrice;    public OrderItem(Guid productId, int quantity, decimal unitPrice)    {        ProductId = productId;        Quantity = quantity;        UnitPrice = unitPrice;    }}
// Domain/Repositories/IOrderRepository.cspublic interface IOrderRepository : IRepository<OrderGuid>{    Task<Order> GetByOrderNumberAsync(string orderNumber);    Task<List<Order>> GetByCustomerIdAsync(Guid customerId);}
这一章我们建立了 DDD 的基础模型和分层结构。Order 作为聚合根,封装了创建订单的业务规则,并通过领域事件解耦后续操作。

05

第二章:CQRS 模式详解与在 .NET Core 中的实现

2.1 什么是 CQRS?

CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种架构模式,其核心思想是将**写操作(命令)读操作(查询)**分离,使用不同的模型来处理。

  • 命令(Command)
    :用于修改系统状态的操作,如创建订单、更新用户信息。它不返回数据,只返回操作结果(成功/失败)。
  • 查询(Query)
    :用于读取数据的操作,如获取订单详情、查询用户列表。它不修改系统状态。

CQRS 的本质:一个系统可以有多个“读模型”,但只有一个“写模型”。

06

2.2 为什么需要 CQRS?

虽然 CRUD 模式简单直接,但在复杂系统中会遇到以下问题:

  1. 性能瓶颈
    :读写共用同一数据库和模型,高并发读或写时互相影响。
  2. 模型复杂性
    :同一个模型既要满足写操作的业务校验,又要满足前端多样化的查询需求,导致模型臃肿。
  3. 扩展性差
    :读写无法独立扩展。
  4. 数据一致性延迟容忍
    :某些场景下,查询数据可以容忍短暂延迟(最终一致性),从而提升读性能。

CQRS 正是为了解决这些问题而生

07

2.3 CQRS 的基本结构

在 .NET Core 中,CQRS 通常与 MediatR 库结合使用,实现请求的中介模式。

客户端   ↓Web API Controller   ↓MediatR (中介者)   ↙       ↘Command Handler          Query Handler   ↓                       ↓Domain Layer (业务逻辑)     Read Model (DTO/ViewModel)   ↓                       ↓Write Database (EF Core)   Read Database (SQL View / NoSQL / Cache)
  • 命令处理器(Command Handler)
    :处理写操作,调用领域模型,执行业务逻辑,持久化聚合。
  • 查询处理器(Query Handler)
    :处理读操作,直接从优化的读模型(如视图、缓存、Elasticsearch)中获取数据,不经过领域层。

08

2.4 在 .NET Core 中集成 MediatR

在 Application 层中安装 MediatR 和 MediatR.Extensions.Microsoft.DependencyInjection 包。

<PackageReference Include="MediatR" Version="12.2.0" /><PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="12.1.0" />
在 Program.cs 中注册 MediatR:

// WebApi/Program.csbuilder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly));

09

2.5 实现 CQRS:创建订单命令

我们以“创建订单”为例,展示命令的定义与处理。

// Application/Commands/CreateOrderCommand.cspublic record CreateOrderCommand(    Guid CustomerId,    List<OrderItemDto> Items) : IRequest<Guid>; // 返回订单ID// DTO 用于传输public record OrderItemDto(Guid ProductId, int Quantity, decimal UnitPrice);
// Application/Commands/CreateOrderCommandHandler.cspublic class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommandGuid>{    private readonly IOrderRepository _orderRepository;    private readonly IUnitOfWork _unitOfWork;    public CreateOrderCommandHandler(IOrderRepository orderRepository, IUnitOfWork unitOfWork)    {        _orderRepository = orderRepository;        _unitOfWork = unitOfWork;    }    public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken ct)    {        // 1. 使用领域模型创建订单        var order = Order.Create(request.CustomerId, request.Items);        // 2. 持久化        await _orderRepository.AddAsync(order, ct);        await _unitOfWork.CommitAsync(ct);        // 3. 可选:发布领域事件(用于后续处理,如发送邮件、库存扣减)        // 事件处理将在后续章节讲解        return order.Id;    }}

10

2.6 实现 CQRS:查询订单详情

查询操作不经过领域模型,直接从优化的读模型中获取。

// Application/Queries/GetOrderQuery.cspublic record GetOrderQuery(Guid OrderId) : IRequest<OrderDto>;// Application/Queries/GetOrderQueryHandler.cspublic class GetOrderQueryHandler : IRequestHandler<GetOrderQueryOrderDto>{    private readonly DapperContext _dapperContext; // 使用 Dapper 高效查询    public GetOrderQueryHandler(DapperContext dapperContext)    {        _dapperContext = dapperContext;    }    public async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken ct)    {        const string sql = @"            SELECT o.Id, o.OrderNumber, o.TotalAmount, o.Status,                   c.Name as CustomerName,                   oi.ProductId, oi.Quantity, oi.UnitPrice            FROM Orders o            JOIN Customers c ON o.CustomerId = c.Id            LEFT JOIN OrderItems oi ON o.Id = oi.OrderId            WHERE o.Id = @OrderId";        using var connection = _dapperContext.CreateConnection();        var lookup = new Dictionary<Guid, OrderDto>();        await connection.QueryAsync<OrderDto, OrderItemDto, OrderDto>(            sql,            (order, item) =>            {                if (!lookup.TryGetValue(order.Id, out var existingOrder))                {                    existingOrder = order;                    existingOrder.Items = new List<OrderItemDto>();                    lookup.Add(order.Id, existingOrder);                }                if (item != null) existingOrder.Items.Add(item);                return existingOrder;            },            request,            splitOn: "ProductId"        );        return lookup.Values.FirstOrDefault();    }}
// DTOspublic record OrderDto(    Guid Id,    string OrderNumber,    decimal TotalAmount,    OrderStatus Status,    string CustomerName,    List<OrderItemDto> Items);

11

广告

12

2.7 控制器中使用 CQRS

// WebApi/Controllers/OrdersController.cs[ApiController][Route("api/[controller]")]public class OrdersController : ControllerBase{    private readonly IMediator _mediator;    public OrdersController(IMediator mediator)    {        _mediator = mediator;    }    [HttpPost]    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)    {        var orderId = await _mediator.Send(command);        return CreatedAtAction(nameof(GetOrder), new { id = orderId }, orderId);    }    [HttpGet("{id}")]    public async Task<IActionResult> GetOrder(Guid id)    {        var query = new GetOrderQuery(id);        var order = await _mediator.Send(query);        return Ok(order);    }}

13

2.8 CQRS 的优势总结

  • ✅ 职责分离:写和读逻辑清晰分离。
  • ✅ 性能优化:读模型可独立优化(视图、缓存、NoSQL)。
  • ✅ 可扩展性:读写数据库可独立部署和扩展。
  • ✅ 灵活性:前端查询需求变化不影响写模型。

注意:CQRS 增加了系统复杂性,不是所有项目都需要 CQRS。建议在业务复杂、读写负载差异大的系统中使用。

14

第三章:领域事件与事件溯源(Event Sourcing)

3.1 什么是领域事件?

领域事件(Domain Event)是在领域中发生的重要事情,一旦发生就不可变。它代表了系统状态的改变,例如:

  • OrderPlacedEvent
    (订单已创建)
  • PaymentCompletedEvent
    (支付已完成)
  • InventoryDeductedEvent
    (库存已扣减)

领域事件是实现业务解耦最终一致性的关键。

15

3.2 领域事件的价值

  1. 解耦业务逻辑
    :将主流程与后续操作分离。例如,创建订单后自动发送邮件、更新积分,这些都可以通过事件触发,而无需在订单服务中硬编码。
  2. 实现最终一致性
    :在分布式系统中,通过事件通知其他服务更新状态。
  3. 审计与追溯
    :所有事件可持久化,用于审计或重建状态。
  4. 支持事件溯源
    :为更高级的架构模式打下基础。

又到一年毕业季  到了说珍重的时候  总说  时光不老,我们不散  毕业遥遥无期 转眼间就各奔东西  毕业,有着说不完的话题  因为那是懵懂的结束  成熟的开始。

16

3.3 在 .NET Core 中实现领域事件

我们通过 MediatR 来发布和处理领域事件。

17

3.3.1 定义领域事件
// Domain/Events/OrderCreatedEvent.cspublic record OrderCreatedEvent(Guid OrderId) : INotification;
实现 INotification 接口,表示这是一个广播事件,可以有多个处理器。

18

3.3.2 在聚合根中发布事件
// Domain/Entities/Order.cspublic class Order : AggregateRoot{    // ... 其他代码    public static Order Create(Guid customerId, List<OrderItemDto> items)    {        var order = new Order        {            Id = Guid.NewGuid(),            CustomerId = customerId,            Status = OrderStatus.Pending        };        foreach (var item in items)        {            order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);        }        // 发布事件        order.AddDomainEvent(new OrderCreatedEvent(order.Id));
        return order;    }}

19

3.3.3 在仓储中发布事件

事件的发布通常在事务提交后进行。我们可以在 UnitOfWork 提交后,遍历所有聚合的领域事件并发布。

// Infrastructure/Data/UnitOfWork.cspublic class UnitOfWork : IUnitOfWork{    private readonly AppDbContext _context;    private readonly IPublisher _mediator; // MediatR 的发布者    public UnitOfWork(AppDbContext context, IPublisher mediator)    {        _context = context;        _mediator = mediator;    }    public async Task<boolCommitAsync(CancellationToken ct)    {        // 1. 保存实体变更        var result = await _context.SaveChangesAsync(ct);        // 2. 发布所有聚合根的领域事件        await PublishDomainEvents(ct);        return result > 0;    }    private async Task PublishDomainEvents(CancellationToken ct)    {        var aggregates = _context.ChangeTracker            .Entries<IAggregateRoot>()            .Where(x => x.Entity.DomainEvents.Any())            .Select(x => x.Entity)            .ToList();        foreach (var aggregate in aggregates)        {            var events = aggregate.DomainEvents.ToList();            aggregate.ClearDomainEvents(); // 清空已发布的事件            foreach (var @event in events)            {                await _mediator.Publish(@event, ct);            }        }    }}

20

3.3.4 事件处理器(Event Handler)
// Application/Handlers/OrderCreatedEventHandler.cspublic class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>{    private readonly ILogger<OrderCreatedEventHandler> _logger;    private readonly IEmailService _emailService;    private readonly IInventoryService _inventoryService; // 可能是 gRPC 或 HTTP 客户端    public OrderCreatedEventHandler(        ILogger<OrderCreatedEventHandler> logger,        IEmailService emailService,        IInventoryService inventoryService)    {        _logger = logger;        _emailService = emailService;        _inventoryService = inventoryService;    }    public async Task Handle(OrderCreatedEvent notification, CancellationToken ct)    {        _logger.LogInformation("订单 {OrderId} 已创建,正在处理后续操作...", notification.OrderId);        // 1. 发送订单确认邮件        await _emailService.SendOrderConfirmationAsync(notification.OrderId, ct);        // 2. 调用库存服务扣减库存(可能是异步消息)        await _inventoryService.DeductInventoryAsync(notification.OrderId, ct);        // 3. 更新用户积分(领域服务)        // await _pointsService.AddPointsAsync(...);    }}
事件处理器可以有多个,每个关注不同的业务。

21

3.4 事件溯源(Event Sourcing)

事件溯源是一种更激进的持久化模式:不保存实体的当前状态,而是保存导致状态变化的所有事件。实体的状态是通过重放事件来重建的。

22

3.4.1 事件溯源的核心思想
  • 状态是事件的投影
    CurrentState = Apply(Events...)
  • 事件是唯一真相源
    (Source of Truth)

23

3.4.2 事件溯源的结构
Command → [Aggregate] → Events → Event Store → (Replay) → Current State                                 ↓                             Projections → Read Models

24

3.4.3 何时使用事件溯源?
  • 需要完整审计日志
  • 需要时间旅行(查看历史状态)
  • 系统由事件驱动,状态变化频繁
  • 与 CQRS 天然结合

25

26

3.4.4 简单示例:事件存储
// Domain/Events/OrderPlacedEvent.cspublic record OrderPlacedEvent(    Guid OrderId,    Guid CustomerId,    List<OrderItem> Items,    DateTime PlacedAt) : IEvent;// Infrastructure/EventStore/IEventStore.cspublic interface IEventStore{    Task SaveEventsAsync<T>(Guid aggregateId, IEnumerable<IEvent> events, int expectedVersion);    Task<List<IEvent>> GetEventsAsync(Guid aggregateId);}// 简单实现(实际可用 EventStoreDB、Cosmos DB 等)public class SqlEventStore : IEventStore{    public async Task SaveEventsAsync<T>(Guid aggregateId, IEnumerable<IEvent> events, int expectedVersion)    {        // 将事件保存到数据库表        // 包含 AggregateId, AggregateType, Version, EventType, Data (JSON)    }    public async Task<List<IEvent>> GetEventsAsync(Guid aggregateId)    {        // 从数据库加载所有事件并反序列化    }}
事件溯源会显著增加复杂性,建议在有明确需求时再引入。

27

3.5 领域事件 vs 事件溯源

特性
领域事件
事件溯源
持久化
事件可选持久化
事件是唯一持久化形式
状态存储
保存当前状态
不保存状态,通过事件重建
复杂度
中等
适用场景
解耦、通知
审计、历史追溯、CQRS 组合

28

第四章:CQRS 与 DDD 的深度整合与实战模式

在前几章中,我们分别介绍了 DDD 的分层、CQRS 的分离以及领域事件的使用。本章将聚焦于如何将 CQRS 与 DDD 深度整合,并介绍一些在 .NET Core 项目中常见的实战模式和最佳实践。

29

4.1 CQRS + DDD 的典型数据流

理解完整的请求生命周期是掌握架构的关键:

HTTP Request (WebApi)       ↓MediatR Request (Command / Query)       ↙                    ↘[Command Handler]        [Query Handler]       ↓                         ↓Domain Layer:              Read-Optimized:- Load Aggregate           - Direct SQL/Dapper- Execute Business Logic   - Cache (Redis)- Validate Rules           - Elasticsearch- Emit Domain Events       - Return DTO       ↓Persist via Repository & UnitOfWork       ↓Publish Domain Events → Event Handlers (Async)

关键点

  • 写路径
    :必须经过领域模型,确保业务规则被强制执行。
  • 读路径
    :绕过领域模型,直接访问优化的数据源,追求性能。

30

4.2 实战模式一:异步最终一致性

在分布式系统中,强一致性往往带来性能瓶颈。通过领域事件实现最终一致性是常见做法。

场景:用户下单后,需要扣减库存。

传统做法(同步强一致)

// 在命令处理器中直接调用库存服务await _inventoryService.DeductAsync(orderId); // 失败则订单创建失败
问题:库存服务不可用会导致订单无法创建。

改进做法(异步最终一致)

// OrderCreatedEventHandler.cspublic async Task Handle(OrderCreatedEvent notification, CancellationToken ct){    // 发布一个集成事件到消息队列(如 RabbitMQ/Kafka)    await _messageBus.PublishAsync(new InventoryDeductionRequestedEvent(        OrderId: notification.OrderId,        Items: orderItems // 可以从仓储加载    ), ct);}

库存服务监听 InventoryDeductionRequestedEvent,执行扣减。如果失败,可重试或进入死信队列人工处理。

✅ 优势:订单服务不再依赖库存服务,系统更健壮。 ❌ 代价:短暂时间内数据不一致(订单已创建但库存未扣)。

31

4.3 实战模式二:查询模型的优化策略

CQRS 的查询侧有多种优化方式:

策略
描述
适用场景
数据库视图
创建 SQL 视图,预计算关联数据
查询结构稳定,数据量不大
独立读库
主库写,从库读(读写分离)
读远多于写
缓存(Redis)
将查询结果缓存
高频访问、低频更新的数据
物化视图/Projection
用事件驱动的方式维护一个专用的读表
复杂聚合查询
Elasticsearch
全文搜索、复杂过滤
商品搜索、日志分析

示例:使用 Redis 缓存订单详情

// GetOrderQueryHandler.cspublic async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken ct){    var cacheKey = $"order:{request.OrderId}";
    // 先查缓存    var cached = await _cache.GetStringAsync(cacheKey, ct);    if (!string.IsNullOrEmpty(cached))    {        return JsonSerializer.Deserialize<OrderDto>(cached);    }    // 缓存未命中,查数据库    var order = await LoadFromDatabase(request.OrderId, ct);
    // 写入缓存(设置过期时间)    await _cache.SetStringAsync(        cacheKey,         JsonSerializer.Serialize(order),         TimeSpan.FromMinutes(10),         ct);    return order;}
注意:当订单状态更新时,需清除或更新缓存,保证一致性。

32

4.4 实战模式三:Saga 分布式事务管理

当一个业务操作跨越多个限界上下文(微服务)时,需要用 Saga 模式来管理分布式事务。

场景:下单流程涉及 订单服务 → 支付服务 → 库存服务 → 物流服务。

Saga 流程

  1. Order Service
    : 创建订单(初始状态为 “PendingPayment”)
  2. 发布 OrderCreatedEvent
  3. Payment Service
    : 监听事件,发起支付
  4. 支付成功,发布 PaymentCompletedEvent
  5. Inventory Service
    : 扣减库存,发布 InventoryDeductedEvent
  6. Shipping Service
    : 创建发货单

补偿机制:如果任一环节失败,触发补偿事务(Compensating Transaction):

  • 支付失败 → 订单取消
  • 库存不足 → 支付退款

Saga 可以是编排式(Orchestration)或协同式(Choreography)。DDD 中常用协同式(通过事件驱动)。

33

4.5 实战模式四:限界上下文与微服务划分

DDD 的“限界上下文”是划分微服务的理想依据。

电商系统的限界上下文示例

限界上下文
聚合根
职责
订单上下文
Order, OrderItem
订单生命周期管理
支付上下文
Payment, Refund
支付、退款
库存上下文
ProductStock
库存扣减、回滚
客户上下文
Customer, Address
客户信息管理
营销上下文
Coupon, Promotion
优惠券、促销活动

每个上下文可以独立部署为微服务,通过 API 或事件进行通信。

34

4.6 最佳实践总结

  1. 不要过度设计
    :简单 CRUD 场景无需 CQRS 和事件溯源。
  2. 先做 CQRS,再考虑事件溯源
    :事件溯源复杂度高,慎用。
  3. 领域事件命名
    :使用过去时态,如 OrderShippedEvent
  4. 事件幂等性
    :确保事件处理器可安全重试。
  5. 监控与重试
    :对事件总线、消息队列进行监控,实现失败重试机制。
  6. 文档化上下文映射
    :明确各限界上下文之间的关系(合作关系、防腐层等)。

35

本章小结:我们探讨了 CQRS 与 DDD 在实际项目中的整合方式,包括最终一致性、查询优化、Saga 事务和微服务划分。这些模式帮助我们在保持业务清晰的同时,提升系统的可扩展性和健壮性。

36

第五章:项目结构优化、测试策略与部署考量

在掌握了 DDD 和 CQRS 的核心概念与实战模式后,本章将关注项目的可维护性、可测试性生产部署的实际考量。一个成功的架构不仅要在设计上合理,更要在工程实践中可持续。

37

5.1 项目结构优化与模块化

随着业务增长,项目可能变得庞大。合理的模块化能提升代码可维护性。

38

5.1.1 按限界上下文组织项目
eShop.Solution├── eShop.Ordering                # 订单上下文(微服务)│   ├── Ordering.Domain│   ├── Ordering.Application│   ├── Ordering.Infrastructure│   └── Ordering.WebApi├── eShop.Payment                 # 支付上下文│   ├── Payment.Domain│   ├── Payment.Application│   └── ...├── eShop.SharedKernel            # 共享内核(ID、时间、异常基类)├── eShop.EventBus                # 事件总线抽象与实现└── eShop.UnitTests               # 共享测试基类
优点:每个上下文独立开发、部署、扩展。

39

5.1.2 使用功能切片(Vertical Slice Architecture)

对于单体应用,可采用“功能切片”替代传统分层,减少层间依赖。

// Features/Orders/CreateOrder/├── CreateOrderCommand.cs├── CreateOrderCommandHandler.cs├── CreateOrderValidator.cs      // FluentValidation└── CreateOrderResponse.cs// Features/Orders/GetOrder/├── GetOrderQuery.cs├── GetOrderQueryHandler.cs└── GetOrderValidator.cs
优点:功能高度内聚,新增功能无需跨多层修改。

40

5.2 测试策略

DDD + CQRS 架构需要分层测试:

测试类型
范围
工具
说明
单元测试
领域模型、服务
xUnit, Moq
测试实体行为、业务规则
集成测试
命令/查询处理器
xUnit, TestServer
测试应用层与基础设施集成
端到端测试
API 接口
xUnit, SpecFlow
模拟用户场景
契约测试
微服务接口
Pact
确保服务间兼容

41

5.2.1 领域模型单元测试示例
// Ordering.Domain.Tests/OrderTests.cspublic class OrderTests{    [Fact]    public void CreateOrder_WithValidItems_ShouldSucceed()    {        // Arrange        var customerId = Guid.NewGuid();        var items = new List<OrderItemDto>        {            new(Guid.NewGuid(), 2100m)        };        // Act        var order = Order.Create(customerId, items);        // Assert        Assert.Equal(200m, order.TotalAmount);        Assert.Single(order.Items);        Assert.Contains(order.DomainEvents, e => e is OrderCreatedEvent);    }    [Fact]    public void AddItem_WithInvalidQuantity_ShouldThrow()    {        // Arrange        var order = Order.Create(Guid.NewGuid(), new List<OrderItemDto>());        // Act & Assert        Assert.Throws<DomainException>(() =>             order.AddItem(Guid.NewGuid(), 0100m));    }}

42

5.2.2 命令处理器集成测试
// Ordering.Application.Tests/CreateOrderCommandHandlerTests.cspublic class CreateOrderCommandHandlerTests : IClassFixture<DatabaseFixture>{    private readonly DatabaseFixture _fixture;    public CreateOrderCommandHandlerTests(DatabaseFixture fixture)    {        _fixture = fixture;    }    [Fact]    public async Task Handle_ValidCommand_ShouldPersistOrder()    {        // Arrange        var handler = _fixture.GetService<IRequestHandler<CreateOrderCommand, Guid>>();        var command = new CreateOrderCommand(            CustomerId: Guid.NewGuid(),            Items: new List<OrderItemDto> { /* ... */ }        );        // Act        var orderId = await handler.Handle(command, CancellationToken.None);        // Assert        var order = await _fixture.OrderRepository.GetByIdAsync(orderId);        Assert.NotNull(order);        Assert.Equal(OrderStatus.Pending, order.Status);    }}
DatabaseFixture 使用 Testcontainers 或内存数据库(如 SQLite)隔离测试。

43

5.3 生产部署考量

5.3.1 数据库策略
  • 写库
    :使用高性能关系型数据库(如 PostgreSQL, SQL Server),确保 ACID。
  • 读库:
    • 使用物化视图CQRS 读表,通过事件驱动更新。
    • 高频查询使用 Redis 缓存。
    • 复杂搜索使用 Elasticsearch

44

45

5.3.2 事件总线选型
方案
适用场景
In-Memory

 (MediatR)
单体应用,事件处理器在同一进程
RabbitMQ
微服务,需要可靠传递、重试
Kafka
高吞吐、事件溯源、流处理
Azure Service Bus
Azure 生态

建议:初期使用 RabbitMQ,成熟后根据需求迁移。

46

5.3.3 监控与可观测性
  • 日志
    :使用 Serilog 结构化日志,记录命令、事件、错误。
  • 追踪
    :集成 OpenTelemetry,追踪请求链路。
  • 指标
    :暴露 Prometheus 指标(如命令处理时间、事件发布延迟)。
  • 告警
    :对事件积压、失败任务设置告警。

47

5.3.4 部署模式
  • 蓝绿部署 / 金丝雀发布
    :降低发布风险。
  • 健康检查
    :实现 /health 端点,检查数据库、事件总线连接。
  • 配置管理
    :使用 IConfiguration + 配置中心(如 Azure App Configuration)。

48

5.4 常见陷阱与规避

  1. 过度工程
    :不是所有项目都需要 CQRS 和事件溯源。从简单开始。
  2. 事件风暴滥用
    :领域事件应代表业务关键决策,而非所有状态变更。
  3. 事务边界不清晰
    :确保 UnitOfWork 正确管理事务,避免部分提交。
  4. 循环依赖
    :避免服务间循环发布事件。
  5. 缺乏文档
    :用 上下文映射图(Context Map)记录限界上下文关系。

49

结语

DDD 与 CQRS 是强大的架构工具,但它们不是银弹。成功的关键在于:

  • 以业务为核心
    :模型必须准确反映领域知识。
  • 渐进式演进
    :从单体开始,逐步拆分限界上下文。
  • 团队共识
    :统一术语(通用语言),确保开发、产品、领域专家理解一致。
  • 持续重构
    :架构随业务发展而演进。

50

第六章:应对分布式挑战——一致性、性能与弹性

当我们将 DDD 和 CQRS 应用于生产级系统,尤其是微服务架构时,会面临一系列分布式系统特有的挑战。本章将聚焦于如何在保持业务清晰的同时,确保系统的数据一致性、高性能和高可用性

51

6.1 数据一致性:最终一致性 vs 强一致性

在 CQRS 架构中,写模型读模型是分离的。这意味着,当一个命令成功执行后,查询结果可能不会立即反映最新状态。

图片

问题:在 t2 到 t4 之间,客户端查询可能看到旧数据或“未找到”。

解决方案
  1. 接受最终一致性
    • 说明
      :这是 CQRS 的默认模式。向客户端明确说明数据是“最终一致”的。
    • 适用场景
      :大多数业务场景(如社交动态、商品列表)可以容忍短暂延迟。
  2. 读写后读取(Read-Your-Writes Consistency)
    • 方案 A(简单)
      :命令成功后,不立即让客户端查询,而是直接返回完整的创建结果(DTO)。
    • 说明
      :确保用户能立即看到自己刚刚写入的数据。
    • 实现:
// Command Handler 返回完整 OrderDtopublic async Task<OrderDtoHandle(CreateOrderCommand request, ...){    var order = Order.Create(...);    await _repo.AddAsync(order);    await _uow.CommitAsync();    // 直接返回,避免查询延迟    return _mapper.Map<OrderDto>(order);}
  1. 方案 B(复杂)

    :在查询时,如果请求的是“自己刚创建的资源”,则回退到从写库查询(牺牲一点性能换取一致性)。
  2. 会话一致性(Session Consistency)
    • 说明
      :确保同一个用户在一次会话中看到的数据是单调递增的。
    • 实现
      :使用时间戳版本号。查询时带上上次操作的时间戳,只返回该时间戳之后的数据。

52

6.2 性能优化:CQRS 的读写优化策略

CQRS 的核心优势之一就是可以独立优化读写路径。

53

6.2.1 写路径优化
  • 批量处理
    :对于高频写操作(如日志、指标),使用批量提交减少数据库压力。
  • 异步持久化
    :将事件先写入高速队列(如 Kafka),再由后台消费者持久化到数据库。
  • 聚合设计
    :避免大聚合。过大的聚合会导致并发冲突(乐观锁失败率高)。合理拆分聚合根。

54

6.2.2 读路径优化(核心)
技术
描述
适用场景
数据库读写分离
主库写,多个从库读
读远多于写的场景
Redis 缓存
将热点数据(如商品详情)缓存
高频访问、低频更新
物化视图 (Materialized View)
预计算复杂查询结果,存为物理表
报表、聚合统计
Elasticsearch
全文检索、复杂过滤、高亮
商品搜索、日志分析
CDN
缓存静态资源或 API 响应
全球用户访问

示例:用事件驱动更新物化视图

// 当 OrderCreatedEvent 发生时,更新 OrderSummary 表
public class OrderCreatedProjection : INotificationHandler<OrderCreatedEvent>{    private readonly AppDbContext _readContext; // 专用的读库上下文    public async Task Handle(OrderCreatedEvent e, CancellationToken ct)    {        var summary = new OrderSummary        {            OrderId = e.OrderId,            Status = "Pending",            CreatedAt = DateTime.UtcNow,            // ... 其他预计算字段        };        await _readContext.OrderSummaries.AddAsync(summary, ct);        await _readContext.SaveChangesAsync(ct);    }}
关键:投影(Projection)是幂等的,可以安全重放。

55

6.3 弹性与容错:处理失败与重试

在分布式系统中,失败是常态。我们必须设计有弹性的系统。

56

6.3.1 命令处理的幂等性

确保同一个命令被多次处理时,结果一致。

// CreateOrderCommand.cs
public record CreateOrderCommand(
    Guid CommandId, // 唯一标识
    Guid CustomerId,
    List<OrderItemDto> Items) : IRequest<Guid>;
// Command Handler
public async Task<GuidHandle(CreateOrderCommand request, CancellationToken ct){
    // 1. 检查该 CommandId 是否已处理
    if (await _commandLog.IsProcessedAsync(request.CommandId, ct))    {        // 返回上次的结果(幂等)        return await _commandLog.GetResultAsync<Guid>(request.CommandId, ct);    }    // 2. 正常处理逻辑    var order = Order.Create(request.CustomerId, request.Items);    await _repo.AddAsync(order);    await _uow.CommitAsync();    
// 3. 记录命令已处理
    await _commandLog.LogProcessedAsync(request.CommandId, order.Id, ct);
    return order.Id;}

57

6.3.2 事件处理的重试机制

领域事件或集成事件发布失败时,必须重试。

  • 使用消息队列
    :RabbitMQ、Kafka 天然支持消息持久化和重试。
  • 死信队列 (DLQ)
    :处理多次重试仍失败的消息,便于人工干预。
  • 指数退避
    :重试间隔逐渐增加,避免雪崩。
// RabbitMQ Consumer 示例
try{
    await HandleEvent(event);
    await model.BasicAck(deliveryTag, false);
 // 确认}
catch (Exception ex){
    
// 记录日志    
// 消息将自动重回队列或进入DLQ
    throw// 不确认,触发重试}

58

59

6.4 分布式事务与 Saga 模式的进阶

在第五章我们提到了 Saga 模式。这里深入其**编排式(Orchestration)**实现。

场景:订单流程(创建 → 支付 → 发货)

图片

Orchestrator (编排器) 的职责

  1. 定义 Saga 的执行流程。
  2. 发送命令给各个服务。
  3. 监听各步骤的完成事件。
  4. 处理失败,触发补偿命令。

优点:流程清晰,易于调试和监控。缺点:编排器可能成为单点。

选择建议

  • 流程简单、服务少:用协同式(事件驱动)。
  • 流程复杂、需要精确控制:用编排式

60

6.5 监控、追踪与调试

复杂的分布式系统必须具备强大的可观测性。

  • 结构化日志
    :使用 Serilog + Seq 或 ELK,记录命令、事件、错误。
  • 分布式追踪
    :集成 OpenTelemetry,追踪一个请求从 API 到数据库再到其他服务的完整链路。
  • 指标监控
    :使用 Prometheus + Grafana 监控:
    • 命令处理延迟
    • 事件发布/消费速率
    • 数据库连接数
    • 缓存命中率
  • 健康检查
    :实现 /health 端点,检查数据库、Redis、消息队列等依赖。

61

本章小结:我们探讨了在真实生产环境中,如何应对 CQRS 和 DDD 带来的分布式挑战。核心思想是:接受最终一致性、通过独立优化读写路径提升性能、设计幂等和重试机制保证弹性

62

第七章:演进式架构——从单体到微服务的平滑过渡

在前几章中,我们构建了一个基于 DDD 和 CQRS 的强大单体应用。然而,随着业务规模扩大、团队增多,单体架构可能成为瓶颈。本章将探讨如何以领域驱动的方式,将单体应用平滑演进为微服务架构,避免“大爆炸式”重写。

63

7.1 何时拆分微服务?

并非所有系统都需要微服务。以下信号表明可能是时候考虑拆分了:

  1. 团队协作困难
    :多个团队频繁修改同一代码库,导致合并冲突和发布阻塞。
  2. 技术栈异构需求
    :某个功能需要特定技术(如 AI 服务用 Python,核心交易用 .NET)。
  3. 伸缩性需求差异
    :订单服务需要 10 台服务器,而客服系统只需 2 台。
  4. 发布频率不同
    :营销活动需要每日发布,而财务系统每月发布一次。
  5. DDD 限界上下文清晰
    :业务边界明确,上下文映射图已定义。

关键以业务能力(Bounded Context)为单位拆分,而非技术分层。

64

7.2 演进策略:Strangler Fig 模式

“绞杀者模式”(Strangler Fig Pattern)是一种安全的演进策略:新建的微服务逐步“绞杀”旧的单体功能,直到单体被完全替代

65

7.2.1 阶段一:识别并隔离限界上下文

在单体应用中,首先通过命名空间或项目来物理隔离不同的限界上下文。

// 单体应用中的模块化

MyApp.Solution

├── MyApp.Ordering          // 订单上下文├── MyApp.Customer          // 客户上下文├── MyApp.Catalog           // 商品目录上下文├── MyApp.SharedKernel

└── MyApp.WebApi            // API 网关入口

  • 目标:
    减少上下文间的耦合,明确依赖方向(如 Ordering 依赖 Catalog)。
  • 实践:
    • 使用 InternalsVisibleTo 限制程序集访问。
    • 通过 领域事件 耦合,而非直接调用服务。

66

7.2.2 阶段二:暴露 API 与防腐层(ACL)

为即将拆分的上下文设计稳定的 API,并为外部依赖创建防腐层(Anti-Corruption Layer)

//Ordering/Infrastructure/CatalogAcl.cs

public class CatalogAcl : ICatalogService{    private readonly HttpClient _client;    public async Task<ProductDto> GetProductAsync(GuidproductId)    {

        // 调用 Catalog 微服务的 HTTP API        var response = await _client.GetAsync($"/api/products/{productId}");        // 将外部模型转换为内部模型        var external = await response.Content.ReadFromJsonAsync<ExternalProductDto>();        return _mapper.Map<ProductDto>(external);    }}

防腐层的作用:防止外部服务的模型变化污染内部领域模型。

67

7.2.3 阶段三:逐步迁移流量
  1. 部署新微服务
    :将 Catalog 上下文部署为独立的微服务 catalog-service
  2. 双写或双读:
    • 双写
      :在单体和新服务中同时写入数据(确保数据同步)。
    • 双读
      :新服务先从单体数据库读取,验证数据一致性。
  3. 切换流量
    :通过 API 网关(如 Ocelot、YARP)将 /api/products/* 的流量逐步导向新服务。
  4. 移除旧代码
    :确认新服务稳定后,移除单体中的 Catalog 模块和双写逻辑。
图片

68

7.3 微服务间的通信模式

拆分后,服务间通信至关重要。

模式
描述
工具
适用场景
同步 HTTP/REST
请求-响应模式
HttpClient, Refit
简单查询、强一致性要求
gRPC
高性能 RPC,强类型
gRPC .NET
高频调用、低延迟
异步消息
通过消息队列通信
RabbitMQ, Kafka
解耦、最终一致性、事件驱动

建议:优先使用异步消息保持松耦合,必要时使用同步调用。

69

7.4 数据管理:避免分布式事务

微服务中应尽量避免跨服务的分布式事务(如两阶段提交),因其性能差且复杂。

替代方案

  1. Saga 模式
    :如第六章所述,用补偿事务管理长流程。
  2. CQRS + 事件溯源
    :通过事件流保证数据一致性。
  3. API 组合器
    :在查询时,由网关或前端服务聚合多个微服务的数据。

70

7.5 运维与治理

微服务带来运维复杂性,需建立配套体系:

  • 服务发现
    :Consul, Eureka, Kubernetes DNS。
  • 配置中心
    :Azure App Configuration, Consul KV。
  • API 网关
    :路由、认证、限流、熔断。
  • CI/CD
    :每个服务独立的流水线。
  • 日志与监控
    :集中式日志(ELK)、分布式追踪(Jaeger)、指标(Prometheus)。

71

7.6 案例:电商系统拆分路径

  1. 第一阶段
    :拆分 Catalog(商品目录),因其独立性强、访问量大。
  2. 第二阶段
    :拆分 Ordering(订单),因其业务复杂,需独立伸缩。
  3. 第三阶段
    :拆分 Payment(支付),因其涉及第三方集成和安全要求。
  4. 第四阶段
    Customer(客户)和 Marketing(营销)作为独立服务。

每一步都小步迭代,确保系统始终可用

72

本章小结:我们学习了如何以 Strangler Fig 模式,将一个 DDD + CQRS 的单体应用,安全、可控地演进为微服务架构。关键在于以限界上下文为拆分单元,使用防腐层隔离变化,并通过 API 网关逐步切换流量

这种演进式方法最大限度地降低了风险,是大型系统架构演进的推荐实践。

希望这些内容能帮助你在 .NET Core 项目中成功应用 DDD 和 CQRS。如需深入某个主题,请随时提问!

73

Chrome浏览器的跨域设置----包含新老版本两种设置_谷歌浏览器跨域设置-CSDN博客

mikel阅读(156)

来源: Chrome浏览器的跨域设置—-包含新老版本两种设置_谷歌浏览器跨域设置-CSDN博客

注意:此方案仅适用于windows系统哦!!!
这是一篇姗姗来迟的文章,一直在使用,但是一直没时间做一下总结,今天抽空就分享给大家,操作很简单,耐心跟着操作一下。

做前后端分离的开发的时候,出于一些原因往往需要将浏览器设置成支持跨域的模式,而且chrome浏览器支持可跨域的设置,但是新版本的chrome浏览器提高了跨域设置的门槛,原来的方法不再适用了。其实网上也有很多大神总结的chrome跨域设置教程,都是差不多。

个人开发中的使用习惯
下载好谷歌浏览器以后,快捷方式我会复制两份放在桌面上,这时候两个是一模一样的,我会将其中一个重命名(我是跨域的命名为 dev),另外一个正常命名,然后在对命名为“dev”的浏览器打开方式进行寡跨域设置,这样我们就会拥有一个正常的浏览器,一个跨域的浏览器啦~

 

老版本Chrome浏览器(版本号49之前的跨域设置)
1、右键点击谷歌浏览器,选择属性

2、 在目标输入框尾部加上 –disable-web-security

注意:这里 –disable-web-security 前面有一个空格

 

3.点击应用和确定后关闭属性页面,并打开chrome浏览器。如果浏览器出现提示“你使用的是不受支持的命令标记 –disable-web-security”,那么说明配置成功。

新版浏览器跨域设置(版本号49之后的跨域设置)
1、在电脑上新建一个目录(任意位置) 例如 C:\MyChromeDevUserData

2、右键点击谷歌浏览器,选择属性;

3、在目标输入框尾部加上 –disable-web-security –user-data-dir=C:\MyChromeDevUserData

注意:

1.两个 — 前面都是有空格的哦~

2. 如果目标地址原先有引号,那么 –disable-web-security –user-data-dir=C:\MyChromeDevUserData 要加在引号外面。

 

4、点击应用和确定后关闭属性页面,并打开chrome浏览器。

再次打开chrome,发现有“–disable-web-security”相关的提示,说明chrome就能正常跨域工作了。

————————————————
版权声明:本文为CSDN博主「前端菜菜DayDayUp」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_48594833/article/details/124345191

windows10同步时间出错,终于找到解决办法了!_w32time windows time-CSDN博客

mikel阅读(96)

来源: windows10同步时间出错,终于找到解决办法了!_w32time windows time-CSDN博客

Windows同步Internet时间出错的解决方法
系统时间老是同步不正确,百度半天找到了一个可行的方法,贴出来

附上我的成功截图

分割线

操纵步骤
管理员身份启动 cmd 然后依次输入以下命令 不放心最后有我输入的截图,上面有说明每一步的作用
net stop w32time
w32tm /unregister
w32tm /register
net start w32time
w32tm /resync /nowait

最后关闭CMD 然后重新更新时间就能更新成功了
之前百度看到了改时间校准的地址
我也是改过地址,发现都没用,最后用这种方式的时候,然后更新时间的地址不是默认的时候更新也失败了,吧地址改成默认的在更新就成功了,也不太清楚原理是啥,不过还好最后时间终于校准了。
最后附上默认校准地址

time.windows.com
————————————————
版权声明:本文为CSDN博主「既见君子」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_44624742/article/details/104688940

ASP.NET Core WebApi 集成 MCP 协议完全指南 - 扫地僧2015 - 博客园

mikel阅读(118)

来源: ASP.NET Core WebApi 集成 MCP 协议完全指南 – 扫地僧2015 – 博客园

前言

Model Context Protocol (MCP) 是一个标准化协议,让 AI 客户端(如 Claude、ChatGPT 等)能够通过统一的接口调用你的 API。本文将详细介绍如何在 ASP.NET Core WebApi 项目中集成 MCP 支持,实现 AI 与你的服务无缝对接。

什么是 MCP?

MCP(Model Context Protocol)是一个开放协议,旨在标准化 AI 应用与外部工具、数据源之间的通信方式。通过 MCP,你的 API 可以:

  • 被 AI 助手自动发现和调用
  • 提供标准化的工具描述和参数定义
  • 支持多种传输模式(HTTP、Stdio)
  • 实现安全的认证和授权

核心特性

本项目实现了以下功能:

  • ✅ 使用官方 ModelContextProtocol.AspNetCore SDK
  • ✅ 通过 [McpServerTool] 特性快速定义工具
  • ✅ 自动参数绑定和 JSON Schema 生成
  • ✅ 支持 HTTP 和 Stdio 双传输模式
  • ✅ 基于 Token 的认证和授权
  • ✅ 与现有 WebApi 完美共存

快速开始

第一步:安装 NuGet 包

dotnet add package ModelContextProtocol.AspNetCore --version 0.4.0-preview.3

第二步:配置 MCP 服务

在 Program.cs 中添加 MCP 配置:

using ModelContextProtocol.Server;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

// 添加 MCP 服务器(支持 HTTP 和 Stdio 双模式)
builder.Services
    .AddMcpServer(options =>
    {
        options.ServerInfo = new ModelContextProtocol.Protocol.Implementation
        {
            Name = "Weather API",
            Version = "1.0.0"
        };
    })
    .WithHttpTransport()           // HTTP 模式:用于 Web 客户端
    .WithStdioServerTransport()    // Stdio 模式:用于 Kiro IDE 等本地工具
    .WithToolsFromAssembly();

var app = builder.Build();

// 添加认证中间件(可选)
app.UseMiddleware<McpAuthenticationMiddleware>();

app.UseAuthorization();
app.MapControllers();

// 映射 MCP 端点
app.MapMcp("/mcp");

app.Run();

第三步:定义 MCP 工具

创建 Tools/WeatherTools.cs

using System.ComponentModel;
using ModelContextProtocol.Server;

[McpServerToolType]
public static class WeatherTools
{
    [McpServerTool]
    [Description("Get weather forecast for the next 5 days")]
    public static IEnumerable<WeatherForecast> GetWeatherForecast()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        }).ToArray();
    }

    [McpServerTool]
    [Description("Get current weather for a specific city")]
    public static WeatherForecast GetWeatherByCity(
        [Description("The name of the city")] string city)
    {
        var rng = new Random();
        return new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now),
            TemperatureC = rng.Next(-20, 55),
            Summary = $"Weather in {city}: {Summaries[rng.Next(Summaries.Length)]}"
        };
    }

    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };
}

第四步:配置认证(可选)

在 appsettings.json 中配置:

{
  "McpAuth": {
    "Enabled": true,
    "ValidTokens": ["your-secret-token-here"]
  }
}

开发环境可以禁用认证(appsettings.Development.json):

{
  "McpAuth": {
    "Enabled": false
  }
}

第五步:运行和测试

dotnet run

应用启动后,可以访问:

  • Swagger UIhttp://localhost:5000/swagger
  • WebApihttp://localhost:5000/weatherforecast
  • MCP 端点http://localhost:5000/mcp

传输模式详解

HTTP 模式

适用于 Web 应用、Claude Desktop、远程访问等场景。

测试示例

# 列出所有工具
curl -X POST http://localhost:5000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# 调用工具
curl -X POST http://localhost:5000/mcp \
  -H "Authorization: Bearer your-secret-token-here" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "id":2,
    "method":"tools/call",
    "params":{
      "name":"GetWeatherForecast",
      "arguments":{}
    }
  }'

Claude Desktop 配置

编辑配置文件(Windows: %APPDATA%\Claude\claude_desktop_config.json):

{
  "mcpServers": {
    "weather-api": {
      "url": "http://localhost:5000/mcp",
      "headers": {
        "Authorization": "Bearer your-secret-token-here"
      }
    }
  }
}

Stdio 模式

适用于 Kiro IDE、本地命令行工具等场景,无需网络端口。

Kiro IDE 配置

编辑 .kiro/settings/mcp.json

{
  "mcpServers": {
    "weather-api": {
      "command": "dotnet",
      "args": ["run", "--project", "path/to/NetCoreApiMcpDemo.csproj"],
      "env": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

模式对比

特性 HTTP 模式 Stdio 模式
传输方式 HTTP POST 标准输入/输出
适用场景 Web 应用、远程访问 本地工具、IDE 集成
认证 HTTP Header 环境变量/配置
网络 需要网络端口 无需网络
性能 网络开销 进程间通信,更快

认证和授权

实现认证中间件

创建 Middleware/McpAuthenticationMiddleware.cs

public class McpAuthenticationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration _configuration;
    private readonly ILogger<McpAuthenticationMiddleware> _logger;

    public McpAuthenticationMiddleware(
        RequestDelegate next,
        IConfiguration configuration,
        ILogger<McpAuthenticationMiddleware> logger)
    {
        _next = next;
        _configuration = configuration;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 只对 MCP 端点进行认证
        if (!context.Request.Path.StartsWithSegments("/mcp"))
        {
            await _next(context);
            return;
        }

        // 检查是否启用认证
        var authEnabled = _configuration.GetValue<bool>("McpAuth:Enabled");
        if (!authEnabled)
        {
            await _next(context);
            return;
        }

        // 验证 Token
        var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
        if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsJsonAsync(new { error = "Unauthorized" });
            return;
        }

        var token = authHeader.Substring("Bearer ".Length).Trim();
        var validTokens = _configuration.GetSection("McpAuth:ValidTokens").Get<string[]>();

        if (validTokens == null || !validTokens.Contains(token))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsJsonAsync(new { error = "Invalid token" });
            return;
        }

        await _next(context);
    }
}

安全最佳实践

  1. 使用强 Token:至少 32 字符的随机字符串
  2. 定期轮换:定期更换 Token
  3. 使用 HTTPS:生产环境必须使用 HTTPS
  4. 环境隔离:开发和生产使用不同的 Token
  5. 日志安全:不要在日志中记录完整 Token

客户端集成示例

C# 客户端

using ModelContextProtocol;
using ModelContextProtocol.Client;

var transport = new HttpClientTransport(new HttpClientTransportOptions
{
    BaseUrl = new Uri("http://localhost:5000/mcp"),
    Headers = new Dictionary<string, string>
    {
        ["Authorization"] = "Bearer your-secret-token-here"
    }
});

var client = await McpClient.CreateAsync(transport);

await client.InitializeAsync(new InitializeParams
{
    ProtocolVersion = "2025-06-18",
    ClientInfo = new Implementation
    {
        Name = "MyApp",
        Version = "1.0.0"
    }
});

// 列出工具
var tools = await client.ListToolsAsync();

// 调用工具
var result = await client.CallToolAsync(
    "GetWeatherForecast",
    new Dictionary<string, object?>()
);

JavaScript/Vue 客户端

<script setup>
import { ref } from 'vue';

const weather = ref('');
const MCP_URL = 'http://localhost:5000/mcp';
const TOKEN = 'your-secret-token-here';

const callMcp = async (method, params = {}) => {
  const response = await fetch(MCP_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${TOKEN}`,
    },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: Date.now(),
      method,
      params,
    }),
  });
  return response.json();
};

const getWeather = async () => {
  const data = await callMcp('tools/call', {
    name: 'GetWeatherForecast',
    arguments: {},
  });
  weather.value = data.result.content[0].text;
};
</script>

MCP Tools 最佳实践

让 AI 更准确地使用你的工具是成功的关键。以下是经过实践验证的最佳实践。

核心原则

AI 通过以下信息决定是否使用你的工具:

  1. 工具名称 – 清晰、描述性
  2. Description – 详细的功能说明
  3. 参数描述 – 明确的参数用途
  4. 使用场景 – 何时应该使用这个工具

1. 使用清晰的命名

// ❌ 不好 - 名称模糊
[McpServerTool]
public static string Get() { }

// ✅ 好 - 动词开头,描述清晰
[McpServerTool]
public static string GetWeatherForecast() { }

// ✅ 更好 - 包含具体信息
[McpServerTool]
public static string GetWeatherForecastForNextDays() { }

命名建议:

  • 使用动词开头:Get, Search, Calculate, Compare, Analyze
  • 包含操作对象:Weather, Temperature, Forecast
  • 避免缩写和简称
  • 使用 PascalCase

2. 编写详细的 Description(最重要!)

这是最关键的部分!AI 主要通过 Description 判断是否使用工具。

// ❌ 不好 - 太简短
[Description("Get weather")]

// ⚠️ 一般 - 有基本信息但不够
[Description("Get weather forecast for the next 5 days")]

// ✅ 好 - 包含详细信息和使用场景
[Description(@"Get detailed weather forecast for the next several days including temperature, weather conditions, and trends.

Use this tool when users ask about:
- Future weather (tomorrow, next week, upcoming days)
- Weather predictions or forecasts
- Planning activities based on weather
- Temperature trends

Examples of user queries:
- 'What's the weather forecast for the next 5 days?'
- 'Will it rain this week?'
- 'What's the temperature trend?'")]

Description 应该包含:

  1. 功能说明 – 工具做什么
  2. 使用场景 – 何时使用(”Use this tool when…”)
  3. 示例查询 – 用户可能的提问方式
  4. 支持的功能 – 特殊能力或限制

3. 详细的参数描述

[McpServerTool]
public static string GetWeatherByCity(
    // ❌ 不好
    [Description("city")] string city,

    // ✅ 好
    [Description("The name of the city in English or Chinese (e.g., 'Beijing', '北京', 'Shanghai', 'New York')")]
    string city,

    // ✅ 更好 - 包含默认值说明
    [Description("Number of days to forecast (1-7 days). Default is 5 days if not specified.")]
    int days = 5
)

参数描述应该包含:

  • 参数的用途
  • 支持的格式或值范围
  • 示例值
  • 默认值(如果有)

4. 返回格式化、易读的结果

// ❌ 不好 - 返回原始对象
public static WeatherForecast GetWeather(string city)
{
    return new WeatherForecast { ... };
}

// ✅ 好 - 返回格式化的文本
public static string GetWeather(string city)
{
    var weather = GetWeatherData(city);

    return $@"🌍 Current Weather in {city}
📅 Date: {weather.Date:yyyy-MM-dd}
🌡️ Temperature: {weather.TemperatureC}°C ({weather.TemperatureF}°F)
☁️ Conditions: {weather.Summary}
⏰ Updated: {DateTime.Now:HH:mm:ss}";
}

5. 完整示例:查询工具

[McpServerTool]
[Description(@"Get detailed weather forecast for the next several days including temperature, weather conditions, and trends.

Use this tool when users ask about:
- Future weather (tomorrow, next week, upcoming days)
- Weather predictions or forecasts
- Planning activities based on weather
- Temperature trends
- Weather conditions for travel planning

Examples of user queries:
- 'What's the weather forecast for the next 5 days?'
- 'Will it rain this week?'
- 'What's the temperature trend?'
- 'Should I bring a jacket tomorrow?'
- '未来几天天气怎么样?'
- '这周会下雨吗?'")]
public static string GetWeatherForecast(
    [Description("Number of days to forecast (1-7 days). Default is 5 days if not specified.")]
    int days = 5)
{
    var forecasts = GenerateForecasts(days);

    var result = new StringBuilder();
    result.AppendLine($"🌤️ Weather Forecast for Next {days} Days");
    result.AppendLine();

    foreach (var forecast in forecasts)
    {
        result.AppendLine($"📅 {forecast.Date:yyyy-MM-dd (ddd)}");
        result.AppendLine($"   🌡️ Temperature: {forecast.TemperatureC}°C ({forecast.TemperatureF}°F)");
        result.AppendLine($"   ☁️ Conditions: {forecast.Summary}");
        result.AppendLine();
    }

    return result.ToString();
}

6. 完整示例:比较工具

[McpServerTool]
[Description(@"Compare weather conditions between two cities to help with travel decisions or general comparison.

Use this tool when users want to:
- Compare weather between cities
- Decide which city has better weather
- Plan travel between cities
- Compare temperatures
- Choose destination based on weather

Examples of user queries:
- 'Compare weather between Beijing and Shanghai'
- 'Which city is warmer, Tokyo or Seoul?'
- 'Weather difference between New York and London'
- '北京和上海哪个城市天气更好?'
- '东京和首尔哪里更暖和?'")]
public static string CompareWeatherBetweenCities(
    [Description("First city name (English or Chinese)")] string city1,
    [Description("Second city name (English or Chinese)")] string city2)
{
    var weather1 = GetWeatherData(city1);
    var weather2 = GetWeatherData(city2);

    return $@"🌍 Weather Comparison

📍 {city1}:
   🌡️ Temperature: {weather1.TemperatureC}°C
   ☁️ Conditions: {weather1.Summary}

📍 {city2}:
   🌡️ Temperature: {weather2.TemperatureC}°C
   ☁️ Conditions: {weather2.Summary}

📊 Difference: {Math.Abs(weather1.TemperatureC - weather2.TemperatureC)}°C
{(weather1.TemperatureC > weather2.TemperatureC ? $"🔥 {city1} is warmer" : $"🔥 {city2} is warmer")}";
}

7. Description 模板

基础模板:

[Description(@"[简短功能说明]

Use this tool when users ask about:
- [使用场景1]
- [使用场景2]
- [使用场景3]

Examples of user queries:
- '[示例问题1]'
- '[示例问题2]'
- '[示例问题3]'")]

完整模板:

[Description(@"[详细功能说明,包括返回的数据类型和格式]

Use this tool when users want to:
- [使用场景1]
- [使用场景2]
- [使用场景3]

Supports:
- [支持的功能1]
- [支持的功能2]

Examples of user queries:
- '[英文示例1]'
- '[英文示例2]'
- '[中文示例1]'
- '[中文示例2]'

Note: [特殊说明或限制]")]

8. 优化检查清单

在发布工具前,检查以下项目:

高级特性

依赖注入支持

工具方法可以注入服务:

[McpServerTool]
[Description("Get weather with logging")]
public static string GetWeatherWithLogging(
    ILogger<WeatherTools> logger,
    IWeatherService weatherService,
    string city)
{
    logger.LogInformation("Getting weather for {City}", city);
    return weatherService.GetWeather(city);
}

添加 Prompts

[McpServerPromptType]
public static class WeatherPrompts
{
    [McpServerPrompt]
    [Description("Creates a prompt to help plan outdoor activities based on weather")]
    public static ChatMessage PlanOutdoorActivity(
        [Description("The city name")] string city,
        [Description("The activity type")] string activity)
    {
        return new ChatMessage(
            ChatRole.User,
            $@"I want to plan a {activity} activity in {city}.
            Please check the weather forecast and suggest the best day and time.
            Consider temperature, conditions, and provide detailed recommendations."
        );
    }
}

复杂参数类型

SDK 自动支持:

  • 基本类型:stringintbooldouble 等
  • 复杂对象:自动序列化/反序列化
  • 可选参数:使用默认值
  • 数组和集合

故障排除

工具未被发现

检查项:

  • 类是否有 [McpServerToolType] 特性
  • 方法是否有 [McpServerTool] 特性
  • 类是否是静态的
  • 是否重启了应用

认证失败

检查项:

  • Token 是否正确
  • appsettings.json 中 Enabled 设置
  • Authorization header 格式
  • 环境配置(Development vs Production)

CORS 问题

在 Program.cs 中添加 CORS 支持:

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowMcpClients", policy =>
    {
        policy.WithOrigins("http://localhost:3000")
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

app.UseCors("AllowMcpClients");

项目结构

NetCoreApiMcpDemo/
├── Controllers/
│   └── WeatherForecastController.cs  # 标准 WebApi 控制器
├── Tools/
│   └── WeatherTools.cs                # MCP 工具定义
├── Middleware/
│   └── McpAuthenticationMiddleware.cs # 认证中间件
├── Program.cs                          # 应用配置
├── appsettings.json                    # 配置文件
└── appsettings.Development.json        # 开发配置

为什么选择官方 SDK?

  1. 代码更少:无需自定义特性和提供者
  2. 更可靠:官方维护和更新
  3. 更强大:自动 Schema、DI 支持
  4. 更标准:完全符合 MCP 规范
  5. 更易维护:无需维护自定义代码

总结

通过本文,我们学习了如何在 ASP.NET Core WebApi 中集成 MCP 协议支持。使用官方 SDK,只需几行代码就能让你的 API 被 AI 客户端调用。MCP 协议的标准化特性,让 AI 应用与后端服务的集成变得前所未有的简单。

参考资源

源码地址

完整示例代码请访问:[GitHub 仓库地址]


如果本文对你有帮助,欢迎点赞、收藏、关注!有任何问题欢迎在评论区讨论。

DeepSeek-OCR 如何使用?分步教程指南

mikel阅读(268)

来源: DeepSeek-OCR 如何使用?分步教程指南

DeepSeek-OCR 是一个用于光学字符识别(OCR)的强大工具,它可以将图像和 PDF 文档转换为结构化文本。本教程将一步步指导你如何安装、配置和使用 DeepSeek-OCR。

开源项目地址:https://github.com/deepseek-ai/DeepSeek-OCR/tree/main

DeepSeek-OCR 如何使用

 

第一步:环境准备

系统要求

  • 操作系统:Linux/Windows/macOS
  • Python 版本:3.12.9
  • CUDA 版本:11.8 或更高
  • PyTorch 版本:2.6.0

 

硬件要求

  • 推荐 GPU:A100-40G 或同等性能显卡
  • 内存:至少 16GB RAM
  • 存储空间:至少 10GB 可用空间

 

第二步:下载和克隆项目

克隆 GitHub 仓库

git clone https://github.com/deepseek-ai/DeepSeek-OCR.git
cd DeepSeek-OCR

 

查看项目结构项目包含以下主要文件:

  • DeepSeek-OCR-master/ – 主要代码目录
  • assets/ – 资源文件
  • requirements.txt – 依赖包列表
  • README.md – 项目说明文档
DeepSeek-OCR 如何使用

 

第三步:环境配置

创建 Conda 环境

conda create -n deepseek-ocr python=3.12.9 -y
conda activate deepseek-ocr

 

安装 PyTorch

pip install torch==2.6.0 torchvision==0.21.0 torchaudio==2.6.0 --index-url https://download.pytorch.org/whl/cu118

 

安装 vLLM(推荐)

# 下载并安装vLLM-0.8.5 whl文件
pip install vllm-0.8.5+cu118-cp38-abi3-manylinux1_x86_64.whl

 

安装其他依赖

pip install -r requirements.txt
pip install flash-attn==2.7.3 --no-build-isolation

 

第四步:模型下载和配置

自动下载模型

当你首次运行 DeepSeek-OCR 时,模型会自动从 Hugging Face 下载:

model_name = 'deepseek-ai/DeepSeek-OCR'

 

配置文件设置

编辑配置文件 DeepSeek-OCR-master/DeepSeek-OCR-vllm/config.py

  • 设置输入路径(INPUT_PATH)
  • 设置输出路径(OUTPUT_PATH)
  • 调整其他相关参数

 

第五步:使用方法详解

使用 vLLM 进行推理(推荐)

方法一:处理单张图像

cd DeepSeek-OCR-master/DeepSeek-OCR-vllm
python run_dpsk_ocr_image.py

 

方法二:处理 PDF 文档

python run_dpsk_ocr_pdf.py

注:处理 PDF 时可达到约 2500tokens/s 的速度(在 A100-40G 上)

 

方法三:批量评估

python run_dpsk_ocr_eval_batch.py

 

使用 Transformers 进行推理

创建 Python 脚本

from transformers import AutoModel, AutoTokenizer
import torch
import os

# 设置GPU
os.environ["CUDA_VISIBLE_DEVICES"] = '0'

# 加载模型和分词器
model_name = 'deepseek-ai/DeepSeek-OCR'
tokenizer = AutoTokenizer.from_pretrained(
    model_name, 
    trust_remote_code=True
)

model = AutoModel.from_pretrained(
    model_name,
    _attn_implementation='flash_attention_2',
    trust_remote_code=True,
    use_safetensors=True
)

# 设置模型参数
model = model.eval().cuda().to(torch.bfloat16)

# 定义提示词和图像路径
prompt = "<image><|grounding|>Convert the document to markdown."
image_file = 'your_image.jpg'
output_path = 'your/output/dir'

# 执行推理
res = model.infer(
    tokenizer,
    prompt=prompt,
    image_file=image_file,
    output_path=output_path,
    base_size=1024,
    image_size=640,
    crop_mode=True,
    save_results=True,
    test_compress=True
)

 

或使用现成脚本

cd DeepSeek-OCR-master/DeepSeek-OCR-hf
python run_dpsk_ocr.py

 

第六步:支持的模式和配置

原生分辨率模式

  • Tiny: 512×512 (64 个视觉 tokens)
  • Small: 640×640 (100 个视觉 tokens)
  • Base: 1024×1024 (256 个视觉 tokens)
  • Large: 1280×1280 (400 个视觉 tokens)

 

动态分辨率模式

  • Gundam: n×640×640 + 1×1024×1024

 

第七步:提示词模板常用提示词

# 文档转换
prompt = "<image>\n<|grounding|>Convert the document to markdown."

# 普通OCR
prompt = "<image>\n<|grounding|>OCR this image."

# 无布局OCR
prompt = "<image>\nFree OCR."

# 图表解析
prompt = "<image>\nParse the figure."

# 详细描述
prompt = "<image>\nDescribe this image in detail."

# 定位特定内容
prompt = "<image>\nLocate <|ref|>目标文字<|/ref|> in the image."

 

第八步:常见问题解决

安装问题如果遇到 vLLM 安装错误:

vllm 0.8.5+cu118 requires transformers>=4.51.1

这是正常现象,不会影响使用。

 

内存不足

  • 降低 batch_size
  • 使用较小的分辨率模式
  • 关闭不必要的程序

 

GPU 显存不足

  • 使用量化模型
  • 减少并发处理数量
  • 调整图像尺寸

 

第九步:性能优化建议

硬件优化

  • 使用高性能 GPU(如 A100、H100)
  • 确保足够的显存
  • 使用 SSD 存储以提高 I/O 速度

 

软件优化

  • 使用 vLLM 而非 Transformers 获得更好性能
  • 开启 Flash Attention 2
  • 根据任务选择合适的分辨率模式

 

总结

DeepSeek-OCR 是一个功能强大的 OCR 工具,通过本教程的分步指导,你应该能够:

  1. 成功安装和配置环境
  2. 掌握基本使用方法
  3. 了解不同的推理模式
  4. 解决常见问题
  5. 优化性能表现

如果你遇到任何问题,可以参考 GitHub 仓库的 Issues 部分或查看官方文档。