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

基于Crystal Reports与DevExpress的报表生成工厂类放置位置咨询

解决工厂模式下避免不必要引用的问题

你的核心问题是:在主应用仅引用包含IReport和IReportFactory的Default项目的前提下,让工厂能够创建Crystal Reports和DevExpress的具体报表实例,同时不让主应用间接依赖这两个报表库。下面是两种最实用的解决方案:


方案一:利用依赖注入(DI)解耦

这种方式适合使用DI容器的项目(比如ASP.NET Core、WPF等),核心思路是把具体报表类的注册逻辑从Default项目移到启动层,让工厂通过DI容器获取实例,而不是自己直接创建。

步骤1:修改Default项目中的ReportFactory

让工厂依赖DI容器和一个扩展名到报表类型的映射字典,去掉硬编码的创建逻辑:

public sealed class ReportFactory : IReportFactory {
    private readonly IServiceProvider _serviceProvider;
    private readonly Dictionary<string, Type> _reportTypeMap;

    public ReportFactory(IServiceProvider serviceProvider, Dictionary<string, Type> reportTypeMap) {
        _serviceProvider = serviceProvider;
        _reportTypeMap = reportTypeMap ?? throw new ArgumentNullException(nameof(reportTypeMap));
    }

    public IReport Get(string extension) {
        var lowerExt = extension.ToLower();
        if (!_reportTypeMap.TryGetValue(lowerExt, out var reportType)) {
            throw new ArgumentException($"不支持的报表扩展名:{extension}", nameof(extension));
        }

        var report = _serviceProvider.GetService(reportType) as IReport;
        if (report == null) {
            throw new InvalidOperationException($"无法创建类型为{reportType.Name}的报表实例");
        }
        return report;
    }
}

步骤2:在启动项目中注册具体实现

主应用(或专门的启动配置项目)直接引用两个报表类库,然后在DI容器中注册报表实例和工厂:

// 以ASP.NET Core为例,在Program.cs中注册
var builder = WebApplication.CreateBuilder(args);

// 注册具体报表类
builder.Services.AddScoped<IReport, CrystalReportImpl>();
builder.Services.AddScoped<IReport, DevExpressReportImpl>();

// 注册工厂,并传入扩展名映射
builder.Services.AddSingleton<IReportFactory>(sp => new ReportFactory(
    sp,
    new Dictionary<string, Type> {
        { "rpt", typeof(CrystalReportImpl) },
        { "rpx", typeof(DevExpressReportImpl) }
    }
));

这样一来,Default项目完全不依赖具体报表库,主应用虽然引用了报表库,但这是启动配置的必要操作,不会污染业务逻辑层的依赖。


方案二:插件模式(动态加载)

如果希望主应用也完全不引用两个报表库,可以用反射动态加载报表类库,把具体报表做成独立的插件dll。

步骤1:定义扩展属性(用于标记报表对应的扩展名)

在Default项目中添加一个自定义属性,用来标记具体报表类对应的扩展名:

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class ReportExtensionAttribute : Attribute {
    public string Extension { get; }

    public ReportExtensionAttribute(string extension) {
        Extension = extension.ToLower();
    }
}

步骤2:修改Default项目中的ReportFactory

让工厂在初始化时扫描指定目录下的插件dll,通过反射加载实现了IReport的类型:

public sealed class ReportFactory : IReportFactory {
    private readonly Dictionary<string, Type> _reportTypeCache = new();

    public ReportFactory(string pluginDirectory) {
        if (!Directory.Exists(pluginDirectory)) {
            throw new DirectoryNotFoundException($"插件目录不存在:{pluginDirectory}");
        }
        LoadPluginReports(pluginDirectory);
    }

    private void LoadPluginReports(string pluginDirectory) {
        foreach (var dllPath in Directory.GetFiles(pluginDirectory, "*.dll")) {
            try {
                var assembly = Assembly.LoadFrom(dllPath);
                // 扫描所有实现IReport的非抽象类
                foreach (var type in assembly.GetTypes()
                    .Where(t => typeof(IReport).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract)) {
                    // 获取标记的扩展名
                    var extAttr = type.GetCustomAttribute<ReportExtensionAttribute>();
                    if (extAttr == null) continue;

                    if (!_reportTypeCache.ContainsKey(extAttr.Extension)) {
                        _reportTypeCache.Add(extAttr.Extension, type);
                    }
                }
            }
            catch (Exception ex) {
                // 处理加载异常,比如日志记录
                Console.WriteLine($"加载插件失败:{dllPath},错误:{ex.Message}");
            }
        }
    }

    public IReport Get(string extension) {
        var lowerExt = extension.ToLower();
        if (!_reportTypeCache.TryGetValue(lowerExt, out var reportType)) {
            throw new ArgumentException($"不支持的报表扩展名:{extension}", nameof(extension));
        }

        // 创建实例(如果需要依赖注入,可以用ActivatorUtilities.CreateInstance)
        return (IReport)Activator.CreateInstance(reportType);
    }
}

步骤3:在具体报表类库中标记属性

在Crystal Reports和DevExpress的报表实现类上添加属性:

// Crystal报表类库中
[ReportExtension("rpt")]
public class CrystalReportImpl : IReport {
    // 实现IReport接口的方法
}

// DevExpress报表类库中
[ReportExtension("rpx")]
public class DevExpressReportImpl : IReport {
    // 实现IReport接口的方法
}

步骤4:主应用配置插件目录

主应用只需要把两个报表类库的dll放到指定的插件目录,然后初始化工厂:

// 主应用中初始化工厂
var pluginDir = Path.Combine(AppContext.BaseDirectory, "ReportPlugins");
var reportFactory = new ReportFactory(pluginDir);

// 使用工厂创建报表
var crystalReport = reportFactory.Get("rpt");

这种方式下,主应用和Default项目都不需要引用具体的报表库,完全实现了解耦,还能动态添加新的报表类型。


选择建议

  • 如果你的项目已经在使用DI容器,优先选方案一,实现简单且易于维护。
  • 如果需要动态扩展报表类型,或者完全隔离主应用与报表库的依赖,选方案二。

内容的提问来源于stack exchange,提问作者Ivan-Mark Debono

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 08:40:19