Condividi tramite


Conversioni di valori

I convertitori di valori consentono la conversione dei valori delle proprietà durante la lettura o la scrittura nel database. Questa conversione può essere da un valore a un altro dello stesso tipo (ad esempio, crittografando stringhe) o da un valore di un tipo a un valore di un altro tipo ,ad esempio convertendo i valori enumerazione in e da stringhe nel database.

Suggerimento

È possibile eseguire ed eseguire il debug in tutto il codice di questo documento scaricando il codice di esempio da GitHub.

Panoramica

I convertitori di valori vengono specificati in termini di e ModelClrType .ProviderClrType Il tipo di modello è il tipo .NET della proprietà nel tipo di entità. Il tipo di provider è il tipo .NET riconosciuto dal provider di database. Ad esempio, per salvare le enumerazioni come stringhe nel database, il tipo di modello è il tipo dell'enumerazione e il tipo di provider è String. Questi due tipi possono essere uguali.

Le conversioni vengono definite usando due Func alberi delle espressioni: uno da ModelClrType a ProviderClrType e l'altro da ProviderClrType a ModelClrType. Gli alberi delle espressioni vengono usati in modo che possano essere compilati nel delegato di accesso al database per conversioni efficienti. L'albero delle espressioni può contenere una semplice chiamata a un metodo di conversione per conversioni complesse.

Nota

Una proprietà configurata per la conversione di valori può anche dover specificare un oggetto ValueComparer<T>. Per altre informazioni, vedere gli esempi seguenti e la documentazione di Confronto valori.

Configurazione di un convertitore di valori

Le conversioni di valori vengono configurate in DbContext.OnModelCreating. Si consideri, ad esempio, un'enumerazione e un tipo di entità definiti come:

public class Rider
{
    public int Id { get; set; }
    public EquineBeast Mount { get; set; }
}

public enum EquineBeast
{
    Donkey,
    Mule,
    Horse,
    Unicorn
}

Le conversioni possono essere configurate in OnModelCreating per archiviare i valori di enumerazione come stringhe come "Donkey", "Mule" e così via nel database. È sufficiente fornire una funzione che converte da ModelClrType a ProviderClrTypee un'altra per la conversione opposta:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(
            v => v.ToString(),
            v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}

Nota

Un null valore non verrà mai passato a un convertitore di valori. Un valore Null in una colonna di database è sempre null nell'istanza dell'entità e viceversa. In questo modo l'implementazione delle conversioni risulta più semplice e consente di condividerle tra proprietà nullable e non nullable. Per altre informazioni, vedere Problema di GitHub n. 13850 .

Configurazione bulk di un convertitore di valori

È comune configurare lo stesso convertitore di valori per ogni proprietà che usa il tipo CLR pertinente. Anziché eseguire questa operazione manualmente per ogni proprietà, è possibile usare la configurazione del modello pre-convenzione per eseguire questa operazione una sola volta per l'intero modello. A tale scopo, definire il convertitore di valori come classe:

public class CurrencyConverter : ValueConverter<Currency, decimal>
{
    public CurrencyConverter()
        : base(
            v => v.Amount,
            v => new Currency(v))
    {
    }
}

Eseguire quindi l'override nel tipo di contesto e configurare il convertitore ConfigureConventions come segue:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder
        .Properties<Currency>()
        .HaveConversion<CurrencyConverter>();
}

Conversioni predefinite

EF Core contiene molte conversioni predefinite che evitano la necessità di scrivere manualmente le funzioni di conversione. EF Core sceglierà invece la conversione da usare in base al tipo di proprietà nel modello e al tipo di provider di database richiesto.

Ad esempio, le conversioni di enumerazione in stringhe vengono usate come esempio precedente, ma EF Core eseguirà questa operazione automaticamente quando il tipo di provider viene configurato come string usando il tipo generico di HasConversion:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion<string>();
}

