Mayo de 2016

Volumen 31, número 5

ASP.NET: escritura de código limpio en ASP.NET Core con inserción de dependencias

Por Steve Smith 

ASP.NET Core 1.0 es una reescritura completa de ASP.NET, y uno de los principales objetivos de este nuevo marco es un diseño más modular. Es decir, las aplicaciones deben poder usar solo las partes del marco que necesitan, y el marco proporcionar las dependencias a medida que se solicitan. Además, los desarrolladores que compilen aplicaciones con ASP.NET Core deben poder aprovechar esta misma funcionalidad para mantener sus aplicaciones libremente acopladas y modulares. Con ASP.NET MVC, el equipo de ASP.NET mejoró enormemente la compatibilidad del marco para la escritura de código acoplado libremente, pero seguía siendo muy fácil caer en la trampa del acoplamiento estrecho, especialmente en las clases de controlador.

Acoplamiento estrecho

El acoplamiento estrecho es idóneo para software de demostración. Si observa la aplicación de ejemplo típica que muestra cómo compilar sitios de ASP.NET MVC (versiones 3 a 5), probablemente encontrará código como este (de la clase DinnersController del ejemplo de NerdDinner MVC 4):

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

Este tipo de código es muy difícil de probar de forma unitaria, ya que NerdDinnerContext se crea como parte de la construcción de la clase y requiere una base de datos a la que conectarse. No es sorprendente que estas aplicaciones de demostración a menudo no incluyan pruebas unitarias. No obstante, su aplicación podría usar algunas pruebas unitarias, aunque no esté realizando pruebas de desarrollo, por lo que sería mejor escribir el código de manera que pudiera probarse. Además, este código infringe el principio de Una vez y solo una (DRY), porque cada clase de controlador que realiza un acceso a datos incluye el mismo código para crear un contexto de base de datos de Entity Framework (EF). Esto hace que los futuros cambios sean más caros y propensos a errores, especialmente a medida que la aplicación crece con el tiempo.

Al observar el código para evaluar su acoplamiento, recuerde la frase "lo nuevo se pega". Es decir, siempre que vea la palabra clave "nuevo" creando una instancia de una clase, observe que está pegando su implementación al código de implementación específico. El principio de inversión de dependencias (bit.ly/DI-Principle) manifiesta lo siguiente: "Las abstracciones no deben depender de detalles; los detalles deben depender de abstracciones." En este ejemplo, los detalles de cómo el controlador reúne los datos para pasarlos a la vista depende de los detalles sobre la manera de obtener esos datos, concretamente, EF.

Además de la nueva palabra clave, "adhesión estática" es otra fuente de acoplamiento estrecho que dificulta la prueba y el mantenimiento de las aplicaciones. En el ejemplo anterior, existe una dependencia en el reloj del sistema de la máquina en ejecución, con el formato de una llamada a DateTime.Now. Este acoplamiento dificultaría la creación de un conjunto de instancias Dinners de prueba para usar en algunas pruebas unitarias, ya que sus propiedades EventDate deberían establecerse en relación con la configuración actual del reloj. Este acoplamiento se podría quitar de este método de distintas maneras. La más sencilla consiste en permitir que independientemente de lo que devuelva la nueva abstracción, las instancias Dinners se preocupen de ello, de manera que ya no forme parte de este método. De manera alternativa, podría convertir el valor en un parámetro, de modo que el método podría devolver todas las instancias Dinners detrás de un parámetro DateTime proporcionado, en lugar de usar siempre DateTime.Now. Por último, podría crear una abstracción para la hora actual y hacer referencia a la hora actual a través de esta abstracción. Este enfoque puede ser adecuado si la aplicación hace referencia a DateTime.Now con frecuencia. (También vale la pena observar que, dado que supuestamente estas cenas tienen lugar en distintas zonas horarias, el tipo DateTimeOffset podría ser una opción más adecuada en una aplicación real).

Honestidad

Otro problema con la facilidad de mantenimiento de código como este es que no es honesto con sus colaboradores. Debería evitar escribir clases de las que se pueda crear una instancia en estados no válidos, ya que estas son fuentes de error frecuentes. Así, cualquier cosa que su clase necesite para realizar sus tareas debería suministrarse a través de su constructor. Como el principio de dependencias explícitas (bit.ly/ED-Principle) manifiesta, "los métodos y las clases deben requerir explícitamente todos los objetos de colaboración que necesiten para funcionar correctamente". La clase DinnersController solo tiene un constructor predeterminado, lo que implica que no debería necesitar ningún colaborador para funcionar correctamente. ¿Qué sucede si lo pone a prueba? ¿Qué hará este código si lo ejecuta desde una nueva aplicación de consola que haga referencia al proyecto de MVC?

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

