C#控制台应用程序中正确使用Autofac的方法

23

我刚开始使用Autofac,所以对于这个新手问题请见谅。我阅读了互联网上解释使用Autofac(或像Structuremap、Unity等其他工具)基础知识的所有手册。但是我发现所有的例子都很基础。我需要了解如何在我的代码中更深入地实现Autofac。让我尝试用这个控制台应用程序的例子来解释我需要了解的内容。

class Program
{
    static void Main(string[] args)
    {
        var container = BuildContainer();
        var employeeService = container.Resolve<EmployeeService>();
        Employee employee = new Employee
        {
            EmployeeId = 1,
            FirstName = "Peter",
            LastName = "Parker",
            Designation = "Photographer"
        };

        employeeService.Print(employee);
    }

    static IContainer BuildContainer()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<EmployeeRepository>().As<IEmployeeRepository>();
        builder.RegisterType<EmployeeService>();
        return builder.Build();
    }
}

这很简单易懂。我想要弄清楚的是,当你深入代码时,你如何实现它。在这个例子中,当您执行此行时:

This is simple and easy. What I'm trying to figure out is how do you implement this when you go deeper in the code. In this example, when you execute this line

employeeService.Print(employee);

假设“Print”方法有些复杂,并且需要使用其他依赖项/类来完成其任务。我们仍然在使用Autofac,因此我认为我们需要像上面的示例一样创建这些依赖项。是这样吗?在我的“print”方法内,当我需要使用另一个类时,我必须创建另一个容器,填充它,使用Resolve()等等吗?有更简单的方法吗?一个包含所有所需依赖项的静态类可以在整个解决方案中使用吗?如何实现?希望我表达清楚。也许我无法表达自己的需求。 :( 对不起,我的英语很差。 我还在学习Autofac。


看起来你走在正确的轨道上。你的 EmployeeService 需要一个 IEmployeeRepository,所以你注册了它。如果 EmployeeService 依赖于其他注入到其构造函数中的东西,那么你只需要将其与已有的两个注册一起注册到同一个容器中。当你解析 EmployeeService 时,容器将解析它所需的任何内容,即使这些类具有依赖关系,以及这些类所依赖的类,以此类推,只要这些类型都已注册。这就是它如此强大的原因。 - Scott Hannen
@NightOwl888 我的问题比这还要深入一些。我需要知道当你必须在依赖于Main()的子进程中创建依赖关系时会发生什么。 - Sebastian Garcia
1
你不需要这样做。DI容器会自动处理。你只需使用autofac注册它们,它将一次性解析整个应用程序的完整对象图 - NightOwl888
@SebastianGarcia - 我更新了“重复的”答案,以更清晰地说明它的工作原理。在您的情况下,您将在ApplicationLogic构造函数中接受EmployeeService,并将创建Employee和调用employeeService.Print(employee)移至ApplicationLogic.Run()方法。当然,注入接口而不是具体类型(如下面的答案所指出的)也是一个好建议。 - NightOwl888
@NightOwl888 谢谢。现在你指出的解决方案更清晰了,我能更好地理解它。再次感谢! - Sebastian Garcia
4个回答

63

静态是问题的关键

控制台程序的主要问题在于主Program类通常是静态的。这对于单元测试不利,对于IoC也不利;例如,静态类永远不会被构造,因此没有机会进行构造函数注入。结果就是,在主代码库中您最终使用new或从IoC容器中提取实例,这违反了该模式(在那一点上更像是服务定位器模式)。我们可以通过返回将代码放入实例方法的做法来摆脱这个混乱情况,这意味着我们需要某个东西的对象实例。但又是什么呢?

两类模式

我在编写控制台应用程序时遵循一个特定的轻量级模式。您可以遵循这个模式,因为它对我而言非常有效。

该模式涉及两个类:

  1. 原始的Program类,它是静态的、非常简短并且未包含在代码覆盖范围内。该类作为操作系统调用到应用程序本身的“传递”,
  2. 实例化的Application类,它是完全注入和可单元测试的。这就是您真正代码的所在。

Program Class

操作系统需要一个静态的Main入口点。Program类只存在于满足此要求。

保持您的静态程序非常简洁;它应该包含(1)组合根和(2)简单的“传递”入口点,该入口点调用真正的应用程序(如我们将会看到的那样是实例化的)。

Program中的所有代码都不值得单元测试,因为它所做的只是组合对象图(在进行测试时会有所不同)并调用应用程序的主入口点。通过隔离不可单元测试的代码,您现在可以从代码覆盖范围中排除整个类(使用ExcludeFromCodeCoverageAttribute)。

以下是一个示例:

[ExcludeFromCodeCoverage]
static class Program
{
    private static IContainer CompositionRoot()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<Application>();
        builder.RegisterType<EmployeeService>().As<IEmployeeService>();
        builder.RegisterType<PrintService>().As<IPrintService>();
        return builder.Build();
    }

    public static void Main()  //Main entry point
    {
        CompositionRoot().Resolve<Application>().Run();
    }
}

