You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ArchUnit测试异常排查:合规Command类触发失败提示

问题描述

使用ArchUnit进行架构测试时,配置规则要求:实现IRequest的非抽象且名称以Command结尾的类,需位于匹配.+\.Commands\.的命名空间下。但符合条件的Command类(如SynchronizeMasterDataCommand等)却触发了FailedArchRuleException异常,错误提示反而显示这些类确实处于对应Commands命名空间中。

错误信息

Message: 
  ArchUnitNET.xUnit.FailedArchRuleException : "Classes that are Classes that are Classes that implement 'IRequest' and are not abstract and have name matching ".+Command$" and have name matching ".+Command$" should reside in namespace with full name matching ".+\.Commands\."" failed:
    Core.Application.MasterDataSync.Commands.SynchronizeMasterDataCommand does reside in Core.Application.MasterDataSync.Commands
    Core.Application.ManagerChain.Commands.CalculateManagerChainCommand does reside in Core.Application.ManagerChain.Commands
    Core.Application.EmployeeSync.Commands.SynchronizeEmployeeCommand does reside in Core.Application.EmployeeSync.Commands
    Core.Application.EmployeeFixedPaymentSummaries.Commands.SynchronizeLatestYearlyPaymentsCommand does reside in Core.Application.EmployeeFixedPaymentSummaries.Commands

相关代码

加载程序集代码

using System.Reflection;
using ArchUnitNET.Domain;
using ArchUnitNET.Loader;
using Assembly = System.Reflection.Assembly;

namespace Corsica.Tests.ArchUnit;

public static class Corsica
{
    public static readonly Architecture ARCHITECTURE =
        new ArchLoader()
            .LoadAssemblies(GetSolutionAssemblies())
            .Build();

    private static Assembly[] GetSolutionAssemblies()
    {
        var assemblies = Directory
            .GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll")
            .Select(x => Assembly.Load(AssemblyName.GetAssemblyName(x)))
            .Where(x => x.FullName!.StartsWith("Core"));
        return assemblies.ToArray();
    }
}

Command类代码

using MediatR;

namespace Core.Application.EmployeeFixedPaymentSummaries.Commands;
public class SynchronizeLatestYearlyPaymentsCommand : IRequest { }

using MediatR;

namespace Core.Application.EmployeeSync.Commands;

public class SynchronizeEmployeeCommand : IRequest { }

using MediatR;

namespace Core.Application.MasterDataSync.Commands;

public class SynchronizeMasterDataCommand : IRequest
{
}

using MediatR;

namespace Core.Application.ManagerChain.Commands;

public class CalculateManagerChainCommand : IRequest { }

测试类代码

using ArchUnitNET.Domain;
using ArchUnitNET.xUnit;
using MediatR;
using static ArchUnitNET.Fluent.ArchRuleDefinition;

namespace Corsica.Tests.ArchUnit;

public class Cqrs
{
    private static readonly IObjectProvider<IType> NON_ABSTRACT_CLASSES_IMPLEMENTING_IREQUEST =
            Classes()
                .That().AreAssignableTo(typeof(IRequest<>)).Or().AreAssignableTo(typeof(IRequest)).As("Classes that implement 'IRequest'")
                .And().AreNotAbstract()
                .And().HaveName(".+Command$", true);

    private static readonly IObjectProvider<IType> COMMANDS =
        Classes().That().Are(NON_ABSTRACT_CLASSES_IMPLEMENTING_IREQUEST)
            .And().HaveName(".+Command$", true);

    private static readonly IObjectProvider<IType> QUERIES =
        Classes().That().Are(NON_ABSTRACT_CLASSES_IMPLEMENTING_IREQUEST)
            .And().HaveName(".+Query$", true);

    [Fact]
    public void ClassesInheritingFromIRequestShouldHaveNameEndingWithCommandOrQuery()
    {
        Classes().That().Are(NON_ABSTRACT_CLASSES_IMPLEMENTING_IREQUEST)
            .Should().HaveName(".+(Command|Query)$", true).As("should have names ending with 'Command' or 'Query'")
            .Check(Corsica.ARCHITECTURE);
    }

    [Fact]
    public void CommandsShouldResideInCommandsNamespace()
    {
        Classes().That().Are(COMMANDS)
            .Should().ResideInNamespace(".+\.Commands\.", true)
            .Check(Corsica.ARCHITECTURE);
    }

    [Fact]
    public void QueriesShouldResideInQueriesNamespace()
    {
        Classes().That().Are(QUERIES)
            .Should().ResideInNamespace(".+\.Queries\.", true)
            .Check(Corsica.ARCHITECTURE);
    }

}
原因分析
  1. 正则表达式匹配错误:规则中使用的命名空间正则.+\.Commands\.要求命名空间必须包含.Commands.(末尾带点),但实际类的命名空间是Core.Application.MasterDataSync.Commands(末尾无点),导致正则不匹配,触发异常。
  2. 重复过滤条件冗余:NON_ABSTRACT_CLASSES_IMPLEMENTING_IREQUEST已经过滤了名称以Command结尾的类,COMMANDS又重复添加了相同过滤条件,导致规则描述出现冗余(错误信息里的重复文字就是这个原因)。
解决方案
  1. 修正命名空间正则:将.+\.Commands\.改为.+\.Commands$,确保匹配以Commands结尾的命名空间:

    [Fact]
    public void CommandsShouldResideInCommandsNamespace()
    {
        Classes().That().Are(COMMANDS)
            .Should().ResideInNamespace(".+\.Commands$", true)
            .Check(Corsica.ARCHITECTURE);
    }
    

    也可以使用更直观的ArchUnit API:ResideInNamespaceContaining("Commands"),不过正则.+\.Commands$的匹配精度更高。

  2. 移除重复过滤条件:优化NON_ABSTRACT_CLASSES_IMPLEMENTING_IREQUEST的定义,去掉提前过滤.+Command$的逻辑,因为后续需要区分Command和Query:

    private static readonly IObjectProvider<IType> NON_ABSTRACT_CLASSES_IMPLEMENTING_IREQUEST =
            Classes()
                .That().AreAssignableTo(typeof(IRequest<>)).Or().AreAssignableTo(typeof(IRequest)).As("Classes that implement 'IRequest'")
                .And().AreNotAbstract();
    

内容的提问来源于stack exchange,提问作者Lombardo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 10:07:03