Lo primero que falla en este caso es el intento de crear una instancia del contexto de EF. El código lanza una excepción InvalidOperationException: "No se pudo encontrar ninguna cadena de conexión denominada 'NerdDinnerContext' en el archivo de configuración de la aplicación". ¡Qué decepción! Esta clase necesita más para funcionar de lo que su constructor declara. Si la clase necesita una manera de acceder a las colecciones de instancias Dinner, debe solicitarla a través de su constructor (o, de forma alternativa, como parámetros en sus métodos).

Inserción de dependencias

La inserción de dependencias (DI) hace referencia a la técnica de pasar las dependencias de una clase o un método como parámetros, en lugar de codificar estas relaciones a través de llamadas nuevas o estáticas. Es una técnica común que se usa cada vez más en el desarrollo de .NET, debido al desacoplamiento que permite a las aplicaciones que la usan. Las primeras versiones de ASP.NET no usaban DI y, aunque ASP.NET MVC y la API web progresaron para admitirla, ninguno de ellos llegó a integrar total compatibilidad, incluido un contenedor para administrar las dependencias y los ciclos de vida de sus objetos, en el producto. Con ASP.NET Core 1.0, DI no solo es compatible desde el inicio, sino que el propio producto la usa de manera extensiva.

ASP.NET Core no solo admite DI, sino que también incluye un contenedor de DI, también conocido como contenedor de inversión de control (IoC) o contenedor de servicios. Cada aplicación de ASP.NET Core configura sus dependencias mediante este contenedor en el método ConfigureServices de la clase Startup. Este contenedor proporciona el soporte técnico básico necesario, pero se puede reemplazar por una implementación personalizada si se quiere. Además, EF Core también ofrece soporte técnico integrado para DI, de modo que configurarlo con la aplicación ASP.NET Core es tan simple como llamar a un método de extensión. He creado una escisión de NerdDinner, denominada GeekDinner, para este artículo. EF Core está configurado de la siguiente manera:

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

Con esto dispuesto, es bastante simple usar DI para solicitar una instancia de GeekDinnerDbContext desde una clase de controlador como DinnersController:

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

Observe que no existe una instancia única de la nueva palabra clave; las dependencias que el controlador necesita se pasan a través de su constructor y el contenedor de DI de ASP.NET lo hace por mi. Mientras estoy centrada en escribir la aplicación, no tengo que preocuparme por los mecanismos que implica cumplir con las dependencias que mis clases solicitan a través de sus constructores. Obviamente, quiero y puedo personalizar este comportamiento, incluso reemplazar completamente el contenedor predeterminado por otra implementación. Dado que mi clase de controlador sigue ahora el principio de dependencias explícitas, sé que, para que funcione, debo proporcionarle una instancia de GeekDinnerDbContext. Con escasa configuración de la clase DbContext, puedo crear una instancia del controlador aislado, como demuestra esta aplicación de consola:

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

La construcción de una clase DbContext de EF Core implica un poco más de trabajo que la de EF6, que solo requería una cadena de conexión. Esto se debe a que, del mismo modo que ASP.NET Core, EF Core está diseñado para ser más modular. Normalmente, no necesitará tratar con DbContextOptionsBuilder directamente, porque se usa entre bastidores al configurar EF mediante métodos de extensión, como AddEntityFramework y AddSqlServer.

Pero, ¿puede probarlo?

Probar la aplicación manualmente es importante: quiere poder ejecutarla, así como ver que se ejecuta realmente y que produce el resultado esperado. Pero tener que hacerlo cada vez que realiza un cambio es un desperdicio de tiempo. Una de las grandes ventajas de las aplicaciones acopladas libremente es que tienden a estar más dispuestas para las pruebas unitarias que las acopladas estrechamente. Y aún mejor, ASP.NET Core y EF Core son mucho más fáciles de probar que sus predecesores. Para empezar, escribiré una prueba simple directamente en el controlador pasando una clase DbContext configurada para usar un almacén en memoria. Configuraré la instancia GeekDinnerDbContext mediante el parámetro DbContextOptions que expone a través de su constructor como parte del código de instalación de mi prueba:

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

