Dela via


Första konventioner för anpassad kod

Anmärkning

ENDAST EF6 – De funktioner, API:er osv. som beskrivs på den här sidan introducerades i Entity Framework 6. Om du använder en tidigare version gäller inte en del av eller all information.

När du använder Code First beräknas din modell från dina klasser med hjälp av en uppsättning konventioner. Standardkonventionerna för kod första avgör saker som vilken egenskap som blir primärnyckeln för en entitet, namnet på tabellen som en entitet mappar till och vilken precision och skala en decimalkolumn har som standard.

Ibland är dessa standardkonventioner inte idealiska för din modell, och du måste kringgå dem genom att konfigurera många enskilda entiteter med hjälp av dataanteckningar eller Fluent API. Med de första konventionerna för anpassad kod kan du definiera dina egna konventioner som tillhandahåller konfigurationsstandarder för din modell. I den här genomgången utforskar vi de olika typerna av anpassade konventioner och hur du skapar var och en av dem.

Modellbaserade konventioner

Den här sidan beskriver DBModelBuilder-API:et för anpassade konventioner. Det här API:et bör vara tillräckligt för att redigera de flesta anpassade konventioner. Det finns dock också möjlighet att skapa modellbaserade konventioner – konventioner som manipulerar den slutliga modellen när den väl har skapats – för att hantera avancerade scenarier. För mer information, se Model-Based Konventioner.

 

Vår modell

Vi börjar med att definiera en enkel modell som vi kan använda med våra konventioner. Lägg till följande klasser i projektet.

    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;

    public class ProductContext : DbContext
    {
        static ProductContext()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
        }

        public DbSet<Product> Products { get; set; }
    }

    public class Product
    {
        public int Key { get; set; }
        public string Name { get; set; }
        public decimal? Price { get; set; }
        public DateTime? ReleaseDate { get; set; }
        public ProductCategory Category { get; set; }
    }

    public class ProductCategory
    {
        public int Key { get; set; }
        public string Name { get; set; }
        public List<Product> Products { get; set; }
    }

 

Introduktion till anpassade konventioner

Nu ska vi skriva en konvention som konfigurerar alla egenskaper med namnet Key som primärnyckel för dess entitetstyp.

Konventioner aktiveras på modellverktyget, som kan nås genom att åsidosätta OnModelCreating i kontexten. Uppdatera klassen ProductContext på följande sätt:

    public class ProductContext : DbContext
    {
        static ProductContext()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
        }

        public DbSet<Product> Products { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Properties()
                        .Where(p => p.Name == "Key")
                        .Configure(p => p.IsKey());
        }
    }

Nu kommer varje attribut i vår modell med namnet Key att konfigureras som primärnyckel för den entitet det tillhör.

Vi skulle också kunna göra våra konventioner mer specifika genom att filtrera efter den typ av egenskap som vi ska konfigurera:

    modelBuilder.Properties<int>()
                .Where(p => p.Name == "Key")
                .Configure(p => p.IsKey());

Detta konfigurerar alla egenskaper som kallas Nyckel som primärnyckel för deras entitet, men bara om de är ett heltal.

En intressant funktion i IsKey-metoden är att den är additiv. Det innebär att om du anropar IsKey för flera egenskaper så blir de alla en del av en sammansatt nyckel. En varning för detta är att när du anger flera egenskaper för en nyckel måste du också ange en order för dessa egenskaper. Du kan göra detta genom att anropa metoden HasColumnOrder som nedan:

    modelBuilder.Properties<int>()
                .Where(x => x.Name == "Key")
                .Configure(x => x.IsKey().HasColumnOrder(1));

    modelBuilder.Properties()
                .Where(x => x.Name == "Name")
                .Configure(x => x.IsKey().HasColumnOrder(2));

Den här koden konfigurerar typerna i vår modell så att de har en sammansatt nyckel som består av kolumnen int Key och strängen Namn. Om vi visar modellen i designern skulle den se ut så här:

sammansatt nyckel

Ett annat exempel på egenskapskonventioner är att konfigurera alla DateTime-egenskaper i min modell för att mappa till datetime2-typen i SQL Server i stället för datetime. Du kan göra detta med följande:

    modelBuilder.Properties<DateTime>()
                .Configure(c => c.HasColumnType("datetime2"));

 

Konventionsklasser

Ett annat sätt att definiera konventioner är att använda en konventionsklass för att kapsla in din konvention. När du använder en konventionsklass skapar du en typ som ärver från klassen Convention i namnområdet System.Data.Entity.ModelConfiguration.Conventions.

Vi kan skapa en konventionsklass med datetime2-konventionen som vi visade tidigare genom att göra följande:

    public class DateTime2Convention : Convention
    {
        public DateTime2Convention()
        {
            this.Properties<DateTime>()
                .Configure(c => c.HasColumnType("datetime2"));        
        }
    }

