Maggio 2016

Volume 31 Numero 5

Il presente articolo è stato tradotto automaticamente.

ASP.NET - Scrittura di codice pulito in ASP.NET Core con inserimento delle dipendenze

Da Smith Steve

Componenti di base di ASP.NET 1.0 è una riscrittura completa di ASP.NET e uno degli obiettivi principali di questo nuovo framework è una progettazione più modulare. Vale a dire applicazioni devono essere in grado di utilizzare solo le parti del framework che hanno bisogno, con il framework di fornire le dipendenze sono state richieste. Inoltre, gli sviluppatori di creare applicazioni con ASP.NET di base devono essere in grado di sfruttare la stessa funzionalità per mantenere le proprie applicazioni loosely coupled modulare. Con ASP.NET MVC, il team di ASP.NET è notevolmente migliorato il supporto del framework per la scrittura di codice ad accoppiamento debole, ma era comunque molto facile cadere in trappola di accoppiamento stretto, soprattutto nelle classi controller.

Accoppiamento stretto

L'accoppiamento stretto va bene per software demo. Se si esamina l'applicazione di esempio che illustrano come creare siti (versioni 3-5) ASP.NET MVC, si noterà probabilmente codice simile al seguente (dalla classe di DinnersController NerdDinner MVC 4 dell'esempio):

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));
}

Questo tipo di codice è molto difficile lo unit test, perché il NerdDinnerContext viene creato come parte della costruzione della classe e richiede un database a cui connettersi. Non sorprende, tali applicazioni demo spesso non includono tutti gli unit test. Tuttavia, l'applicazione può risultare vantaggioso alcuni unit test, anche se si sta test di sviluppo, non sarebbe preferibile scrivere il codice di conseguenza, è possibile testarlo. Inoltre, il codice viola il principio non ripetere manualmente (sorgente), poiché ogni classe controller che esegue qualsiasi accesso ai dati ha lo stesso codice, creare un contesto di database di Entity Framework (EF). In questo modo le modifiche future più costosa e soggetta a errori, in particolare quando l'applicazione aumenta nel tempo.

Nel codice per valutare il tipo di controllo, tenere presente la frase "nuovo glue." Vale a dire, ovunque si vedere la parola chiave "new" Creazione di una classe, realizzare che si sta associando l'implementazione per il codice di implementazione specifica. Il principio di inversione delle dipendenze (DI/bit.ly-principio) degli stati: "Astrazioni non devono dipendere dettagli; dettagli devono dipendere da astrazioni." In questo esempio, i dettagli di come il controller raccoglie i dati da passare alla visualizzazione dipendono dai dettagli di come recuperare i dati, vale a dire, Entity Framework.