Una vez configurada en mi clase de prueba, es fácil escribir una prueba que muestre que se han devuelto los datos correctos en el modelo de 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);
}

Por supuesto, aquí todavía no hay mucha lógica para probar, por lo que la prueba no es muy extensa. Los críticos argumentarán que esta prueba no tiene un gran valor, algo con lo que estaré de acuerdo. No obstante, es un punto de partida para cuando haya más lógica a punto, lo que sucederá pronto. Primero, aunque EF Core puede admitir las pruebas unitarias con la opción en memoria, seguiré evitando el acoplamiento directo en EF en mi controlador. No existe ningún motivo para mezclar las preocupaciones de la UI con las de la infraestructura de acceso a datos. De hecho, infringe otro principio, el de la separación de preocupaciones.

No dependa de lo que no usa

El principio de segregación de interfaces (bit.ly/LS-Principle) manifiesta que las clases deben depender solo de la funcionalidad que usan realmente. En el caso de la nueva clase DinnersController habilitada para DI, sigue dependiendo de la clase DbContext completa. En lugar de pegar la implementación del controlador a EF, se podría usar una abstracción que proporcionase la funcionalidad necesaria (y poco o nada más).

¿Que necesita realmente este método de acción para funcionar? Definitivamente, no requiere la clase DbContext completa. Tampoco necesita acceso a la propiedad Dinners completa del contexto. Todo lo que necesita es la capacidad de mostrar las instancias Dinner apropiadas de la página. La abstracción de .NET más simple que representa esto es IEnumerable<Dinner>. Por tanto, definiré una interfaz que simplemente devuelva IEnumerable<Dinner> y que satisfará (la mayoría de) los requisitos del método Index:

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

Lo llamo repositorio porque sigue este patrón: Abstrae el acceso a datos detrás de una interfaz similar a una colección. Si por algún motivo, el patrón o el nombre del repositorio no le gustan, puede llamarlo IGetDinners, IDinnerService o el nombre que prefiera (mi revisor técnico sugiere ICanHasDinner). Independientemente del nombre que asigne al tipo, servirá para el mismo propósito.

Con esto en orden, ahora ajusto DinnersController para que acepte una instancia IDinnerRepository como parámetro constructor, en lugar de una instancia GeekDinnerDbContext, y llamo al método List en lugar de acceder a Dinners DbSet directamente:

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

En este punto, puede compilar y ejecutar su aplicación web, pero encontrará una excepción si navega a /Dinners: Invalid­OperationException: no se puede resolver el servicio del tipo "Geek­Dinner.Core.Interfaces.IdinnerRepository" mientras intenta activar GeekDinner.Controllers.DinnersController. Todavía no he implementado la interfaz y, cuando lo haga, también deberé configurar mi implementación para que se use cuando DI cumpla las solicitudes de IDinnerRepository. La implementación de la interfaz es trivial:

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

Tenga en cuenta que es perfectamente correcto acoplar una implementación de repositorio a EF directamente. Si necesito cambiar EF, simplemente crearé una nueva implementación de esta interfaz. Esta clase de implementación es una parte de la infraestructura de mi aplicación, que es la ubicación de la aplicación donde mis clases dependen de implementaciones específicas.

Para configurar ASP.NET Core para insertar la implementación correcta cuando las clases soliciten una instancia IDinnerRepository, debo agregar la siguiente línea de código al final del método ConfigureServices mostrado anteriormente:

services.AddScoped<IDinnerRepository, DinnerRepository>();

Esta instrucción indica al contenedor de DI de ASP.NET Core que use una instancia DinnerRepository siempre que resuelva un tipo que dependa de una instancia IDinnerRepository. Scoped significa que se usará una instancia para cada solicitud web que ASP.NET controle. Los servicios también se pueden agregar mediante las duraciones Transient o Singleton. En este caso, Scoped es adecuado porque mi instancia DinnerRepository depende de una clase DbContext, que también usa la duración Scoped. A continuación, tiene un resumen de las duraciones de objeto disponibles.

  • Transient: una nueva instancia del tipo se usa cada vez que se solicita el tipo.
  • Scoped: una nueva instancia del tipo se crea la primera vez que se solicita en una solicitud HTTP determinada y, después, se reutiliza para todos los tipos posteriores resueltos durante dicha solicitud.
  • Singleton: una instancia única del tipo se crea una vez y se usa en todas las solicitudes posteriores de ese tipo.

