Convenzioni per l'individuazione delle relazioni

EF Core usa un set di convenzioni per l'individuazione e la creazione di un modello basato su classi di tipi di entità. Questo documento riepiloga le convenzioni usate per individuare e configurare le relazioni tra tipi di entità.

Importante

Le convenzioni descritte di seguito possono essere sostituite da una configurazione esplicita della relazione usando attributi di mapping o l'API di compilazione del modello.

Suggerimento

Il codice seguente è disponibile in RelationshipConventions.cs.

Individuazione degli spostamenti

L'individuazione delle relazioni inizia individuando gli spostamenti tra i tipi di entità.

Spostamenti di riferimento

Una proprietà di un tipo di entità viene individuata come navigazione di riferimento quando:

  • La proprietà è pubblica.
  • La proprietà ha un getter e un setter.
    • Il setter non deve essere pubblico; può essere privato o avere qualsiasi altra accessibilità.
    • Il setter può essere solo Init.
  • Il tipo di proprietà è o può essere un tipo di entità. Ciò significa che il tipo
    • Deve essere un tipo riferimento.
    • Non deve essere stato configurato in modo esplicito come tipo di proprietà primitiva.
    • Non deve essere mappato come tipo di proprietà primitiva dal provider di database in uso.
    • Non deve essere convertibile automaticamente in un tipo di proprietà primitiva mappato dal provider di database in uso.
  • La proprietà non è statica.
  • La proprietà non è una proprietà dell'indicizzatore.

Si considerino ad esempio i tipi di entità seguenti:

public class Blog
{
    // Not discovered as reference navigations:
    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public Uri? Uri { get; set; }
    public ConsoleKeyInfo ConsoleKeyInfo { get; set; }
    public Author DefaultAuthor => new() { Name = $"Author of the blog {Title}" };

    // Discovered as a reference navigation:
    public Author? Author { get; private set; }
}

public class Author
{
    // Not discovered as reference navigations:
    public Guid Id { get; set; }
    public string Name { get; set; } = null!;
    public int BlogId { get; set; }

    // Discovered as a reference navigation:
    public Blog Blog { get; init; } = null!;
}

Per questi tipi Blog.Author e Author.Blog vengono individuati come spostamenti di riferimento. D'altra parte, le proprietà seguenti non vengono individuate come spostamenti di riferimento:

  • Blog.Id, perché int è un tipo primitivo mappato
  • Blog.Title, perché 'string' è un tipo primitivo mappato
  • Blog.Uri, perché Uri viene convertito automaticamente in un tipo primitivo mappato
  • Blog.ConsoleKeyInfo, perché ConsoleKeyInfo è un tipo di valore C#
  • Blog.DefaultAuthor, perché la proprietà non dispone di un setter
  • Author.Id, perché Guid è un tipo primitivo mappato
  • Author.Name, perché 'string' è un tipo primitivo mappato
  • Author.BlogId, perché int è un tipo primitivo mappato

Spostamenti nella raccolta

Una proprietà di un tipo di entità viene individuata come navigazione nella raccolta quando:

  • La proprietà è pubblica.
  • La proprietà ha un getter. Gli spostamenti della raccolta possono avere setter, ma questo non è obbligatorio.
  • Il tipo di proprietà è o implementa IEnumerable<TEntity>, dove TEntity è, o può essere, un tipo di entità. Ciò significa che il tipo di TEntity:
    • Deve essere un tipo riferimento.
    • Non deve essere stato configurato in modo esplicito come tipo di proprietà primitiva.
    • Non deve essere mappato come tipo di proprietà primitiva dal provider di database in uso.
    • Non deve essere convertibile automaticamente in un tipo di proprietà primitiva mappato dal provider di database in uso.
  • La proprietà non è statica.
  • La proprietà non è una proprietà dell'indicizzatore.

Ad esempio, nel codice seguente, sia Blog.Tags che Tag.Blogs vengono individuati come spostamenti nella raccolta:

public class Blog
{
    public int Id { get; set; }
    public List<Tag> Tags { get; set; } = null!;
}

public class Tag
{
    public Guid Id { get; set; }
    public IEnumerable<Blog> Blogs { get; } = new List<Blog>();
}

Associazione di spostamenti