La stessa operazione può essere ottenuta specificando in modo esplicito il tipo di colonna del database. Ad esempio, se il tipo di entità è definito come segue:

public class Rider2
{
    public int Id { get; set; }

    [Column(TypeName = "nvarchar(24)")]
    public EquineBeast Mount { get; set; }
}

I valori di enumerazione verranno quindi salvati come stringhe nel database senza ulteriori configurazioni in OnModelCreating.

Classe ValueConverter

Chiamando HasConversion come illustrato in precedenza, verrà creata un'istanza ValueConverter<TModel,TProvider> e impostata sulla proprietà . Può ValueConverter invece essere creato in modo esplicito. Ad esempio:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var converter = new ValueConverter<EquineBeast, string>(
        v => v.ToString(),
        v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));

    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(converter);
}

Ciò può essere utile quando più proprietà usano la stessa conversione.

Convertitori predefiniti

Come accennato in precedenza, EF Core viene fornito con un set di classi predefinite ValueConverter<TModel,TProvider> , disponibile nello spazio dei Microsoft.EntityFrameworkCore.Storage.ValueConversion nomi . In molti casi EF sceglierà il convertitore predefinito appropriato in base al tipo della proprietà nel modello e al tipo richiesto nel database, come illustrato in precedenza per le enumerazioni. Ad esempio, l'uso di .HasConversion<int>() in una bool proprietà fa sì che EF Core converta i valori bool in zero numerici e uno:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<User>()
        .Property(e => e.IsActive)
        .HasConversion<int>();
}

Ciò equivale a creare un'istanza dell'istanza predefinita BoolToZeroOneConverter<TProvider> e impostarla in modo esplicito:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var converter = new BoolToZeroOneConverter<int>();

    modelBuilder
        .Entity<User>()
        .Property(e => e.IsActive)
        .HasConversion(converter);
}

La tabella seguente riepiloga le conversioni predefinite comunemente usate dai tipi di modello/proprietà ai tipi di provider di database. Nella tabella any_numeric_type si intende uno di int, shortlong, byte, , uint, ushortulong, decimalfloatsbytecharo .double

Tipo di modello/proprietà Tipo di provider/database Conversione Utilizzo
bool any_numeric_type False/true a 0/1 .HasConversion<any_numeric_type>()
any_numeric_type False/true a due numeri Utilizzare BoolToTwoValuesConverter<TProvider>.
string False/true su "N"/"Y" .HasConversion<string>()
string False/true per due stringhe Utilizzare BoolToStringConverter.
any_numeric_type bool Da 0/1 a false/true .HasConversion<bool>()
any_numeric_type Cast semplice .HasConversion<any_numeric_type>()
string Numero come stringa .HasConversion<string>()
Enum any_numeric_type Valore numerico dell'enumerazione .HasConversion<any_numeric_type>()
string Rappresentazione di stringa del valore di enumerazione .HasConversion<string>()
string bool Analizza la stringa come valore bool .HasConversion<bool>()
any_numeric_type Analizza la stringa come tipo numerico specificato .HasConversion<any_numeric_type>()
char Primo carattere della stringa .HasConversion<char>()
Data/Ora Analizza la stringa come DateTime .HasConversion<DateTime>()
DateTimeOffset Analizza la stringa come DateTimeOffset .HasConversion<DateTimeOffset>()
TimeSpan Analizza la stringa come TimeSpan .HasConversion<TimeSpan>()
GUID Analizza la stringa come GUID .HasConversion<Guid>()
byte[] Stringa come byte UTF8 .HasConversion<byte[]>()
char string Una singola stringa di caratteri .HasConversion<string>()
Data/Ora long Data/ora codificata che mantiene DateTime.Kind .HasConversion<long>()
long Ticks Utilizzare DateTimeToTicksConverter.
string Stringa di data/ora delle impostazioni cultura invarianti .HasConversion<string>()
DateTimeOffset long Data/ora codificate con offset .HasConversion<long>()
string Stringa di data/ora delle impostazioni cultura invariante con offset .HasConversion<string>()
TimeSpan long Ticks .HasConversion<long>()
string Stringa dell'intervallo di tempo delle impostazioni cultura invarianti .HasConversion<string>()
URI string URI come stringa .HasConversion<string>()
PhysicalAddress string Indirizzo come stringa .HasConversion<string>()
byte[] Byte in ordine di rete big-endian .HasConversion<byte[]>()
IPAddress string Indirizzo come stringa .HasConversion<string>()
byte[] Byte in ordine di rete big-endian .HasConversion<byte[]>()
GUID string GUID nel formato 'ddddd-dddd-dddd-dddd' .HasConversion<string>()
byte[] Byte nell'ordine di serializzazione binaria .NET .HasConversion<byte[]>()

