当前位置:首页 > 文章列表 > 文章 > 软件教程 > C#.NET MediatR 深入解析:进程内消息分发、CQRS、通知事件与管道行为实战

C#.NET MediatR 深入解析:进程内消息分发、CQRS、通知事件与管道行为实战

2026-08-20 21:24:32 0浏览 收藏

简介

MediatR.NET 里很常见的进程内消息分发库。

C#.NET MediatR 深入解析:进程内消息分发、CQRS、通知事件与管道行为实战

它做的事情不复杂:

调用方发出一个请求,MediatR 找到对应的处理器执行。

这听起来像“多绕了一层”,但在业务代码越来越复杂时,这一层很有用。

没有 MediatR 时,控制器、服务类里经常会堆很多依赖:

public class OrdersController : ControllerBase
{
    private readonly IOrderRepository _orders;
    private readonly IInventoryService _inventory;
    private readonly IPaymentService _payment;
    private readonly IEmailService _email;
    private readonly ILogger _logger;
}

一开始还好,后面新增库存、支付、优惠券、消息通知、审计日志,控制器和服务层就会越来越重。

MediatR 的思路是把这些操作拆成一条条消息:

await sender.Send(new CreateOrderCommand(...));

控制器只表达“要创建订单”,真正的处理逻辑放到对应的 Handler 里。

一句话概括:

MediatR 不是消息队列,也不是分布式事件总线,它是应用进程内部的请求分发器。

这篇文章主要讲清楚:

  • MediatR 到底解决什么问题;
  • SendPublish 分别适合什么场景;
  • Command、Query、Notification 怎么写;
  • Pipeline Beha vior 怎么处理日志、验证、事务;
  • ASP.NET Core 里怎么注册和使用;
  • 实际项目里容易踩哪些坑。

先把定位说清楚

MediatR 实现的是中介者模式。

中介者模式的核心不是“高级”,而是减少直接依赖。

普通调用是这样:

Controller -> Service -> Repository / OtherService / Logger / MessageSender

使用 MediatR 后变成:

Controller -> MediatR -> Handler -> Repository / OtherService

也就是说:

  • 调用方不直接依赖具体业务处理类;
  • 请求和处理器通过类型匹配;
  • 横切逻辑可以放进管道;
  • 每个业务动作可以拆成一个独立处理单元。

但这不代表所有项目都要上 MediatR

如果项目很小,业务逻辑也简单,直接写服务类更清楚。

MediatR 更适合这些场景:

  • 控制器越来越薄,业务逻辑想拆到独立用例里;
  • 项目采用 CQRS,读写操作希望分开组织;
  • 需要统一加日志、验证、权限、事务、性能监控;
  • 有领域事件或进程内通知;
  • 业务动作很多,按 feature / use case 组织更清晰。

它不是消息队列

这个点很重要。

MediatR 是进程内分发。

也就是说:

  • 不跨进程;
  • 不跨服务;
  • 不负责持久化消息;
  • 不负责失败重试;
  • 不负责削峰;
  • 不负责最终一致性。

下面这些事情不是 MediatR 的职责:

  • 服务 A 给服务 B 发消息;
  • 消息失败后自动重试;
  • 消息落库防丢;
  • 多个消费者分布式消费。

这些场景要看:

  • RabbitMQ
  • Kafka
  • Redis Stream
  • Azure Service Bus
  • CAP
  • MassTransit

MediatR 解决的是同一个应用内部的调用组织问题。

核心接口

MediatR 常用接口不多。

接口作用常见场景
IRequest有返回值请求查询、创建后返回 ID
IRequest无返回值请求命令、执行动作
IRequestHandler处理有返回值请求Command / Query Handler
IRequestHandler处理无返回值请求Command Handler
INotification通知事件订单已创建、用户已注册
INotificationHandler通知处理器发邮件、写日志、同步缓存
IPipelineBeha vior请求管道日志、验证、事务、性能统计
ISender发送 RequestSend
IPublisher发布 NotificationPublish
IMediator同时包含发送和发布能力兼容常见写法

现在更推荐按职责注入:

private readonly ISender _sender;
private readonly IPublisher _publisher;

