Zur Zeit wird gefiltert nach: July 1
Filter zurücksetzen

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 Middle out als Vertreter der modellzentrierten Anwendungsentwicklung
Abbildung 1
Abbildung 2 Top down für die codezentrierte Anwendungsentwicklung
Abbildung 2

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 Ausführungsplan mit und ohne Fremdschlüssel-Indizes
Abbildung 3

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.

24.07.2011
15:03

T4 und der Zugriff auf die App.config

Zur Zeit sitze ich mal wieder tief in der T4-Materie. Dabei war mal wieder die zufällige Erstellung von AppDomains eine kleine Bremse. Das ist eine ganz spezielle Eigenart, die zum Beispiel auch dafür sorgt, dass auf die App.config der Solution nicht direkt zugegriffen werden kann.

Im Blog von Sky Sander fand ich einen schönen Workaround, damit der Zugriff doch wieder ermöglicht werden kann.

Bei Gelegenheit muss ich mal überprüfen, ob sich die zufällige Erstellung der T4-AppDomains irgendwie in den Griff bekommen lässt.

Nachfolgend der Workaround, der bei mir in Form einer Include-Datei mit Namen ConfigurationAccessor.CS.ttinclude verwendet wird:


<#@ assembly name="System.Configuration" #>
<#@ assembly name="EnvDTE" #>
<#@ import namespace="System.Configuration" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#+
/// <summary>
/// Provides strongly typed access to the hosting EnvDTE.Project and app.config/web.config 
/// configuration file, if present.
/// 
/// Typical usage from T4 template:
/// <code>ConfigurationAccessor config = new ConfigurationAccessor((IServiceProvider)this.Host);</code>
/// </summary>
/// <author>Sky Sanders [sky.sanders@gmail.com, http://skysanders.net/subtext]</author>
/// <date>01-23-10</date>
/// <copyright>The contents of this file are a Public Domain Dedication.</copyright>
public class ConfigurationAccessor
{
   /// <summary>
   /// Typical usage from T4 template:
   /// <code>ConfigurationAccessor config = new ConfigurationAccessor((IServiceProvider)this.Host);</code>
   /// </summary>
   public ConfigurationAccessor(IServiceProvider host)
   {
      // Get the instance of Visual Studio that is hosting the calling file
      EnvDTE.DTE env = (EnvDTE.DTE)host.GetService(typeof(EnvDTE.DTE));
      // Gets an array of currently selected projects. Since you are either in this file saving it or
      // right-clicking the item in solution explorer to invoke the context menu it stands to reason
      // that there is 1 ActiveSolutionProject and that it is the parent of this file....
      _project = (EnvDTE.Project)((Array)env.ActiveSolutionProjects).GetValue(0);
      string configurationFilename=null;  
      // examine each project item's filename looking for app.config or web.config
      foreach (EnvDTE.ProjectItem item in _project.ProjectItems)
      {
            if (Regex.IsMatch(item.Name,"(app|web).config",RegexOptions.IgnoreCase))
            {
                // TODO: try this with linked files. is the filename pointing to the source?
                configurationFilename=item.get_FileNames(0);
                break;
            }
      }

      if(!string.IsNullOrEmpty(configurationFilename))
      {
           // found it, map it and expose salient members as properties
           ExeConfigurationFileMap configFile = null;
           configFile = new ExeConfigurationFileMap();
           configFile.ExeConfigFilename=configurationFilename;

           _configuration = System.Configuration.ConfigurationManager.OpenMappedExeConfiguration(configFile, ConfigurationUserLevel.None);
      }
  }

  private EnvDTE.Project _project;
  private System.Configuration.Configuration _configuration;

  /// <summary>
  /// Provides access to the host project.
  /// </summary>
  /// <remarks>see http://msdn.microsoft.com/en-us/library/envdte.project.aspx</remarks>
  public EnvDTE.Project Project
  {
      get { return _project; }
  }

  /// <summary>
  /// Convenience getter for Project.Properties.
  /// Examples:
  /// <code>string thisAssemblyName = config.Properties.Item("AssemblyName").Value.ToString();</code>

  /// <code>string thisAssemblyName = config.Properties.Item("AssemblyName").Value.ToString();</code>
  /// </summary>
  /// <remarks>see http://msdn.microsoft.com/en-us/library/envdte.project_properties.aspx</remarks>
  public EnvDTE.Properties Properties 
  {
       get { return _project.Properties;}
  }

  /// <summary>
  /// Provides access to the application/web configuration file.
  /// </summary>
  /// <remarks>see http://msdn.microsoft.com/en-us/library/system.configuration.configuration.aspx</remarks>
  public System.Configuration.Configuration Configuration
  {
       get { return _configuration; }
  }   

