Este artículo es una mini-solución a una situación muy concreta que se nos dió en colaborador y que, sin ser la más adecuada bajo el punto de vista del diseño de software, es la que con mayor rapidez nos cubría el problema. Por lo tanto, ha sido la primera aproximación que se ha decidido implementar, con vistas a refactorizarlo a posteriori, con el objeto de entregar valor lo antes posible (ya que la fecha de entrega era crítica).
Necesitábamos que un endpoint en concreto, tras haber respondido ya a la petición, siguiera ejecutando X proceso en segundo plano. La opción más lógica habría sido que antes de responder, se mandase un mensaje a un servicio de mensajería y que luego, un proceso se suscribiera a dichos mensajes por otra parte. Sin embargo y por lo comentado previamente de entregar valor de manera casi inmediata, optamos por buscar otra solución que no requiere del desarrollo de dicho envío y recepción de mensajes, ni de la infraestructura para dicha implementación.
Buscando referencias por internet, encontramos que .NetCore provee de una forma con la cual podemos hacer exactamente lo que queremos, ejecutar algo tras haber respondido.
Veamos un ejemplo sencillo de controlador:
[HttpPost(Name = "sayHello")]
public IActionResult Post()
{
try
{
Console.Out.WriteLine($"{DateTime.Now} - Request Received...");
return Ok("Hello World!");
}
finally
{
Response.OnCompleted(async () =>
{
await Task.Delay(5000);
await Console.Out.WriteLineAsync($"{DateTime.Now} - After request response...");
});
}
}
Si hacemos una petición a ese endpoint, el resultado en logs será de la siguiente manera:
1/12/2023 12:17:48 - Request Received...
1/12/2023 12:17:53 - After request response...
Y si le echamos un ojo a la respuesta, tendremos lo siguiente:
Como vemos, en la cabecera date
tenemos la fecha con el segundo exacto en que mostramos por pantalla el mensaje de petición recibida. Tarda nada, y lo mismo en responder…
Y ahora dirás, sí, con un Console.Out.WriteLine
es muy sencillo ver que eso ejecuta cosas levantando la aplicación, pero ¿cómo hacemos para que se ejecute una acción de dominio? Y más importante aún, ¿cómo hacemos que se pueda probar de forma automática que esa acción es llamada?
Aquí es donde viene la parte complicada ya que, para poder hacer que el código resultante se pueda testear, hay que aislar ese Response.OnComplete
en una función que podamos sobre-escribir, y además, pasarla hacia el servicio para poder envolver la llamada desde el servicio sin importar lo que haya por encima envolviendo esa llamada. Veámoslo en código:
// SomeController.cs
using Microsoft.AspNetCore.Mvc;
namespace AfterResponseJobCSharp.Controllers;
[ApiController]
[Route("[controller]")]
public class SomeController : ControllerBase
{
private readonly SomeService someService;
public SomeController(SomeService someService)
{
this.someService = someService;
}
[HttpGet]
public IActionResult Get()
{
var value = "";
try
{
value = someService.DoSomething();
return Ok(value);
}
finally
{
someService.DoPostResponseJobCallWrapped(
value,
WrapCallWithResponseOnCompleted);
}
}
protected virtual void WrapCallWithResponseOnCompleted(
Func<Task> method) => Response.OnCompleted(method);
}
// SomeService.cs
namespace AfterResponseJobCSharp.Domain;
public class SomeService
{
public virtual string DoSomething()
{
return "Doing something...";
}
public void DoPostResponseJobCallWrapped(string value, Action<Func<Task>> wrappingMethod)
{
wrappingMethod(() =>
{
DoPostResponseJob(value);
return Task.CompletedTask;
});
}
public virtual void DoPostResponseJob(string value)
{
Console.Out.WriteLine($"Executed with value: {value}");
}
}
// SomeControllerShould.cs
using AfterResponseJobCSharp.Controllers;
using NSubstitute;
namespace AfterResponseJobCSharpTest;
public class SomeControllerShould
{
[Fact]
public void CallTheBackgroundProcessOnceItHasAnswerTheRequest()
{
var randomValue = "Random value...";
var superDuperService = Substitute.For<SomeService>();
superDuperService.DoSomething().Returns(randomValue);
var controller = new SomeControllerSome(superDuperService);
controller.Get();
superDuperService.Received(1).DoPostResponseJob(randomValue);
}
internal sealed class SomeControllerSome : SomeController
{
public SomeControllerSome(SomeService someService)
: base(someService) {}
protected override void WrapCallWithResponseOnCompleted(Func<Task> method)
{
method();
}
}
}
La parte más compleja de todo esto, es el paso de funciones del controlador al servicio, para finalmente en el test, acabar sobreescribiendo esta función, que pasamos para no usar cosas del framework en ámbito de test.
Obviamente, y por tercera vez consecutiva, esto no es algo que debamos hacer a la ligera. Presenta la ejecución de un “side effect”, el cual, para probarlo dependemos del framework, o de estrategias no muy intuitivas, ya que debemos reemplazar comportamiento del controlador para poder ejecutar el test.
¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.
Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?
Pero espera 🖐 que tenemos un conflicto interno. A nosotros las newsletter nos parecen 💩👎👹 Por eso hemos creado la LEAN LISTA, la primera lista zen, disfrutona y que suena a rock y reggaeton del sector de la programación. Todos hemos recibido newsletters por encima de nuestras posibilidades 😅 por eso este es el compromiso de la Lean Lista