Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 26 de septiembre de 2023
.NET

Seguro que más de una vez habéis tenido que construir una abstracción sobre DateTime para poder controlar apropiadamente la obtención de la fecha/hora actual y otras operaciones relacionadas con el tiempo.

Suelen ser bastante útiles cuando creamos pruebas unitarias de métodos que dependan del momento actual. Por ejemplo, ¿cómo testearíamos de forma automática que las dos líneas de ejecución del siguiente método DoSomething() funcionan correctamente? Sería imposible salvo que ejecutásemos las pruebas a una hora determinada, algo que se antoja complicado 😉

public class MyClass
{
    public string DoSomething()
    {
        var now = DateTime.Now;
        return now.Second == 0
            ? "A new minute is starting"
            : "Current second " + now.Second;
    }
}  

Sin duda, una forma mejor y más test friendly sería contar con una abstracción sobre el proveedor de tiempos capaz de retornar la fecha y hora actual, por ejemplo:

public interface IDateTimeProvider
{
    DateTime GetCurrentDateTime();
}

De esta forma, podríamos reescribir la clase MyClass de forma que recibiera por inyección de dependencias nuestro proveedor IDateTimeProvider. Así sería realmente sencillo crear un par de pruebas unitarias que, suministrando los valores correctos a través de esta dependencia, podrían recrear los escenarios a cubrir:

public class MyClass
{
    private readonly IDateTimeServices _dateTimeServices;
    public TimeHelpers(IDateTimeServices dateTimeServices)
    {
        _dateTimeServices = dateTimeServices;
    }
    public string DoSomething()
    {
        var now = _dateTimeServices.GetCurrentDateTime();
        return now.Second == 0
            ? "A new minute is starting"
            : "Current second " + now.Second;
    }
}  

Aunque hacerlo de esta manera en nuestras aplicaciones es lo ideal, hay partes que se quedarían fuera de esta posibilidad, como las bibliotecas de terceros que de alguna forma dependan de las funcionalidades proporcionadas por DateTime.

Por esta razón, .NET 8 va a introducir una abstracción que nos permitirá gestionar estos escenarios de forma más homogénea y generalizada en aplicaciones y componentes .NET. 

Os presento la clase abstracta TimeProvider 😁

La abstracción TimeProvider

La clase abstracta TimeProvider se define en el espacio de nombres System de la siguiente manera:

public abstract class TimeProvider
{
    public static TimeProvider System { get; }

    public virtual TimeZoneInfo LocalTimeZone { get; }
    public virtual long TimestampFrequency { get; }

    public virtual ITimer CreateTimer(TimerCallback callback, object? state, 
                                      TimeSpan dueTime, TimeSpan period);

    public virtual DateTimeOffset GetUtcNow();
    public virtual long GetTimestamp();
    public DateTimeOffset GetLocalNow();
    public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp);
    public TimeSpan GetElapsedTime(long startingTimestamp);
}

Como podemos observar, esta abstracción proporciona métodos de uso frecuente a la hora de gestionar el tiempo, como los destinados a obtener la fecha y hora local actual, la fecha y hora UTC actual, el timestamp actual, o medir el tiempo transcurrido entre dos timestamps. Todos los métodos y propiedades están implementados por defecto, pero los marcados como virtual pueden ser sobreescritos por las clases que hereden de TimeProvider para adaptarlos a sus necesidades.

Aunque esta clase es abstracta y no puede ser instanciada de forma directa, su miembro estático System ofrece un objeto que podemos utilizar en nuestras aplicaciones de forma directa. Este objeto utiliza la implementación por defecto de TimeProvider, que obtiene los datos del entorno de ejecución.

De esta forma, podemos utilizar TimeProvider para conseguir componentes totalmente desacoplados del reloj del sistema. Por ejemplo, el siguiente método muestra por consola la fecha/hora UTC actual, sin acceder a los clásicos miembros estáticos de DateTime:

void ShowUtcDateTime(TimeProvider timeProvider)
{
    Console.WriteLine("Current UTC datetime: " + timeProvider.GetUtcNow());
}

Luego, podríamos invocar este método con el objeto System de TimeProvider para obtener la fecha/hora UTC actual, o con un objeto TimeProvider personalizado que nos permita simular una fecha/hora determinada:

// Usar el reloj del sistema:
ShowUtcDateTime(TimeProvider.System);

// Usar un reloj personalizado que devuelve siempre la misma fecha/hora:
class FixedTimeProvider : TimeProvider
{
    public override DateTimeOffset GetUtcNow() 
        => new (2021, 03, 01, 15, 35, 00, TimeSpan.Zero);
}

ShowUtcDateTime(new FixedTimeProvider());

También, en caso de utilizar ASP.NET Core o cualquier otro framework donde programemos usando inyección de dependencias, podríamos registrar el proveedor de tiempo por defecto con gran facilidad:

builder.Services.AddSingleton(TimeProvider.System);

Y de esta forma, podremos inyectar TimeProvider en cualquier componente de nuestra aplicación donde necesitemos las funcionalidades basadas en tiempo. Por ejemplo, en el siguiente snippet podremos observar cómo registramos el servicio proveedor de tiempo en el inyector de dependencias y luego, en una Minimal API lo utilizamos para retornar la fecha/hora UTC actual:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(TimeProvider.System);
var app = builder.Build();

app.MapGet("/", (TimeProvider tp) => "Current UTC datetime: " + tp.GetUtcNow());

app.Run();

Una vez nuestros componentes están implementados usando esta abstracción, crear pruebas unitarias sobre ellos es trivial, puesto que, como hemos visto anteriormente, podemos crear descendientes de TimeProvider donde sobrescribamos los métodos necesarios para simular el comportamiento deseado durante los tests.

¿Interesante, verdad? No es que sea algo revolucionario, muchos de nosotros ya estábamos usando abstracciones similares en nuestros componentes, pero no estaban estandarizadas y cada uno las implementaba a su manera, mientras que con TimeProvider podemos hacerlo de forma más homogénea. Al ser el propio marco de trabajo el que establece esta abstracción, seguro que poco a poco comenzará a ser adoptada por bibliotecas y componentes de .NET, y en algún tiempo será un estándar de facto en nuestras aplicaciones.

¡Espero que os sea útil! 😊

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!