Una volta individuata una navigazione da, ad esempio, al tipo di entità A al tipo di entità B, è necessario determinare se questo spostamento ha un inverso che va in direzione opposta, ovvero dal tipo di entità B al tipo di entità A. Se viene trovato un tale inverso, i due spostamenti vengono associati insieme per formare una singola relazione bidirezionale.

Il tipo di relazione è determinato dal fatto che lo spostamento e il relativo inverso siano spostamenti di riferimento o raccolta. In particolare:

  • Se uno spostamento è uno spostamento di raccolta e l'altro è una navigazione di riferimento, la relazione è uno-a-molti.
  • Se entrambi gli spostamenti sono spostamenti di riferimento, la relazione è uno-a-uno.
  • Se entrambi gli spostamenti sono spostamenti di raccolta, la relazione è molti-a-molti.

L'individuazione di ognuno di questi tipi di relazione è illustrata negli esempi seguenti:

Viene individuata una singola relazione uno-a-molti tra Blog e Post viene individuata associando gli Blog.Posts spostamenti e Post.Blog :

public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? BlogId { get; set; }
    public Blog? Blog { get; set; }
}

Una singola relazione uno-a-uno viene individuata tra Blog e Author viene individuata associando gli Blog.Author spostamenti e Author.Blog :

public class Blog
{
    public int Id { get; set; }
    public Author? Author { get; set; }
}

public class Author
{
    public int Id { get; set; }
    public int? BlogId { get; set; }
    public Blog? Blog { get; set; }
}

Una singola relazione molti-a-molti viene individuata tra Post e Tag viene individuata associando gli Post.Tags spostamenti e Tag.Posts :

public class Post
{
    public int Id { get; set; }
    public ICollection<Tag> Tags { get; } = new List<Tag>();
}

public class Tag
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

Nota

Questa associazione di spostamenti potrebbe non essere corretta se i due spostamenti rappresentano due relazioni unidirezionali diverse. In questo caso, le due relazioni devono essere configurate in modo esplicito.

L'associazione di relazioni funziona solo quando è presente una singola relazione tra due tipi. È necessario configurare in modo esplicito più relazioni tra due tipi.

Nota

Le descrizioni di seguito sono in termini di relazioni tra due tipi diversi. Tuttavia, è possibile che lo stesso tipo si trova in entrambe le estremità di una relazione e pertanto per un singolo tipo abbiano due spostamenti entrambi associati tra loro. Si tratta di una relazione che si riferisce automaticamente.

Individuazione delle proprietà della chiave esterna

Dopo che gli spostamenti per una relazione sono stati individuati o configurati in modo esplicito, questi spostamenti vengono usati per individuare le proprietà della chiave esterna appropriate per la relazione. Una proprietà viene individuata come chiave esterna quando:

  • Il tipo di proprietà è compatibile con la chiave primaria o alternativa nel tipo di entità principale.
    • I tipi sono compatibili se sono uguali o se il tipo di proprietà chiave esterna è una versione nullable del tipo di proprietà di chiave primaria o alternativa.
  • Il nome della proprietà corrisponde a una delle convenzioni di denominazione per una proprietà di chiave esterna. Le convenzioni di denominazione sono:
    • <navigation property name><principal key property name>
    • <navigation property name>Id
    • <principal entity type name><principal key property name>
    • <principal entity type name>Id
  • Inoltre, se l'estremità dipendente è stata configurata in modo esplicito usando l'API di compilazione del modello e la chiave primaria dipendente è compatibile, la chiave primaria dipendente verrà usata anche come chiave esterna.

Suggerimento

Il suffisso "Id" può contenere maiuscole e minuscole.

I tipi di entità seguenti mostrano esempi per ognuna di queste convenzioni di denominazione.

