Entity Framework 4 – N-Tier mit Self Tracking Entities
In der Version 1 des Entity Framework ist der Support für N-Tier Applikation sehr beschränkt. Ausnahmen wie zum Beispiel:
InvalidOperationException: "Das Objekt kann nicht angefügt werden, da es sich bereits im Objektkontext befindet. Ein Objekt kann nur erneut angefügt werden, wenn es sich im unveränderten Zustand befindet."
sind keine Seltenheit und Workarounds die Realität. Damit dies trotzdem funktioniert, gibt es im EF1 zwei Herangehensweisen.
Die erste erfolgt über den ObjectStateManager des Context-Objects, mit dessen Hilfe alle Eigenschaften des Objekts als modifiziert gesetzt werden. Das funktioniert aber nicht bei Objektreferenzen, die die Fremdschlüssel-Beziehungen abbilden. Varianten, wie dieses Problem mit weiteren Workarounds behoben werden kann, will ich hier nicht beschreiben, da sie aus meiner Sicht nicht zur Nachahmung geeignet sind. ;-)
C# Workaround Erweiterungsmethode SetAllModified
public static void SetAllModified<T>(this T entity, global::System.Data.Objects.ObjectContext context) where T : global::System.Data.Objects.DataClasses.IEntityWithKey
{
var stateEntry = context.ObjectStateManager.GetObjectStateEntry(entity.EntityKey);
var propertyNameList = stateEntry.CurrentValues.DataRecordInfo.FieldMetadata.Select(pn => pn.FieldType.Name);
foreach (var propName in propertyNameList)
{
stateEntry.SetModifiedProperty(propName);
}
}
Der andere geläufige Workaround ist die Erweiterungsmethode AttachUpdated. Mit diesen können auch die Objektreferenzen übernommen werden. Er funktioniert bei Objekten, die keine Auflistungen haben ohne Probleme. Die ultimative Lösung ist dieser Workaround aber auch nicht.
C# Workaround Erweiterungsmethode AttachUpdated
public static void ApplyReferencePropertyChanges(
this ObjectContext context,
IEntityWithRelationships newEntity,
IEntityWithRelationships oldEntity)
{
foreach (var relatedEnd in oldEntity.RelationshipManager.GetAllRelatedEnds())
{
var oldRef = relatedEnd as EntityReference;
if (oldRef != null)
{
var newRef = newEntity.RelationshipManager.
GetRelatedEnd(oldRef.RelationshipName,
oldRef.TargetRoleName) as EntityReference;
oldRef.EntityKey = newRef.EntityKey;
}
}
}
public static void AttachUpdated(this ObjectContext context, EntityObject objectDetached)
{
if (objectDetached.EntityState == EntityState.Detached)
{
object currentEntityInDb = null;
Type type = context.GetType();
context.MetadataWorkspace.LoadFromAssembly(type.Assembly);
if (context.TryGetObjectByKey(objectDetached.EntityKey, out currentEntityInDb))
{
context.ApplyPropertyChanges(objectDetached.EntityKey.EntitySetName, objectDetached);
context.ApplyReferencePropertyChanges((IEntityWithRelationships)objectDetached, (IEntityWithRelationships)currentEntityInDb);
}
else
{
throw new ObjectNotFoundException();
}
}
}
Mit der Basisvorlage im EF 4 besteht das Problem weiterhin, die Workarounds können hier mit einem einzigen Unterschied weiter verwendet werden. Die Methode "ApplyPropertyChanges" im Objektkontext ist veraltet und muss durch "ApplyCurrentValues" ersetzt werden.
Eine bessere Alternative ist jedoch die Vorlage Self Tracking Entities zu verwenden. Die wesentlichen Unterschiede zur Basisvorlage sind folgende:
- Es werden 2 T4-Vorlagen dem Projekt hinzugefügt
- Für jede Klasse wird eine separate Datei angelegt
- In der deutschen Umgebung kommt es zu Fehlern und die T4-Vorlagen müssen angepasst werden ;-)
- Die Objekte haben die Möglichkeit der selbstständigen Nachverfolgung von Änderungen
Wesentliche Unterschiede zur Basisvorlage liegen im Objektkontext. Dieser ist anders aufgebaut und die sonst typischen Methoden "Attach", Erweiterungsmethode "AttachUpdated" verweigern ihre Dienste mit der Meldung "Einige ungültige Argumente", Objektmethoden wie "AddToEntityName" existieren nicht mehr.
Eine der neuen Methoden im Objektkontext ist die "ApplyChanges". Mit dieser kann das jeweilige Objekt wieder dem Kontext hinzugefügt werden. Als kleine Probe hole ich ein Objekt, ändere den Namen einer Eigenschaft und füge einer Auflistung ein Objekt hinzu, bevor es wieder zurück zum Server gesendet wird. Dieses Szenario funktioniert im EF 1 ohne Workarounds nicht.
C# Methode auf Server
public void Update(Mandator mandator)
{
////this.context.AttachUpdated(mandator);
this.context.ApplyChanges("Mandators", mandator);
this.context.SaveChanges();
}
C# Aufruf durch Client
var entity = manager.GetById(2);
entity.maName = "Neuer Name";
BusinessUnit unit = new BusinessUnit()
{
buInactive = false,
buName = "business unit name",
};
entity.BusinessUnits.Add(unit);
manager.Update(entity);
T-SQL Profiler
exec sp_executesql N'update [dbo].[tbMandator]
set [maName] = @0
where ([maAutoID] = @1)
',N'@0 varchar(100),@1 int',@0='Neuer Name',@1=2
exec sp_executesql N'insert [dbo].[tbBusinessUnit]([buMandatorAutoID], [buName], [buInactive])
values (@0, @1, @2)
select [buAutoID]
from [dbo].[tbBusinessUnit]
where @@ROWCOUNT > 0 and [buAutoID] = scope_identity()',N'@0 int,@1 varchar(100),@2 bit',@0=2,@1='business unit name',@2=0
Wenn ich mir das Resultat im SQL-Profiler anschaue, dann wurde ein Objekt geändert und eins hinzugefügt. Es sieht zwar unbedeutend aus, aber mit dieser Vorlage lässt sich mit dem Entity-Framework im N-Tier Bereich Code schreiben, der wesentlich sauberer ist, als derjenige mit den Workarounds. Das wichtigste: Ich kann mich auf die Anforderungen konzentrieren.
- 1 Kommentar(e)


POCO / Code First Ansatz
Schade, dass EF bereits jetzt derart inkonsistent ist.
Ich warte immer noch auf den offiziellen Code-Only / Code-First ansatz im EF4.
Dieser hat es leider nicht ins EF4 geschafft und muss als EF4 CTP3 runtergeladen werden.
Dann haben wir dann schon einen dritten Ansatz und wenn das API auch wieder kleine Differenzen aufweist, dann ist das EF bals unhandelbar ...