Zur Zeit wird gefiltert nach: Code First
Filter zurücksetzen

Entity Framework 4.3 - Schema Migrations für Code First (DbContext)

Eine Anwendung lebt bekanntlich von Änderungen. Nicht selten haben diese Änderungen auch Auswirkungen auf das Datenbankmodell. Das Entity Framework unterstützt mittlerweile DB First, Model First und Code First. Für DB First und Model First ist es kein grösseres Problem Änderungen auf der Datenbank nachzuführen, mit ein paar Tricks und Tools sind sogar Roundtrips zwischen Datenbank und Modell möglich.

Die möglichen Vorgehensweisen sind in der Präsentation Tipps und Tricks Entity Framework ersichtlich.

Bei Code First war dieser Ansatz bisher ein Ding der Unmöglichkeit. Lediglich mit Zusatztools wie SQL Delta und einer ITIL-konformen Umgebung konnten die Änderungen vorgenommen werden, ohne dass die produktiven Daten negativ beeinflusst wurden. Der Störfaktor war jedoch immer der Modelhash in der Tabelle EdmMetadata. Entweder hat man diesen Eintrag mit aktualisiert oder aber die Konvention entfernt.

Seit ein paar Tagen ist nun die Beta 1 vom Entity Framework 4.3 draussen und ich wollte natürlich auch gleich die Anpassungen ausprobieren. Die Pakete gibt es über NuGet und die Unterstützung für die Installation von Vorabversionen benötigt im minium die Version 1.6. Ich musste zuvor auf diese Version aktualisieren. Die ältere Version Entity Framework.Migrations muss noch vom System entfernt werden (erfordert einen Neustart von Visual Studio).

Nach diesem Update besteht die Möglichkeit die Vorabversion mit dem Befehl:

Abbildung 1
Abbildung 1 Install-Package Entity Framework -IncludePreRelease

auf dem System zu installieren.

Also beginne ich mit einem kleinen Beispiel:


    public abstract class Product
    {
      public int Id { get; set; }
 
      [StringLength(50)]
      [Required]
      public string Name { get; set; }
 
      [StringLength(400)]
      public string Description { get; set; }
    }
 
    public class Book : Product
    {
      [StringLength(10)]
      [Required]
      public string ISBN10 { get; set; }
 
      [StringLength(13)]
      [Required]
      public string ISBN13 { get; set; }
 
      public int LanguageCD { get; set; }
 
      [Required]
      public int Pages { get; set; }
    }
 
    public class EBook : Book
    {
      [Required]
      public string Filename { get; set; }
    }
 
    public class Hardcover : Book
    {
      [StringLength(20)]
      [Required]
      public string Size { get; set; }
 
      [Required]
      public double Weight { get; set; }
    }
 
    public class BookInheritanceContext : DbContext
    {
 
      public BookInheritanceContext()
        : base("EfCodeFirstMigrations")
      { 
 
      }
 
      public IDbSet<Product> Products { get; set; }
 
      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
        //  TPH ist Standardkonvention, da beste Performance.
        //  Für  Individual-Lösungen  macht das auch durchaus Sinn , wenn
        //  jedoch die Release-Tauglichkeit  gewährleistet werden muss
        //  ist TPT die bessere Wahl.
 
        //  Mapping  für benutzerdefinierten Diskriminator 
        // // const  string   discriminator  = "ProductTypeNbr ";
        // // modelBuilder . Entity <Product >( )
        ////  .Map<Book>(m => m.Requires(discriminator).HasValue(2))
        ////  .Map<EBook>(m => m.Requires(discriminator).HasValue(3))
        ////  .Map<Hardcover>(m => m.Requires(discriminator).HasValue(4))
        ////  .ToTable("Product");
 
        base.OnModelCreating(modelBuilder);
      }
    }

Nun beginnt die Phase der Erweiterung. In diesem Zusammenhang ist es wichtig zu akzeptieren, dass die Arbeit mit NuGet ein wenig intensiver wird. Im neuen Release sollen nun ein paar Erweiterungen vorgenommen werden, die Auswirkungen auf das Datenmodell haben werden.

Der erste Schritt ist nun das öffnen der NuGet-Konsole und die Eingabe von:

Abbildung 2
Abbildung 2 Enable-Migrations

auszuführen. Nach dieser Aktion befindet sich ein neuer Ordner Migrations im Projekt.

Abbildung 3
Abbildung 3 Neuer Ordner Migrations

Ich beginne nun mit meinen Anpassungen an der Klasse EBook. In der neuen Version soll die Möglichkeit bestehen, dass pro Buch mehrere Dateien für alternative Dateiformate (mobi, epub, pdf usw.) hinterlegt werden können.