Post.TheBlogKey viene individuato come chiave esterna perché corrisponde al modello <navigation property name><principal key property name>:

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? TheBlogKey { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.TheBlogID viene individuato come chiave esterna perché corrisponde al modello <navigation property name>Id:

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? TheBlogID { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.BlogKey viene individuato come chiave esterna perché corrisponde al modello <principal entity type name><principal key property name>:

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? BlogKey { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.Blogid viene individuato come chiave esterna perché corrisponde al modello <principal entity type name>Id:

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? Blogid { get; set; }
    public Blog? TheBlog { get; set; }
}

Nota

Nel caso di spostamenti uno-a-molti, le proprietà della chiave esterna devono trovarsi nel tipo con lo spostamento di riferimento, perché si tratta dell'entità dipendente. Nel caso di relazioni uno-a-uno, l'individuazione di una proprietà di chiave esterna viene utilizzata per determinare quale tipo rappresenta la fine dipendente della relazione. Se non viene individuata alcuna proprietà di chiave esterna, è necessario configurare la fine dipendente tramite HasForeignKey. Per esempi di questo tipo, vedere Relazioni uno-a-uno.

Le regole precedenti si applicano anche alle chiavi esterne composite, in cui ogni proprietà del composito deve avere un tipo compatibile con la proprietà corrispondente della chiave primaria o alternativa e ogni nome di proprietà deve corrispondere a una delle convenzioni di denominazione descritte in precedenza.

Determinazione della cardinalità

Entity Framework usa gli spostamenti individuati e le proprietà della chiave esterna per determinare la cardinalità della relazione insieme alle relative estremità principali e dipendenti:

  • Se ne esiste uno, lo spostamento di riferimento non abbinato, la relazione viene configurata come unidirezionale uno-a-molti, con lo spostamento di riferimento sulla fine dipendente.
  • Se ne esiste uno, lo spostamento della raccolta non abbinato, la relazione viene configurata come unidirezionale uno-a-molti, con lo spostamento della raccolta alla fine dell'entità.
  • Se sono presenti spostamenti tra riferimenti e raccolte abbinati, la relazione viene configurata come bidirezionale uno-a-molti, con lo spostamento della raccolta sulla fine principale.
  • Se uno spostamento di riferimento è associato a un altro spostamento di riferimento, allora:
    • Se una proprietà di chiave esterna è stata individuata su un lato ma non l'altra, la relazione viene configurata come bidirezionale uno-a-uno, con la proprietà di chiave esterna sulla fine dipendente.
    • In caso contrario, non è possibile determinare il lato dipendente e ENTITY genera un'eccezione che indica che il dipendente deve essere configurato in modo esplicito.
  • Se uno spostamento della raccolta è associato a un'altra navigazione raccolta, la relazione viene configurata come bidirezionale molti-a-molti.

Proprietà della chiave esterna ombreggiatura

Se Entity Framework ha determinato la fine dipendente della relazione, ma non è stata individuata alcuna proprietà di chiave esterna, Entity Framework creerà una proprietà shadow per rappresentare la chiave esterna. Proprietà shadow:

  • Ha il tipo della proprietà chiave primaria o alternativa alla fine dell'entità della relazione.
    • Il tipo è reso nullable per impostazione predefinita, rendendo la relazione facoltativa per impostazione predefinita.
  • Se è presente uno spostamento sulla fine dipendente, la proprietà della chiave esterna shadow viene denominata usando questo nome di navigazione concatenato con il nome della proprietà chiave primaria o alternativa.
  • Se non è presente alcuna navigazione sulla fine dipendente, la proprietà della chiave esterna shadow viene denominata usando il nome del tipo di entità principale concatenato con il nome della proprietà chiave primaria o alternativa.

Eliminazione a catena

Per convenzione, le relazioni necessarie sono configurate per l'eliminazione a catena. Le relazioni facoltative sono configurate per non eliminare a catena.

Molti-a-molti

Le relazioni molti-a-molti non hanno estremità principali e dipendenti e nessuna delle due terminazioni contiene una proprietà di chiave esterna. Al contrario, le relazioni molti-a-molti usano un tipo di entità join che contiene coppie di chiavi esterne che puntano a una delle estremità molti-a-molti. Si considerino i tipi di entità seguenti, per i quali viene individuata una relazione molti-a-molti per convenzione:

public class Post
{
    public int Id { get; set; }
    public ICollection<Tag> Tags { get; } = new List<Tag>();
}

public class Tag
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

Le convenzioni usate in questa individuazione sono:

  • Il tipo di entità join è denominato <left entity type name><right entity type name>. Quindi, PostTag in questo esempio.
    • La tabella join ha lo stesso nome del tipo di entità join.
  • Al tipo di entità join viene assegnata una proprietà di chiave esterna per ogni direzione della relazione. Questi sono denominati <navigation name><principal key name>. In questo esempio, quindi, le proprietà della chiave esterna sono PostsId e TagsId.
    • Per un oggetto molti-a-molti unidirezionale, la proprietà della chiave esterna senza una struttura di spostamento associata è denominata <principal entity type name><principal key name>.
  • Le proprietà della chiave esterna non sono nullable, rendendo necessarie entrambe le relazioni con l'entità join.
    • Le convenzioni di eliminazione a catena indicano che queste relazioni verranno configurate per l'eliminazione a catena.
  • Il tipo di entità join è configurato con una chiave primaria composta costituita dalle due proprietà di chiave esterna. In questo esempio, quindi, la chiave primaria è costituita da PostsId e TagsId.

Il risultato è il modello di Entity Framework seguente:

Model:
  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    Skip navigations:
      Tags (ICollection<Tag>) CollectionTag Inverse: Posts
    Keys:
      Id PK
  EntityType: Tag
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    Skip navigations:
      Posts (ICollection<Post>) CollectionPost Inverse: Tags
    Keys:
      Id PK
  EntityType: PostTag (Dictionary<string, object>) CLR Type: Dictionary<string, object>
    Properties:
      PostsId (no field, int) Indexer Required PK FK AfterSave:Throw
      TagsId (no field, int) Indexer Required PK FK Index AfterSave:Throw
    Keys:
      PostsId, TagsId PK
    Foreign keys:
      PostTag (Dictionary<string, object>) {'PostsId'} -> Post {'Id'} Cascade
      PostTag (Dictionary<string, object>) {'TagsId'} -> Tag {'Id'} Cascade
    Indexes:
      TagsId

E si traduce nello schema di database seguente quando si usa SQLite:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tag" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tag_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tag" ("Id") ON DELETE CASCADE);

CREATE INDEX "IX_PostTag_TagsId" ON "PostTag" ("TagsId");

Indici

Per convenzione, Entity Framework crea un indice di database per la proprietà o le proprietà di una chiave esterna. Il tipo di indice creato è determinato da:

  • Cardinalità della relazione
  • Indica se la relazione è facoltativa o obbligatoria
  • Numero di proprietà che costituiscono la chiave esterna

Per una relazione uno-a-molti, viene creato un indice semplice per convenzione. Lo stesso indice viene creato per le relazioni facoltative e obbligatorie. Ad esempio, in SQLite:

CREATE INDEX "IX_Post_BlogId" ON "Post" ("BlogId");

Oppure in SQL Server:

CREATE INDEX [IX_Post_BlogId] ON [Post] ([BlogId]);

Per una relazione uno-a-uno necessaria, viene creato un indice univoco. Ad esempio, in SQLite:

CREATE UNIQUE INDEX "IX_Author_BlogId" ON "Author" ("BlogId");

Oppure in SQL Sever:

CREATE UNIQUE INDEX [IX_Author_BlogId] ON [Author] ([BlogId]);

Per le relazioni uno-a-uno facoltative, l'indice creato in SQLite è lo stesso:

CREATE UNIQUE INDEX "IX_Author_BlogId" ON "Author" ("BlogId");

Tuttavia, in SQL Server viene aggiunto un IS NOT NULL filtro per gestire meglio i valori di chiave esterna Null. Ad esempio:

CREATE UNIQUE INDEX [IX_Author_BlogId] ON [Author] ([BlogId]) WHERE [BlogId] IS NOT NULL;

Per le chiavi esterne composite, viene creato un indice che copre tutte le colonne chiave esterna. Ad esempio:

CREATE INDEX "IX_Post_ContainingBlogId1_ContainingBlogId2" ON "Post" ("ContainingBlogId1", "ContainingBlogId2");

Nota

Ef non crea indici per le proprietà già coperte da un vincolo di indice o chiave primaria esistente.

Come arrestare la creazione di indici di Entity Framework per chiavi esterne

Gli indici hanno un sovraccarico e, come richiesto qui, potrebbe non essere sempre appropriato crearli per tutte le colonne FK. A tale scopo, è possibile rimuovere l'oggetto ForeignKeyIndexConvention durante la compilazione del modello:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

Quando si desidera, gli indici possono comunque essere creati in modo esplicito per le colonne chiave esterna che ne hanno bisogno.

Nomi dei vincoli di chiave esterna

Per convenzione, i vincoli di chiave esterna sono denominati FK_<dependent type name>_<principal type name>_<foreign key property name>. Per le chiavi esterne composite, <foreign key property name> diventa un elenco di caratteri di sottolineatura separati da nomi di proprietà di chiave esterna.

Risorse aggiuntive