Während Entity Framework prinzipiell mit Stored Functions bzw. Table Valued Functions, die innerhalb von Abfragen eingesetzt werden können, umgehen kann, hat diese Möglichkeit noch nicht den Weg in die Welt von Code First geschafft. Allerdings können Entwickler seit Entity Framework 6.1 auf die Mapping API zugreifen. Dabei handelt es sich um ein Objektmodell, welches in vergangenen Versionen lediglich interne zur Verfügung stand und sowohl lesenden als auch schreibenden Zugriff auf das Entity Data Model gewährt. Dies kann ma
n sich zunutze machen, um eine Unterstützung für Stored Functions bzw. Table Valued Functions zu implementieren. Da das Entity Data Model und seine Repräsentation durch die Mapping API recht komplex sind, handelt es sich bei dieser Aufgabe nicht um etwas, das man nebenbei erledigen kann.
Glücklicherweise hat sich darum bereits jemand angenommen und das Ergebnis dieses Unterfangens als freies Projekt bereitgestellt. Dieses findet man unter [1]. Es bietet für die Konfiguration von Sotred Functions bzw. Table Valued Functions in Code-First-Szenarien eine einfache API, welche die Komplexität der Mapping-API verbirgt.
Jenen, die wissen möchten, wie dieses Projekt die Mapping-API verwendet oder nicht von einem externen Projekt abhängig sein wollen, bietet dieser Beitrag eine nähere Beschreibung des von diesem Projekt verfolgten Ansatzes.
Für jede gewünschte Stored Function ist eine Methode in der verwendeten DbContext-Implementierung einzurichten. Diese kann beliebige Parameter, die sich auf primitive Typen abstützen, entgegennehmen und liefert ein IQueryable retour. Obwohl es sich bei T um einen primitiven Typen oder auch um einen komplexen Typen handeln könnte, gehen die nachfolgenden Beispiele davon aus, dass T den Typ einer Entität wiederspiegelt. Die einzige Aufgabe dieser Methoden besteht darin, an die Methode CreateQuery zu delegieren, damit diese die gewünschte Stored Function aufruft. CreateQuery ist Teil des vom DbContext verwendeten ObjectContext-Objektes. Im betrachteten Beispiel wird auf diese Weise die Stored Function GetHotels angestoßen.
public virtual IQueryable<Hotel> GetHotels(int? regionId)
{
var regionIdGenerator = regionId.HasValue ?
new ObjectParameter("regionId", regionId.Value) :
new ObjectParameter("regionId", typeof(string));
return ((IObjectContextAdapter)this).ObjectContext
.CreateQuery(
string.Format("[{0}].{1}", GetType().Name, "[GetHotels](@regionId)"), regionIdGenerator);
}
Damit Entity Framework die aufgerufene Stored Function kennt, muss der Entwickler dafür sorgen, dass sie im Entity Data Model definiert wird. Dazu kommt eine StoreModelConvention, welche dem Entwickler Zugriff auf die Mapping-API gibt, zum Einsatz. Da diese Implementierung auch den verwendeten DbContext benötigt, erhält sie einen Typparameter T, über den der Konsument ihren Typ angeben kann.
public class CustomFunctionsConvention<T> : IStoreModelConvention<EntityContainer>
where T : DbContext
{
[…]
}
Das Interface IStoreModelConvention gibt eine Methode Apply vor, welche zu implementieren ist. Das nachfolgende Beispiel zeigt eine Implementierung, welche die zuvor erwähnte Stored Function GetHotels ins sowohl zum Store Model als auch zum Conceptual Model des Entity Data Models hinzufügt und eine Verbindung über das Mapping herstellt. Dieses Beispiel stützt sich auf zwei nachfolgend beschriebene Hilfsmethoden: CreateConceptualFunctionDefinition erzeugt die Definition für das Conceptual Model und CreateStoreFunctionDefinition erstellt das Gegenstück für das Store Model.
public void Apply(EntityContainer item, DbModel model)
{
var functionImportDefinition
= CreateConceptualFunctionDefinition (model);
var storeFunctionDefinition
= CreateStoreFunctionDefinition(model);
model.ConceptualModel.Container.AddFunctionImport(functionImportDefinition);
model.StoreModel.AddItem(storeFunctionDefinition);
var mapping = new FunctionImportMappingComposable(
functionImportDefinition,
storeFunctionDefinition,
new FunctionImportResultMapping(),
model.ConceptualToStoreMapping);
model.ConceptualToStoreMapping.AddFunctionImportMapping(mapping);
}
Die Methode CreateConceptualFunctionDefinition findet sich im nachfolgenden Beispiel. Sie erzeugt Beschreibungen der Übergabeparameter und des Rückgabewertes. Um zu den Beschreibungen der hiervon betroffenen Datentypen zu gelangen, stützt sie sich auf die Hilfsmethoden GetPrimitiveType und GetEntityType, welche weiter unten abgebildet sind. Die Variable functionPayload repräsentiert die gesamte Stored Function inkl. Parameter und Rückgabewerte. Interessant sind hier die Eigenschaften IsComposable und IsFunctionImport, welche auf true gesetzt werden. Erstere gibt an, dass es sich hierbei um eine Funktion handelt, die von SQL-Anweisungen (z. B. innerhalb eines SELECTs) aufgerufen werden kann. Letztere gibt an, dass es sich hiermit eine Stored Function auf konzeptioneller Ebene handelt. Die Funktion EdmFunction.Create erzeugt mit der functionPayload ein Objekt vom Typ EdmFunction und liefert dieses retour. Im Zuge dessen wird auch der Name der Funktion angegeben sowie die Tatsache, dass es sich hiermit um ein Element für das Conceptual Model handelt, durch Angabe von DataSpace.CSpace unterstrichen.
private EdmFunction CreateConceptualFunctionDefinition(DbModel model)
{
// Describe In-Parameter
var parameterType = GetPrimitiveType(typeof(int));
var parameter = FunctionParameter.Create("regionId",
parameterType, ParameterMode.In);
// Describe returnValue
var returnValueType = GetEntityType(model, "Hotel");
var returnValue = FunctionParameter.Create(
"ReturnParam",
returnValueType.GetCollectionType(),
ParameterMode.ReturnValue);
// Betroffene EntitySets
var entitySets = model
.ConceptualModel
.Container
.EntitySets
.Where(s => s.ElementType == returnValueType)
.ToList();
var functionPayload = new EdmFunctionPayload {
Parameters = new [] { parameter },
ReturnParameters = new[] { returnValue },
IsComposable = true,
IsFunctionImport = true,
EntitySets = entitySets
};
return EdmFunction.Create(
"GetHotels",
model.ConceptualModel.Container.Name,
DataSpace.CSpace,
functionPayload,
null);
}
GetPrimitiveType findet in einer von Entity Framework vordefinierten Auflistung, welche sämtliche primitive Datentypen beinhaltet, eine Beschreibung für den übergebenen CLR-Typen:
private static PrimitiveType GetPrimitiveType(Type type)
{
var parameterType = PrimitiveType
.GetEdmPrimitiveTypes()
.Where(pt => pt.ClrEquivalentType == type)
.First();
return parameterType;
}
GetEntityType muss hingegen im Entity Data Model nach der Beschreibung des Typs, welcher die gewünschte Entität wiederspiegelt, suchen. Dazu wird die Auflistung EntityTypes verwendet. Für komplexe Typen und Enum-Typen stehen analog dazu weitere Auflistungen zur Verfügung:
private static EntityType GetEntityType(DbModel model, string name)
{
var returnValueType = (EntityType) model.ConceptualModel.EntityTypes.Where(t => t.Name == name).First();
return returnValueType;
}
Die Methode CreateStoreFunctionDefinition erzeugt eine Beschreibung der Stored Function für das Store Model. Sie ist analog zur oben betrachteten Funktion CreateConceptualFunctionDefinition aufgebaut. Allerdings verwendet sie anstatt der Beschreibungen der Typen aus dem Conceptual Model Beschreibungen der Typen der jeweiligen Datenbank, welche über die Methode GetStoreType in Erfahrung gebracht werden. Der Rückgabewert, bei dem es sich im Conceptual Model um eine Entität handelt, wird hier durch einen RowType, der aus verschiedenen Spalten besteht, beschrieben.
private EdmFunction CreateStoreFunctionDefinition(DbModel model)
{
// Describe In-Parameter
var conceptualParameterType = GetPrimitiveType(typeof(int));
var parameterUsage = TypeUsage.CreateDefaultTypeUsage(conceptualParameterType);
var parameterType = model.ProviderManifest.GetStoreType(parameterUsage).EdmType;
var parameter = FunctionParameter.Create("regionId",
parameterType,
ParameterMode.In);
// Describe returnValue
var conceptualReturnType = GetEntityType(model, "Hotel");
var returnProperties = new List();
var propertyToStoreTypeUsage = FindStoreTypeUsages(conceptualReturnType, model);
foreach (var p in conceptualReturnType.Properties)
{
var usage = propertyToStoreTypeUsage[p];
returnProperties.Add(EdmProperty.Create(p.Name, usage));
}
var returnValueType = RowType.Create(returnProperties, null);
var returnValue = FunctionParameter.Create(
"ReturnParam",
returnValueType.GetCollectionType(),
ParameterMode.ReturnValue);
var functionPayload = new EdmFunctionPayload
{
Parameters = new[] { parameter },
ReturnParameters = new[] { returnValue },
IsComposable = true,
Schema = "dbo"
};
return EdmFunction.Create(
"GetHotels",
"CodeFirstDatabaseSchema",
DataSpace.SSpace,
functionPayload,
null);
}
Um die richtigen Typen hierfür zu finden, stützt sich das betrachtete Beispiel auf die Hilfsmethode FindStoreTypeUsages, welche aus [1] übernommen wurde.
private Dictionary FindStoreTypeUsages(EntityType entityType, DbModel model)
{
var propertyToStoreTypeUsage = new Dictionary();
var entityTypeMapping =
model.ConceptualToStoreMapping.EntitySetMappings.SelectMany(s => s.EntityTypeMappings)
.Single(t => t.EntityType == entityType);
foreach (var property in entityType.Properties)
{
var propertyMapping = (ScalarPropertyMapping)entityTypeMapping.Fragments.SelectMany(f => f.PropertyMappings).Single(p => p.Property == property);
propertyToStoreTypeUsage[property] = TypeUsage.Create(
propertyMapping.Column.TypeUsage.EdmType,
propertyMapping.Column.TypeUsage.Facets.Where(
f => f.Name != "StoreGeneratedPattern" && f.Name != "ConcurrencyMode"));
}
return propertyToStoreTypeUsage;
}
Diese unter Schweiß geschaffene Konvention ist am Ende noch in der Methode OnModelCreating des DbContextes zu registrieren:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
[…]
modelBuilder.Conventions.Add(new CustomFunctionsConvention());
}
Das hier gezeigte Beispiel kann unter [2] heruntergeladen werden. Die verwendete Datenbank kann mittels Migrations erzeugt werden. Anschließend sind noch die Stored Function, deren Quellcode sich in der Datei migrations\functions.sql befindet, einzuspielen und ein paar Testdatensätze (Regionen und Hotels) anzulegen. Die Id der abzufragenden Region wird in der Konstante Program.REGION_ID festgelegt.
Links