Generische Methode um gleichnamige Properties einer Klasse zu einem Objekt einer anderen Klasse zu kopieren – C#

Bei meiner Arbeit kommt es häufiger vor, dass ich umfangreiche Klassen (welche die Daten einer Datenbank repräsentieren) verwende die mal schnell über 50 oder 100 Public Properties (Datenbankfelder) enthalten.

imageNun kommt immer wieder vor, dass ich zum Beispiel zum Archivieren solcher Daten, damit meine ich die Daten aus einer Tabelle in eine andere zu verschieben, nicht auf SQL Skripte auf dem Server zugreifen kann oder möchte, da dieses verschieben noch mit zusätzlicher Businesslogik hinterlegt werden muss.

Was bleibt ist dann so etwas in dieser Art:

var auftrag = new Auftrag
    { Position = 1, Artikel = "Mars", Beschreibung = "Schoko Riegel", Preis = 1.50 };

var auftragsArchiv = new AuftragArchiv();
auftragsArchiv.Artikel = auftrag.Artikel;
auftragsArchiv.Beschreibung = auftrag.Beschreibung;
auftragsArchiv.Preis = auftrag.Preis;

if (auftragsArchiv.Save())
{
    auftrag.Delete();
}

Diese Vorgehensweise kann bei umfangreichen Klassen sehr arbeitsintensiv sein.

Nun gibt es verschiedene Ansätze um sich diesem Problem anzunehmen. Nachfolgend möchte ich einige Lösungsansätze unter Verwendung von Reflection aufzeigen.

Ich nehme an, jeder kennt die Methode ToString().

Die Idee ist nun eine generische To…. Methode zu erstellen, welche die Aufgaben wie im obigen Beispiel dargestellt, universell erledigt.

Das Ziel soll sein, dass die Public Property Werte eines Objektes, durch Aufruf einer Methode, alle Werte die den gleichen Property Namen  haben in ein anderes Objekt einer anderen Klasse kopiert werden.

Hier nun die Variante 1 (Methode einer Klasse):

public T ToTobject<T>(T target)
{
    const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public;
    var sourceProperties = this.GetType().GetProperties(Flags);

    foreach (PropertyInfo pi in sourceProperties)
        {
            if (pi.CanWrite)
            {
				var propInfoObj = target.GetType().GetProperty(pi.Name);
				if (propInfoObj != null)
				{
					var propValue = pi.GetValue(this, null);
					propInfoObj.SetValue(target, propValue, null);
				}                             
            }
        }

    return target;
}

Der Aufruf um diese Variante zu verwenden sieht dann so aus:

var auftrag = new Auftrag
	{ Position = 1, Artikel = "Mars", Beschreibung = "Schoko Riegel", Preis = 1.50 };

var auftragsArchiv = new AuftragArchiv();

auftragsArchiv = auftrag.ToTobject(auftragsArchiv);

if (auftragsArchiv.Save())
{
	auftrag.Delete();
}

Sieht doch schon ganz gut aus.

Hier noch eine Variante, die noch etwas mehr generisch arbeitet und nur noch mitgeteilt bekommt, welchen Type ich als Rückgabe der Methode erwarte, die Instanz des Objekts der neuen Klasse wird in der Methode selbst erzeugt.

public T ToTobject<T>()
{
	var constructorInfo = typeof(T).GetConstructor(new Type[] { });
	if (constructorInfo != null)
	{
		var target = (T)constructorInfo.Invoke(new object[] { });

		const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public;
		var sourceProperties = this.GetType().GetProperties(Flags);

		foreach (PropertyInfo pi in sourceProperties)
		{
			if (pi.CanWrite)
			{
				if (!pi.IsDefined(typeof(XmlIgnoreAttribute), false))
				{
					var propInfoObj = target.GetType().GetProperty(pi.Name);
					if (propInfoObj != null)
					{
						var propValue = pi.GetValue(this, null);
						propInfoObj.SetValue(target, propValue, null);
					}
				}
			}
		}

		return target;
	}

	return default(T);
}

Diese Variante wird dann so verwendet:

var auftrag = new Auftrag
	{ Position = 1, Artikel = "Mars", Beschreibung = "Schoko Riegel", Preis = 1.50 };

var auftragsArchiv = auftrag.ToTobject<AuftragArchiv>();

if (auftragsArchiv.Save())
{
	auftrag.Delete();
}

Diese Variante gefällt mir schon etwas besser

Aber das ist noch nicht das Ende der Methoden Evolution!

Es geht noch generischer (und statisch für alle Klassen verwendbar) und diese Variante folgt nun.