如果只需要发送命令或查询,用 ISender 就够了。
如果只需要发布通知,用 IPublisher 就够了。

IMediator 也能用,只是能力更宽。

安装和注册

安装:

dotnet add package MediatR

在现代版本当中,MediatR已经直接对Microsoft.Extensions.DependencyInjection注册予以支持。

Program.cs

using MediatR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
});

如果 Handler 在其他程序集里,就注册对应程序集:

builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
});

或者注册多个程序集:

builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssemblies(
        typeof(Program).Assembly,
        typeof(CreateOrderCommand).Assembly);
});

老资料里经常出现:

dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

这是旧版本常见写法。新项目通常先看当前 MediatR 包本身的注册方式,不要直接照搬旧教程。

关于 License Key

较新的 MediatR 版本引入了 license key 配置。

注册时可以这样设置:

builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    cfg.LicenseKey = builder.Configuration["MediatR:LicenseKey"];
});

如果没有配置,可能会看到 license 相关日志。

这类日志并非意味着功能无法使用,但在生产环境中,需要按照实际的许可要求进行处理。客户端应用切勿将license key嵌入前端或桌面客户端包中。

如果只是想屏蔽客户端缺失 license 的日志,可以按日志分类过滤:

builder.Logging.AddFilter("LuckyPennySoftware.MediatR.License", LogLevel.None);

这部分属于版本和许可策略问题,实际项目里要以当前包版本和团队许可为准。

Command:处理写操作

Command 通常表示“执行一个会改变系统状态的动作”。

比如创建订单、取消订单、修改用户资料。

先定义请求:

using MediatR;

public sealed record CreateOrderCommand(
    long UserId,
    IReadOnlyList Items) : IRequestlong>;

public sealed record CreateOrderItem(
    long ProductId,
    int Quantity);

这里 CreateOrderCommand 返回 long,表示创建后的订单 ID。

再写处理器:

using MediatR;

public sealed class CreateOrderCommandHandler
    : IRequestHandlerCreateOrderCommand, long>
{
    private readonly IOrderRepository _orders;
    private readonly IProductRepository _products;
    private readonly IUnitOfWork _unitOfWork;

    public CreateOrderCommandHandler(
        IOrderRepository orders,
        IProductRepository products,
        IUnitOfWork unitOfWork)
    {
        _orders = orders;
        _products = products;
        _unitOfWork = unitOfWork;
    }

    public async Tasklong> Handle(
        CreateOrderCommand request,
        CancellationToken cancellationToken)
    {
        if (request.Items.Count == 0)
        {
            throw new InvalidOperationException("订单明细不能为空。");
        }

        var order = new Order(request.UserId);

        foreach (CreateOrderItem item in request.Items)
        {
            Product product = await _products.GetByIdAsync(
                item.ProductId,
                cancellationToken);

            order.AddItem(product.Id, product.Price, item.Quantity);
        }

        await _orders.AddAsync(order, cancellationToken);
        await _unitOfWork.Sa veChangesAsync(cancellationToken);

        return order.Id;
    }
}

控制器只负责接收请求、发送命令:

[ApiController]
[Route("api/orders")]
public sealed class OrdersController : ControllerBase
{
    private readonly ISender _sender;

    public OrdersController(ISender sender)
    {
        _sender = sender;
    }

    [HttpPost]
    public async Tasklong>> Create(
        CreateOrderRequest request,
        CancellationToken cancellationToken)
    {
        var command = new CreateOrderCommand(
            request.UserId,
            request.Items
                .Select(x => new CreateOrderItem(x.ProductId, x.Quantity))
                .ToArray());

        long orderId = await _sender.Send(command, cancellationToken);

        return Ok(orderId);
    }
}

这样控制器不再关心:

  • 怎么查商品;
  • 怎么组装订单;
  • 怎么保存;
  • 怎么提交事务。

这些都在对应的 Handler 里。

Query:处理读操作

Query 通常表示“查询数据,不改变系统状态”。

定义查询:

public sealed record GetOrderDetailQuery(long OrderId)
    : IRequest;

public sealed record OrderDetailDto(
    long Id,
    long UserId,
    decimal TotalAmount,
    IReadOnlyList Items);