  /// <summary>
  /// Provides access to the appSettings section of the configuration file.
  /// Behavior differs from typical AppSettings usage in that the indexed
  /// item's .Value must be explicitly addressed.
  /// <code>string setting = config.AppSettings["MyAppSetting"].Value;</code>

  /// </summary>
  /// <remarks>see http://msdn.microsoft.com/en-us/library/system.configuration.configuration.appsettings.aspx</remarks>
  public  KeyValueConfigurationCollection AppSettings
  {
       get { return _configuration.AppSettings.Settings;}
  }

  /// <summary>
  /// Provides access to the connectionStrings section of the configuration file.
  /// Behavior is as expected; items are accessed by string key or integer index.
  /// <code>string northwindProvider = config.ConnectionStrings["northwind"].ProviderName;</code>
  /// </summary>
  /// <remarks>see http://msdn.microsoft.com/en-us/library/system.configuration.configuration.connectionstrings.aspx</remarks>
  public  ConnectionStringSettingsCollection ConnectionStrings
  {
        get { return _configuration.ConnectionStrings.ConnectionStrings;}
  }
}#>

Entity Framework 4.2 - Facelifting am EDM-Designer

Die JuneCTP des Entity Frameworks ist nun schon ein Moment verfügbar und ich nutzte die Gelegenheit, mir zuerst die Neuerungen im Designer zu betrachten. Ein Nachteil, der sich abzuzeichnen scheint ist die Tatsache, dass die zukünftige Version mit aller Wahrscheinlichkeit nicht mehr auf Rechnern mit Windows XP laufen wird. Einige werden denken: Wer hat noch so ein altes Betriebssystem installiert? Auf jeden Fall ein Teil der Fortune Global 500. ;-) In dieser Hinsicht könnte das recht interessant werden.

Betrachten wir die Neuerungen. Neu gibt es bei den Datenbankobjekten eine Hierarchie nach Schema, das erleichtert schon mal die Übersicht der Schemaobjekte. Zusätzlich gibt es nun auch eine neue Checkbox, damit ausgewählte Prozeduren gleich in das Model importiert werden. Der bisherige Zwischenschritt über den Modellbrowser ist somit nicht mehr zwingend notwendig.

Abbildung 1 Neuer Wizard mit Hierarchie nach Schema und Import selected procedures
Abbildung 1

Aktuell werden die Positionsdaten des EDM in der Edmx-Datei gespeichert. Mit dem neuen Designer werden diese Daten in einer separaten Datei gespeichert, welche der Edmx-Datei untergeordnet ist. Somit befinden sich nur noch die Angaben zum Domain-Modell in der Edmx-Datei.

Abbildung 2 Positionsdaten befinden sich neu in einer separaten Datei
Abbildung 2

Ein weiteres neues Feature ist die Möglichkeit Entitäten farblich zu unterscheiden. Dies kann bei grossen Modellen eine Hilfe darstellen, um eine bessere Trennung zu ermöglichen.

Abbildung 3 zeigt die Möglichkeit der farblichen Kennzeichnung von Entitäten
Abbildung 3

Ein Problem bei grossen Modellen wird aber bestehen bleiben, an welcher Position befinden sich die Entitäten, die ich gerade betrachten will? Wenn ich nur einen Teilausschnitt betrachten will, ist das ein Mehraufwand, den ich nicht unbedingt leisten will.

Abbildung 4 Bei grossen Modellen ist die Suche von Teilausschnitten mühsam
Abbildung 4

Aus diesem Grund können im neuen Modellbrowser Diagramme definiert werden, um Teilausschnitte besser hervorheben zu können. Bei grossen Modellen wird das recht interessant, da so die Teilausschnitte (Package) übersichtlicher dargestellt werden können, was den Suchaufwand minimieren kann. ;-)

Abbildung 5 zeigt einen Teilausschnitt Customer aus dem Modell
Abbildung 5

Diese Darstellungsform bietet einige Vorteile. Ein grosses Modell kann so auf mehrere Sichten aufgeteilt werden, ohne auf dem Komfort einer Edmx-Datei verzichten zu müssen. So erspare ich mir auch das Management mehrerer Kontexte im Code, was auch zur Vereinfachung beiträgt. So habe ich beides und muss mich nicht mehr entscheiden zwischen: schöne kleine Modelle und komplexer Code oder einfacher Code dafür ein grosses unübersichtliches Modell.

Ein weiteres neues Feature ist auch die Änderung der Reihenfolge einzelner Eigenschaften einer Entität, dies wird über das Kontextmenü ermöglicht. In Verbindung mit Code-Generierung kann das sehr nützlich sein.