Om du vill be EF att använda den här konventionen lägger du till den i samlingen Konventioner i OnModelCreating, som om du har följt med i genomgången ser ut så här:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Properties<int>()
                    .Where(p => p.Name.EndsWith("Key"))
                    .Configure(p => p.IsKey());

        modelBuilder.Conventions.Add(new DateTime2Convention());
    }

Som du ser lägger vi till en instans av vår konvention i samlingen med konventioner. Att ärva från konventionen är ett bekvämt sätt att gruppera och dela konventioner mellan team eller projekt. Du kan till exempel ha ett klassbibliotek med en gemensam uppsättning konventioner som alla dina organisationers projekt använder.

 

Anpassade attribut

En annan bra användning av konventioner är att aktivera nya attribut som ska användas när du konfigurerar en modell. För att illustrera detta ska vi skapa ett attribut som vi kan använda för att markera strängegenskaper som icke-Unicode.

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class NonUnicode : Attribute
    {
    }

Nu ska vi skapa en konvention för att tillämpa det här attributet på vår modell:

    modelBuilder.Properties()
                .Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any())
                .Configure(c => c.IsUnicode(false));

Med den här konventionen kan vi lägga till attributet NonUnicode till någon av våra strängegenskaper, vilket innebär att kolumnen i databasen lagras som varchar i stället för nvarchar.

En sak att notera om den här konventionen är att om du lägger attributet NonUnicode på något annat än en strängegenskap så utlöser det ett undantag. Det gör det eftersom du inte kan konfigurera IsUnicode på någon annan typ än en sträng. Om detta händer kan du göra din konvention mer specifik, så att den filtrerar bort allt som inte är en sträng.

Även om ovanstående konvention fungerar för att definiera anpassade attribut finns det ett annat API som kan vara mycket enklare att använda, särskilt när du vill använda egenskaper från attributklassen.

I det här exemplet ska vi uppdatera attributet och ändra det till ett IsUnicode-attribut, så det ser ut så här:

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    internal class IsUnicode : Attribute
    {
        public bool Unicode { get; set; }

        public IsUnicode(bool isUnicode)
        {
            Unicode = isUnicode;
        }
    }

När vi har detta kan vi ange en bool för vårt attribut för att tala om för konventionen om en egenskap ska vara Unicode eller inte. Vi kan göra detta i konventionen som vi redan har genom att komma åt ClrProperty för konfigurationsklassen så här:

    modelBuilder.Properties()
                .Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any())
                .Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));

Det här är enkelt nog, men det finns ett mer kortfattat sätt att uppnå detta med hjälp av Having-metoden för konventions-API:et. Metoden Having har en parameter av typen Func<PropertyInfo, T> som accepterar PropertyInfo på samma sätt som metoden Where, men förväntas returnera ett objekt. Om det returnerade objektet är null kommer egenskapen inte att konfigureras, vilket innebär att du kan filtrera bort egenskaper med det precis som Var, men det är annorlunda eftersom det också avbildar det returnerade objektet och skickar det till metoden Konfigurera. Detta fungerar så här:

    modelBuilder.Properties()
                .Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
                .Configure((config, att) => config.IsUnicode(att.Unicode));

Anpassade attribut är inte den enda anledningen till att använda metoden Having, det är användbart var du än behöver resonera om något som du filtrerar på när du konfigurerar dina typer eller egenskaper.

 

Konfigurera typer

Hittills har alla våra konventioner varit för egenskaper, men det finns ett annat område i konventions-API:et för att konfigurera typerna i din modell. Upplevelsen liknar de konventioner som vi har sett hittills, men alternativen i konfigurera kommer att finnas på entitet i stället för egenskapsnivå.

En av de saker som typnivåkonventioner kan vara riktigt användbara för är att ändra namngivningskonventionen för tabeller, antingen för att mappa till ett befintligt schema som skiljer sig från EF-standardvärdet eller för att skapa en ny databas med en annan namngivningskonvention. För att göra detta behöver vi först en metod som kan acceptera TypeInfo för en typ i vår modell och returnera vad tabellnamnet för den typen ska vara:

    private string GetTableName(Type type)
    {
        var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);

        return result.ToLower();
    }

Den här metoden tar en typ och returnerar en sträng som använder små bokstäver med understreck i stället för CamelCase. I vår modell innebär det att klassen ProductCategory mappas till en tabell med namnet product_category i stället för ProductCategories.

När vi har den metoden kan vi kalla den i en konvention som den här:

    modelBuilder.Types()
                .Configure(c => c.ToTable(GetTableName(c.ClrType)));

Den här konventionen konfigurerar varje typ i vår modell att mappa till tabellnamnet som returneras från vår GetTableName-metod. Den här konventionen motsvarar att anropa Metoden ToTable för varje entitet i modellen med hjälp av Fluent API.

