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

C#分层架构下层级继承DTO模型无侵入扩展实现方法

问题场景

我使用C#进行开发(编程语言不影响问题本质),核心类库中现有如下服务层公用DTO定义:

public abstract class SuspensionRequestCreatedBaseDto
{
    public int RequestId { get; set; }
    public string DeviceIMEI { get; set; }
    public string DeviceName { get; set; }
}

public class SuspensionRequestCreatedExpirationTimeDto : SuspensionRequestCreatedBaseDto
{
    public DateTime? LastActive { get; set; }
    public DateTime ApprovePlannedDateTime { get; set; }
}

public class SuspensionRequestCreatedReachDataLimitDto : SuspensionRequestCreatedBaseDto
{
    public int DataUsedMb { get; set; }
}

以上DTO归属于核心类库,禁止耦合任何UI层专属逻辑。但在UI项目中,需要为不同类型的暂停申请DTO实现差异化的邮件内容生成逻辑,理想状态下可以直接调用request.PrepareEmailPart(...),无需显式判断DTO具体类型,期望的逻辑结构如下:

public abstract class SuspensionRequestCreatedBaseDto
{
    public int RequestId { get; set; }
    public string DeviceIMEI { get; set; }
    public string DeviceName { get; set; }

    public virtual string PrepareEmailPart(string body)
    {
        // 逻辑1(基础逻辑)
    }
}

public class SuspensionRequestCreatedExpirationTimeDto : SuspensionRequestCreatedBaseDto
{
    public DateTime? LastActive { get; set; }
    public DateTime ApprovePlannedDateTime { get; set; }

    public override string PrepareEmailPart(string body)
    {
        // 逻辑2
    }
}

public class SuspensionRequestCreatedReachDataLimitDto : SuspensionRequestCreatedBaseDto
{
    public int DataUsedMb { get; set; }

    public override string PrepareEmailPart(string body)
    {
        // 逻辑3
    }
}

由于PrepareEmailPart属于UI专属逻辑,不能直接添加到核心类库的DTO定义中。最初尝试通过外层包装Model的方式实现,代码如下:

public class SuspensionRequestCreatedBaseModel
{
    public virtual SuspensionRequestCreatedBaseDto SuspensionRequest { get; set; }

    public virtual string PrepareEmailPart(string body)
    {
         // 基础逻辑
    }
}

public class SuspensionRequestCreatedExpirationTimeModel : SuspensionRequestCreatedBaseModel
{
    public override SuspensionRequestCreatedExpirationTimeDto SuspensionRequest {get; set;}

    public override string PrepareEmailPart(string body)
    {
        // 逻辑2
    }
}

编译时出现如下报错:

'SuspensionRequestCreatedExpirationTimeModel.SuspensionRequest':
type must be 'SuspensionRequestCreatedBaseDto' to match overridden
member 'SuspensionRequestCreatedBaseModel.SuspensionRequest'

需要实现的目标:

  • 避免使用case/类型判断的反模式
  • 不将UI层专属逻辑混入核心类库
  • 调用时可以直接执行对应类型的差异化逻辑,无需手动判断类型

可行实现方案

方案1:泛型包装类(改动最小,适配当前思路)

之前的编译报错是因为C#不支持普通属性重写时修改返回类型(不支持属性协变重写),通过泛型加类型约束可以完美解决这个问题,完全不需要修改核心类库代码:

// 先抽非泛型基类,用于统一处理所有类型的包装实例
public abstract class SuspensionRequestCreatedBaseModel
{
    public abstract string PrepareEmailPart(string body);
}

// 泛型基类,约束泛型参数必须是核心层的基础DTO类型
public abstract class SuspensionRequestCreatedBaseModel<T> : SuspensionRequestCreatedBaseModel 
    where T : SuspensionRequestCreatedBaseDto
{
    public T SuspensionRequest { get; set; }

    public override string PrepareEmailPart(string body)
    {
        // 基础通用逻辑,可直接访问基类公共属性
        return $"申请ID:{SuspensionRequest.RequestId},设备:{SuspensionRequest.DeviceName}\n{body}";
    }
}

// 到期类型的包装实现
public class SuspensionRequestCreatedExpirationTimeModel : SuspensionRequestCreatedBaseModel<SuspensionRequestCreatedExpirationTimeDto>
{
    public override string PrepareEmailPart(string body)
    {
        // 差异化逻辑,可直接访问子类专属属性LastActive、ApprovePlannedDateTime
        var extraInfo = $"计划审批时间:{SuspensionRequest.ApprovePlannedDateTime:yyyy-MM-dd HH:mm}";
        return base.PrepareEmailPart($"{extraInfo}\n{body}");
    }
}