public class EBook : Book
    {
      [Required]
      public string Filename { get; set; }
 
      [Required]
      public ICollection<BookFile> AlternativeFiles { get; internal set; }
    }
 
    public class BookFile
    {
      public int Id { get; set; }
      public int Type { get; set; }
      public int Filename { get; set; }
    }

Nach der Fertigstellung geht es nun darum das Update der Datenbank vorzunehmen. Auch hier ist die primäre Schaltzentrale die NuGet-Konsole. Zuerst muss jedoch eine kleine Anpassung in der Configuration-Klasse vorgenommen werden. Im Konstruktur muss der Wert von AutomaticMigrationsEnabled auf true gesetzt werden.

Kommen wir zurück auf die NuGet-Konsole, mit dem Befehl:

Abbildung 4
Abbildung 4 Update-Database -Script

wird ein SQL-Skript mit den notwendigen Änderungen auf der Datenbank erstellt.

Der Output für dieses Beispiel:


CREATE TABLE [BookFiles] (
    [Id] [int] NOT NULL IDENTITY,
    [Type] [int] NOT NULL,
    [Filename] [int] NOT NULL,
    [EBook_Id] [int],
    CONSTRAINT [PK_BookFiles] PRIMARY KEY ([Id])
)
CREATE INDEX [IX_EBook_Id] ON [BookFiles]([EBook_Id])
ALTER TABLE [BookFiles] ADD CONSTRAINT [FK_BookFiles_Products_EBook_Id] FOREIGN KEY ([EBook_Id]) REFERENCES [Products] ([Id])
CREATE TABLE [__MigrationHistory] (
    [MigrationId] [nvarchar](255) NOT NULL,
    [CreatedOn] [datetime] NOT NULL,
    [Model] [varbinary](max) NOT NULL,
    [ProductVersion] [nvarchar](32) NOT NULL,
    CONSTRAINT [PK___MigrationHistory] PRIMARY KEY ([MigrationId])
)
BEGIN TRY
    EXEC sp_MS_marksystemobject '__MigrationHistory'
END TRY
BEGIN CATCH
END CATCH
INSERT INTO [__MigrationHistory] ([MigrationId], [CreatedOn], [Model], [ProductVersion]) VALUES ('201201151955537_AutomaticMigration', '2012-01-15T19:55:54.189Z', 0x1F8B...EDMX-Modell...0, '4.3.0-beta1')

Neben den Tabellen wird neu auch für die Fremdschlüssel-Spalten ein Fremdschlüsselindex erstellt. Bisher war das ein negativer Unterschied zum Model First - Ansatz, der mit der Version EF 4.3 der Vergangenheit angehören wird.

Was auch auffällt ist die Systemtabelle __MigrationHistory, in der das aktuelle Abbild des Modells gespeichert wird. Die Tabelle EdmMetadata existiert nicht mehr. Mit EF 4.3 gehört diese ebenfalls der Vergangenheit an. Bei bestehenden Modellen wird diese jedoch erst entfernt, wenn die Datenbank neu erstellt wird. Ein Detail, welches sich daraus ergibt: Migrations funktionieren nur mit dem SQL-Server!!!!

Ohne den Swich "-Script" lassen sich die Änderungen direkt an die Datenbank übertragen. Bei dem automatischen Ansatz würde ich persönlich darauf verzichten, da es keine Möglichkeit der Versionierung gibt. Dieser Teil lässt sich mit dem Speichern der SQL-Skripts jedoch organisatorisch in den Griff bekommen. Beim codebasierten Ansatz ist es ein wenig eleganter gelöst, jedoch sind die Möglichkeiten auch hier begrenzt.

Bei dieser Beta soll es sich um die Letzte handeln, sodass im Laufe des ersten Quartals die finale Version Entity Framework 4.3 zur Verfügung stehen wird.

Ich finde diesen Ansatz sehr interessant, da nun auch beim codezentrierten Ansatz mit dem Entity Framework Schema-Migrations möglich werden. Als Entwickler muss man jedoch berücksichtigen, dass diese nur mit dem SQL-Server funktionieren. Zudem ist ein Round-Trip nicht, bzw. nur mit einem nicht im Verhältnis stehenden Aufwand möglich. (Stichwort: Meet in the Middle). Hier beginnt das Problem aber häufig mit der Planung.

Weitere nützliche Informationen befinden sich in den Blogs vom ADO.NET - Team unter:

Entity Framework 4.1 - Change Tracking

Zwei Monate lang keinen Blogpost mehr verfasst. Was Sharepoint alles anrichten kann, aber das ist eine andere Geschichte.

Im Bereich Change Tracking kann Code First schnell sehr langsam werden, ist in etwa mit Oracle und ANSI-SQL zu vergleichen. ;-)