Oltre la nuova parola chiave "adesive statico" è un'altra origine di accoppiamento stretto che rende più difficile da testare e gestire le applicazioni. Nell'esempio precedente, esiste una dipendenza su orologio di sistema del computer di esecuzione, sotto forma di una chiamata a DateTime. Now. Questo accoppiamento renderebbe creando un set di test Dinners utilizzare alcuni unit test difficile, poiché le relative proprietà EventDate sarebbe necessario impostare relativi all'impostazione corrente dell'orologio. Questo accoppiamento potrebbe essere rimosso da questo metodo in diversi modi, è più semplice di cui consentire qualsiasi nuova astrazione restituisce le preoccupazioni legate Dinners su di esso, pertanto non è più una parte di questo metodo. In alternativa, avrei potuto apportare il valore di un parametro, pertanto il metodo può restituire tutti Dinners dopo un parametro DateTime fornito, anziché utilizzare sempre Now. Infine, potrei creare un'astrazione per l'ora corrente e fare riferimento all'ora corrente tramite tale astrazione. Può trattarsi di un buon approccio se l'applicazione fa riferimento a DateTime. Now frequentemente. (È anche importante notare che poiché questi dinners presumibilmente avviene in fusi orari diversi, il tipo DateTimeOffset potrebbe essere una scelta migliore in un'applicazione reale).

Essere onesti

Un altro problema con la gestibilità del codice simile al seguente è che non sia accurato con relativi collaboratori. È necessario evitare di scrivere classi che è possibile creare istanze in stati non validi, come queste sono frequenti origini degli errori. Pertanto, qualsiasi che classe necessaria per eseguire le attività devono essere fornite tramite il relativo costruttore. Il principio di dipendenze esplicite (ED/bit.ly-principio) indica, "classi e metodi in modo esplicito richiede tutti gli oggetti in collaborazione hanno bisogno per funzionare correttamente." La classe DinnersController è solo un costruttore predefinito, che implica che non necessario qualsiasi collaboratori per il corretto funzionamento. Ma cosa succede se si uniscono al test? Cosa verrà questo codice?, se si esegue da una nuova applicazione console che fa riferimento al progetto MVC

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

La prima cosa che si verifica un errore in questo caso è il tentativo di creare un'istanza di contesto di Entity Framework. Il codice genera un'eccezione InvalidOperationException: "Non Impossibile trovare alcuna stringa di connessione denominata 'NerdDinnerContext' nel file di configurazione dell'applicazione". Sono stato deceived! Questa classe richiede ulteriori funzioni rispetto a quali le attestazioni di costruttore! Se la classe deve disporre di un modo per accedere alle raccolte di istanze di Dinner, è necessario richiedere che tramite il relativo costruttore (o, in alternativa, come parametri nei propri metodi).

Inserimento delle dipendenze

Inserimento delle dipendenze (DI) si riferisce alla tecnica di passando le dipendenze di un metodo o della classe come parametri, anziché a livello di codice queste relazioni tramite chiamate nuove o statiche. A causa la separazione che consente alle applicazioni che utilizzano il è una tecnica sempre più comune nello sviluppo .NET. Le versioni precedenti di ASP.NET non si avvalgono di inserimento delle dipendenze e nonostante l'avanzamento il supporto ASP.NET MVC e API Web, né sono andato finora la creazione di un supporto completo, incluso un contenitore per la gestione di vita, oggetto e le dipendenze nel prodotto. Con ASP.NET 1.0 Core, inserimento di dipendenze non è supportato solo predefiniti, verrà utilizzato ampiamente dal prodotto stesso.

ASP.NET Core supporta non solo DI, include anche un contenitore, detta anche un contenitore Inversion of Control (IoC) o un contenitore di servizi. Tutte le applicazioni ASP.NET di base consente di configurare le relative dipendenze utilizzando questo contenitore nel metodo ConfigureServices della classe di avvio. Questo contenitore fornisce il supporto di base necessario, ma può essere sostituito con un'implementazione personalizzata se si desidera. In più, Entity Framework Core offre inoltre supporto incorporato per l'inserimento di dipendenze, in modo che la configurazione all'interno di un'applicazione ASP.NET di base è semplice come richiamare un metodo di estensione. Ho creato una spinoff di NerdDinner, denominato GeekDinner, per questo articolo. Entity Framework Core è configurato come illustrato di seguito:

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

A questo punto, è piuttosto semplice da utilizzare DI per richiedere un'istanza di GeekDinnerDbContext da una classe controller come DinnersController:

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

Si noti che non è disponibile una singola istanza la nuova parola chiave. le dipendenze delle esigenze di controller sono tutti passate tramite il costruttore e il contenitore DI ASP.NET si occupa di questo oggetto per l'utente corrente. Sono concentrati sulla scrittura di applicazioni, non è necessario preoccuparsi di plumbing per soddisfare la richiesta di classi tramite i relativi costruttori le dipendenze. Naturalmente, se si desidera, è possibile personalizzare questo comportamento, anche sostituire completamente il contenitore predefinito con un'altra implementazione. Poiché la classe controller ora segue il principio di dipendenze esplicite, so che per tale funzione che è necessario fornire un'istanza di un GeekDinnerDbContext. È possibile, con un bit del programma di installazione per la classe DbContext, creare l'istanza il controller in isolamento, come illustrato di seguito questa applicazione 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();

È un po' più lavoro coinvolti nella costruzione di un Core EF DbContext rispetto a un Entity Framework 6 uno che eseguisse una stringa di connessione. Questo avviene perché, come ASP.NET di base, Entity Framework Core è stato progettato per essere più modulare. In genere, non è necessario gestire i DbContextOptionsBuilder direttamente, poiché viene utilizzato in background quando si configura EF tramite metodi di estensione come AddEntityFramework e AddSqlServer.

Ma è possibile testare il?

Test dell'applicazione manualmente è importante, si desidera essere in grado di eseguirlo per vedere che viene effettivamente eseguito e produce l'output previsto. Ma a tale scopo ogni volta che si apporta una modifica è uno spreco di tempo. Uno dei principali vantaggi di applicazioni loosely coupled è che tendono a essere più resistente agli unit test rispetto alle App consequenziale. Meglio ancora, ASP.NET di base ed EF Core sono entrambi molto più semplice eseguire il test rispetto alle precedenti. Per iniziare, scriverò un semplice test direttamente in base al controller tramite il passaggio di un elemento DbContext che è stato configurato per utilizzare un archivio in memoria. Configurerà il GeekDinnerDbContext utilizzando il parametro DbContextOptions che espone tramite il relativo costruttore come parte del codice di programma di installazione del 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();

Con questa configurazione nella mia classe di test, è facile scrivere un test che mostra che i dati corretti viene restituiti nel modello dell'elemento 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);
}