// 流量超限类型的包装实现
public class SuspensionRequestCreatedReachDataLimitModel : SuspensionRequestCreatedBaseModel<SuspensionRequestCreatedReachDataLimitDto>
{
    public override string PrepareEmailPart(string body)
    {
        // 差异化逻辑,可直接访问子类专属属性DataUsedMb
        var extraInfo = $"已使用流量:{SuspensionRequest.DataUsedMb}MB";
        return base.PrepareEmailPart($"{extraInfo}\n{body}");
    }
}

使用时只需要把核心层返回的DTO包装成对应的Model类型,后续统一以SuspensionRequestCreatedBaseModel类型传递,直接调用PrepareEmailPart就会自动执行对应子类的逻辑,完全不需要类型判断。

方案2:访问者模式(适合多逻辑扩展场景)

如果后续除了生成邮件,还有大量针对不同DTO类型的差异化操作(比如生成弹窗内容、导出数据、推送差异化通知),可以用访问者模式,核心类库只需要依赖抽象访问者接口,完全不耦合具体UI逻辑:
首先在核心类库添加抽象访问者定义和基类Accept方法,核心层不引用任何UI层代码:

// 核心层定义抽象访问者接口
public interface ISuspensionRequestVisitor
{
    void Visit(SuspensionRequestCreatedExpirationTimeDto dto);
    void Visit(SuspensionRequestCreatedReachDataLimitDto dto);
}

public abstract class SuspensionRequestCreatedBaseDto
{
    public int RequestId { get; set; }
    public string DeviceIMEI { get; set; }
    public string DeviceName { get; set; }
    // 仅依赖抽象接口,不耦合具体实现
    public abstract void Accept(ISuspensionRequestVisitor visitor);
}

// 两个子类实现Accept方法
public class SuspensionRequestCreatedExpirationTimeDto : SuspensionRequestCreatedBaseDto
{
    public DateTime? LastActive { get; set; }
    public DateTime ApprovePlannedDateTime { get; set; }
    public override void Accept(ISuspensionRequestVisitor visitor) => visitor.Visit(this);
}

public class SuspensionRequestCreatedReachDataLimitDto : SuspensionRequestCreatedBaseDto
{
    public int DataUsedMb { get; set; }
    public override void Accept(ISuspensionRequestVisitor visitor) => visitor.Visit(this);
}

然后在UI层实现具体的邮件生成访问者:

public class EmailPartGenerateVisitor : ISuspensionRequestVisitor
{
    public string EmailContent { get; private set; }
    private readonly string _rawBody;

    public EmailPartGenerateVisitor(string rawBody)
    {
        _rawBody = rawBody;
    }

    public void Visit(SuspensionRequestCreatedExpirationTimeDto dto)
    {
        // 到期类型的邮件生成逻辑
        var extraInfo = $"计划审批时间:{dto.ApprovePlannedDateTime:yyyy-MM-dd HH:mm}";
        EmailContent = $"申请ID:{dto.RequestId},设备:{dto.DeviceName}\n{extraInfo}\n{_rawBody}";
    }

    public void Visit(SuspensionRequestCreatedReachDataLimitDto dto)
    {
        // 流量超限类型的邮件生成逻辑
        var extraInfo = $"已使用流量:{dto.DataUsedMb}MB";
        EmailContent = $"申请ID:{dto.RequestId},设备:{dto.DeviceName}\n{extraInfo}\n{_rawBody}";
    }
}

调用时不需要判断类型,直接执行Accept即可:

var visitor = new EmailPartGenerateVisitor(body);
dto.Accept(visitor);
var emailPart = visitor.EmailContent;

这个方案完全符合开闭原则,后续新增差异化逻辑只需要添加新的访问者实现,不需要修改DTO和现有逻辑代码。


方案选择建议

  • 如果只有少量UI侧差异化逻辑,优先选泛型包装类,实现简单、改动量小
  • 如果后续会持续新增针对不同DTO的差异化操作,选访问者模式,长期可维护性更高

两种方案都不需要写switch/case做类型判断,也不会把UI层逻辑泄露到核心类库。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.31 10:15:38