In dieser Hinsicht sind die beiden Beiträge von Arthur Vickers sehr wertvoll zum Verständnis dieser Problematik. Der erste Beitrag geht auf die unterstützten Typen für Lazy Loading und Change Tracking ein, während der zweite Beitrag die Performance-Aspekte beleuchet. 

Sehr interessant...

Entity Framework Versionierung

Versionierung ist in manchen Situationen nicht einfach. Ein Beispiel wie das Ganze recht komplex und mühsam für einen Anwender oder Developer werden kann, ist das Entity Framework.

Um ein wenig Licht ins Dunkel zu bekommen, hat das ADO.NET - Team einen Blogpost verfasst und um Feedback gebeten. Weitere Informationen gibt es hier.

Entity Framework 4.1 - Code-/Model First und die Unterschiede einer Gemeinsamkeit

Code- bzw. Model First mit dem Entity Framework sind 2 unterschiedliche Ansatzmodelle. Bei Model First wird in der Regel mit dem Mapping begonnen, die Vorgehensweise dazu ist auch als Middle out bekannt. Anhand des Mappings wird anschliessend der Code und das DDL-Skript für die Datenbank erstellt. Bei Code First wird zuerst der Code geschrieben und daraus das Mapping und das DDL-Skript für die Datenbank erstellt. Dieser Ansatz ist auch häufig als Top down geläufig.

Abbildung 1
Abbildung 1 Middle out als Vertreter der modellzentrierten Anwendungsentwicklung
Abbildung 2
Abbildung 2 Top down für die codezentrierte Anwendungsentwicklung

Die Gemeinsamkeit beider Vorgehensweisen ist die Erstellung der Datenbank. Der Unterschied liegt jedoch darin, dass bei der modellzentrierten Anwendungsentwicklung mit Hilfe von T4 Einfluss auf das DDL-Skript genommen werden kann, während bei Code First die Datenbank erstellt wird.

Wo liegt also nun der Unterschied in dieser Gemeinsamkeit, ausser bei T4? Dazu muss man sich nur das von der T4 erstellte SQL-Skript betrachten:


-- --------------------------------------------------
-- Creating all FOREIGN KEY constraints
-- --------------------------------------------------
 
-- Creating foreign key on [OrderID] in table 'OrderItems'
ALTER TABLE [dbo].[OrderItems]
ADD CONSTRAINT [FK_OrderOrderItem]
    FOREIGN KEY ([OrderID])
    REFERENCES [dbo].[Orders]
        ([ID])
    ON DELETE NO ACTION ON UPDATE NO ACTION;
 
-- Creating non-clustered index for FOREIGN KEY 'FK_OrderOrderItem'
CREATE INDEX [IX_FK_OrderOrderItem]
ON [dbo].[OrderItems]
    ([OrderID]);
GO

Im Bereich der Fremdschlüssel werden im Gegensatz zum Code First - Ansatz die FK-Indizes angelegt. Darin liegt der Unterschied. Bei wenigen Daten wird das nicht besonders schmerzhaft sein, wenn jedoch das Datenvolumen wächst und der Datenbankserver zudem noch virtualisiert ist, können da an der einen oder anderen Ecke Performance-Probleme entstehen, natürlich schleichend. Betrachten wir es anhand eines Beispiels mit wenigen Daten. In meinem Testszenario existieren ein paar Tausend Datensätze in der Orders-Tabelle aus der 10 Datensätze selektiert werden sollen.

Die SQL-Abfrage hat folgenden Aufbau:


SELECT
	*
FROM dbo.Orders o
INNER JOIN dbo.OrderItems oi ON o.Id = oi.OrderId 
WHERE o.id IN (1,5,10,34,80,92,500,897,3456,4567)

Diese wird einmal mit und ohne Fremdschlüssel-Index auf die OrderId-Spalte ausgeführt. Der Ausführungsplan zeigt folgendes:

Abbildung 3
Abbildung 3 Ausführungsplan mit und ohne Fremdschlüssel-Indizes

Wenn man die Kosten in Relation betrachtet, so benötigt die Datenbank ohne Fremdschlüssel mehr Ressourcen.

Nun gibt es mehrere Möglichkeiten die Indizes zu erstellen. Ich bevorzuge die Art der Metaprogrammierung. Sprich der SQL-Server genauer gesagt ein SQL-Skript soll mithilfe der Angaben die notwendigen Inidizes selbst erstellen. Dafür habe ich folgendes Skript geschrieben:


DECLARE @table nvarchar(100), 
	@fkName nvarchar(100), 
	@columnName nvarchar(100), 
	@index nvarchar(800)