public sealed record OrderItemDto(
    long ProductId,
    string ProductName,
    int Quantity,
    decimal Price);

处理器:

public sealed class GetOrderDetailQueryHandler
    : IRequestHandlerGetOrderDetailQuery, OrderDetailDto?>
{
    private readonly IOrderReadRepository _orders;

    public GetOrderDetailQueryHandler(IOrderReadRepository orders)
    {
        _orders = orders;
    }

    public Task Handle(
        GetOrderDetailQuery request,
        CancellationToken cancellationToken)
    {
        return _orders.GetDetailAsync(request.OrderId, cancellationToken);
    }
}

控制器:

[HttpGet("{id:long}")]
public async Task> GetById(
    long id,
    CancellationToken cancellationToken)
{
    OrderDetailDto? order = await _sender.Send(
        new GetOrderDetailQuery(id),
        cancellationToken);

    if (order is null)
    {
        return NotFound();
    }

    return Ok(order);
}

CommandQuery 分开以后,代码结构会更清楚:

  • 写操作关注业务规则和状态变化;
  • 读操作关注查询效率和返回模型;
  • 两边不必强行共用同一个服务方法。

这就是 CQRS 的基本味道。

Notification:处理一对多事件

Send 是一对一。

一个请求对应一个处理器。

Publish 是一对多。

一个通知可以有多个处理器。

比如订单创建后:

  • 写订单日志;
  • 发站内信;
  • 清理购物车;
  • 推送统计事件。

定义通知:

public sealed record OrderCreatedNotification(
    long OrderId,
    long UserId,
    decimal TotalAmount) : INotification;

发布通知:

public sealed class CreateOrderCommandHandler
    : IRequestHandlerCreateOrderCommand, long>
{
    private readonly IOrderRepository _orders;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IPublisher _publisher;

    public CreateOrderCommandHandler(
        IOrderRepository orders,
        IUnitOfWork unitOfWork,
        IPublisher publisher)
    {
        _orders = orders;
        _unitOfWork = unitOfWork;
        _publisher = publisher;
    }

    public async Tasklong> Handle(
        CreateOrderCommand request,
        CancellationToken cancellationToken)
    {
        var order = Order.Create(request.UserId);

        await _orders.AddAsync(order, cancellationToken);
        await _unitOfWork.Sa veChangesAsync(cancellationToken);

        await _publisher.Publish(
            new OrderCreatedNotification(
                order.Id,
                order.UserId,
                order.TotalAmount),
            cancellationToken);

        return order.Id;
    }
}

第一个通知处理器:

public sealed class ClearCartWhenOrderCreatedHandler
    : INotificationHandlerOrderCreatedNotification>
{
    private readonly ICartService _cart;

    public ClearCartWhenOrderCreatedHandler(ICartService cart)
    {
        _cart = cart;
    }

    public Task Handle(
        OrderCreatedNotification notification,
        CancellationToken cancellationToken)
    {
        return _cart.ClearAsync(notification.UserId, cancellationToken);
    }
}

第二个通知处理器:

public sealed class WriteOrderAuditLogHandler
    : INotificationHandlerOrderCreatedNotification>
{
    private readonly IAuditLogRepository _logs;

    public WriteOrderAuditLogHandler(IAuditLogRepository logs)
    {
        _logs = logs;
    }

    public Task Handle(
        OrderCreatedNotification notification,
        CancellationToken cancellationToken)
    {
        return _logs.AddAsync(
            $"订单已创建:{notification.OrderId}",
            cancellationToken);
    }
}

这里的重点是:

  • 创建订单的主流程不用知道有几个后续动作;
  • 新增一个通知处理器,不需要改发布方;
  • 通知不适合返回业务结果。

Publish 不等于可靠事件

Publish 很容易被误用成“事件总线”。

但它仍然是进程内执行。

如果通知处理器失败,行为取决于发布策略和异常处理方式。它没有自动持久化、自动重试、死信队列这些能力。

所以这类事情不要只靠 INotification

  • 扣款成功后必须发送出库消息;
  • 订单创建事件必须可靠投递到其他服务;
  • 外部系统失败后要重试;
  • 消息丢失会造成数据不一致。