public static T ToTobject<T>(object source)
{
	var constructorInfo = typeof(T).GetConstructor(new Type[] { });
	if (constructorInfo != null)
	{
		var target = (T)constructorInfo.Invoke(new object[] { });

		const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public;
		var sourceProperties = source.GetType().GetProperties(Flags);

		foreach (PropertyInfo pi in sourceProperties)
		{
			if (pi.CanWrite)
			{
				var propInfoObj = target.GetType().GetProperty(pi.Name);
				if (propInfoObj != null)
				{
					var propValue = pi.GetValue(source, null);
					propInfoObj.SetValue(target, propValue, null);
				}
			}
		}

		return target;
	}

	return default(T);
}

Diese nun statische Variante kann auf jedes beliebige Objekt jeder beliebigen Klasse angewendet und für die Rückgabe jeder beliebigen Klasse verwendet werden. Natürlich nur solange diese Klassen auch gleiche Properties enthalten.

Und so wird diese statische Methode verwendet:

var auftrag = new Auftrag
	{ Position = 1, Artikel = "Mars", Beschreibung = "Schoko Riegel", Preis = 1.50 };

var auftragsArchiv = Auftrag.ToTobject<AuftragArchiv>(auftrag);

if (auftragsArchiv.Save())
{
	auftrag.Delete();
}

Mir hat es Spaß gemacht diese Methodik zu entwickeln, evtl. hilft es auch mal jemand anderem beim Lösen “seines” Problem.

Anregungen und Kritik wie immer gerne als Kommentar zum Beitrag.

10 Gedanken zu „Generische Methode um gleichnamige Properties einer Klasse zu einem Objekt einer anderen Klasse zu kopieren – C#“

  1. Ich persönlich würde das Konstrukt
    var constructorInfo = typeof(T).GetConstructor(new Type[] { });
    if (constructorInfo != null)

    noch durch
    var target = new T();
    und die Methodensignatur um ein Constraint ergänzen:
    public static T ToTobject(object source) where T:new()

    und dann das ganze noch als Extension-Methode ala
    public static T ToTobject(this object source) where T:new()

    Regards

  2. Hallo Torsten,
    var target = new T();
    ist natürlich viel eleganter und die Methodensignatur „informell“ zu erweitern hat auch was. Das mit der Extension Method, kann ich nicht umsetzen, da ich noch zu .NET Framework 2 kompatibel sein muss, aber für alle die ausschließlich 3.5 und höher einsetzen, sicherlich auch ein gute Lösung.

    Besten Dank für deinen Kommentar
    HP

  3. Ja, ist ein weg ich bin da mittlerweile übergegangen das ganze mit AutoMapper zu machen. Folgende Vorteile habe ich da für mich rausgezogen.

    Es gibt eine Validierung des Mappings ob auch alles gemappt ist.
    Ich kann eingreifen wenn ein Simples Mapping nicht ausreicht.
    Durch Code Generierung ist es fix

    Ein Nachteil is dass ich jedes Mapping definieren muss. Wobei es bei 0815 Mappings nur eine Amweisung ist.

    1. Hallo Albert,
      den AutoMapper hatte ich so gar nicht auf dem Schirm, ich werde mir den aber unbedingt mal anschauen, die Eigenschaften haben ja nicht immer zwingend die gleichen Namen, vor allem. dann nicht, wenn man auf vorhandene Systeme, Datenbanken etc aufbauen muss.

      Danke
      HP

  4. Hi HP!
    Cooles Teil, sehr praktisch! Da du schon so viele Versionen der Methode hast, habe ich mir gedacht dass vielleicht noch eine dazu passt! Als Mixin gebaut (Erweiterungsmethode) kannst du sie auf jedem Objekt aufrufen. Die Einschränkung des generischen Typen durch Where erlaubt eine einfachere Erzeugung, da die Findung der Constructor Info entfällt. Und zu guter Letzt, wenns auch nicht jedermanns Sache ist, etwas Linq, um die Prozedurale Logik und damit die Nestung der Ifs zu entschärfen. Zusätzliche Filter, wie du sie weiter oben brauchst (e.g. if (!pi.IsDefined(typeof(XmlIgnoreAttribute), false))) können dann mit einer weiteren Where Clause hinzugefügt werden. Die Kopie der eigentlichen Attribute habe ich bewusst in der Foreach-Schleife gelassen, da sie Seiteneffekte hat und sich nur schwer durch Linq darstellen lässt!

    lg

    Johannes


    public static T CloneAs(this object source) where T : class, new()
    {
    var target = new T();
    const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public;
    var sourceProperties = typeof (T)
    .GetProperties(flags)
    .Where(p => p.CanWrite)
    .Select(p => source.GetType().GetProperty(p.Name, flags));

    foreach (PropertyInfo property in sourceProperties)
    {
    object value = property.GetValue(source, null);
    property.SetValue(target, value, null);
    }

    return target;
    }

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.