Aber, aktuell werden eigene Eigenschaften im Eigenschaftsfenster des EDM noch nicht unterstützt. Gerade im Zusammenhang mit dem Ansatz pragmatischer Modelle bzw. Metaprogrammierung kann dieses Feature sehr mächtig sein.

Weitere neue Funktionen der JuneCTP sind in diesem Beitrag beschrieben.

Bei Gelegenheit werde ich mich auch mit dem Enum-Support auseinander setzen. Primär werde ich dabei einen Blick auf die Worst-Case-Szenarien setzen, denen ich in Zukunft wohl auch begegnen werde.

Entity Framework 4 - ORM und die Unterschiede zwischen Domänen- und ER-Modell

Brownfield bzw. DB First ist unter vielen Entwicklern unbeliebt. Am liebsten würden sie auf der grünen Wiese beginnen. Es gibt viele Begriffe und Ideologien, häufig stellt es aber eine Herausforderung dar, diese in der Praxis zu vereinen.

DB First ist grundsätzlich nicht immer schlecht, da bei diesem Ansatz der Blickpunkt auf den Daten liegt. Gerade auch im Hinblick, wenn die Datenbank bereits besteht, macht es nicht immer Sinn ein neues Datenbankmodell zu erstellen und eine Migration der Daten ins Auge zu fassen.

Die Herausforderungen, die dann auf einen zukommen, hat Patrick in seinem Beitrag Data quality as a business value beschrieben.

Bei solchen Szenarien habe ich gelegentlich den Eindruck, dass einige Entwickler den Bezug zur Realität verlieren. Tragisch wird es dann, wenn auch die Einsicht für Kompromisse fehlt und auf biegen und brechen der gewünschte Ansatz durchgedrückt werden muss. Dabei wird gelegentlich vergessen, dass beim ORM auf der einen Seite mit zwei Arten von Technologien gearbeitet wird, die auf unterschiedliche Weise funktionieren und optimiert werden. Auf der anderen Seite dienen die relationalen Daten nicht selten als Quelle für ein DWH oder Reporting mit anderen Tools wie zum Beispiel Cognos. Hier stellt sich dann die Frage, welcher Ansatz ist nun günstiger? Sind es ständig ändernde Datenmodelle oder ein O/R Modell, bei dem sich die konzeptionelle Sicht von der Datensicht unterscheiden darf?

Ich habe nun so einen Fall, dass eine Anwendung von .NET 3.5 auf .NET 4.0 portiert wird. Dadurch habe ich die Möglichkeit im Entity Data Model (EDM) auch die Fremdschlüssel in das Modell mit einzubeziehen und ein paar Workarounds zu entfernen.

Es gibt einen Punkt im Modell, wo Vererbung verwendet wird. Bedingt durch einen fehlenden Diskriminator in der Basistabelle war die Modellierung in der ersten Version des Entity Framework sehr komplex und an eine Arbeit mit dem Designer war nicht mehr zu denken.

Die Typerkennung konnte ich aus mehreren existierenden Spalten ableiten und so habe ich im Model mit <definingquery></definingquery>den Diskriminator bilden können.

Mit dem Einsatz mehrerer Spalten auf Tabellen-Ebene sollte der Feldmissbrauch verhindert werden, eine Regel im Datenbankdesign, die nicht unbedingt in der Objektorientieren Welt vorkommt. Das sich diese Felder alle in einer Tabelle (TPH) befinden ist primär ein Optimierungsentscheid, da der Einsatz von Super-Entitäten (TPT) einen erhöhten Join-Aufwand zur Folge haben können und im Entity Framework 3.5 ganz klar einen Fehlentscheid darstellen. Im EDM unterscheidet sich diese Tabelle vom Design, da eine Vererbungshierarchie vorhanden ist.

Mit der Portierung war nun die Idee, dass diese Tabelle einen Diskriminator erhält, damit die komplizierte <definingquery></definingquery>aus dem Modell entfernt werden kann. Bei Modellaktualisierungen wird diese häufig zerstört und eine manuelle Nachbearbeitung wird dadurch erforderlich, natürlich im XML.

Ein zusätzliches Feld hätte in diesem Zusammenhang auch eine Datenmigration zur Folge und so schlug der Verantwortliche für die Datenbank vor stattdessen eine View zu verwenden, da der bisher abgeleitete Diskriminator-Wert über eine andere Tabelle zur Verfügung steht.

Im ersten Moment mag das ein wenig ungewohnt klingen, aber dieser Ansatz verursacht auf Ebene der Datenbank geringe bis keine Kosten, da eine Datenmigration nicht erforderlich wird.

Im zweiten Moment kommen dann die Überlegungen:

  • Vererbung, kein Problem
  • Fremdschlüssel von einer Tabelle zur View, geht nicht aber in der konzeptionellen Sicht lässt sich die Zuordnung theoretisch realisieren