Naturalmente, non c'è molta logica per verificare, in modo che la maggior parte non è realmente test di questo test. Critics verrà sostengono che questo non è un test molto importanti e accetto con essi. Tuttavia, è un punto di partenza per quando è più logica sul posto, non appena sarà presente. Ma in primo luogo, anche se EF Core può supportare unit test con l'opzione in memoria, sarà comunque evitare la correlazione diretta per Entity Framework nel controller. Non esiste alcun motivo per problemi dell'interfaccia utente di alcuni problemi di infrastruttura di accesso ai dati, infatti, viola un principio alternativo, la separazione dei problemi.

Non dipendono da ciò che non si utilizza

Il principio di separazione dell'interfaccia (bit.ly/LS-principio) indica che le classi devono dipendere solo da funzionalità utilizzano effettivamente. Nel caso di DinnersController abilitato DI nuovo, è ancora in base al DbContext intero. Anziché a incollare l'implementazione del controller da Entity Framework, è possibile utilizzare un'astrazione fornita la funzionalità necessaria (e poco o niente più).

Ciò che questo metodo di azione realmente necessita per funzionare? Certamente non l'intero DbContext. Non è necessario anche l'accesso alla proprietà Dinners completa del contesto. È necessario è la possibilità di visualizzare le istanze di Dinner della pagina appropriata. L'astrazione .NET più semplice che rappresenta questo oggetto è IEnumerable < Dinner >. Quindi, definire un'interfaccia che restituisce semplicemente un oggetto IEnumerable < Dinner > e che soddisferà (la maggior parte delle) i requisiti del metodo Index:

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

Mi chiamo questo repository in quanto segue il modello: E astrae l'accesso ai dati protetti da un'interfaccia analoga a quella di raccolta. Se per qualche motivo non si desidera che il nome o il modello di archivio, è possibile chiamarlo IGetDinners o IDinnerService o qualsiasi nome che si preferisce (il revisore tech suggerisce ICanHasDinner). Indipendentemente dal nome del tipo, verrà distribuito allo stesso scopo.

A questo punto, ora DinnersController per accettare un IDinnerRepository come un parametro del costruttore, anziché un GeekDinnerDbContext, modificare e chiamare il metodo di elenco anziché accedere direttamente alla classe DbSet Dinners:

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

A questo punto, è possibile compilare ed eseguire l'applicazione Web, ma si verificherà un'eccezione se si passa a /Dinners: InvalidOperationException: Impossibile risolvere il servizio per il tipo 'GeekDinner.Core.Interfaces.IdinnerRepository' durante il tentativo di attivare GeekDinner.Controllers.DinnersController. Ancora non ho implementato l'interfaccia e al termine, inoltre necessario configurare l'implementazione da utilizzare durante l'inserimento di dipendenze soddisfa le richieste per IDinnerRepository. Implementazione dell'interfaccia è davvero semplice:

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

Si noti che è una soluzione attuabile accoppiare direttamente l'implementazione di un repository per Entity Framework. Se è necessario eseguire lo swapping di Entity Framework, semplicemente creare una nuova implementazione di questa interfaccia. Questa classe di implementazione è una parte dell'infrastruttura dell'applicazione in uso, ovvero un'unica posizione nell'applicazione in cui classi my dipendono da implementazioni specifiche.

Per configurare ASP.NET di base per inserire l'implementazione corretta quando classi richiedono un IDinnerRepository, è necessario aggiungere la riga di codice seguente alla fine del metodo ConfigureServices illustrato in precedenza:

services.AddScoped<IDinnerRepository, DinnerRepository>();

Questa istruzione indica il contenitore DI base di ASP.NET per utilizzare un'istanza di DinnerRepository ogni volta che il contenitore la risoluzione di un tipo che dipende da un'istanza di IDinnerRepository. Indica l'ambito di un'istanza da utilizzare per ogni handle ASP.NET Web richiesta. Servizi possono essere aggiunti utilizzando durate temporanei o Singleton. In questo caso, nell'ambito è appropriato in quanto il DinnerRepository dipende da un elemento DbContext, che utilizza anche la durata nell'ambito. Di seguito è riportato un riepilogo della durata degli oggetti disponibili:

  • Temporaneo: Una nuova istanza del tipo viene utilizzata ogni volta che viene richiesto il tipo.
  • Ambito: La prima volta che viene creata una nuova istanza del tipo è richiesto all'interno di una determinata richiesta HTTP e quindi riutilizzarlo per tutti i tipi successive risolti durante la richiesta HTTP.
  • Singleton: Una singola istanza del tipo è una volta creata e utilizzata da tutte le richieste successive per quel tipo.