正如您所看到的,非常简单。

应用程序类

现在来实现您的 Application 类,就像它是唯一的程序一样。但是现在,因为它已经实例化了,您可以按照通常的模式注入依赖项。

class Application
{
    protected readonly IEmployeeService _employeeService;
    protected readonly IPrintService _printService;

    public Application(IEmployeeService employeeService, IPrintService printService)
    {
        _employeeService = employeeService; //Injected
        _printService = printService; //Injected
    }

    public void Run()
    {
        var employee = _employeeService.GetEmployee();
        _printService.Print(employee);
    }
}

这种方法保持了关注点的分离,避免了过多的静态“内容”,并且让您能够在不过多麻烦的情况下遵循IoC模式。请注意-我的代码示例除了实例化ContainerBuilder之外没有包含任何一个new关键字。

如果依赖关系自己有依赖关系怎么办?

因为我们遵循这种模式,如果 PrintServiceEmployeeService 有自己的依赖关系,容器现在将负责处理所有这些事情。只要在组合根中将它们注册到适当的接口下面,您就无需实例化或编写任何代码来注入这些服务。

class EmployeeService : IEmployeeService
{
    protected readonly IPrintService _printService;

    public EmployeeService(IPrintService printService)
    {
        _printService = printService; //injected
    }

    public void Print(Employee employee)
    {
        _printService.Print(employee.ToString());
    }
}

通过这种方式,容器会处理一切,您不需要编写任何代码,只需注册您的类型和接口即可。


2
谢谢。这向我展示了我该如何做。再次感谢您的时间和出色的解释。 - Sebastian Garcia
@SebastianGarcia,看起来你需要通过点击勾选来标记答案为正确。谢谢John。 - Alexcei Shmakov
1
这个例子非常有帮助,应该放在Autofac代码示例中,这些示例似乎更偏向于较新版本的.Net。 - undefined

1
您可以通过构造函数注入依赖项(Autofac还支持属性和方法注入)。
通常,在完成依赖项注册后,不应在类内部使用容器,因为这会使您的类与容器耦合。但有些情况下,您可能希望使用子容器(内部作用域),在其中可以定义一个特定的类来实现这一点,并使您的代码独立于容器。
在您的示例中,您只需要解析IEmployeeService,所有它的依赖项都将由容器自动解析。
以下是一个示例,演示如何实现此目的:
using Autofac;
using System;
using System.Collections.Generic;
using System.Linq;

namespace AutofacExample
{
    public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public interface IEmployeeRepository
    {
        Employee FindById(int id);
    }

    public interface IEmployeeService
    {
        void Print(int employeeId);
    }

    public class EmployeeRepository : IEmployeeRepository
    {
        private readonly List<Employee> _data = new List<Employee>()
        {
            new Employee { Id = 1, Name = "Employee 1"},
            new Employee { Id = 2, Name = "Employee 2"},
        };
        public Employee FindById(int id)
        {
            return _data.SingleOrDefault(e => e.Id == id);
        }
    }