DECLARE missing_FkIndex_cursor CURSOR FOR SELECT
      t.name as TB_Name
      , fk.name as FK_Name
      , c.name as Column_Name
FROM sys.foreign_key_columns fkc
INNER JOIN sys.tables t ON t.object_id = fkc.parent_object_id
INNER JOIN sys.foreign_keys fk ON fk.parent_object_id =
fkc.parent_object_id
INNER JOIN sys.columns c ON c.object_id = fk.parent_object_id AND
c.column_id = fkc.parent_column_id
LEFT OUTER JOIN sys.index_columns indc ON indc.object_id = c.object_id AND
c.column_id = indc.column_id
LEFT OUTER JOIN sys.indexes ind ON ind.object_id = indc.object_id AND
ind.index_id = indc.index_id
WHERE fk.type = 'F' AND ind.name is null

OPEN missing_FkIndex_cursor;

FETCH NEXT FROM missing_FkIndex_cursor 
INTO  @table, @fkName, @columnName

WHILE @@FETCH_STATUS = 0
BEGIN
	SET @index = 'CREATE INDEX IX_FK_' + @fkName + '_' + @columnName + ' ON ' + @table + ' (' + @columnName + ')'
	EXECUTE sp_executesql @index
	PRINT @index
	
	FETCH NEXT FROM missing_FkIndex_cursor 
		INTO  @table, @fkName, @columnName
END
CLOSE missing_FkIndex_cursor;
DEALLOCATE missing_FkIndex_cursor;

Dieses Skript erstellt den Fremschlüsselindex nur, wenn dieser noch nicht existiert. Nun liegt es in der Natur des Menschen, dass solche Anpassungen, gerade wenn man die gewohnte Umgebung verlassen muss, vergessen werden.

Für die Code First Variante wäre es nun möglich, dieses Skript in einen Initializer zu packen, der bei der Erstellung der Datenbank ausgeführt wird. Der Vorteil dieser Variante, der Initalizer kann in eine Library verstaut und zur Erstellung der Datenbank genutzt werden.

Der Initializer könnte folgenden Aufbau haben:


 public class CreateSqlServerDatabaseWithFkIndizesIfNotExists<T> 
    : CreateDatabaseIfNotExists<T> where T : DbContext
  {
    protected override void Seed(T context)
    {
      context.Database.ExecuteSqlCommand(GetFkIndexSqlScript());
      base.Seed(context);
    }

    private string GetFkIndexSqlScript()
    {
      return @"
        DECLARE @table nvarchar(100), 
	        @fkName nvarchar(100), 
	        @columnName nvarchar(100), 
	        @index nvarchar(800)

        DECLARE missing_FkIndex_cursor CURSOR FOR SELECT
              t.name as TB_Name, 
              fk.name as FK_Name
              , c.name as Column_Name
              --, ind.name as Index_Name
              --, fk.type_desc as FK_Type
        FROM sys.foreign_key_columns fkc
        INNER JOIN sys.tables t ON t.object_id = fkc.parent_object_id
        INNER JOIN sys.foreign_keys fk ON fk.parent_object_id =
        fkc.parent_object_id
        INNER JOIN sys.columns c ON c.object_id = fk.parent_object_id AND
        c.column_id = fkc.parent_column_id
        LEFT OUTER JOIN sys.index_columns indc ON indc.object_id = c.object_id AND
        c.column_id = indc.column_id
        LEFT OUTER JOIN sys.indexes ind ON ind.object_id = indc.object_id AND
        ind.index_id = indc.index_id
        WHERE fk.type = 'F' AND ind.name is null

        OPEN missing_FkIndex_cursor;

        FETCH NEXT FROM missing_FkIndex_cursor 
        INTO  @table, @fkName, @columnName

        WHILE @@FETCH_STATUS = 0
        BEGIN
	        SET @index = 'CREATE INDEX IX_FK_' + @fkName + '_' + @columnName + ' ON ' + @table + ' (' + @columnName + ')'
	        EXECUTE sp_executesql @index
	        PRINT @index
	
	        FETCH NEXT FROM missing_FkIndex_cursor 
		        INTO  @table, @fkName, @columnName
        END
        CLOSE missing_FkIndex_cursor;
        DEALLOCATE missing_FkIndex_cursor;      ";
    }
  }

Wie die Datenbank mit dem Skript initialisiert werden kann, zeigt folgender Code:


      var initializer = new CreateSqlServerDatabaseWithFkIndizesIfNotExists<TestContext>();
      Database.SetInitializer<TestContext>
        (initializer);
      initializer.InitializeDatabase(new TestContext());

Dieser Ansatz ist etwas tricky, aber es ist aktuell der einzige lauffähige Code. Muss es mal mit dem Update 1 von EF 4.1 nochmals durchtesten.