En sak att notera om detta är att när du anropar ToTable EF kommer att ta strängen som du anger som det exakta tabellnamnet, utan någon av de pluraliseringar som det normalt skulle göra när du fastställer tabellnamn. Därför är tabellnamnet från vår konvention product_category i stället för product_categories. Vi kan lösa detta i vår konvention genom att själva anropa pluraliseringstjänsten.

I följande kod använder vi funktionen Beroendematchning som lagts till i EF6 för att hämta pluraliseringstjänsten som EF skulle ha använt och pluralisera vårt tabellnamn.

    private string GetTableName(Type type)
    {
        var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>();

        var result = pluralizationService.Pluralize(type.Name);

        result = Regex.Replace(result, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);

        return result.ToLower();
    }

Anmärkning

Den allmänna versionen av GetService är en tilläggsmetod i namnområdet System.Data.Entity.Infrastructure.DependencyResolution. Du måste lägga till en using-instruktion i kontexten för att kunna använda den.

ToTable och arv

En annan viktig aspekt av ToTable är att om du uttryckligen mappar en typ till en viss tabell kan du ändra den mappningsstrategi som EF ska använda. Om du anropar ToTable för varje typ i en arvshierarki och skickar typnamnet som namnet på tabellen som vi gjorde ovan, ändrar du standardstrategin för TPH-mappning (Table-Per-Hierarchy) till Table-Per-Type (TPT). Det bästa sättet att beskriva detta är med ett konkret exempel:

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

    public class Manager : Employee
    {
        public string SectionManaged { get; set; }
    }

Som standard mappas både medarbetare och chef till samma tabell (Anställda) i databasen. Tabellen innehåller både anställda och chefer med en diskriminerande kolumn som visar vilken typ av instans som lagras på varje rad. Det här är TPH-mappning eftersom det finns en enda tabell för hierarkin. Men om du anropar ToTable på båda klasserna, kommer varje typ i stället att mappas till sin egen tabell, även kallad TPT eftersom varje typ har en egen tabell.

    modelBuilder.Types()
                .Configure(c=>c.ToTable(c.ClrType.Name));

Koden ovan mappas till en tabellstruktur som ser ut så här:

tpt-exempel

Du kan undvika detta och underhålla standard-TPH-mappningen på ett par sätt:

  1. Anropa ToTable med samma tabellnamn för varje typ i hierarkin.
  2. Anropa ToTable endast på basklassen i hierarkin, i vårt exempel skulle det vara "Employee".

 

Körningsordning

Konventioner fungerar på ett sätt som vinner sist, på samma sätt som Fluent-API:et. Det innebär att om du skriver två konventioner som konfigurerar samma alternativ för samma egenskap, så vinner den sista som kör. I koden under maxlängden för alla strängar anges till exempel 500, men sedan konfigurerar vi alla egenskaper som kallas Namn i modellen så att de har en maximal längd på 250.

    modelBuilder.Properties<string>()
                .Configure(c => c.HasMaxLength(500));

    modelBuilder.Properties<string>()
                .Where(x => x.Name == "Name")
                .Configure(c => c.HasMaxLength(250));

Eftersom konventionen för att ange maxlängd till 250 är efter den som anger alla strängar till 500, har alla egenskaper som kallas Namn i vår modell en MaxLength på 250 medan andra strängar, till exempel beskrivningar, skulle vara 500. Att använda konventioner på det här sättet innebär att du kan tillhandahålla en allmän konvention för typer eller egenskaper i din modell och sedan åsidosätta dem för underuppsättningar som är olika.

Fluent API och dataanteckningar kan också användas för att åsidosätta en konvention i specifika fall. I vårt exempel ovan om vi hade använt Fluent API för att ange den maximala längden på en egenskap skulle vi ha kunnat placera den före eller efter konventionen, eftersom det mer specifika Fluent API kommer att vinna över den mer allmänna konfigurationskonventionen.

 

Inbyggda konventioner

Eftersom anpassade konventioner kan påverkas av standardkonventionerna code first kan det vara användbart att lägga till konventioner som ska köras före eller efter en annan konvention. För att göra detta kan du använda metoderna AddBefore och AddAfter i samlingen Conventions på din härledda DbContext. Följande kod skulle lägga till den konventionsklass som vi skapade tidigare så att den körs före den inbyggda konventionen för nyckelidentifiering.

    modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());

Detta kommer att vara till störst nytta när du lägger till konventioner som måste köras före eller efter de inbyggda konventionerna. Här finns en lista över de inbyggda konventionerna: System.Data.Entity.ModelConfiguration.Conventions Namespace.

Du kan också ta bort konventioner som du inte vill tillämpa på din modell. Om du vill ta bort en konvention använder du metoden Ta bort. Här är ett exempel på hur du tar bort PluralizingTableNameConvention.

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
    }