    public class EmployeeService : IEmployeeService
    {
        private readonly IEmployeeRepository _repository;
        public EmployeeService(IEmployeeRepository repository)
        {
            _repository = repository;
        }
        public void Print(int employeeId)
        {
            var employee = _repository.FindById(employeeId);
            if (employee != null)
            {
                Console.WriteLine($"Id:{employee.Id}, Name:{employee.Name}");
            }
            else
            {
                Console.WriteLine($"Employee with Id:{employeeId} not found.");
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var container = BuildContainer();
            var employeeSerive = container.Resolve<IEmployeeService>();
            employeeSerive.Print(1);
            employeeSerive.Print(2);
            employeeSerive.Print(3);
            Console.ReadLine();
        }

        static IContainer BuildContainer()
        {
            var builder = new ContainerBuilder();
            builder.RegisterType<EmployeeRepository>()
                   .As<IEmployeeRepository>()
                   .InstancePerDependency();
            builder.RegisterType<EmployeeService>()
                   .As<IEmployeeService>()
                   .InstancePerDependency();
            return builder.Build();
        }
    }
}

0

这个想法是在启动时注册所有依赖项,然后稍后解决它们。你看起来已经接近了,只需要做一些改变:

class Program
{
    // Declare your container as a static variable so it can be referenced later
    static IContainer Container { get; set; }

    static void Main(string[] args)
    {
        // Assign the container to the static IContainer
        Container = BuildContainer();
        var employeeService = container.Resolve<EmployeeService>();
        Employee employee = new Employee
        {
            EmployeeId = 1,
            FirstName = "Peter",
            LastName = "Parker",
            Designation = "Photographer"
        };

        employeeService.Print(employee);
    }

    static IContainer BuildContainer()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<EmployeeRepository>().As<IEmployeeRepository>();
        builder.RegisterType<EmployeeService>();
        return builder.Build();
    }
}

然后您可以稍后解决它,例如在employeeService.Print()函数中:

public void Print(Employee employee)
{
        // Create the scope, resolve your EmployeeRepository,
        // use it, then dispose of the scope.
        using (var scope = Container.BeginLifetimeScope())
        {
            var repository = scope.Resolve<IEmployeeRepository>();
            repository.Update(employee);
        }
}

这是对官方入门指南中的代码进行轻微调整(以适应您的代码)


0
假设您有一个名为EmployeeService的类,它需要另一个类才能进行打印:
public class EmployeeService 
{
    private readonly IEmployeeRepository _employeeRepository;
    private readonly IEmployeePrinter _printer;

    public EmployeeService(IEmployeeRepository employeeRepository, 
        IEmployeePrinter printer)
    {
        _employeeRepository = employeeRepository;
        _printer = printer;
    }
    public void PrintEmployee(Employee employee)
    {
        _printer.PrintEmployee(employee);
    }
}

然后你有一个IEmployeePrinter的实现,它仍然有更多的依赖关系:

public class EmployeePrinter : IEmployeePrinter
{
    private readonly IEmployeePrintFormatter _printFormatter;

    public EmployeePrinter(IEmployeePrintFormatter printFormatter)
    {
        _printFormatter = printFormatter;
    }

    public void PrintEmployee(Employee employee)
    {
        throw new NotImplementedException();
    }
}

你不需要更多的容器。你所要做的就是将每种类型注册到一个容器中,基本上就像你已经做的那样:

static IContainer BuildContainer()
{
    var builder = new ContainerBuilder();
    builder.RegisterType<EmployeeRepository>().As<IEmployeeRepository>();
    builder.RegisterType<EmployeePrinter>().As<IEmployeePrinter>();
    builder.RegisterType<SomeEmployeeFormatter>().As<IEmployeePrintFormatter>();
    builder.RegisterType<EmployeeService>();
    return builder.Build();
}

当您调用Resolve<EmployeeService>()时,它会发现它需要一个IEmployeeRepository和一个IEmployeePrinter。因此,在幕后它将调用Resolve<IEmployeeRepository>()Resolve<IEmployeePrinter>()。然后它发现EmployeePrinter需要一个IEmployeePrintFormatter,所以也解决了这个问题。
只要您已经注册了需要解决的所有内容,它就可以正常工作。这很棒,因为它允许您不断将开发分解为易于测试的较小类。这将导致一堆嵌套的类,如果您必须像这样创建它们,那么将非常难以使用:
var service = new EmployeeService(
    new EmployeeRespository("connectionString"),
    new EmployeePrinter(new SomeEmployeeformatter()));

但是容器使得你不必担心创建所有这些类,即使它们嵌套多层。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接