Wenn nun die Datenbank erstellt wird, läuft das o.a. Skript und der Unterschied einer Gemeinsamkeit ist verschwunden. ;-)

Entity Framework - DbContext und der 2nd-Level Cache mit dem EFCachingProvider

Der DbContext stellt eigentlich ein Wrapper auf den ObjectContext dar. Ich bevorzuge mittlerweile den DbContext, da dieser einige Funktionalitäten bietet, die ich beim ObjectContext vermisse. Da wären bspw. das vereinfachte Change Tracking, der Support der DataAnnotations, der veinfachte Zugriff auf dem Context-Cache über die Local-Eigenschaft und das dieser neben Code First auch mit dem EDM-Designer verwendet werden kann.

Ein Nachteil ist jedoch die fehlende Unterstützung für das Caching. Für den ObjectContext gibt es den EFCachingProvider, der seine Aufgaben relativ gut verrichtet. Über den Konstruktor im ObjectContext wird dieser aktiviert.

Nun ist der DbContext ja nichts anderes als ein Wrapper und diesen kann auch eine ObjectContext-Instanz übergeben werden. Also kam ich auf eine ganz „wilde“ Idee, um den DbContext mit einer Zusammenarbeit mit dem EFCachingProvider zu überreden. Alternativ liesse sich das auch mit dem Proxy-Pattern realisieren, aber ich gebe die Hoffnung nicht auf, dass der EFCachingProvider ein fester Bestandteil vom EF werden wird.

Mit Hilfe der mitgelieferten T4-Vorlage, die den Einsatz des DbContext für DbFirst und ModelFirst ermöglicht, lässt sich der Caching-Support sehr einfach realisieren. Die angepasste T4-Vorlage sieht so aus:


<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ include file="EF.Utility.CS.ttinclude"#><#@
 output extension=".cs"#><#

var loader = new MetadataLoader(this);
var region = new CodeRegion(this);
var inputFile = @"SimpleModel.edmx";
var ItemCollection = loader.CreateEdmItemCollection(inputFile);

Code = new CodeGenerationTools(this);
EFTools = new MetadataTools(this);
ObjectNamespace = Code.VsNamespaceSuggestion();
ModelNamespace = loader.GetModelNamespace(inputFile);

EntityContainer container = ItemCollection.GetItems<EntityContainer>().FirstOrDefault();
if (container == null)
{
    return string.Empty;
}
#>
//------------------------------------------------------------------------------
// <auto-generated>
// <#=GetResourceString("Template_GeneratedCodeCommentLine1")#>
//
// <#=GetResourceString("Template_GeneratedCodeCommentLine2")#>
// <#=GetResourceString("Template_GeneratedCodeCommentLine3")#>
// </auto-generated>
//------------------------------------------------------------------------------

<#