Si noti che queste conversioni presuppongono che il formato del valore sia appropriato per la conversione. Ad esempio, la conversione di stringhe in numeri avrà esito negativo se i valori stringa non possono essere analizzati come numeri.

L'elenco completo dei convertitori predefiniti è:

Si noti che tutti i convertitori predefiniti sono senza stato e quindi una singola istanza può essere condivisa in modo sicuro da più proprietà.

Facet di colonna e hint di mapping

Alcuni tipi di database hanno facet che modificano la modalità di archiviazione dei dati. tra cui:

  • Precisione e scala per le colonne decimali e di data/ora
  • Dimensioni/lunghezza per le colonne binarie e stringa
  • Unicode per le colonne stringa

Questi facet possono essere configurati nel modo normale per una proprietà che usa un convertitore di valori e verranno applicati al tipo di database convertito. Ad esempio, quando si esegue la conversione da un'enumerazione a stringhe, è possibile specificare che la colonna del database deve essere non Unicode e archiviare fino a 20 caratteri:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion<string>()
        .HasMaxLength(20)
        .IsUnicode(false);
}

In alternativa, quando si crea il convertitore in modo esplicito:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var converter = new ValueConverter<EquineBeast, string>(
        v => v.ToString(),
        v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));

    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(converter)
        .HasMaxLength(20)
        .IsUnicode(false);
}

Ciò comporta la visualizzazione di una varchar(20) colonna quando si usano le migrazioni di EF Core su SQL Server:

CREATE TABLE [Rider] (
    [Id] int NOT NULL IDENTITY,
    [Mount] varchar(20) NOT NULL,
    CONSTRAINT [PK_Rider] PRIMARY KEY ([Id]));

Tuttavia, se per impostazione predefinita tutte le EquineBeast colonne devono essere varchar(20), queste informazioni possono essere fornite al convertitore di valori come .ConverterMappingHints Ad esempio:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var converter = new ValueConverter<EquineBeast, string>(
        v => v.ToString(),
        v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v),
        new ConverterMappingHints(size: 20, unicode: false));

    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(converter);
}

A questo punto, ogni volta che viene usato questo convertitore, la colonna del database sarà non Unicode con una lunghezza massima di 20. Tuttavia, questi sono solo suggerimenti poiché vengono sottoposti a override da qualsiasi facet impostato in modo esplicito sulla proprietà mappata.

Esempi

Oggetti valore semplice

In questo esempio viene usato un tipo semplice per eseguire il wrapping di un tipo primitivo. Ciò può essere utile quando si vuole che il tipo nel modello sia più specifico (e quindi più indipendente dai tipi) rispetto a un tipo primitivo. In questo esempio, il tipo è Dollars, che esegue il wrapping della primitiva decimale:

public readonly struct Dollars
{
    public Dollars(decimal amount) 
        => Amount = amount;
        
    public decimal Amount { get; }

    public override string ToString() 
        => $"${Amount}";
}

Questa operazione può essere usata in un tipo di entità:

public class Order
{
    public int Id { get; set; }

    public Dollars Price { get; set; }
}