Beginnen wir mit dem Beispiel, ich habe eine Tabelle, die nun als View zur Verfügung steht und zusätzlich den Diskriminator enthält. Im Modell sollen die Zuordnungen und die Vererbung beibehalten werden, damit Code-Anpassungen ausgeschlossen werden können. Das Ziel soll nun sein, die <definingquery></definingquery>für diesen Bereich zu entfernen, damit das EDM zukünftig wieder einfacher erweitert werden kann.

Ein kleiner Teilausschnitt des bestehenden Modells sieht so aus:

Abbildung 1 Teilauszug eines Modells mit Vererbung (TPH). Die Entität Calculation soll durch eine View ersetzt werden
Abbildung 1

Die Entität Calculation ist der Knackpunkt, bisher wird der Diskriminator innerhalb <definingquery></definingquery>definiert und macht die Aktualisierung des Modells zu einer Herausforderung.

Der Unterbau soll durch ein View ersetzt werden. Grundsätzlich liesse sich dass im XML per Hand realisieren, doch ich verwende mal den Designer und entferne die Entität Calculation aus dem Modell.

Abbildung 2 Modell nach der Entfernung der Entität
Abbildung 2

Nun füge ich die View hinzu, in dem ich den Punkt Modell aus Datenbank aktualisieren wähle, und nenne diese wieder Calculation. Nach diesem Schritt habe ich jedoch noch nicht die Vererbung sowie die Beziehung zur Header-Entität.

Abbildung 3 zeigt das Modell nachdem die View hinzugefügt wurde
Abbildung 3

Was das Entity Framework immer noch macht, ist die eigenständige Definition von Entitätsschlüsseln bei Views. In der aktuellen Version können die Schlüssel der Eigenschaften abgewählt werden, sodass in meinen Fall nur eine Eigenschaft als Primärschlüssel existiert. In der Vorgängerversion war das ein wenig Gefrickel.

Als nächstes erstelle ich die Zuordnung zur Header Entität. Dazu wähle ich auf der Entität Header den Menüpunkt Hinzufügen/Zuordnung.

Abbildung 4 Zuordnung zwischen Tabelle und View erstellen
Abbildung 4

Wenn ich die Zuordnung erstellt habe, dann begrüsst mich folgende Fehlermeldung:

Fehler 11009: Die Eigenschaft 'HeaderPH_AutoID' ist nicht zugeordnet.

Dieser Fehler kommt zustande, weil die View die ID bereits enthält und eine neue Eigenschaft für die Zuordnung erstellt wurde. Als Erstes muss also die ID aus der Entität entfernt werden und die ID des Storage Modell auf die HeaderID über die Mapping Details realisiert werden.

Abbildung 5 zeigt die Anpassung des Mappings
Abbildung 5

Anschliessend geht es darum, die Vererbungshierarchie wieder herzustellen, dazu leite ich die Entität MarketShare von der Entität Calculation ab. Dann entferne ich die bereits zugeordneten Spalten für MarketShare aus der Calculation-Entität. Das Gleiche gilt auch für den nun existierenden Diskriminator. Dieser wird ebenfalls aus der Calculation-Entität entfernt.

Nun muss für die Entität MarketShare das Mapping auf die View des Storage-Layers definiert und die Bedingung hinzugefügt werden. Nach diesen Anpassungen sieht mein Modell wieder aus wie in Abbildung 1 und die Validierung ist auch erfolgreich.

In meiner Business-Logik sind nun keine Anpassungen notwendig, da das Modell identisch ist. Der Datenbankverantwortliche ist zufrieden, da die Anpassung keine Migration zur Folge hat.

Dieses Beispiel funktioniert nur in Verbindung mit DB First (ggf. auch Model First im Roundtrip) und dem EDM-Designer.

Mit diesem Kompromiss lässt sich eigentlich leben, da keine Anpassungen in der Business-Logik notwendig werden.

Das Entity Framework macht die Migration trotzdem nicht einfach, wenn im alten Modell Workarounds zum Einsatz kamen, um die Fremschlüssel abzubilden oder die Prozedur-Signaturen im Context von Hand geschrieben worden sind. Diese müssen dann entfernt werden. Weiterhin kann es auch passieren, dass einige Eigenschaften ihre Werte für die Einstellung Nullable verlieren, auch hier ist dann Handarbeit angesagt, damit das Modell wieder fehlerfrei validiert wird.

Das Beispiel zeigt aber auch, dass die Philosophie des ER-Modells von Dr. P. Chen, auf welchen das EDM aufbaut, sehr flexibel sein kann.

Translate this page

Kategorien