Modifiche di rilievo in EF Core 8 (EF8)
Questa pagina illustra le modifiche all'API e al comportamento che potrebbero interrompere l'aggiornamento delle applicazioni esistenti da EF Core 7 a EF Core 8. Assicurarsi di esaminare le modifiche di rilievo precedenti se si esegue l'aggiornamento da una versione precedente di EF Core:
Framework di destinazione
EF Core 8 è destinato a .NET 8. Le applicazioni destinate a versioni precedenti di .NET, .NET Core e .NET Framework dovranno eseguire l'aggiornamento per .NET 8.
Riepilogo
Modifiche ad alto impatto
Contains
nelle query LINQ potrebbe smettere di funzionare nelle versioni precedenti di SQL Server
Problema di rilevamento n. 13617
Comportamento precedente
EF aveva un supporto specializzato per le query LINQ usando l'operatore Contains
su una lista di valori parametrizzati.
var names = new[] { "Blog1", "Blog2" };
var blogs = await context.Blogs
.Where(b => names.Contains(b.Name))
.ToArrayAsync();
Prima di EF Core 8.0, EF ha inserito i valori con parametri come costanti in SQL:
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (N'Blog1', N'Blog2')
Nuovo comportamento
A partire da EF Core 8.0, EF genera ora SQL più efficiente in molti casi, ma non è supportato in SQL Server 2014 e versioni successive:
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (
SELECT [n].[value]
FROM OPENJSON(@__names_0) WITH ([value] nvarchar(max) '$') AS [n]
)
Si noti che le versioni più recenti di SQL Server possono essere configurate con un livello di compatibilità precedente, rendendole incompatibili anche con il nuovo SQL. Ciò può verificarsi anche con un database SQL di Azure di cui è stata eseguita la migrazione da un'istanza di SQL Server locale precedente, con il livello di compatibilità precedente.
Perché
L'inserimento di valori costanti in SQL crea molti problemi di prestazioni, vanificando la memorizzazione nella cache del piano di query e causando rimozioni non necessarie di altre query. La nuova conversione di EF Core 8.0 usa la funzione SQL Server OPENJSON
per trasferire invece i valori come matrice JSON. Questo risolve i problemi di prestazioni intrinseci nella tecnica precedente; Tuttavia, la OPENJSON
funzione non è disponibile in SQL Server 2014 e versioni successive.
Per altre informazioni su questa modifica, vedere questo post di blog.
Soluzioni di prevenzione
Se il database è SQL Server 2016 (13.x) o versione successiva o se si usa Azure SQL, controllare il livello di compatibilità configurato del database tramite il comando seguente:
SELECT name, compatibility_level FROM sys.databases;
Se il livello di compatibilità è inferiore a 130 (SQL Server 2016), è consigliabile modificarlo in un valore più recente (documentazione).
In caso contrario, se la versione del database è effettivamente precedente a SQL Server 2016 o è impostata su un livello di compatibilità precedente che non è possibile modificare per qualche motivo, è possibile configurare Entity Framework per ripristinare sql precedente, precedente alla versione 8.0. Se si usa EF 9, è possibile usare il TranslateParameterizedCollectionsToConstantsappena introdotto:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer("<CONNECTION STRING>", o => o.TranslateParameterizedCollectionsToConstants())
Se si usa EF 8, è possibile ottenere lo stesso effetto quando si usa SQL Server configurando il livello di compatibilità SQL di EF:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"<CONNECTION STRING>", o => o.UseCompatibilityLevel(120));
Possibili regressioni delle prestazioni intorno a Contains
nelle query LINQ
problema di rilevamento n. 32394
Comportamento precedente
EF aveva un supporto specializzato per le query LINQ, utilizzando l'operatore Contains
su un elenco di valori parametrizzati.
var names = new[] { "Blog1", "Blog2" };
var blogs = await context.Blogs
.Where(b => names.Contains(b.Name))
.ToArrayAsync();
Prima di EF Core 8.0, EF ha inserito i valori con parametri come costanti in SQL:
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (N'Blog1', N'Blog2')
Nuovo comportamento
A partire da EF Core 8.0, EF genera ora quanto segue:
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (
SELECT [n].[value]
FROM OPENJSON(@__names_0) WITH ([value] nvarchar(max) '$') AS [n]
)
Tuttavia, dopo il rilascio di EF 8 si è scoperto che, mentre il nuovo SQL è più efficiente per la maggior parte dei casi, può essere notevolmente meno efficiente in una minoranza di casi, anche causando timeout delle query in alcuni casi
Soluzioni di prevenzione
Se stai usando EF 9, puoi usare il TranslateParameterizedCollectionsToConstants appena introdotto per ripristinare la traduzione Contains
per tutte le query tornando al comportamento della versione precedente alla 8.0:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer("<CONNECTION STRING>", o => o.TranslateParameterizedCollectionsToConstants())
Se si usa EF 8, è possibile ottenere lo stesso effetto quando si usa SQL Server configurando il livello di compatibilità SQL di EF:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"<CONNECTION STRING>", o => o.UseCompatibilityLevel(120));
Infine, è possibile controllare la traduzione per ogni query usando EF.Constant come indicato di seguito:
var blogs = await context.Blogs
.Where(b => EF.Constant(names).Contains(b.Name))
.ToArrayAsync();
Le enumerazioni in JSON vengono archiviate come ints anziché come stringhe per impostazione predefinita
Problema di rilevamento n. 13617
Comportamento precedente
In EF7 le enumerazioni mappate a JSON sono, per impostazione predefinita, archiviate come valori stringa nel documento JSON.
Nuovo comportamento
A partire da EF Core 8.0, EF ora, per impostazione predefinita, esegue il mapping delle enumerazioni ai valori interi nel documento JSON.
Perché
Ef ha sempre, per impostazione predefinita, enumerazioni mappate a una colonna numerica nei database relazionali. Poiché EF supporta le query in cui i valori di JSON interagiscono con valori di colonne e parametri, è importante che i valori in JSON corrispondano ai valori nella colonna non JSON.
Soluzioni di prevenzione
Per continuare a usare le stringhe, configurare la proprietà enumerazione con una conversione. Ad esempio:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().Property(e => e.Status).HasConversion<string>();
}
In alternativa, per tutte le proprietà del tipo di enumerazione::
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<StatusEnum>().HaveConversion<string>();
}
Modifiche a impatto medio
SQL Server date
e time
ora eseguire lo scaffolding in .NET DateOnly
e TimeOnly
Problema di rilevamento n. 24507
Comportamento precedente
In precedenza, quando si esegue lo scaffolding di un database di SQL Server con date
colonne o time
, Entity Framework generava proprietà di entità con tipi DateTime e TimeSpan.
Nuovo comportamento
A partire da EF Core 8.0 date
e time
vengono scaffolding come DateOnly e TimeOnly.
Perché
DateOnly e TimeOnly sono stati introdotti in .NET 6.0 e sono una corrispondenza perfetta per il mapping dei tipi di data e ora del database.
DateTime contiene in particolare un componente temporale che passa inutilizzato e può causare confusione durante il mapping a date
e TimeSpan rappresenta un intervallo di tempo, possibilmente inclusi i giorni, anziché un'ora del giorno in cui si verifica un evento. L'uso dei nuovi tipi impedisce bug e confusione e fornisce chiarezza sulla finalità.
Soluzioni di prevenzione
Questa modifica influisce solo sugli utenti che eseguono regolarmente lo scaffolding del database in un modello di codice EF ("flusso database-first").
È consigliabile reagire a questa modifica modificando il codice per usare i nuovi tipi e DateOnly scaffoldingTimeOnly. Tuttavia, se non è possibile, è possibile modificare i modelli di scaffolding per ripristinare il mapping precedente. A tale scopo, configurare i modelli come descritto in questa pagina. Modificare quindi il EntityType.t4
file, individuare dove vengono generate le proprietà dell'entità (cercare property.ClrType
) e modificare il codice come segue:
var clrType = property.GetColumnType() switch
{
"date" when property.ClrType == typeof(DateOnly) => typeof(DateTime),
"date" when property.ClrType == typeof(DateOnly?) => typeof(DateTime?),
"time" when property.ClrType == typeof(TimeOnly) => typeof(TimeSpan),
"time" when property.ClrType == typeof(TimeOnly?) => typeof(TimeSpan?),
_ => property.ClrType
};
usings.AddRange(code.GetRequiredUsings(clrType));
var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !clrType.IsValueType;
var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !clrType.IsValueType;
#>
public <#= code.Reference(clrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
Le colonne booleane con un valore generato dal database non vengono più scaffolding come nullable
Problema di rilevamento n. 15070
Comportamento precedente
In precedenza, le colonne non nullable bool
con un vincolo predefinito del database venivano scaffolding come proprietà nullable bool?
.
Nuovo comportamento
A partire da EF Core 8.0, le colonne non nullable bool
vengono sempre scaffolding come proprietà non nullable.
Perché
Una bool
proprietà non avrà il valore inviato al database se tale valore è false
, ovvero l'impostazione predefinita CLR. Se il database ha un valore predefinito per true
la colonna, anche se il valore della proprietà è false
, il valore nel database finisce come true
. In EF8, tuttavia, sentinel usato per determinare se una proprietà ha un valore può essere modificato. Questa operazione viene eseguita automaticamente per bool
le proprietà con un valore generato dal database , true
il che significa che non è più necessario eseguire lo scaffolding delle proprietà come nullable.
Soluzioni di prevenzione
Questa modifica influisce solo sugli utenti che eseguono regolarmente lo scaffolding del database in un modello di codice EF ("flusso database-first").
È consigliabile reagire a questa modifica modificando il codice per usare la proprietà bool non nullable. Tuttavia, se non è possibile, è possibile modificare i modelli di scaffolding per ripristinare il mapping precedente. A tale scopo, configurare i modelli come descritto in questa pagina. Modificare quindi il EntityType.t4
file, individuare dove vengono generate le proprietà dell'entità (cercare property.ClrType
) e modificare il codice come segue:
#>
var propertyClrType = property.ClrType != typeof(bool)
|| (property.GetDefaultValueSql() == null && property.GetDefaultValue() != null)
? property.ClrType
: typeof(bool?);
#>
public <#= code.Reference(propertyClrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
<#
Modifiche a basso impatto
I metodi SQLite Math
ora si traducono in SQL
Problema di rilevamento n. 18843
Comportamento precedente
In precedenza solo i metodi Abs, Max, Min e Round in Math
sono stati convertiti in SQL. Tutti gli altri membri verranno valutati sul client se sono visualizzati nell'espressione Select finale di una query.
Nuovo comportamento
In EF Core 8.0 tutti i Math
metodi con le funzioni matematiche SQLite corrispondenti vengono convertiti in SQL.
Queste funzioni matematiche sono state abilitate nella libreria SQLite nativa che viene fornito per impostazione predefinita (tramite la dipendenza dal pacchetto NuGet SQLitePCLRaw.bundle_e_sqlite3). Sono state abilitate anche nella libreria fornita da SQLitePCLRaw.bundle_e_sqlcipher. Se si usa una di queste librerie, l'applicazione non deve essere interessata da questa modifica.
È tuttavia possibile che le applicazioni che includono la libreria SQLite nativa non possano abilitare le funzioni matematiche. In questi casi, i Math
metodi verranno convertiti in SQL e non si verificano errori di tale funzione durante l'esecuzione.
Perché
SQLite ha aggiunto funzioni matematiche predefinite nella versione 3.35.0. Anche se sono disabilitati per impostazione predefinita, sono diventati abbastanza diffusi che abbiamo deciso di fornire traduzioni predefinite per loro nel provider SQLite di EF Core.
Abbiamo anche collaborato con Eric Sink nel progetto SQLitePCLRaw per abilitare le funzioni matematiche in tutte le librerie SQLite native fornite come parte di tale progetto.
Soluzioni di prevenzione
Il modo più semplice per correggere le interruzioni è, quando possibile, per abilitare la funzione matematica è la libreria SQLite nativa specificando l'opzione SQLITE_ENABLE_MATH_FUNCTIONS in fase di compilazione.
Se non si controlla la compilazione della libreria nativa, è anche possibile correggere le interruzioni tramite la creazione delle funzioni in fase di esecuzione usando le API Microsoft.Data.Sqlite .
sqliteConnection
.CreateFunction<double, double, double>(
"pow",
Math.Pow,
isDeterministic: true);
In alternativa, è possibile forzare la valutazione client suddividendo l'espressione Select in due parti separate da AsEnumerable
.
// Before
var query = dbContext.Cylinders
.Select(
c => new
{
Id = c.Id
// May throw "no such function: pow"
Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
});
// After
var query = dbContext.Cylinders
// Select the properties you'll need from the database
.Select(
c => new
{
c.Id,
c.Radius,
c.Height
})
// Switch to client-eval
.AsEnumerable()
// Select the final results
.Select(
c => new
{
Id = c.Id,
Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
});
ITypeBase sostituisce IEntityType in alcune API
Problema di rilevamento n. 13947
Comportamento precedente
In precedenza, tutti i tipi strutturali mappati erano tipi di entità.
Nuovo comportamento
Con l'introduzione di tipi complessi in EF8, alcune API usate in precedenza usano IEntityType
ora ITypeBase
in modo che le API possano essere usate con tipi complessi o di entità. Valuta gli ambiti seguenti:
-
IProperty.DeclaringEntityType
è ora obsoleto eIProperty.DeclaringType
deve essere invece usato. -
IEntityTypeIgnoredConvention
è ora obsoleto eITypeIgnoredConvention
deve essere invece usato. -
IValueGeneratorSelector.Select
accetta ora un oggettoITypeBase
che può essere, ma non deve essere unIEntityType
oggetto .
Perché
Con l'introduzione di tipi complessi in EF8, queste API possono essere usate con IEntityType
o IComplexType
.
Soluzioni di prevenzione
Le API precedenti sono obsolete, ma non verranno rimosse fino a EF10. Il codice deve essere aggiornato per usare la nuova API ASAP.
Le espressioni ValueConverter e ValueComparer devono usare API pubbliche per il modello compilato
Problema di rilevamento n. 24896
Comportamento precedente
In precedenza, ValueConverter
e ValueComparer
le definizioni non erano incluse nel modello compilato e quindi potevano contenere codice arbitrario.
Nuovo comportamento
Ef estrae ora le espressioni dagli ValueConverter
oggetti e ValueComparer
e include questi C# nel modello compilato. Ciò significa che queste espressioni devono usare solo l'API pubblica.
Perché
Il team ef sta gradualmente spostando più costrutti nel modello compilato per supportare l'uso di EF Core con AOT in futuro.
Soluzioni di prevenzione
Rendere pubbliche le API usate dall'operatore di confronto. Si consideri ad esempio questo semplice convertitore:
public class MyValueConverter : ValueConverter<string, byte[]>
{
public MyValueConverter()
: base(v => ConvertToBytes(v), v => ConvertToString(v))
{
}
private static string ConvertToString(byte[] bytes)
=> ""; // ... TODO: Conversion code
private static byte[] ConvertToBytes(string chars)
=> Array.Empty<byte>(); // ... TODO: Conversion code
}
Per usare questo convertitore in un modello compilato con EF8, i ConvertToString
metodi e ConvertToBytes
devono essere resi pubblici. Ad esempio:
public class MyValueConverter : ValueConverter<string, byte[]>
{
public MyValueConverter()
: base(v => ConvertToBytes(v), v => ConvertToString(v))
{
}
public static string ConvertToString(byte[] bytes)
=> ""; // ... TODO: Conversion code
public static byte[] ConvertToBytes(string chars)
=> Array.Empty<byte>(); // ... TODO: Conversion code
}
ExcludeFromMigrations non esclude più altre tabelle in una gerarchia TPC
Problema di rilevamento n. 30079
Comportamento precedente
In precedenza, l'uso ExcludeFromMigrations
di in una tabella in una gerarchia TPC escludeva anche altre tabelle nella gerarchia.
Nuovo comportamento
A partire da EF Core 8.0, ExcludeFromMigrations
non influisce sulle altre tabelle.
Perché
Il comportamento precedente era un bug e impediva l'uso delle migrazioni per gestire gerarchie tra progetti.
Soluzioni di prevenzione
Utilizzare ExcludeFromMigrations
in modo esplicito in qualsiasi altra tabella che deve essere esclusa.
Le chiavi integer non shadow vengono mantenute nei documenti Cosmos
Problema di rilevamento n. 31664
Comportamento precedente
In precedenza, le proprietà di interi non shadow che corrispondono ai criteri per essere una proprietà chiave sintetizzata non verrebbero rese persistenti nel documento JSON, ma venivano invece sintetizzate nuovamente all'uscita.
Nuovo comportamento
A partire da EF Core 8.0, queste proprietà sono ora persistenti.
Perché
Il comportamento precedente era un bug e impediva la persistenza delle proprietà che corrispondono ai criteri della chiave sintetizzata in Cosmos.
Soluzioni di prevenzione
Escludere la proprietà dal modello se il relativo valore non deve essere salvato in modo permanente.
Inoltre, è possibile disabilitare completamente questo comportamento impostando Microsoft.EntityFrameworkCore.Issue31664
l'opzione AppContext su true
, vedere AppContext per i consumer di libreria per altri dettagli.
AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue31664", isEnabled: true);
Il modello relazionale viene generato nel modello compilato
Problema di rilevamento n. 24896
Comportamento precedente
In precedenza, il modello relazionale veniva calcolato in fase di esecuzione anche quando si usava un modello compilato.
Nuovo comportamento
A partire da EF Core 8.0, il modello relazionale fa parte del modello compilato generato. Tuttavia, per modelli particolarmente di grandi dimensioni, la compilazione del file generato potrebbe non riuscire.
Perché
Questa operazione è stata eseguita per migliorare ulteriormente il tempo di avvio.
Soluzioni di prevenzione
Modificare il file generato *ModelBuilder.cs
e rimuovere la riga AddRuntimeAnnotation("Relational:RelationalModel", CreateRelationalModel());
e il metodo CreateRelationalModel()
.
Lo scaffolding può generare nomi di navigazione diversi
Problema di rilevamento n. 27832
Comportamento precedente
In precedenza, quando si esegue lo scaffolding di tipi DbContext
di entità e da un database esistente, i nomi di navigazione per le relazioni venivano talvolta derivati da un prefisso comune di più nomi di colonna chiave esterna.
Nuovo comportamento
A partire da EF Core 8.0, i prefissi comuni dei nomi di colonna da una chiave esterna composita non vengono più usati per generare nomi di navigazione.
Perché
Si tratta di una regola di denominazione oscura che a volte genera nomi molto poveri come , S
Student_
, o anche solo _
. Senza questa regola, i nomi strani non vengono più generati e anche le convenzioni di denominazione per gli spostamenti sono rese più semplici, semplificando così la comprensione e la stima dei nomi che verranno generati.
Soluzioni di prevenzione
EF Core Power Tools ha la possibilità di continuare a generare spostamenti nel vecchio modo. In alternativa, il codice generato può essere completamente personalizzato usando i modelli T4. Può essere usato per esempio le proprietà di chiave esterna delle relazioni di scaffolding e usare qualsiasi regola appropriata per il codice per generare i nomi di navigazione necessari.
I discriminatori hanno ora una lunghezza massima
Problema di rilevamento n. 10691
Comportamento precedente
In precedenza, le colonne discriminatorie create per il mapping di ereditarietà TPH sono state configurate come nvarchar(max)
in SQL Server/Azure SQL o il tipo di stringa non associato equivalente in altri database.
Nuovo comportamento
A partire da EF Core 8.0, le colonne discriminatorie vengono create con una lunghezza massima che copre tutti i valori discriminatori noti. Entity Framework genererà una migrazione per apportare questa modifica. Tuttavia, se la colonna discriminatoria è vincolata in qualche modo, ad esempio come parte di un indice, l'oggetto AlterColumn
creato dalle migrazioni potrebbe non riuscire.
Perché
nvarchar(max)
le colonne sono inefficienti e non necessarie quando sono note le lunghezze di tutti i valori possibili.
Soluzioni di prevenzione
Le dimensioni delle colonne possono essere rese esplicitamente non associate:
modelBuilder.Entity<Foo>()
.Property<string>("Discriminator")
.HasMaxLength(-1);
I valori delle chiavi di SQL Server vengono confrontati senza distinzione tra maiuscole e minuscole
Problema di rilevamento n. 27526
Comportamento precedente
In precedenza, durante il rilevamento delle entità con chiavi stringa con i provider di database SQL Server/Azure SQL, i valori delle chiavi venivano confrontati usando l'operatore di confronto ordinale con distinzione tra maiuscole e minuscole .NET predefinito.
Nuovo comportamento
A partire da EF Core 8.0, i valori di chiave stringa SQL Server/Azure SQL vengono confrontati usando l'operatore di confronto ordinale senza distinzione tra maiuscole e minuscole .NET predefinito.
Perché
Per impostazione predefinita, SQL Server usa confronti senza distinzione tra maiuscole e minuscole durante il confronto dei valori di chiave esterna per le corrispondenze con i valori di chiave principale. Ciò significa che quando Ef usa confronti con distinzione tra maiuscole e minuscole, potrebbe non connettere una chiave esterna a una chiave principale quando deve.
Soluzioni di prevenzione
È possibile usare confronti con distinzione tra maiuscole e minuscole impostando un oggetto personalizzato ValueComparer
. Ad esempio:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var comparer = new ValueComparer<string>(
(l, r) => string.Equals(l, r, StringComparison.Ordinal),
v => v.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);
});
}
Più chiamate AddDbContext vengono applicate in ordine diverso
Problema di rilevamento n. 32518
Comportamento precedente
In precedenza, quando sono state effettuate più chiamate a AddDbContext
, AddDbContextPool
AddDbContextFactory
o AddPooledDbContextFactor
con lo stesso tipo di contesto ma la configurazione in conflitto, la prima ha vinto.
Nuovo comportamento
A partire da EF Core 8.0, la configurazione dell'ultima chiamata avrà la precedenza.
Perché
Questa modifica è stata modificata in modo che sia coerente con il nuovo metodo ConfigureDbContext
che può essere usato per aggiungere la configurazione prima o dopo i Add*
metodi.
Soluzioni di prevenzione
Invertire l'ordine delle Add*
chiamate.
EntityTypeAttributeConventionBase sostituito con TypeAttributeConventionBase
Nuovo comportamento
In EF Core 8.0 EntityTypeAttributeConventionBase
è stato rinominato in TypeAttributeConventionBase
.
Perché
TypeAttributeConventionBase
rappresenta la funzionalità migliore perché ora può essere usata per tipi complessi e tipi di entità.
Soluzioni di prevenzione
Sostituire EntityTypeAttributeConventionBase
gli utilizzi con TypeAttributeConventionBase
.