E convertito nell'oggetto sottostante decimal quando archiviato nel database:

modelBuilder.Entity<Order>()
    .Property(e => e.Price)
    .HasConversion(
        v => v.Amount,
        v => new Dollars(v));

Nota

Questo oggetto valore viene implementato come struct readonly. Questo significa che EF Core può creare snapshot e confrontare i valori senza problemi. Per altre informazioni, vedere Confronto valori .

Oggetti valore composito

Nell'esempio precedente, il tipo di oggetto value conteneva solo una singola proprietà. È più comune che un tipo di oggetto valore componi più proprietà che insieme formano un concetto di dominio. Ad esempio, un tipo generale Money che contiene sia l'importo che la valuta:

public readonly struct Money
{
    [JsonConstructor]
    public Money(decimal amount, Currency currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public override string ToString()
        => (Currency == Currency.UsDollars ? "$" : "£") + Amount;

    public decimal Amount { get; }
    public Currency Currency { get; }
}

public enum Currency
{
    UsDollars,
    PoundsSterling
}

Questo oggetto valore può essere usato in un tipo di entità come prima:

public class Order
{
    public int Id { get; set; }

    public Money Price { get; set; }
}

I convertitori di valori possono attualmente convertire solo i valori in e da una singola colonna di database. Questa limitazione indica che tutti i valori delle proprietà dell'oggetto devono essere codificati in un singolo valore di colonna. Questa operazione viene in genere gestita serializzando l'oggetto durante l'inserimento nel database e quindi deserializzandolo nuovamente all'uscita. Ad esempio, usando System.Text.Json:

modelBuilder.Entity<Order>()
    .Property(e => e.Price)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null));

Nota

Si prevede di consentire il mapping di un oggetto a più colonne in una versione futura di EF Core, eliminando la necessità di usare la serializzazione qui. Questo problema viene rilevato dal problema di GitHub n. 13947.

Nota

Come nell'esempio precedente, questo oggetto valore viene implementato come struct readonly. Questo significa che EF Core può creare snapshot e confrontare i valori senza problemi. Per altre informazioni, vedere Confronto valori .

Raccolte di primitive

La serializzazione può essere usata anche per archiviare una raccolta di valori primitivi. Ad esempio:

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Contents { get; set; }

    public ICollection<string> Tags { get; set; }
}

Usando System.Text.Json di nuovo:

modelBuilder.Entity<Post>()
    .Property(e => e.Tags)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions)null),
        new ValueComparer<ICollection<string>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => (ICollection<string>)c.ToList()));

ICollection<string> rappresenta un tipo riferimento modificabile. Ciò significa che è necessario un oggetto ValueComparer<T> in modo che EF Core possa tenere traccia e rilevare correttamente le modifiche. Per altre informazioni, vedere Confronto valori .

Raccolte di oggetti valore

Combinando i due esempi precedenti, è possibile creare una raccolta di oggetti valore. Si consideri, ad esempio, un AnnualFinance tipo che modella le finanze del blog per un singolo anno:

public readonly struct AnnualFinance
{
    [JsonConstructor]
    public AnnualFinance(int year, Money income, Money expenses)
    {
        Year = year;
        Income = income;
        Expenses = expenses;
    }

    public int Year { get; }
    public Money Income { get; }
    public Money Expenses { get; }
    public Money Revenue => new Money(Income.Amount - Expenses.Amount, Income.Currency);
}

Questo tipo compone diversi tipi Money creati in precedenza:

public readonly struct Money
{
    [JsonConstructor]
    public Money(decimal amount, Currency currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public override string ToString()
        => (Currency == Currency.UsDollars ? "$" : "£") + Amount;

    public decimal Amount { get; }
    public Currency Currency { get; }
}

public enum Currency
{
    UsDollars,
    PoundsSterling
}

È quindi possibile aggiungere una raccolta di al tipo di AnnualFinance entità:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IList<AnnualFinance> Finances { get; set; }
}