Il contenitore predefinito supporta diversi modi per creare i tipi che saranno disponibili. Il caso più comune consiste nel fornire semplicemente il contenitore con un tipo, e tenterà di creare un'istanza di tale tipo, fornendo tutte le dipendenze richieste tipo durante la sua esecuzione. È anche possibile fornire un'espressione lambda per costruire il tipo o, per una durata Singleton, è possibile specificare l'istanza costruita completamente in ConfigureServices in fase di registrazione.

Con inserimento di dipendenze collegata, l'applicazione viene eseguita esattamente come in precedenza. A questo punto, come Figura 1 illustrato, è possibile testare con questa nuova astrazione sul posto, utilizzando un'implementazione fittizia o dell'interfaccia IDinnerRepository, anziché basarsi su Entity Framework direttamente nel mio codice di test.

Figura 1 test DinnersController utilizzando un oggetto fittizio

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);
  }
}

Questo test funziona indipendentemente dalla provenienza l'elenco delle istanze cena. È possibile riscrivere il codice di accesso ai dati per l'utilizzo di un altro database, archiviazione tabelle di Azure o file XML e il controller sarebbe funzionano allo stesso modo. Naturalmente, in questo caso non accade molto, pertanto è lecito chiedersi...

Per quanto riguarda la logica reale?

Finora ho effettivamente non ho implementato qualsiasi logica di business reale, è stato appena semplici metodi che restituiscono raccolte semplice dei dati. Il valore effettivo di test viene fornito quando si dispone di logica e casi particolari, che è necessario avere la certezza che saranno quello previsto. Per dimostrarlo, devo aggiungere alcuni requisiti per il sito GeekDinner. Il sito espone un'API che consentono a chiunque a RSVP un prezioso. Tuttavia, dinners avrà una capacità massima facoltativa e inviate risposte non devono superare la capacità. Gli utenti che richiedono inviate risposte oltre alla capacità massima devono essere aggiunti a una lista di attesa. Infine, dinners possibile specificare una scadenza mediante il quale devono essere ricevuti inviate risposte rispetto all'ora di inizio, dopo il quale cui interrompere l'accettazione inviate risposte.

Avrei potuto codificare tutta questa logica in un'azione, ma credo che sia troppo responsabilità per inserire un metodo, in particolare un metodo dell'interfaccia utente che deve essere incentrato sugli aspetti dell'interfaccia utente, non la logica di business. Il controller è necessario verificare che gli input che riceve sono validi, e deve assicurare che le risposte che restituisce sono appropriate per il client. Decisioni oltre che e soprattutto la logica di business non appartengono nei controller.

Il modo migliore per mantenere la logica di business è nel modello di dominio dell'applicazione, che non dipende da problemi di infrastruttura (ad esempio i database o le interfacce utente). La classe Dinner più appropriato per gestire il RSVP problemi descritte nei requisiti, perché archivierà la capacità massima per la cena e saprà inviate risposte quante sono state apportate finora. Tuttavia, parte della logica di dipende anche quando si verifica il RSVP, o meno è oltre la scadenza, il metodo deve anche disporre di accesso all'ora corrente.

Potrei semplicemente utilizzare DateTime. Now, ma questo renderebbe difficile testare la logica e di evitare di associare il modello di dominio per l'orologio di sistema. Un'altra opzione consiste nell'utilizzare un'astrazione di IDateTime e inserire questa entità cena. Tuttavia, in base alla mia esperienza che è preferibile mantenere l'entità, ad esempio Dinner privi di dipendenze, soprattutto se si prevede di utilizzare uno strumento O/RM ad esempio Entity Framework recuperare da un livello di persistenza. Non si desidera per popolare le dipendenze dell'entità come parte del processo ed EF certamente non sarà possibile eseguire questa operazione senza codice aggiuntivo da parte dello sviluppatore. Un approccio comune è a questo punto per estrarre la logica di entità Dinner e inserirlo in un tipo di servizio (ad esempio DinnerService o RsvpService) che può avere dipendenze inserite facilmente. Questa impostazione tende a causare il antipattern modello di dominio anemic (bit.ly o anemic-modello), tuttavia, nelle quali entità presentano un comportamento non e sono semplicemente contenitori dello stato. No, in questo caso la soluzione è semplice, il metodo può accettare nella posizione corrente come parametro semplicemente e lasciare che il codice chiamante passare tale.