if (!String.IsNullOrEmpty(ObjectNamespace))
{
#>
namespace <#=Code.EscapeNamespace(ObjectNamespace)#>
{
<#
    PushIndent(CodeRegion.GetIndent(1));
}

#>
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.EntityClient;
using System.Data.Objects;
using System.IO;
using EFProviderWrapperToolkit;
using EFTracingProvider;
using EFCachingProvider;
using EFCachingProvider.Caching;

// Workaround, create an objectcontext for 2nd Level Cache with EFProviderToolkit
<#=Accessibility.ForType(container)#> partial class <#=Code.Escape(container)#>Context : ObjectContext
{
	private TextWriter logOutput;
	
    #region Konstruktoren
    public <#=Code.Escape(container)#>Context() 
    : base(EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(
                    "name=<#=container.Name#>",
                    /*"EFTracingProvider",*/
                    "EFCachingProvider"
            ), "<#=container.Name#>")

	{
		
    }
	
	public <#=Code.Escape(container)#>Context(string connectionString)
            : base(EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(
                    connectionString,
                    /*"EFTracingProvider",*/
                    "EFCachingProvider"
            ))
    {
    }
    #endregion
	
	// ObjectSets are not required, when we use the DbContext.
	
    #region Tracing Extensions

    private EFTracingConnection TracingConnection
    {
        get { return this.UnwrapConnection<EFTracingConnection>(); }
    }

    public event EventHandler<CommandExecutionEventArgs> CommandExecuting
    {
        add { this.TracingConnection.CommandExecuting += value; }
        remove { this.TracingConnection.CommandExecuting -= value; }
    }

    public event EventHandler<CommandExecutionEventArgs> CommandFinished
    {
        add { this.TracingConnection.CommandFinished += value; }
        remove { this.TracingConnection.CommandFinished -= value; }
    }

    public event EventHandler<CommandExecutionEventArgs> CommandFailed
    {
        add { this.TracingConnection.CommandFailed += value; }
        remove { this.TracingConnection.CommandFailed -= value; }
    }

    private void AppendToLog(object sender, CommandExecutionEventArgs e)
    {
        if (this.logOutput != null)
        {
            this.logOutput.WriteLine(e.ToTraceString().TrimEnd());
            this.logOutput.WriteLine();
        }
    }

    public TextWriter Log
    {
        get { return this.logOutput; }
        set
        {
            if ((this.logOutput != null) != (value != null))
            {
                if (value == null)
                {
                    CommandExecuting -= AppendToLog;
                }
                else
                {
                    CommandExecuting += AppendToLog;
                }
            }

            this.logOutput = value;
        }
    }


    #endregion

    #region Caching Extensions

    private EFCachingConnection CachingConnection
    {
        get { return this.UnwrapConnection<EFCachingConnection>(); }
    }

    public ICache Cache
    {
        get { return CachingConnection.Cache; }
        set { CachingConnection.Cache = value; }
    }

    public CachingPolicy CachingPolicy
    {
        get { return CachingConnection.CachingPolicy; }
        set { CachingConnection.CachingPolicy = value; }
    }

    #endregion
}
// Workaround ends here

// DbContext
<#=Accessibility.ForType(container)#> partial class <#=Code.Escape(container)#> : DbContext
{
	// Modification: Constructor with ObjectContext init
    public <#=Code.Escape(container)#>()
        : base(new <#=Code.Escape(container)#>Context(), true)
    {
<#
        WriteLazyLoadingEnabled(container);
#>
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        throw new UnintentionalCodeFirstException();
    }

<#
    foreach (var entitySet in container.BaseEntitySets.OfType<EntitySet>())
    {
#>
    <#=Accessibility.ForReadOnlyProperty(entitySet)#> DbSet<<#=Code.Escape(entitySet.ElementType)#>> <#=Code.Escape(entitySet)#> { get; set; }
<#
    }

    foreach (var edmFunction in container.FunctionImports)
    {
        WriteFunctionImport(edmFunction, false);
    }
#>
}
<#

if (!String.IsNullOrEmpty(ObjectNamespace))
{
    PopIndent();
#>
}
<#
}
#>
<#+
string ModelNamespace { get; set; }
string ObjectNamespace { get; set; }
CodeGenerationTools Code { get; set; }
MetadataTools EFTools { get; set; }

string GetResourceString(string resourceName)
{
	if(_resourceManager == null)
	{
		_resourceManager = new System.Resources.ResourceManager("System.Data.Entity.Design", typeof(System.Data.Entity.Design.MetadataItemCollectionFactory).Assembly);
	}
	
    return _resourceManager.GetString(resourceName, null);
}
System.Resources.ResourceManager _resourceManager;

void WriteLazyLoadingEnabled(EntityContainer container)
{
   string lazyLoadingAttributeValue = null;
   var lazyLoadingAttributeName = MetadataConstants.EDM_ANNOTATION_09_02 + ":LazyLoadingEnabled";
   if(MetadataTools.TryGetStringMetadataPropertySetting(container, lazyLoadingAttributeName, out lazyLoadingAttributeValue))
   {
       bool isLazyLoading;
       if(bool.TryParse(lazyLoadingAttributeValue, out isLazyLoading) && !isLazyLoading)
       {
#>
        this.Configuration.LazyLoadingEnabled = false;
<#+
       }
   }
}

void WriteFunctionImport(EdmFunction edmFunction, bool includeMergeOption)
{
    var parameters = FunctionImportParameter.Create(edmFunction.Parameters, Code, EFTools);
    var paramList = String.Join(", ", parameters.Select(p => p.FunctionParameterType + " " + p.FunctionParameterName).ToArray());
    var returnType = edmFunction.ReturnParameter == null ? null : EFTools.GetElementType(edmFunction.ReturnParameter.TypeUsage);
    var processedReturn = returnType == null ? "int" : "ObjectResult<" + MultiSchemaEscape(returnType) + ">";

    if (includeMergeOption)
    {
        paramList = Code.StringAfter(paramList, ", ") + "MergeOption mergeOption";
    }
#>

    <#=AccessibilityAndVirtual(Accessibility.ForMethod(edmFunction))#> <#=processedReturn#> <#=Code.Escape(edmFunction)#>(<#=paramList#>)
    {
<#+
        if(returnType != null && (returnType.EdmType.BuiltInTypeKind == BuiltInTypeKind.EntityType ||
                                  returnType.EdmType.BuiltInTypeKind == BuiltInTypeKind.ComplexType))
        {
#>
        ((IObjectContextAdapter)this).ObjectContext.MetadataWorkspace.LoadFromAssembly(typeof(<#=MultiSchemaEscape(returnType)#>).Assembly);

<#+
        }

        foreach (var parameter in parameters.Where(p => p.NeedsLocalVariable))
        {
            var isNotNull = parameter.IsNullableOfT ? parameter.FunctionParameterName + ".HasValue" : parameter.FunctionParameterName + " != null";
            var notNullInit = "new ObjectParameter(\"" + parameter.EsqlParameterName + "\", " + parameter.FunctionParameterName + ")";
            var nullInit = "new ObjectParameter(\"" + parameter.EsqlParameterName + "\", typeof(" + parameter.RawClrTypeName + "))";
#>
        var <#=parameter.LocalVariableName#> = <#=isNotNull#> ?
            <#=notNullInit#> :
            <#=nullInit#>;

<#+
        }

        var genericArg = returnType == null ? "" : "<" + MultiSchemaEscape(returnType) + ">";
        var callParams = Code.StringBefore(", ", String.Join(", ", parameters.Select(p => p.ExecuteParameterName).ToArray()));

        if (includeMergeOption)
        {
            callParams = ", mergeOption" + callParams;
        }
#>
        return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction<#=genericArg#>("<#=edmFunction.Name#>"<#=callParams#>);
    }
<#+
    if(!includeMergeOption && returnType != null && returnType.EdmType.BuiltInTypeKind == BuiltInTypeKind.EntityType)
    {
        WriteFunctionImport(edmFunction, true);
    }
}

string AccessibilityAndVirtual(string accessibility)
{
    return accessibility + (accessibility != "private" ? " virtual" : "");
}

string MultiSchemaEscape(TypeUsage usage)
{
    var type = usage.EdmType as StructuralType;
    return type != null && type.NamespaceName != ModelNamespace ?
        Code.CreateFullName(Code.EscapeNamespace(type.NamespaceName), Code.Escape(type)) :
        Code.Escape(usage);
}

#>

Für den CodeFirst-Ansatz wird das Ganze schon ein wenig wilder, da das Model zur Laufzeit erstellt wird und der Metadaten-ConnectionString in dieser Form auch nicht existiert. Aber auch hier habe ich mit ein wenig „Basteln“ eine lauffähige Variante hinbekommen. Da dieser Workaround ziemlich wild ist, habe ich diesen als T4-Vorlage realisiert. Folgende Vorteile ergeben sich daraus: Der Workaround ist dokumentiert und lässt sich leichter entfernen, wenn in einer zukünftigen Version der 2nd-Level Cache endlich mal realisiert wird.

Die T4-Vorlage für den wildesten aller Workarounds ist hier:


<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".txt" #>
<#@ assembly name="Microsoft.CSharp" #>
<#@ assembly name="System.Data.Entity" #>
<#@ assembly name="$(ProjectDir)$(OutDir)EntityFramework.dll" #>
<#@ assembly name="$(TargetPath)" #> // in this case dll with codefirst context
<#@ import namespace="System.Data.EntityClient" #>
<#@ import namespace="System.Data.Entity.Infrastructure" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml" #>
<#@ include file="TemplateFileManager.CS.ttinclude" #>
<#@ include file="VsAutomationHelper.CS.ttinclude" #>
<#@ include file="ConfigurationAccessor.CS.ttinclude" #>

<# 
// ToDo: Initalize an instance of the DbContext (T4 uses connection by convention)
var contextInstance = new DbContextCodeFirst2ndLevelCache.TestContext();

// Change database connection 
// Workaround for accessing the app.config from t4 [random appdomain :-(]
ConfigurationAccessor config = new ConfigurationAccessor((IServiceProvider)this.Host);
// set the connectionstring
string cn = config.ConnectionStrings.Cast<ConnectionStringSettings>()
	.Where(c=>c.Name == contextInstance.GetType().Name).Single().ConnectionString;

contextInstance.Database.Connection.ConnectionString = cn;


string contextName = contextInstance.GetType().Name;
string contextNamespace = contextInstance.GetType().Namespace;
string contextSuffix = "OC";
var fileManager = TemplateFileManager.Create(this);

// File properties for C# output files
var compileProp = new FileProperties();
compileProp.BuildAction = BuildAction.Compile;

// File properties for EDMX output
var edmxProp = new FileProperties();
edmxProp.BuildAction = BuildAction.EntityDeploy;
edmxProp.CustomTool = "EntityModelCodeGenerator";

// Create Edmx file
fileManager.StartNewFile(String.Format("{0}.edmx", contextName)
						, fileProperties:edmxProp);

this.Write(this.GetEdmxFileFromDbContext(contextInstance));

// Create the ObjectContext with ef provider wrapper
fileManager.StartNewFile(String.Format("{0}{1}.cs", contextName, contextSuffix)
						, fileProperties:compileProp);

CreateObjectContextFromDbContext(contextName, contextNamespace, contextSuffix);


fileManager.Process(true);

#>

<#+ 

void CreateObjectContextFromDbContext(string contextName, string contextNamespace, string contextSuffix)
{
#>
namespace <#= contextNamespace #>
{
	using System;
	using System.Collections.Generic;
	using System.Configuration;
	using System.Linq;
	using System.Text;
	using System.Data;
	using System.Data.Entity;
	using System.Data.EntityClient;
	using System.Data.Objects;
	using EFProviderWrapperToolkit;
    using EFTracingProvider;
    using EFCachingProvider;
    using EFCachingProvider.Caching;
	
	public class <#= contextName #><#= contextSuffix #> : ObjectContext
	{
	
	    public <#= contextName #><#= contextSuffix #>()
        	: base (EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(
          		TransformToMetaDataConnectionString("<#= contextName #>"),
          			/*"EFTracingProvider",*/
          			"EFCachingProvider"
          		), "<#= contextName #>")
	    {
	    }
		
		public static string TransformToMetaDataConnectionString(string contextName)
		{
		  string folderres = "Workaround."; //Subfolder of workaround with edmx
          var con = System.Configuration.ConfigurationManager.ConnectionStrings
            .Cast<ConnectionStringSettings>()
            .Where(c => c.Name == contextName)
            .Single();

          if (con == null)
          {
            throw new ArgumentException("No connection with name '{0}' found.", contextName);
          }

          var conn = new EntityConnectionStringBuilder();
          conn.ProviderConnectionString = con.ConnectionString;
          conn.Provider = con.ProviderName;
          conn.Metadata = String.Format("res://*/{1}{0}.csdl|res://*/{1}{0}.ssdl|res://*/{1}{0}.msl", contextName, folderres);
          return conn.ConnectionString;
		}
	}
}
<#+ 
}

/// <summary>
/// The code first context must already exists and compiled. Be sure that 
/// the connection string exists in the app.config and the database is created. 
/// Required for edmx file creation
/// </summary
string GetEdmxFileFromDbContext(System.Data.Entity.DbContext ctx)
{	
	string xml = String.Empty;
	
    var sw = new UTF8StringWriter();
    using (var writer = new XmlTextWriter(sw))
    {
      EdmxWriter.WriteEdmx(ctx, writer);
    }
	
    xml = sw.ToString();
    

	return xml;
}

public class UTF8StringWriter : StringWriter
{
  public override Encoding Encoding
  {
	get { return Encoding.UTF8; }
  }
}

#>

Die Beispiel-Solution kann hier heruntergeladen werden. Für die DbFirst bzw. ModelFirst-Variante muss zuerst eine Datenbank mit den Namen DbContextTest angelegt werden, dass SQL-Skript befindet sich im Verzeichnis SQL-Skripts. Der EFCachingProvider in der Solution ist modifiziert, die Anpassungen sind auch in diesem Blog beschrieben.

Translate this page

Kategorien

  • [-].NET Development (207)
  • [-]Datenbank (24)
  • HTML (1)
  • Konfiguration (12)
  • Mind Map (9)
  • Off-topic (9)
  • Open Source (3)
  • Qualität (6)
  • Sharepoint (2)
  • Sicherheit (2)

Archiv

Social Bookmarking

Bookmark bei: Mr. Wong Bookmark bei: Webnews Bookmark bei: Icio Bookmark bei: Oneview Bookmark bei: Linkarena Bookmark bei: Favoriten Bookmark bei: Seekxl Bookmark bei: Favit Bookmark bei: Social Bookmarking Tool Bookmark bei: Power Oldie Bookmark bei: Bookmarks.cc Bookmark bei: Newskick Bookmark bei: Newsider Bookmark bei: Linksilo Bookmark bei: Readster Bookmark bei: Folkd Bookmark bei: Yigg Bookmark bei: Digg Bookmark bei: Del.icio.us Bookmark bei: Reddit Bookmark bei: Simpy Bookmark bei: StumbleUpon Bookmark bei: Slashdot Bookmark bei: Netscape Bookmark bei: Furl Bookmark bei: Yahoo Bookmark bei: Spurl Bookmark bei: Google Bookmark bei: Blinklist Bookmark bei: Blogmarks Bookmark bei: Diigo Bookmark bei: Technorati Bookmark bei: Newsvine Bookmark bei: Blinkbits Bookmark bei: Ma.Gnolia Bookmark bei: Smarking Bookmark bei: Netvouz Information