El contenedor integrado admite varios métodos para construir los tipos que proporcionará. El caso más típico consiste simplemente en proporcionar un tipo al contenedor. Intentará crear una instancia de ese tipo y proporcionar todas las dependencias que el tipo necesite en el proceso. También puede proporcionar una expresión lambda para construir el tipo, o bien, para una duración Singleton, puede proporcionar la instancia totalmente construida en el método ConfigureServices cuando realice el registro.

Con la inserción de dependencias conectada, la aplicación se ejecuta como antes. Ahora, como muestra la Figura 1, puedo probarla con esta nueva abstracción lista, usando una implementación falsa o ficticia de la interfaz IDinner­Repository, en lugar de depender de EF directamente en mi código de prueba.

Figura 1 Prueba de DinnersController con un objeto ficticio

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

Esta prueba funciona independientemente de la procedencia de la lista de instancias Dinner. Podría reescribir el código de acceso a datos para usar otra base de datos, Almacenamiento de tablas de Azure o archivos XML, y el controlador seguiría funcionando igual. Por supuesto, en este caso no se hace mucho, por lo que quizás se esté preguntando...

¿Qué sucede con la lógica real?

De momento no he implementado realmente ninguna lógica empresarial real, solo métodos simples que devuelven colecciones de datos simples. Percibirá el valor real de las pruebas cuando tenga lógica y los casos especiales en los que necesite confiar se comportarán según lo previsto. Para demostrarlo, voy a agregar algunos requisitos a mi sitio de GeekDinner. El sitio expondrá una API que permitirá a cualquier persona confirmar la asistencia a una cena. No obstante, las cenas tendrán una capacidad máxima opcional y las confirmaciones de asistencia no deben superar esta capacidad. Los usuarios que soliciten confirmaciones de asistencia más allá de la capacidad máxima deberán agregarse a una lista de espera. Finalmente, las cenas pueden especificar una fecha límite, relacionada con la fecha inicial, para recibir confirmaciones de asistencia, tras la cual estas dejarán de aceptarse.

Podría codificar toda esta lógica en una acción, pero creo que es demasiada responsabilidad para asignar a un método, especialmente un método de UI que debería centrarse en las preocupaciones de la UI, no en la lógica empresarial. El controlador debe comprobar que las entradas que recibe sean válidas, y así como asegurarse de que las respuestas que devuelve sean adecuadas para el cliente. Otras decisiones, especialmente la lógica empresarial, no atañen a los controladores.

El mejor lugar para mantener la lógica empresarial es el modelo de dominio de aplicación, que no debería depender de las preocupaciones de infraestructura (como las bases de datos o las UI). La clase Dinner es la que tiene más sentido a la hora de administrar las preocupaciones de confirmación de asistencia descritas en los requisitos, ya que almacenará la máxima capacidad para la cena y notificará la cantidad de confirmaciones de asistencia realizadas hasta la fecha. No obstante, parte de la lógica también depende de cuando se produzca la confirmación de asistencia (antes o después de la fecha límite), por lo que el método también necesita acceder a la hora actual.

Podría usar DateTime.Now, pero la lógica sería difícil de probar y acoplaría mi modelo de dominio al reloj del sistema. Otra opción es usar una abstracción IDateTime e insertarla en la entidad Dinner. No obstante, según mi experiencia es mejor mantener las entidades como Dinner sin dependencias, especialmente si planea usar una herramienta O/RM, como EF, para extraerlas de una capa de persistencia. No quiero rellenar las dependencias de la entidad como parte del proceso y, desde luego, EF no podrá hacerlo sin código adicional por mi parte. Un enfoque común en este punto consiste en extraer la lógica de la entidad Dinner y colocarla en algún tipo de servicio (como DinnerService o RsvpService) en el que se puedan insertar dependencias fácilmente. No obstante, esto tiende a conducir al antipatrón del modelo de dominio anémico (bit.ly/anemic-model), en el que las entidades presentan un comportamiento escaso o nulo y son solo contenedores de estado. No, en este caso la solución es muy simple, el método puede tomar la hora actual como un parámetro y permitir que el código de llamada la pase.