这类场景更适合:

  • 事务内写业务数据和 Outbox 表;
  • 后台任务扫描 Outbox;
  • 通过消息队列投递到外部系统。

MediatRNotification 更适合进程内副作用:

  • 清理缓存;
  • 写审计日志;
  • 触发本地领域事件处理;
  • 发送非关键通知;
  • 拆分本地模块之间的后续动作。

Pipeline Beha vior:把横切逻辑放进管道

Pipeline Beha viorMediatR 很重要的能力。

它类似 ASP.NET Core 中间件。

请求进入 Handler 前后,可以统一加一些逻辑:

  • 日志;
  • 参数验证;
  • 权限检查;
  • 性能统计;
  • 事务;
  • 缓存。

基本结构如下:

public sealed class LoggingBeha viorTRequest, TResponse>
    : IPipelineBeha viorTRequest, TResponse>
{
    private readonly ILogger> _logger;

    public LoggingBeha vior(
        ILogger> logger)
    {
        _logger = logger;
    }

    public async TaskHandle(
        TRequest request,
        RequestHandlerDelegate next,
        CancellationToken cancellationToken)
    {
        string requestName = typeof(TRequest).Name;

        _logger.LogInformation("开始处理 {RequestName}", requestName);

        TResponse response = await next();

        _logger.LogInformation("完成处理 {RequestName}", requestName);

        return response;
    }
}

注册:

builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    cfg.AddOpenBeha vior(typeof(LoggingBeha vior));
});

这样所有 Send 请求都会经过这个行为。

实际示例:FluentValidation 验证管道

MediatR 经常和 FluentValidation 搭配。

需要额外安装:

dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions

先定义验证器:

using FluentValidation;

public sealed class CreateOrderCommandValidator
    : AbstractValidatorCreateOrderCommand>
{
    public CreateOrderCommandValidator()
    {
        RuleFor(x => x.UserId)
            .GreaterThan(0)
            .WithMessage("用户 ID 不合法。");

        RuleFor(x => x.Items)
            .NotEmpty()
            .WithMessage("订单明细不能为空。");

        RuleForEach(x => x.Items)
            .ChildRules(item =>
            {
                item.RuleFor(x => x.ProductId).GreaterThan(0);
                item.RuleFor(x => x.Quantity).GreaterThan(0);
            });
    }
}

验证管道:

using FluentValidation;

public sealed class ValidationBeha viorTRequest, TResponse>
    : IPipelineBeha viorTRequest, TResponse>
{
    private readonly IEnumerable> _validators;

    public ValidationBeha vior(IEnumerable> validators)
    {
        _validators = validators;
    }

    public async TaskHandle(
        TRequest request,
        RequestHandlerDelegate next,
        CancellationToken cancellationToken)
    {
        if (!_validators.Any())
        {
            return await next();
        }

        var context = new ValidationContext(request);

        var errors = _validators
            .Select(x => x.Validate(context))
            .SelectMany(x => x.Errors)
            .Where(x => x is not null)
            .ToList();

        if (errors.Count > 0)
        {
            throw new ValidationException(errors);
        }

        return await next();
    }
}

注册:

builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);

builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    cfg.AddOpenBeha vior(typeof(ValidationBeha vior));
});

这样 Handler 里就不用堆一堆基础参数校验。

Handler 更专注业务规则:

  • 用户是否存在;
  • 商品是否可售;
  • 库存是否足够;
  • 价格是否需要重新计算。

实际示例:事务管道

事务也适合放进管道。

不过不是所有请求都需要事务。

可以定义一个标记接口:

public interface ITransactionalRequest
{
}

让需要事务的命令实现它:

public sealed record CreateOrderCommand(
    long UserId,
    IReadOnlyList Items)
    : IRequestlong>, ITransactionalRequest;

事务管道:

