Il blog di Sandro Rizzetto

I nuovi tipi DateOnly e TimeOnly tra EF e Blazor

 

Amo i periodi in cui la mia azienda è chiusa per festività perché mi consentono o di svolgere lavori sistemistici in tranquillità (es. durante l’ultimo Ferragosto 8 server upgradati a Windows 2019 Srv) o come in questi ultimi 10 giorni di dedicarmi con più tranquillità all’auto-formazione e a sperimentare sporcandomi le mani col codice di quanto leggo o vedo (ormai il mio percorso di apprendimento è diviso tra mini-pillole su Twitter, blog e articoli -spesso su Medium-, video free su Youtube o corsi su Pluralsight; di sicuro il materiale non manca).

Questo è il primo post di una serie (di quanti non so) sui molti argomenti che ho affrontato, dalle Minimal Api alla Clean Architecture, da librerie Blazor che sto testando a esperienze devops di deployment.

Visto che la mia azienda è nata nel mondo dei cronometri, mi sembra giusto iniziare con un argomento che parla di "Tempo".

DateOnly e TimeOnly

All’annuncio dei due nuovi tipi di dato introdotti con .NET 6 ero molto contento perché soprattutto per quello che riguarda l’orario si era sempre in dubbio se usare un DateTime con una data fittizia oppure un TimeSpan che però ricordiamo indica una “durata” (es. quanto tempo è passato tra due eventi) e non un’ora del giorno.

Dovendo fare un mini-programmino che tenga d’occhio la nostra giornata lavorativa, registrando quando siamo entrati, la pausa caffè con eventuale sforamento, la pausa pranzo, e calcolando con countdown alla Fantozzi quanto manca all’uscita e il delta dell’uscita effettiva, ho pensato ovviamente di usare i due nuovi tipi.