Con questo approccio, la logica per l'aggiunta di un RSVP è semplice (vedere Figura 2). Questo metodo è un numero di test che illustrano che funziona come previsto. i test sono disponibili nel progetto di esempio associato all'articolo.

Figura 2 logica di Business del modello di dominio

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");
}

Spostando la logica per il modello di dominio, è fatto in modo che metodo API del mio controller rimane piccolo e concentrati sulle proprie problematiche. Di conseguenza, è facile che il controller non è necessario, poiché sono presenti relativamente pochi percorsi tramite il metodo di test.

Responsabilità controller

Parte di responsabilità del controller consiste nel controllare ModelState e verificare che sia valido. Questa scelta nel metodo di azione per maggiore chiarezza, ma in un'applicazione più grande annullerebbe il codice ripetitivo all'interno di ogni azione utilizzando un filtro di azione:

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

Supponendo che il ModelState sia valido, l'azione deve recupero successivo istanza Dinner appropriata utilizzando l'identificatore specificato nella richiesta. Se l'azione non è in grado di trovare un'istanza di Dinner corrispondente a tale Id, deve restituire un risultato non trovato:

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

Una volta completati questi controlli, l'azione è disponibile delegare l'operazione di business rappresentata dalla richiesta per il modello di dominio, la chiamata al metodo AddRsvp sulla classe Dinner che si è visto in precedenza e salvare lo stato aggiornato del modello di dominio (in questo caso, l'istanza cena e il relativo insieme di inviate risposte) prima di restituire una risposta positiva:

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

Tenere presente che ho deciso che la classe Dinner non hanno dipendenze l'orologio di sistema, optano invece per l'ora corrente passato al metodo. Nel controller, si passa in _systemClock.Now per il parametro currentDateTime. Questo è un campo locale che viene popolato mediante l'inserimento di dipendenze, che impedisce che il controller di essere strettamente collegati al clock di sistema, troppo. È opportuno utilizzare dipendenze nel controller, anziché su un'entità di dominio, perché i controller vengono sempre creati da contenitori dei servizi ASP.NET. questo verrà usata per soddisfare tutte le dipendenze che il controller viene dichiarata nel relativo costruttore. _systemClock è un campo di tipo IDateTime, che viene definito e implementato in un paio di righe di codice:

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

Naturalmente, è necessario anche garantire che il contenitore ASP.NET è configurato per utilizzare MachineClockDateTime ogni volta che una classe necessita di un'istanza di IDateTime. Questa operazione viene eseguita in ConfigureServices nella classe di avvio e in questo caso, sebbene funzionerà qualsiasi durata dell'oggetto, si sceglie di utilizzare un Singleton, in quanto un'istanza di MachineClockDateTime funzionerà per l'intera applicazione:

services.AddSingleton<IDateTime, MachineClockDateTime>();

Con questo semplice astrazione sul posto, sono in grado di testare il comportamento del controller in base a se ha superato la scadenza RSVP e assicurarsi che venga restituito il risultato corretto. Poiché si dispone già di test per il metodo Dinner.AddRsvp per verificare che funzioni come previsto, non devo molto molti test di tale comportamento stesso tramite il controller a contattarmi livello di affidabilità che, quando si utilizzano insieme, il modello di dominio e controller funzionino correttamente.

Passaggi successivi

Scaricare il progetto di esempio associato per visualizzare gli unit test per cena e DinnersController. Tenere presente che codice regime di controllo è in genere molto più semplice lo unit test di codice correlato rigidamente pieno di chiamate al metodo "new" o statico che dipendono da problemi di infrastruttura. "Nuovo è glue" e la nuova parola chiave deve essere utilizzata intenzionalmente accidentalmente, nell'applicazione. Ulteriori informazioni su ASP.NET Core e il supporto per l'inserimento delle dipendenze in docs.asp.net.


Steve Smithè un formatore indipendente, mentore e consulente, nonché un MVP di ASP.NET. Ha contribuito decine di articoli per la documentazione ufficiale di ASP.NET di base (docs.asp.net) e funziona con i team di formazione questa tecnologia. Contattarlo all'indirizzo ardalis.com o seguirlo su Twitter: @ardalis.

Grazie all'esperto tecnico Microsoft seguente per la revisione di questo articolo: Doug granpavesi
Doug granpavesi è uno sviluppatore che lavora nel team di MVC in Microsoft. Egli è stato per un periodo di tempo e amano il paradigma DI nuovo nella riscrittura Core MVC.