Usare di nuovo la serializzazione per archiviare il codice seguente:

modelBuilder.Entity<Blog>()
    .Property(e => e.Finances)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<List<AnnualFinance>>(v, (JsonSerializerOptions)null),
        new ValueComparer<IList<AnnualFinance>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => (IList<AnnualFinance>)c.ToList()));

Nota

Come in precedenza, questa conversione richiede un oggetto ValueComparer<T>. Per altre informazioni, vedere Confronto valori .

Oggetti valore come chiavi

A volte le proprietà chiave primitive possono essere incluse in oggetti valore per aggiungere un livello aggiuntivo di sicurezza dei tipi nell'assegnazione di valori. Ad esempio, è possibile implementare un tipo di chiave per i blog e un tipo di chiave per i post:

public readonly struct BlogKey
{
    public BlogKey(int id) => Id = id;
    public int Id { get; }
}

public readonly struct PostKey
{
    public PostKey(int id) => Id = id;
    public int Id { get; }
}

Questi possono quindi essere usati nel modello di dominio:

public class Blog
{
    public BlogKey Id { get; set; }
    public string Name { get; set; }

    public ICollection<Post> Posts { get; set; }
}

public class Post
{
    public PostKey Id { get; set; }

    public string Title { get; set; }
    public string Content { get; set; }

    public BlogKey? BlogId { get; set; }
    public Blog Blog { get; set; }
}

Si noti che Blog.Id non è possibile assegnare accidentalmente un oggetto PostKeye Post.Id non è possibile assegnare accidentalmente un oggetto BlogKey. Analogamente, alla Post.BlogId proprietà della chiave esterna deve essere assegnato un oggetto BlogKey.

Nota

La visualizzazione di questo modello non significa che sia consigliabile. Valutare attentamente se questo livello di astrazione sta aiutando o ostacolando l'esperienza di sviluppo. Prendere in considerazione anche l'uso di spostamenti e chiavi generate invece di gestire direttamente i valori delle chiavi.

È quindi possibile eseguire il mapping di queste proprietà chiave usando convertitori di valori:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var blogKeyConverter = new ValueConverter<BlogKey, int>(
        v => v.Id,
        v => new BlogKey(v));

    modelBuilder.Entity<Blog>().Property(e => e.Id).HasConversion(blogKeyConverter);

    modelBuilder.Entity<Post>(
        b =>
        {
            b.Property(e => e.Id).HasConversion(v => v.Id, v => new PostKey(v));
            b.Property(e => e.BlogId).HasConversion(blogKeyConverter);
        });
}

Nota

Le proprietà chiave con conversioni possono usare solo valori di chiave generati a partire da EF Core 7.0.

Usare ulong per timestamp/rowversion

SQL Server supporta la concorrenza ottimistica automatica usando colonne binarie rowversion/timestamp a 8 byte. Queste operazioni vengono sempre lette e scritte nel database usando una matrice a 8 byte. Tuttavia, le matrici di byte sono un tipo riferimento modificabile, che li rende un po 'dolorosi da gestire. I convertitori di valori consentono invece di eseguire il rowversion mapping a una ulong proprietà, che è molto più appropriata e facile da usare rispetto alla matrice di byte. Si consideri ad esempio un'entità Blog con un token di concorrenza ulong:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ulong Version { get; set; }
}

È possibile eseguire il mapping a una colonna di SQL Server rowversion usando un convertitore di valori:

modelBuilder.Entity<Blog>()
    .Property(e => e.Version)
    .IsRowVersion()
    .HasConversion<byte[]>();

Specificare DateTime.Kind durante la lettura delle date

SQL Server elimina il DateTime.Kind flag quando si archivia un oggetto DateTime come datetime o datetime2. Ciò significa che i valori DateTime restituiti dal database hanno sempre un DateTimeKind valore di Unspecified.