Qui trovate un “mockup” dell’applicazione (l'originale, salvando i dati giornalieri e la configurazione del dipendente è ovviamente dietro login).

Compatibilità con EF Core

La prima curiosità era come veniva trattato il mapping con i tipi di SQL che più si avvicinano ovvero “date” e “time(n)”

Con un approccio Database First, costruendo la tabella e facendo lo Scaffolding con “dotnet ef” si ottiene un mapping purtroppo non con i due nuovi tipi ma appunto con un DateTime e un TimeSpan. Stessa cosa usando il Reverse Engineer di Ef Power Tools dell’ottimo ErikEJ. Il motivo direi è ovvio, mantenere compatibilità se si usano versioni inferiori alla 6 del framework .NET

Se invece lavoriamo al contrario (Code First) e proviamo ad applicare alla nostra classe che ha i due tipi nuovi una migration, non indicando niente nel OnModelCreating del DbContext ci becchiamo un bel

The property Employee.DateOfBirth' could not be mapped because it is of type 'DateOnly', which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

Ingenuamente pensavo bastasse dire come vogliamo mappare il tipo con un semplice

    
    modelBuilder.Entity(entity =>
{
    entity.ToTable("Employees");
    entity.Property(e => e.DateOfBirth).HasColumnType("date");
    entity.Property(e => e.WorkStart).HasColumnType("time(0)");
});

    

Ma la cosa non è sufficiente in quanto è il provider sottostante (Microsoft.Data.SqlClient) che non lo supporta, al contrario del fratellino Sqlite che invece lo fa!

Grazie a questo post del MVP Marco Minerva ho scoperto che bisogna scriversi due Converter (uno per la data e uno per il time) e poi utilizzarli sempre nel modelBuilder

        
entity.Property(x => x.DateOfBirth)
 .HasConversion<DateOnlyConverter, DateOnlyComparer>();
    
entity.Property(x => x.WorkStart)
 .HasConversion<TimeOnlyConverter, TimeOnlyComparer>();
        

Se non indichiamo nessuna “HasColumnType”, la migration che otteniamo è questa:

    
    migrationBuilder.CreateTable(
        name: "MyEmployees",
        columns: table => new
        {
            Id = table.Column(type: "int", nullable: false)
                .Annotation("SqlServer:Identity", "1, 1"),
            DateOfBirth = table.Column(type: "datetime2", nullable: false),
            WorkStart = table.Column(type: "time", nullable: false)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_MyEmployees", x => x.Id);
        });
  

dove vediamo che il DateOnly viene mappato su un datetime2 (why?? perché non date e basta?) e il TimeOnly su un time che di default ha una precision (7) che molto spesso è esagerata (100ns) e assolutamente inutile nel caso di orari dove spesso non servono neanche i secondi. Ricordo che la precision, praticamente è il numero di decimali dei secondi, quindi time(0) = orario con i secondi “pieni”, time(2) con i centesimi, time(3) con i millesimi, ecc.

Se vogliamo quindi ottimizzare lo spazio nelle nostre tabelle sql (come amo fare) consiglio di implementare comunque l’.HasColumnType indicando il tipo più corretto per l’uso che andrete a fare

Binding a controlli html/blazor

Ok, adesso abbiamo le nostre proprietà e le possiamo persistere sul Db, come le utilizziamo con i controlli che offrono una UI moderna, ovvero fornendo un datepicker o un timepicker?

Partiamo da HTML5 plain, dove i tag specifici sembrano accettare di buon grado i due tipi

    
<input type="date" class="form-control" @bind-value="employee.DateOfBirth">
<input type="time" class="form-control" @bind-value="employee.WorkStart">
    

Il binding avviene correttamente, peccato che il browser (chromium per lo meno) li renderizzino nel formato dipendente dalla lingua impostata (quindi mm/dd e AM/PM se per esempio lo usiamo in inglese come faccio io). I campi TimeSpan non sono “bindabili” ad un input type=”time” ma bisogna usare un normale type=”text”

Se passiamo al controllo specializzato di Blazor <InputDate> la cosa non cambia perché sotto lui lo trasforma in un <input type=date> e quindi resta come tutto come prima. Inoltre, stranamente, il team non ha fatto un wrapper per il tempo (un <InputTime> insomma non esiste).

    
<EditForm Model="@employee">
  <InputDate class="form-control" @bind-Value="employee.DateOfBirth" @bind-Value:format="dd/MM/yyyy" />
</EditForm>

Su alcuni blog si parla di utilizzare un @bind-Value:format o un @bind:format nell’input html.

NON funziona! E il motivo lo spiega qui molto chiaramente Steve Sanderson (il papà di Blazor) dicendo appunto che <InputDate> è un wrapper di <input type=”date”> che è a sua volta controllato dal browser.

Vediamo allora alcune librerie esterne come si comportano:

DevExpress

La suite DevExpress che usiamo con soddisfazione da alcuni anni propone due controlli specializzati con dei picker abbastanza user-friendly (su mobile potrebbero avere una UX migliore).

Il controllo DxDate se bindato ad un DateOnly va in errore a runtime (e non in compilazione, quindi occhio)

Mentre il controllo DxTime non va in errore ma il campo resta vuoto e non assume il contenuto della variabile TimeOnly.

Siamo quindi per forza costretti a usare un DateTime e un TimeSpan con però il vantaggio di poter avere più controllo sul formato (si può passare una CultureInfo che traducono anche parte dei picker come i nomi dei mesi e dei giorni) oltre che a dei formati predefiniti (es. ShortTime = HH:MM, ecc.) che altro non sono che dei wrapper ai FormatString. Infine un utile caretmode ci consente di decidere se dopo l’immissione della prima parte di data o di tempo dobbiamo premere enter o l’avanzamento al blocco successivo è automatico.

 

MudBlazor

La nota libreria free che adatta il Material Design ai componenti Blazor (e che sarà oggetto prossimamente di un altro post) offre anch’essa due componenti specializzati e come nel caso precedente nessun binding ai nuovi formati ma necessità di usare i tipi vecchi (ma almeno si viene subito avvisati in fase di compilazione dell’impossibilità di convertire a DateTime? e TimeSpan?)

Anche qui possibilità di passare una Culture, un Format e un sacco di altri parametri utili per personalizzare l’aspetto dei due picker che, devo dirlo, sono superiori alla libreria commerciale

 

Altre Librerie:

Blazorise (che ho utilizzato in passato per questo progettino) offre anche essa dei controlli molto completi e con dei “picker” basati su flatpickr molto compatti e risparmiosi in termini di spazio.

MatBlazor invece non ha un controllo solo per il tempo ma lo integra in un Date Picker completo

Infine Ant Design Blazor (libreria molto diffusa in Cina e con un numero superiore di GitHub Stars -per quello che può significare- alle altre soprannominate) che binda anche il TimePicker a un DateTime? invece che a un TimeSpan? come le 3 precedenti.

Conclusioni

Sicuramente fa piacere l’introduzione di questi due nuovi tipi che specializzano di più il contesto in cui usiamo le nostre variabili. Come abbiamo visto però, causa retro-compatibilità, il loro uso è limitato e dovremo aspettare un po’ prima di vederli utilizzati con frequenza.

PS con la mia solita fortuna, sono incappato in un bug della libreria DevExpress che non permette l'inserimento delle ore nel TimePicker da qualsiasi browser di un device Android (iOS non ho provato...). Ticket aperto e in attesa di risoluzione.

Commenti (2) -

  • Daniele Bisol

    10/01/2022 08:05:01 | Rispondi

    Si ma dopo i poveri dbcentric come me che scrivono tonnellate di datediff in t-sql per reporting e powerbi gli tocca stropicciarsi il cervello per mettere insieme i date e time per usare il datediff con l'interval (second, minute) che più gli aggrada ...
    A farlo bene sarebbero da mappare via EF i due campi UI in un unico campo DB datetime... (ti lancio questa sfida per le vacanze di carnevale .. Smile

    • Sandro

      10/01/2022 08:21:59 | Rispondi

      Come diceva uno speaker ai tempi di WPC "sei pagato per scrivere codice", quindi muto Smile

      è ovvio che vanno usati in contesti dove una delle due informazioni è superflua o mancante o comunque sarebbe ridondante; es. se per ogni record ho N campi ora -entrata, uscita, startevent, finishevent, ecc.-, preferisco avere una volta sola il campo data per fare aggregation e N campi time per fare le sum o i delta.

Pingbacks and trackbacks (1)+

Aggiungi Commento

Copyright © 1997-2024 Sandro Rizzetto | All Rights Reserved | Riproduzione delle fotografie vietata | Powered by me