Con este enfoque, la lógica para agregar una confirmación de asistencia es sencilla (véase la Figura 2). Este método presenta varias pruebas que demuestran que se comporta según lo previsto; las pruebas están disponibles en el proyecto de ejemplo asociado a este artículo.

Figura 2 Lógica empresarial en el modelo de 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");
}

Al desplazar esta lógica al modelo de dominio, me he asegurado de que el método de API de mi controlador mantuviera un tamaño reducido y permaneciera centrado en sus propios asuntos. Como resultado, es muy fácil probar que el controlador hace lo que debe, ya que existen realmente pocas rutas a través del método.

Responsabilidades del controlador

Parte de la responsabilidad del controlador consiste en comprobar la clase ModelState y asegurarse de su validez. Lo estoy haciendo en el método de acción por claridad, pero en un aplicación de mayor tamaño, eliminaría este código repetitivo en cada acción mediante un filtro de acción:

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

Suponiendo que la clase ModelState sea válida, la acción debe capturar la instancia Dinner adecuada mediante el identificador proporcionado en la solicitud. Si la acción no puede encontrar una instancia Dinner que coincida con ese identificador, debe devolver un resultado de no encontrado:

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

Una vez completadas estas comprobaciones, la acción es libre de delegar la operación empresarial representada por la solicitud al modelo de dominio, mediante la llamada al método AddRsvp en la clase Dinner que hemos visto antes y el almacenamiento del estado actualizado del modelo de dominio (en este caso, la instancia Dinner y su colección de confirmaciones de asistencia) antes de devolver una respuesta de aceptación:

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

Recuerde que he decidido que la clase Dinner no debería tener una dependencia en el reloj del sistema y, en su lugar, debería optar por pasar la hora actual al método. En el controlador, paso la propiedad _systemClock.Now para el parámetro currentDateTime. Este es un campo local que se rellena por medio de DI, que también impide que el controlador se acople estrechamente al reloj del sistema. Es apropiado usar DI en el controlador, en lugar de una entidad de dominio, ya que los controladores siempre los crean contenedores de servicios de ASP.NET; así, cumplirá con las dependencias que el controlador declare en su constructor. _systemClock es un campo de tipo IDateTime, que se define e implementa con solo unas pocas líneas de código:

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

Por supuesto, también debo asegurarme de que el contenedor de ASP.NET esté configurado para usar la propiedad MachineClockDateTime siempre que una clase necesite una instancia de IDateTime. Esto se realiza en el método ConfigureServices de la clase Startup y, en este caso, aunque cualquier duración de objeto funcionará, opto por usar Singleton porque una instancia de la propiedad MachineClockDateTime servirá para toda la aplicación:

services.AddSingleton<IDateTime, MachineClockDateTime>();

Con esta simple abstracción establecida, puedo probar el comportamiento del controlador en función de si la fecha límite de confirmación de asistencia ha pasado y asegurarme de que se devuelve el resultado correcto. Dado que ya tengo pruebas para el método Dinner.AddRsvp que comprueban que se comporta según lo previsto, no necesitaré muchas pruebas de este mismo comportamiento en el controlador para estar seguro de que el controlador y el modelo de dominio funcionan juntos correctamente.

Pasos siguientes

Descargue el proyecto de ejemplo asociado para ver las pruebas unitarias de Dinner y DinnersController. Recuerde que suele ser mucho más fácil realizar pruebas unitarias de código acoplado libremente que de código acoplado estrechamente, este último plagado de llamadas a métodos "nuevos" o estáticos que dependen de las preocupaciones de infraestructura. "Lo nuevo se pega" y la nueva palabra clave debe usarse de manera intencionada, no accidental, en la aplicación. Obtenga más información sobre ASP.NET Core y su compatibilidad con la inserción de dependencias en docs.asp.net.


Steve Smithes instructor, mentor y asesor independiente, además de MVP de ASP.NET. Ha contribuido con decenas de artículos en la documentación oficial de ASP.NET Core (docs.asp.net) y trabaja con equipos que están aprendiendo esta tecnología. Puede ponerse en contacto con él en ardalis.com o seguirlo en Twitter: @ardalis.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Doug Bunting
Doug Bunting es un desarrollador que trabaja en el equipo de MVC de Microsoft. Hace tiempo que se dedica a ello y está encantado con el nuevo paradigma de DI de la reescritura de MVC Core.