I convertitori di valori possono essere usati in due modi per gestire questo problema. In primo luogo, EF Core ha un convertitore di valori che crea un valore opaco a 8 byte che mantiene il Kind flag. Ad esempio:

modelBuilder.Entity<Post>()
    .Property(e => e.PostedOn)
    .HasConversion<long>();

In questo modo, i valori DateTime con flag diversi Kind possono essere misti nel database.

Il problema con questo approccio è che il database non ha più colonne o datetime2 riconoscibilidatetime. In alternativa, è comune archiviare sempre l'ora UTC (o, meno comunemente, sempre l'ora locale) e quindi ignorare il Kind flag o impostarlo sul valore appropriato usando un convertitore di valori. Ad esempio, il convertitore seguente garantisce che il DateTime valore letto dal database abbia :DateTimeKind UTC

modelBuilder.Entity<Post>()
    .Property(e => e.LastUpdated)
    .HasConversion(
        v => v,
        v => new DateTime(v.Ticks, DateTimeKind.Utc));

Se viene impostata una combinazione di valori locali e UTC nelle istanze di entità, è possibile usare il convertitore per la conversione in modo appropriato prima dell'inserimento. Ad esempio:

modelBuilder.Entity<Post>()
    .Property(e => e.LastUpdated)
    .HasConversion(
        v => v.ToUniversalTime(),
        v => new DateTime(v.Ticks, DateTimeKind.Utc));

Nota

Valutare attentamente la possibilità di unificare tutto il codice di accesso al database per usare sempre l'ora UTC, solo quando si presentano i dati agli utenti.

Usare chiavi stringa senza distinzione tra maiuscole e minuscole

Alcuni database, incluso SQL Server, eseguono confronti tra stringhe senza distinzione tra maiuscole e minuscole per impostazione predefinita. .NET, d'altra parte, esegue confronti di stringhe con distinzione tra maiuscole e minuscole per impostazione predefinita. Ciò significa che un valore di chiave esterna come "DotNet" corrisponderà al valore della chiave primaria "dotnet" in SQL Server, ma non lo corrisponderà in EF Core. Un operatore di confronto di valori per le chiavi può essere usato per forzare EF Core in confronti di stringhe senza distinzione tra maiuscole e minuscole, ad esempio nel database. Si consideri, ad esempio, un modello di blog/post con chiavi stringa:

public class Blog
{
    public string Id { get; set; }
    public string Name { get; set; }

    public ICollection<Post> Posts { get; set; }
}

public class Post
{
    public string Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public string BlogId { get; set; }
    public Blog Blog { get; set; }
}

Ciò non funzionerà come previsto se alcuni valori Post.BlogId hanno maiuscole e minuscole diverse. Gli errori causati da questo dipenderanno dalle operazioni eseguite dall'applicazione, ma in genere comportano grafici di oggetti non corretti e/o aggiornamenti che hanno esito negativo perché il valore FK non è corretto. Per correggere questo problema, è possibile usare un operatore di confronto di valori:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var comparer = new ValueComparer<string>(
        (l, r) => string.Equals(l, r, StringComparison.OrdinalIgnoreCase),
        v => v.ToUpper().GetHashCode(),
        v => v);

    modelBuilder.Entity<Blog>()
        .Property(e => e.Id)
        .Metadata.SetValueComparer(comparer);

    modelBuilder.Entity<Post>(
        b =>
        {
            b.Property(e => e.Id).Metadata.SetValueComparer(comparer);
            b.Property(e => e.BlogId).Metadata.SetValueComparer(comparer);
        });
}

Nota

I confronti tra stringhe .NET e i confronti tra stringhe di database possono differire in modo più che semplice con distinzione tra maiuscole e minuscole. Questo modello funziona per le semplici chiavi ASCII, ma potrebbe non riuscire per le chiavi con qualsiasi tipo di caratteri specifici delle impostazioni cultura. Per altre informazioni, vedere Regole di confronto e distinzione tra maiuscole e minuscole.

