Mai 2016

Volume 31,numéro 5

Cet article a fait l'objet d'une traduction automatique.

ASP.NET - Écrire un code clair dans ASP.NET Core avec l’injection de dépendance

Par Steve Smith

ASP.NET Core 1.0 est une réécriture complète d'ASP.NET, et un des objectifs principaux de cette nouvelle infrastructure est une conception plus modulaire. Autrement dit, les applications doivent être en mesure d'utiliser uniquement les parties de l'infrastructure que dont ils ont besoin, avec l'infrastructure fournissant des dépendances leur demande. En outre, les développeurs qui créent des applications à l'aide d'ASP.NET Core doivent être peut tirer parti de cette fonctionnalité pour conserver leurs applications faiblement couplées et modulaire. Avec ASP.NET MVC, l'équipe ASP.NET considérablement amélioré la prise en charge de l'infrastructure pour l'écriture de code faiblement couplée, mais ça restait très facile de tomber dans le piège du couplage étroit, en particulier dans les classes de contrôleur.

Couplage étroit

Couplage étroit est parfait pour les logiciels de démonstration. Si vous examinez l'exemple typique d'application montrant comment créer des sites de (versions 3 à 5) ASP.NET MVC, vous trouverez probablement code comme ceci (dans une classe de l'exemple NerdDinner MVC 4 DinnersController) :

private NerdDinnerContext db = new NerdDinnerContext();
private const int PageSize = 25;
public ActionResult Index(int? page)
{
  int pageIndex = page ?? 1;
  var dinners = db.Dinners
    .Where(d => d.EventDate >= DateTime.Now).OrderBy(d => d.EventDate);
  return View(dinners.ToPagedList(pageIndex, PageSize));
}

Ce type de code est très difficile de test unitaire, le NerdDinnerContext est créé dans le cadre de la construction de la classe, et une base de données à laquelle se connecter. Sans surprise, ces applications de démonstration n'incluent souvent que les tests unitaires. Toutefois, votre application peut bénéficier de quelques tests unitaires, même si vous êtes guise pas votre développement, il est donc préférable d'écrire le code afin qu'il pourrait être testé. De plus, ce code enfreint le principe de ne pas répéter vous-même (sec), étant donné que chaque classe de contrôleur qui effectue l'accès aux données comporte le même code pour créer un contexte de base de données Entity Framework (EF). Cela rend futures modifications plus coûteuse et sujette à erreurs, surtout à mesure que l'application augmente au fil du temps.

Lorsque vous examinez le code pour évaluer son couplage, n'oubliez pas la phrase « nouveau est glue ». Autrement dit, n'importe où vous consultez le mot-clé « nouveau », l'instanciation d'une classe, vous devez savoir que vous êtes collant votre implémentation à ce code d'implémentation spécifique. Le principe d'Inversion de dépendance (bit.ly/DI-principe) États : « Abstractions ne doivent pas dépendre des détails ; détails doivent dépendre des abstractions. » Dans cet exemple, les détails de la façon dont le contrôleur rassemble les données à passer à la vue dépendent des détails de l'obtention de ces données, à savoir, Entity Framework.

Outre le nouveau mot clé, « adhésives statique » est une autre source de couplage étroit qui rend plus difficile à tester et à gérer les applications. Dans l'exemple précédent, il existe une dépendance sur l'horloge système de l'ordinateur en cours d'exécution, sous la forme d'un appel à DateTime.Now. Ce couplage rendrait la création d'un ensemble de tests préparés à utiliser dans des tests unitaires difficiles, car leurs propriétés EventDate devra être définie par rapport à la configuration de l'horloge en cours. Cette association peut être supprimée à partir de cette méthode de plusieurs manières, est la plus simple d'entre eux pour vous permettre de toute nouvelle abstraction renvoie l'inquiétude préparés, donc il n'est plus une partie de cette méthode. Je pourrais également apporter à la valeur un paramètre, afin que la méthode peut retourner tous préparés après un paramètre DateTime fourni, plutôt que de toujours à l'aide de DateTime.Now. En dernier lieu, je pourrais créer une abstraction pour l'heure actuelle et faire référence à l'heure actuelle via cette abstraction. Cela peut être une bonne approche si l'application fait référence fréquemment DateTime.Now. (Il est également important de noter que, parce que ces préparés se dérouler sans doute dans des fuseaux horaires différents, le type DateTimeOffset peut être un meilleur choix dans une application réelle).

À vrai dire

Un autre problème avec la maintenabilité du code similaire à celui-ci est qu'il n'est pas honnête avec ses collaborateurs. Évitez d'écrire des classes qui peuvent être instanciées dans des États non valides, car il s'agit des sources fréquentes d'erreurs. Par conséquent, tout ce que votre classe a besoin pour effectuer ses tâches doit être fourni via son constructeur. Le principe de dépendances explicites (bit.ly/ED-principe) déclare, « classes et méthodes doivent spécifier explicitement que tous les objets collaborant dont ils ont besoin pour fonctionner correctement. » La classe DinnersController possède uniquement un constructeur par défaut, ce qui implique qu'il n'est pas nécessaire des collaborateurs pour fonctionner correctement. Mais que se passe-t-il si vous ajoutez ceci au test ? Que ce code faire, si vous l'exécutez à partir d'une application console qui référence le projet MVC ?

var controller = new DinnersController();
var result = controller.Index(1);

La première chose qui échoue dans ce cas est la tentative d'instancier le contexte Entity Framework. Le code lève une exception InvalidOperationException : « Aucune chaîne de connexion nommée « NerdDinnerContext » ne peut être trouvée dans le fichier de configuration d'application. » J'ai été attirer ! Cette classe a besoin de plus de fonctionner que les revendications de constructeur son ! Si la classe a besoin d'un moyen pour accéder aux collections d'instances de dîner, il doit demander via son constructeur (ou en tant que paramètres sur ses méthodes).

Injection de dépendances

L'injection de dépendance (DI) fait référence à la technique du passage des dépendances de méthode ou une de classe paramètres, plutôt que le codage en dur ces relations via des appels de nouveau ou statiques. Il est une technique plus en plus courante dans le développement .NET, en raison de la séparation, qu'il permet aux applications qui l'utilisent. Les versions antérieures d'ASP.NET n'a pas utiliser l'injection de dépendances, et bien qu'ASP.NET MVC et l'API Web en progression de prendre en charge, ni été jusqu'à générer la prise en charge complète, y compris un conteneur pour gérer les dépendances et leur cycle de vie d'objet, dans le produit. Avec la version 1.0 de ASP.NET Core, injection de dépendance n'est pas simplement pris en charge dès le départ, il est largement utilisé par le produit lui-même.

ASP.NET Core prend non seulement en charge DI, il inclut également un conteneur d'injection de dépendance, également appelé un conteneur d'Inversion de contrôle (IoC) ou un conteneur de services. Toutes les applications ASP.NET Core configure ses dépendances à l'aide de ce conteneur dans la méthode ConfigureServices de la classe démarrage. Ce conteneur fournit la prise en charge de base requis, mais elle peut être remplacée par une implémentation personnalisée si vous le souhaitez. De plus, EF Core possède également une prise en charge intégrée pour l'injection de dépendance, afin de configurer une application ASP.NET Core est aussi simple que l'appel d'une méthode d'extension. J'ai créé un spinoff de NerdDinner, appelé GeekDinner, pour cet article. Entity Framework principale est configuré comme indiqué ici :

public void ConfigureServices(IServiceCollection services)
{
  services.AddEntityFramework()
    .AddSqlServer()
    .AddDbContext<GeekDinnerDbContext>(options =>
      options.UseSqlServer(ConnectionString));
  services.AddMvc();
}

Tout cela en place, il est très simple à utiliser l'injection de dépendance pour demander une instance de GeekDinnerDbContext à partir d'une classe de contrôleur comme DinnersController :

public class DinnersController : Controller
{
  private readonly GeekDinnerDbContext _dbContext;
  public DinnersController(GeekDinnerDbContext dbContext)
  {
    _dbContext = dbContext;
  }
  public IActionResult Index()
  {
    return View(_dbContext.Dinners.ToList());
  }
}

Notez qu'il n'est pas une instance unique du nouveau mot clé ; les dépendances que le contrôleur doit est toutes transmis via son constructeur et le conteneur d'injection de dépendances ASP.NET s'occupe de cela pour moi. Bien que je me suis concentré sur l'écriture de l'application, je n'avez pas besoin à vous soucier de la plomberie impliquée dans l'accomplissement de ma demande de classes via leurs constructeurs les dépendances. Bien sûr, si je le souhaite, je puis-je personnaliser ce comportement, même en remplaçant le conteneur par défaut par une autre implémentation entièrement. Étant donné que ma classe de contrôleur suit le principe des dépendances explicites à présent, je sais que pour qu'il de la fonction, que je dois lui fournir une instance d'un GeekDinnerDbContext. Avec un peu de l'installation de la DbContext, instancier le contrôleur de manière isolée, comme le montre cette application Console :

var optionsBuilder = new DbContextOptionsBuilder();
optionsBuilder.UseSqlServer(Startup.ConnectionString);
var dbContext = new GeekDinnerDbContext(optionsBuilder.Options);
var controller = new DinnersController(dbContext);
var result = (ViewResult) controller.Index();

Il est un peu plus de travail impliqués dans la construction d'une base d'EF DbContext à un Entity Framework 6 une, ce qui a simplement une chaîne de connexion. C'est pourquoi, tout comme ASP.NET Core, Entity Framework principale a été conçu pour être plus modulaire. Normalement, vous ne devrez traiter DbContextOptionsBuilder directement, car il est utilisé dans les coulisses lorsque vous configurez Entity Framework via les méthodes d'extension comme AddEntityFramework et AddSqlServer.

Mais vous tester ?

Il est important de tester votre application manuellement, vous souhaitez être en mesure de voir qu'il s'exécute en réalité et génère la sortie attendue pour l'exécuter. Mais cela chaque fois que vous apportez une modification est une perte de temps. Un des principaux avantages des applications faiblement couplées est qu'ils ont tendance à être plus adaptées pour que les applications étroitement couplées de test unitaire. Mieux encore, ASP.NET Core et Entity Framework principales sont beaucoup plus facile de tester que leurs prédécesseurs étaient. Pour commencer, j'écris un test simple directement sur le contrôleur en passant un DbContext qui a été configuré pour utiliser un magasin en mémoire. Je configure le GeekDinnerDbContext à l'aide du paramètre DbContextOptions qu'il expose via son constructeur en tant que partie du code de programme d'installation de mon test :

var optionsBuilder = new DbContextOptionsBuilder<GeekDinnerDbContext>();
optionsBuilder.UseInMemoryDatabase();
_dbContext = new GeekDinnerDbContext(optionsBuilder.Options);
// Add sample data
_dbContext.Dinners.Add(new Dinner() { Title = "Title 1" });
_dbContext.Dinners.Add(new Dinner() { Title = "Title 2" });
_dbContext.Dinners.Add(new Dinner() { Title = "Title 3" });
_dbContext.SaveChanges();

Avec ce type de configuration dans ma classe de test, il est facile d'écrire un test montrant que les données correctes sont retournées dans modèle du ViewResult :

[Fact]
public void ReturnsDinnersInViewModel()
{
  var controller = new OriginalDinnersController(_dbContext);
  var result = controller.Index();
  var viewResult = Assert.IsType<ViewResult>(result);
  var viewModel = Assert.IsType<IEnumerable<Dinner>>(
    viewResult.ViewData.Model).ToList();
  Assert.Equal(1, viewModel.Count(d => d.Title == "Title 1"));
  Assert.Equal(3, viewModel.Count);
}

Bien entendu, il n'est pas beaucoup de logique de test ici encore, pour ce test de test n'est pas réellement tant que ça. Détracteurs prétendent que cela n'est pas un test très précieux, et je pense que les. Toutefois, il est un point de départ pour lorsqu'il y a plus de logique en place, car il sera bientôt. Mais tout d'abord, bien que le cœur d'Entity Framework peut prendre en charge des tests unitaires avec son option en mémoire, allons toujours éviter couplage direct à EF dans mon contrôleur. Il n'existe aucune raison de problèmes de l'interface utilisateur de deux avec l'accès aux données des problèmes d'infrastructure, en fait, elle viole un autre principe, séparation des préoccupations.

Ne dépendent pas ce que vous n'utilisez pas

Le principe de séparation d'Interface (bit.ly/LS-principe) indique que les classes doivent s'appuyer uniquement sur les fonctionnalités qu'ils utilisent. Dans le cas de la nouvelle DinnersController DI compatible, il est toujours en fonction de DbContext entière. Au lieu de coller l'implémentation du contrôleur à Entity Framework, une abstraction qui a fourni la fonctionnalité nécessaire (et peu voire rien plus) peut être utilisée.

Que cette méthode d'action vraiment doit-il pour fonctionner ? DbContext certainement pas ensemble. Il n'a pas besoin même accès à la propriété préparés complet du contexte. Tout dont il a besoin est la possibilité d'afficher les instances de dîner de la page appropriée. L'abstraction plus simple de .NET représentant ce est IEnumerable < dîner >. Par conséquent, je vais définir une interface qui retourne simplement un IEnumerable < dîner >, et qui satisfait (la plupart) la configuration requise de la méthode Index :

public interface IDinnerRepository
{
  IEnumerable<Dinner> List();
}

J'appelle cela un référentiel car il suit ce modèle : Elle résume l'accès aux données derrière une interface semblable à la collection. Si pour une raison quelconque, vous n'aimez pas le nom ou le modèle de référentiel, vous pouvez l'appeler IGetDinners ou IDinnerService ou le nom que vous préférez (mon réviseur technique suggère ICanHasDinner). Quelle que soit la façon dont vous nommez le type, il servira au même objectif.

Tout cela en place, j'ai maintenant ajuster DinnersController pour accepter un IDinnerRepository comme paramètre de constructeur, au lieu d'un GeekDinnerDbContext et appelez la méthode de liste au lieu d'accéder directement à la DbSet préparés :

private readonly IDinnerRepository _dinnerRepository;
public DinnersController(IDinnerRepository dinnerRepository)
{
  _dinnerRepository = dinnerRepository;
}
public IActionResult Index()
{
  return View(_dinnerRepository.List());
}

À ce stade, vous pouvez générer et exécuter votre application Web, mais vous pouvez rencontrer une exception lorsque vous accédez à /Dinners : InvalidOperationException : Impossible de résoudre un service de type « GeekDinner.Core.Interfaces.IdinnerRepository » lors de la tentative d'activation GeekDinner.Controllers.DinnersController. Je n'ai pas encore implémenté l'interface et une fois, je dois également configurer mon implémentation à utiliser lors de l'injection de dépendances effectue les demandes de IDinnerRepository. Implémentation de l'interface est simple :

public class DinnerRepository : IDinnerRepository
{
  private readonly GeekDinnerDbContext _dbContext;
  public DinnerRepository(GeekDinnerDbContext dbContext)
  {
    _dbContext = dbContext;
  }
  public IEnumerable<Dinner> List()
  {
    return _dbContext.Dinners;
  }
}

Notez qu'il est tout à fait suffisante associer une implémentation d'un référentiel à EF directement. Si j'ai besoin d'échanger EF, je vais juste créer une nouvelle implémentation de cette interface. Cette classe d'implémentation est une partie de l'infrastructure de mon application, qui est l'emplacement dans l'application où mes classes dépendent des implémentations spécifiques.

Pour configurer ASP.NET Core pour injecter l'implémentation correcte lorsque des classes demandent un IDinnerRepository, je dois ajouter la ligne de code suivante à la fin de la méthode ConfigureServices présentée précédemment :

services.AddScoped<IDinnerRepository, DinnerRepository>();

Cette instruction indique le conteneur de l'injection de dépendances ASP.NET Core à utiliser une instance DinnerRepository chaque fois que le conteneur est résolution d'un type qui dépend d'une instance IDinnerRepository. Portée signifie qu'une seule instance servira pour chaque handles ASP.NET de demande Web. Les services peuvent également être ajoutés à l'aide des durées de vie temporaires ou Singleton. Dans ce cas, inclus dans l'étendue est appropriée, car mon DinnerRepository dépend d'un DbContext, qui utilise également la durée de vie inclus dans l'étendue. Voici un résumé de la durée de vie des objets disponibles :

  • Temporaire : Une nouvelle instance du type est utilisée chaque fois que le type est demandé.
  • Étendue : Une nouvelle instance du type est créée la première fois qu'il a demandé dans une demande HTTP donnée et réutilisé pour tous les types suivants résolus pendant la demande HTTP.
  • Singleton : Une seule instance du type est créée une seule fois et utilisée par toutes les demandes suivantes pour ce type.

Le conteneur intégré prend en charge plusieurs manières de construire des types que seront proposés. Le cas le plus classique consiste à fournir simplement le conteneur avec un type, et il tente d'instancier ce type fournit toutes les dépendances de type requiert lorsqu'il passe. Vous pouvez également fournir une expression lambda pour construire le type ou, pour une durée de vie Singleton, vous pouvez fournir l'instance entièrement construite dans ConfigureServices lorsque vous l'enregistrez.

Avec l'injection de dépendance associée, l'application s'exécute comme auparavant. Maintenant, en tant que Figure 1 présente, je peux tester il avec cette nouvelle abstraction en place, à l'aide d'une mise en œuvre substitut ou de simulacre de l'interface IDinnerRepository, au lieu de compter sur Entity Framework directement dans mon code de test.

Figure 1 test DinnersController à l'aide d'un objet factice

public class DinnersControllerIndex
{
  private List<Dinner> GetTestDinnerCollection()
  {
    return new List<Dinner>()
    {
      new Dinner() {Title = "Test Dinner 1" },
      new Dinner() {Title = "Test Dinner 2" },
    };
  }
  [Fact]
  public void ReturnsDinnersInViewModel()
  {
    var mockRepository = new Mock<IDinnerRepository>();
    mockRepository.Setup(r =>
      r.List()).Returns(GetTestDinnerCollection());
    var controller = new DinnersController(mockRepository.Object, null);
    var result = controller.Index();
    var viewResult = Assert.IsType<ViewResult>(result);
    var viewModel = Assert.IsType<IEnumerable<Dinner>>(
      viewResult.ViewData.Model).ToList();
    Assert.Equal("Test Dinner 1", viewModel.First().Title);
    Assert.Equal(2, viewModel.Count);
  }
}

Ce test fonctionne, quelle que soit la provenance de la liste des instances de dîner. Vous pouvez réécrire le code d'accès aux données pour utiliser une autre base de données, stockage de Table Azure ou des fichiers XML, et le contrôleur s'exécutera la même. Bien sûr, dans ce cas il ne fait beaucoup, donc vous vous demandez peut-être...

Qu'en est-il logique réelle ?

Jusqu'à présent, je n'ai pas implémenté vraiment de vraie logique métier, il a été simplement les méthodes simples qui retournent des collections de données simples. La valeur réelle des tests est fourni lors de la logique et les cas spéciaux, que vous devez avoir confiance seront comporte comme prévu. Pour illustrer cela, je vais ajouter certaines exigences à mon site GeekDinner. Le site expose une API qui permettre à toute personne RSVP pour un dîner. Toutefois, préparés aura une capacité maximale facultative et RSVP ne doivent pas dépasser cette capacité. Les utilisateurs qui demandent des RSVP au-delà de la capacité maximale doivent être ajoutés à une liste d'attente. Enfin, préparés peuvent spécifier une date d'échéance par lequel les RSVP doivent être reçus par rapport à leur heure de début, après laquelle ils acceptera les RSVP.

Je pourrais code toute cette logique dans une action, mais je pense que beaucoup trop incombe à placer dans une méthode, en particulier une méthode d'interface utilisateur qui doit se concentrer sur les problèmes de l'interface utilisateur, pas une logique métier. Le contrôleur doit vérifier que les entrées qu'il reçoit sont valides, et il doit garantir les réponses qu'il retourne sont appropriés pour le client. Décisions au-delà et en particulier une logique métier, n'appartiennent pas dans les contrôleurs.

Le meilleur endroit pour conserver la logique métier est dans le modèle de domaine de l'application, qui ne doit pas dépendre des problèmes d'infrastructure (comme les bases de données ou des interfaces utilisateur). La classe dîner le mieux adapté pour gérer le protocole RSVP problèmes décrits dans la configuration requise, car elle stockera la capacité maximale pour le dîner et il saura RSVP combien ont déjà été effectuées. Toutefois, la partie de la logique dépend également des cas le protocole RSVP, si elle est au-delà de l'échéance, donc la méthode doit également avoir accès à l'heure actuelle.

Il suffirait d'utiliser DateTime.Now, mais cela rendrait la logique difficile à tester et associerait mon modèle de domaine à l'horloge système. Une autre option consiste à utiliser une abstraction IDateTime et cela injecter dans l'entité dîner. Cependant, d'après mon expérience, qu'il est préférable de conserver des entités telles que Dinner libérer des dépendances, en particulier si vous prévoyez d'utiliser un outil ORM comme Entity Framework pour les extraire d'une couche de persistance. Je ne veux pas obligé de remplir les dépendances de l'entité dans le cadre de ce processus, et EF certainement ne sera pas en mesure de le faire sans code supplémentaire de ma part. Une approche courante consiste à ce stade à extraire la logique de l'entité dîner et le placer dans un type de service (par exemple, DinnerService ou RsvpService) qui peut avoir des dépendances injectés facilement. Cela a tendance à provoquer l'antipattern de modèle de domaine anémique (bit.ly/anémique-modèle), dans les entités qui ont peu ou pas de comportement sont simplement des sacs d'état. Non, dans ce cas la solution est simple, la méthode peut simplement prendre dans l'heure actuelle en tant que paramètre et permettent de passer dans le code appelant.

Avec cette approche, la logique pour l'ajout d'une réponse est simple (voir Figure 2). Cette méthode a un nombre de tests qui montrent qu'il se comporte comme prévu ; les tests sont disponibles dans l'exemple de projet associé à cet article.

Figure 2 une logique métier dans le modèle de domaine

public RsvpResult AddRsvp(string name, string email, DateTime currentDateTime)
{
  if (currentDateTime > RsvpDeadlineDateTime())
  {
    return new RsvpResult("Failed - Past deadline.");
  }
  var rsvp = new Rsvp()
  {
    DateCreated = currentDateTime,
    EmailAddress = email,
    Name = name
  };
  if (MaxAttendees.HasValue)
  {
    if (Rsvps.Count(r => !r.IsWaitlist) >= MaxAttendees.Value)
    {
      rsvp.IsWaitlist = true;
      Rsvps.Add(rsvp);
      return new RsvpResult("Waitlist");
    }
  }
  Rsvps.Add(rsvp);
  return new RsvpResult("Success");
}

En utilisant cette logique du modèle de domaine, je me suis assuré que la méthode d'API de mon contrôleur reste réduite et focalisée sur ses propres difficultés. Par conséquent, il est facile de tester que le contrôleur ne qu'il le devrait, qu'il sont a relativement peu de chemins dans la méthode.

Responsabilités de contrôleur

Partie de la responsabilité du contrôleur consiste à vérifier ModelState et assurez-vous qu'il est valide. Je fais cela dans la méthode d'action par souci de clarté, mais j'éliminerait ce code répétitif dans chaque action à l'aide d'un filtre d'Action dans une application plus importante :

[HttpPost]
public IActionResult AddRsvp([FromBody]RsvpRequest rsvpRequest)
{
  if (!ModelState.IsValid)
  {
    return HttpBadRequest(ModelState);
  }

En supposant que le ModelState est valide, l'action doit ensuite extraire l'instance dîner appropriée à l'aide de l'identificateur fourni dans la demande. Si l'action Impossible de trouver une instance dîner correspondant à cet Id, elle doit retourner un résultat introuvable :

var dinner = _dinnerRepository.GetById(rsvpRequest.DinnerId);
if (dinner == null)
{
  return HttpNotFound("Dinner not found.");
}

Une fois ces vérifications terminées, l'action est disponible déléguer l'opération représentée par la demande pour le modèle de domaine, la méthode AddRsvp sur la classe dîner que vous avez vu précédemment et l'enregistrement de l'état de mise à jour du modèle de domaine (dans ce cas, l'instance dîner et sa collection de RSVP) avant de retourner une réponse OK :

var result = dinner.AddRsvp(rsvpRequest.Name,
    rsvpRequest.Email,
    _systemClock.Now);
  _dinnerRepository.Update(dinner);
  return Ok(result);
}

N'oubliez pas que j'ai décidé de que la classe dîner ne devraient pas avoir une dépendance sur l'horloge système, participent à la place pour que l'heure actuelle est passé dans la méthode. Dans le contrôleur, je passe dans _systemClock.Now pour le paramètre currentDateTime. Il s'agit d'un champ local qui est rempli par l'injection de dépendance, ce qui empêche que le contrôleur est étroitement lié à l'horloge système, trop. Il convient d'utiliser l'injection de dépendances sur le contrôleur, par opposition à une entité de domaine, étant donné que les contrôleurs sont toujours créés par les conteneurs de services ASP.NET ; il effectuera toutes les dépendances que du contrôleur déclare dans son constructeur. _systemClock est un champ de type IDateTime, qui est défini et implémenté dans quelques lignes de code :

public interface IDateTime
{
  DateTime Now { get; }
}
public class MachineClockDateTime : IDateTime
{
  public DateTime Now { get { return System.DateTime.Now; } }
}

Bien sûr, je dois également garantir que le conteneur ASP.NET est configuré pour utiliser MachineClockDateTime chaque fois qu'une classe a besoin d'une instance de IDateTime. Cette opération est effectuée dans ConfigureServices dans la classe de démarrage, et dans ce cas, même si la durée de vie de n'importe quel objet fonctionnent, choisir d'utiliser un Singleton, car une seule instance de MachineClockDateTime fonctionne pour l'ensemble de l'application :

services.AddSingleton<IDateTime, MachineClockDateTime>();

Avec cette abstraction simple en place, je peux tester le comportement du contrôleur en fonction de l'échéance RSVP a passé et vérifiez le résultat correct est retourné. Parce que j'ai déjà des tests autour de la méthode Dinner.AddRsvp qui vérifient qu'il se comporte comme prévu, j'ai besoin ne sont pas très nombreux tests de ce même comportement via le contrôleur pour me donner garantit que, lorsque vous utilisez ensemble, le modèle de contrôleur et le domaine fonctionnent correctement.

Étapes suivantes

Téléchargez le projet exemple associés pour afficher les tests unitaires pour le dîner et DinnersController. N'oubliez pas que code faiblement couplée est généralement plus facile de test unitaire de code étroitement couplé truffée d'appels de méthode « nouveau » ou statique qui dépendent des problèmes d'infrastructure. « Collage est nouveau » et le nouveau mot clé doit être utilisé intentionnellement, pas par erreur, dans votre application. En savoir plus sur ASP.NET Core et sa prise en charge pour l'injection de dépendance à docs.asp.net.


Steve Smithest un formateur indépendant, mentor et consultant, ainsi que MVP ASP.NET. Il a participé à des dizaines d'articles à la documentation officielle ASP.NET (docs.asp.net) et fonctionne avec les équipes de cette technologie. Contactez-le à l'adresse ardalis.com ou le suivre sur Twitter : @ardalis.

Merci à l'experte technique Microsoft suivante d'avoir relu cet article : Doug drapeaux
Doug drapeaux est un développeur qui travaille dans l'équipe MVC chez Microsoft. Il est ainsi pendant un certain temps et adore le nouveau paradigme d'injection de dépendance dans la réécriture de la base de MVC.