public sealed class TransactionBeha viorTRequest, TResponse>
    : IPipelineBeha viorTRequest, TResponse>
{
    private readonly AppDbContext _dbContext;

    public TransactionBeha vior(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async TaskHandle(
        TRequest request,
        RequestHandlerDelegate next,
        CancellationToken cancellationToken)
    {
        if (request is not ITransactionalRequest)
        {
            return await next();
        }

        await using var transaction = await _dbContext.Database
            .BeginTransactionAsync(cancellationToken);

        TResponse response = await next();

        await _dbContext.Sa veChangesAsync(cancellationToken);
        await transaction.CommitAsync(cancellationToken);

        return response;
    }
}

注册顺序要留意。

例如:

builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    cfg.AddOpenBeha vior(typeof(LoggingBeha vior));
    cfg.AddOpenBeha vior(typeof(ValidationBeha vior));
    cfg.AddOpenBeha vior(typeof(TransactionBeha vior));
});

通常希望:

  • 日志最外层;
  • 验证在事务之前;
  • 事务包住真正的业务处理。

如果验证失败,不需要打开事务。

如果采用这种事务管道,Handler 里就不要再对同一个 DbContext 重复提交。

也就是说,下面两种方式选一种:

  • Handler 自己调用 Sa veChangesAsync
  • 事务管道统一调用 Sa veChangesAsync

两边都提交,代码语义会变乱。

Minimal API 里的写法

MediatR 不只适用于控制器。

Minimal API 里也可以直接注入 ISender

app.MapPost("/orders", async (
    CreateOrderRequest request,
    ISender sender,
    CancellationToken cancellationToken) =>
{
    var command = new CreateOrderCommand(
        request.UserId,
        request.Items
            .Select(x => new CreateOrderItem(x.ProductId, x.Quantity))
            .ToArray());

    long orderId = await sender.Send(command, cancellationToken);

    return Results.Ok(new { orderId });
});

查询接口:

app.MapGet("/orders/{id:long}", async (
    long id,
    ISender sender,
    CancellationToken cancellationToken) =>
{
    OrderDetailDto? order = await sender.Send(
        new GetOrderDetailQuery(id),
        cancellationToken);

    return order is null
        ? Results.NotFound()
        : Results.Ok(order);
});

文件组织方式

MediatR 代码如果放得乱,很快会变成另一种混乱。

比较常见的组织方式是按功能分目录:

Features/
  Orders/
    CreateOrder/
      CreateOrderCommand.cs
      CreateOrderCommandHandler.cs
      CreateOrderCommandValidator.cs
    GetOrderDetail/
      GetOrderDetailQuery.cs
      GetOrderDetailQueryHandler.cs
      OrderDetailDto.cs

这种方式的好处是:

  • 一个用例相关的请求、处理器、验证器放在一起;
  • 改创建订单时,不用到处找文件;
  • 比按 ControllersServicesRepositories 横向分层更贴近业务动作。

当然,项目如果已经有成熟分层方式,也没必要为了 MediatR 强行重排目录。

执行流程

一次 Send 大概是这样:

flowchart LR
    A["Controller / Minimal API"] --> B["ISender.Send"]
    B --> C["Pipeline Beha viors"]
    C --> D["Request Handler"]
    D --> E["Repository / DbContext / External Service"]
    D --> C
    C --> B
    B --> A

一次 Publish 大概是这样:

flowchart LR
    A["Handler / Domain Service"] --> B["IPublisher.Publish"]
    B --> C["Notification Handler A"]
    B --> D["Notification Handler B"]
    B --> E["Notification Handler C"]

这两张图的区别很关键:

  • Send 是一对一,有返回值;
  • Publish 是一对多,不适合拿返回值;
  • Pipeline Beha vior 主要包在 Send 的请求处理链路上。

常见坑 1:把所有方法都改成 MediatR

MediatR 是组织业务用例的工具,不是替代所有方法调用的工具。

下面这些代码没必要硬套:

  • 简单工具方法;
  • 实体内部行为;
  • 私有领域计算;
  • 很短的纯函数;
  • 一个类内部的普通协作。

如果一个操作只是同一个类内部拆方法,直接调用方法更清楚。

MediatR 更适合表达“一个应用层用例”。

常见坑 2:Handler 变成大杂烩

用了 MediatR,并不自动等于架构清晰。

如果 Handler 里塞满:

  • 参数校验;
  • 权限判断;
  • 事务;
  • 日志;
  • 复杂查询;
  • 外部接口调用;
  • 各种 if-else;

那只是把胖控制器换成了胖 Handler