Gestire stringhe di database a lunghezza fissa

L'esempio precedente non richiedeva un convertitore di valori. Tuttavia, un convertitore può essere utile per i tipi di stringa di database a lunghezza fissa, ad esempio char(20) o nchar(20). Le stringhe a lunghezza fissa vengono riempite a lunghezza intera ogni volta che un valore viene inserito nel database. Ciò significa che un valore della chiave "dotnet" verrà letto nuovamente dal database come "dotnet..............", dove . rappresenta un carattere di spazio. Questo non verrà quindi confrontato correttamente con i valori di chiave non riempiti.

È possibile usare un convertitore di valori per tagliare la spaziatura interna durante la lettura dei valori delle chiavi. Questa operazione può essere combinata con l'operatore di confronto dei valori nell'esempio precedente per confrontare correttamente le chiavi ASCII a lunghezza fissa senza distinzione tra maiuscole e minuscole. Ad esempio:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var converter = new ValueConverter<string, string>(
        v => v,
        v => v.Trim());

    var comparer = new ValueComparer<string>(
        (l, r) => string.Equals(l, r, StringComparison.OrdinalIgnoreCase),
        v => v.ToUpper().GetHashCode(),
        v => v);

    modelBuilder.Entity<Blog>()
        .Property(e => e.Id)
        .HasColumnType("char(20)")
        .HasConversion(converter, comparer);

    modelBuilder.Entity<Post>(
        b =>
        {
            b.Property(e => e.Id).HasColumnType("char(20)").HasConversion(converter, comparer);
            b.Property(e => e.BlogId).HasColumnType("char(20)").HasConversion(converter, comparer);
        });
}

Crittografare i valori delle proprietà

I convertitori di valori possono essere usati per crittografare i valori delle proprietà prima di inviarli al database e quindi decrittografarli in uscita. Ad esempio, l'uso dell'inversione delle stringhe come sostituto di un algoritmo di crittografia reale:

modelBuilder.Entity<User>().Property(e => e.Password).HasConversion(
    v => new string(v.Reverse().ToArray()),
    v => new string(v.Reverse().ToArray()));

Nota

Attualmente non è possibile ottenere un riferimento all'oggetto DbContext corrente o ad altri stati della sessione all'interno di un convertitore di valori. Questo limita i tipi di crittografia che è possibile usare. Votare per il problema GitHub #11597 per avere rimosso questa limitazione.

Avviso

Assicurarsi di comprendere tutte le implicazioni se si esegue il rollback della crittografia per proteggere i dati sensibili. Considerare invece l'uso di meccanismi di crittografia predefiniti, ad esempio Always Encrypted in SQL Server.

Limiti

Esistono alcune limitazioni correnti note del sistema di conversione dei valori:

  • Come indicato in precedenza, null non può essere convertito. Votare (👍) per il problema di GitHub #13850 , se necessario.
  • Non è possibile eseguire query in proprietà convertite con valori, ad esempio i membri di riferimento nel tipo .NET convertito con valore nelle query LINQ. Votare (👍) per il problema di GitHub #10434 se è necessario, ma prendere in considerazione l'uso di una colonna JSON.
  • Attualmente non è possibile distribuire una conversione di una proprietà in più colonne o viceversa. Votare (👍) per il problema GitHub #13947 , se necessario.
  • La generazione di valori non è supportata per la maggior parte delle chiavi mappate tramite convertitori di valori. Votare (👍) per il problema GitHub #11597 , se necessario.
  • Le conversioni di valori non possono fare riferimento all'istanza dbContext corrente. Votare (👍) per il problema di GitHub #12205 , se necessario.
  • I parametri che usano tipi con conversione con valore non possono essere attualmente usati nelle API SQL non elaborate. Votare (👍) per il problema di GitHub #27534 , se necessario.

La rimozione di queste limitazioni viene considerata per le versioni future.