Je suis nouveau avec Autofac, donc mes excuses pour la question noob. J'ai lu tous les manuels sur Internet expliquant les bases lors de l'utilisation d'Autofac (ou de tout autre outil comme Structuremap, Unity, etc.). Mais tous les exemples que j'ai trouvés sont des bases. J'ai besoin de savoir comment implémenter Autofac plus profondément dans mon code. Permettez-moi d'essayer d'expliquer ce que je dois savoir avec cet exemple, une application console.
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();
}
}
C'est simple et facile. Ce que j'essaie de comprendre, c'est comment implémenter cela lorsque vous approfondissez le code. Dans cet exemple, lorsque vous exécutez cette ligne
employeeService.Print(employee);
Supposons que la méthode "Print" soit un peu complexe et nécessite d'utiliser d'autres dépendances/classes pour accomplir sa tâche. Nous utilisons toujours Autofac, donc je suppose que nous devons faire quelque chose comme l'exemple ci-dessus pour créer ces dépendances. Est-ce exact? Dans ma méthode "print", quand j'ai besoin d'utiliser une autre classe, je dois créer un autre conteneur, le remplir, l'utiliser avec Resolve () et ainsi de suite? Il existe un moyen plus simple de le faire? Une classe statique avec toutes les dépendances nécessaires peut être consommée dans toute la solution? Comment? J'espère être clair. Peut-être que je ne peux pas non plus exprimer ce dont j'ai besoin. :( Désolé pour mon mauvais anglais. Je l'apprends toujours pendant que j'apprends Autofac.
Le principal problème avec un programme de console est que la classe principale Program
est principalement statique. Ce n'est pas bon pour les tests unitaires et ce n'est pas bon pour l'IoC; une classe statique n'est jamais construite, par exemple, il n'y a donc aucune chance d'injection de constructeur. En conséquence, vous finissez par utiliser new
dans la base de code principale, ou extrayez des instances du conteneur IoC, ce qui constitue une violation du modèle (il s'agit davantage d'un modèle de localisateur de service à ce moment). Nous pouvons sortir de ce gâchis en revenant à la pratique de mettre notre code dans les méthodes d'instance, ce qui signifie que nous avons besoin d'une instance d'objet de quelque chose. Mais quel truc?
Je suis un modèle particulier et léger lors de l'écriture d'une application console. Vous êtes invités à suivre ce modèle qui fonctionne assez bien pour moi.
Le modèle implique deux classes:
Program
d'origine, qui est statique, très brève et exclue de la couverture du code. Cette classe agit comme un "passage" de l'invocation O/S à l'invocation de l'application proprement dite.Application
instanciée, qui est entièrement injectée et testable unitaire. C'est là que devrait vivre votre vrai code.Le O/S nécessite un point d'entrée Main
, et il doit être statique. La classe Program
existe uniquement pour répondre à cette exigence.
Gardez votre programme statique très propre; il doit contenir (1) la racine de la composition et (2) un simple point d'entrée "pass-through" qui appelle la vraie application (qui est instanciée, comme nous le verrons).
Aucun du code dans Program
n'est digne d'un test unitaire, car il ne fait que composer le graphe d'objet (qui serait différent quand il est testé de toute façon) et appeler le point d'entrée principal de l'application. Et en séquestrant le code non testable par unité, vous pouvez désormais exclure la classe entière de la couverture de code (en utilisant ExcludeFromCodeCoverageAttribute ).
Voici un exemple:
[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();
}
}
Comme vous pouvez le voir, extrêmement simple.
Maintenant, implémentez votre classe Application
comme s'il s'agissait du programme Un et Seul. Seulement maintenant, parce qu'il est instancié, vous pouvez injecter des dépendances selon le modèle habituel.
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);
}
}
Cette approche maintient la séparation des préoccupations, évite trop de "trucs" statiques et vous permet de suivre le modèle IoC sans trop de peine. Et vous remarquerez que mon exemple de code ne contient pas une seule instance du mot clé new
, sauf pour instancier un ContainerBuilder.
Parce que nous suivons ce modèle, si PrintService
ou EmployeeService
ont leurs propres dépendances, le conteneur va maintenant s'occuper de tout. Vous n'avez pas besoin d'instancier ou d'écrire du code pour obtenir ces services injectés, tant que vous les enregistrez sous l'interface appropriée dans la racine de composition.
class EmployeeService : IEmployeeService
{
protected readonly IPrintService _printService;
public EmployeeService(IPrintService printService)
{
_printService = printService; //injected
}
public void Print(Employee employee)
{
_printService.Print(employee.ToString());
}
}
De cette façon, le conteneur s'occupe de tout et vous n'avez pas à écrire de code, enregistrez simplement vos types et vos interfaces.
Vous pouvez utiliser les dépendances d'injection via le constructeur (Autofac prend également en charge l'injection de propriétés et de méthodes).
Habituellement, lorsque l'enregistrement des dépendances est terminé, vous ne devez pas utiliser de conteneur à l'intérieur des classes, car cela rend votre classe couplée au conteneur, il peut y avoir des cas où vous souhaitez utiliser un conteneur enfant (portée interne) dans lequel vous pouvez définir une classe spécifique qui fait cela et rend votre code indépendant du conteneur.
Dans votre exemple, il vous suffit de résoudre IEmployeeService et toutes ses dépendances seront résolues automatiquement par conteneur.
Voici un exemple pour montrer comment vous pouvez y parvenir:
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();
}
}
}
Supposons que vous ayez votre classe EmployeeService
et qu'elle ait besoin d'une autre classe pour pouvoir imprimer:
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);
}
}
Et puis vous avez une implémentation de IEmployeePrinter
, et elle a encore plus de dépendances:
public class EmployeePrinter : IEmployeePrinter
{
private readonly IEmployeePrintFormatter _printFormatter;
public EmployeePrinter(IEmployeePrintFormatter printFormatter)
{
_printFormatter = printFormatter;
}
public void PrintEmployee(Employee employee)
{
throw new NotImplementedException();
}
}
Vous n'avez pas besoin de plus de conteneurs. Tout ce que vous avez à faire est d'enregistrer chaque type avec le même conteneur, à peu près comme vous l'avez fait:
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();
}
Lorsque vous appelez Resolve<EmployeeService>()
il verra qu'il a besoin d'un IEmployeeRepository
et d'un IEmployeePrinter
. Donc, dans les coulisses, il appellera Resolve<IEmployeeRepository>()
et Resolve<IEmployeePrinter>()
. Ensuite, il voit que EmployeePrinter
nécessite un IEmployeePrintFormatter
, donc il résout cela aussi.
Cela fonctionne tant que vous avez enregistré tout ce qui doit être résolu. C'est génial car cela vous permet de décomposer continuellement votre développement en classes plus petites et faciles à tester. Cela se traduira par un tas de classes imbriquées avec lesquelles il serait très difficile de travailler si vous deviez les créer comme ceci:
var service = new EmployeeService(
new EmployeeRespository("connectionString"),
new EmployeePrinter(new SomeEmployeeformatter()));
Mais le conteneur fait en sorte que vous n'ayez pas à vous soucier de créer toutes ces classes, même si elles sont imbriquées à plusieurs niveaux.
L'idée est que vous enregistrez toutes vos dépendances au démarrage et que vous puissiez ensuite les résoudre plus tard. On dirait que vous y êtes presque, juste quelques changements:
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();
}
}
Ensuite, vous pouvez le résoudre plus tard, par exemple. dans la fonction 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);
}
}
Ceci est une légère adaptation du code (pour s'adapter à votre code) du guide de démarrage officiel