更合适的拆法是:

  • 基础校验放验证器;
  • 日志、事务、性能统计放管道;
  • 复杂领域规则放领域对象或领域服务;
  • 数据访问细节放仓储或查询对象;
  • Handler 负责组织这个用例。

常见坑 3:在 Handler 里互相 Send

一个 Handler 里再调用 _sender.Send(...),有时可以用,但要克制。

如果大量 Handler 互相发送请求,调用链会变得不直观:

AHandler -> Send(BCommand) -> BHandler -> Send(CCommand)

结果是:

  • 调试困难;
  • 事务边界不清楚;
  • 重复经过管道;
  • 容易形成隐式依赖。

更稳的做法是把公共业务能力抽成服务,两个 Handler 都调用这个服务。

常见坑 4:滥用 Notification 做关键业务

Notification 适合解耦本地副作用。

但关键业务不要轻易放进“谁订阅谁执行”的隐式流程里。

例如:

  • 创建订单必须扣库存;
  • 支付成功必须生成流水;
  • 审核通过必须变更主状态。

这些动作如果是主流程的一部分,最好明确写在命令处理逻辑里,或者放在明确的领域服务里。

Notification 更适合“主流程完成后顺带发生”的动作。

常见坑 5:忽略 CancellationToken

Handle 方法自带 CancellationToken

数据库、HTTP、缓存、消息发送等异步调用,都要尽量传下去:

await _orders.AddAsync(order, cancellationToken);
await _unitOfWork.Sa veChangesAsync(cancellationToken);

如果每一层都丢掉 CancellationToken,上游请求取消后,后台工作仍然可能继续执行。

常见坑 6:异常处理边界不清楚

MediatR 不会让异常自动变成统一响应。

Handler 抛出的异常,最终还是要被 ASP.NET Core 的异常处理中间件、过滤器或自定义异常映射处理。

常见做法是:

  • 参数错误抛验证异常;
  • 找不到资源返回 null 或领域结果对象;
  • 业务冲突抛业务异常或返回错误结果;
  • 最外层统一转成 HTTP 响应。

不要把所有异常处理都塞进每个 Handler

什么时候不适合用 MediatR?

这些情况可以先不上:

  • 项目很小,控制器和服务类已经足够清楚;
  • 团队不熟悉这种用例拆分方式;
  • 业务逻辑非常简单,主要是 CRUD;
  • 引入后只是多了一堆空壳 Command / Handler;
  • 调用链可读性比原来更差。

MediatR 的收益来自“拆分复杂度”。

如果本来没有复杂度,引入它只会增加文件数量和跳转成本。

总结

MediatR 的核心价值不是“让代码看起来高级”,而是:

把应用内部的业务动作,拆成清晰的请求、处理器、通知和管道。

它最常用的几类能力是:

  • Send:命令和查询,一对一处理;
  • Publish:通知事件,一对多处理;
  • Pipeline Beha vior:日志、验证、事务、性能统计等横切逻辑;
  • ISender / IPublisher:按职责暴露发送和发布能力;
  • 程序集扫描:自动注册请求处理器和通知处理器。

真正需要记住的是边界:

  • 它是进程内分发,不是消息队列;
  • 它能让控制器变薄,但不能自动让业务变清楚;
  • Notification 不适合承载必须成功的关键业务;
  • Handler 也需要保持单一职责;
  • 简单项目没必要为了模式而引入。

用在合适的位置,MediatR 会让应用层更像一组清晰的用例。

用得过度,就只是把直接调用换成绕路调用。

DBeaver Explain 执行计划怎么看:SQL 编辑器、图形计划与慢点核对DBeaver Explain 执行计划怎么看:SQL 编辑器、图形计划与慢点核对
上一篇
DBeaver Explain 执行计划怎么看:SQL 编辑器、图形计划与慢点核对
C#.NET TaskCompletionSource 深入解析:手动控制 Task、桥接回调事件与实战避坑
下一篇
C#.NET TaskCompletionSource 深入解析:手动控制 Task、桥接回调事件与实战避坑
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ljg-skills -
    ljg-skills
    ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
    5037次使用
  • MELO音乐 - AI 音乐生成平台,支持多模态创作能力
    MELO音乐
    MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
    4573次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    4518次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    4779次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    4730次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码