Ziel: Klassen um Werte erweitern

Ab und an kommt es vor, dass man eine Klasse um einzelne Attribute erweitern möchte, aber diese Erweiterung nicht für alle Einsatzzwecke gebraucht werden. Jetzt gibt es mehrere Möglichkeiten:

  1. Eine Child-Klasse ableiten, die die jeweiligen neuen Attribute enthält
  2. Eine Wrapper-Klasse, die ein Objekt der ursprünglichen Klasse hält, sowie die neuen Attribute
  3. Eine komplett eigenständige Klasse und ein Mapping zwischen beiden Klassen (gerne in Verbindung mit AutoMapper)

Abhängig von der konkreten Aufgabenstellung kann jede Variante sinnvoll sein. In meinem Fall brauchte ich wirklich nur ein neues Feld auf eine der fachlichen Basisklassen. Wrapperklassen erzeugen entweder viel unnötigen Code in der Wrapperklasse selbst oder der Zugriff auf das eingepackte Objekt ist umständlich (WrapperObjekt.WrappedObjekt.Property). Ein Mapping muss auch gepflegt werden, weshalb eine einfache Child-Klasse in diesem Fall der beste Weg war. Die Klassenhierarchie sah so aus:

public class InputDto {
	public string Name {get;set;}
	public int Alter {get;set;}
}

public class InputDtoWithId : InputDto {
	public int Id {get;set;}
}

Problem: Casting von Parent zum Child-Type

Zur Laufzeit gab es allerdings ein neues Problem. Eine Liste von Parent-Objekten sollten zu Child-Objekten werden. Die Parent-Objekte werden an einer Stelle im Code erzeugt, wo die Child-Klasse nicht bekannt ist. Ein direktes Casting von Parent zu Child ist in C# generell nicht möglich. Man ist also gezwungen ein neues Child-Objekt zu erzeugen und jedes Attribut manuell zu mappen.

public class InputDtoWithId : InputDto {
	public int Id {get;set;}
	public InputDtoWithId(InputDto inputDto, int id) {
		Name = inputDto.Name;
		Alter = inputDto.Alter;
		Id = id;
	}
}

Was in diesem Beispiel noch handhabbar ist, wird bei größeren Klassen schnell viel fehleranfällige Schreibarbeit. Kommt man an die Basisklasse heran, kann man auch einen Copy-Konstruktor in das Parent-Klasse nutzen:

public class InputDto {
    public string Name {get;set;}
    public int Alter {get;set;}

    public InputDto(InputDto inputDto)
    {
        Name = inputDto.Name;
        Alter = inputDto.Alter;
    }
    //weitere Konstruktoren...
}

public class InputDtoWithId : InputDto {
    public int Id {get;set;}
    public InputDtoWithId(InputDto inputDto, int id) : base(inputDto) {
        Id = id;
    }
}

Der Copy-Konstruktor sortiert die Verantwortlichkeiten sauberer, andererseits erzeugt man hier einen Konstruktor an einer Stelle, die diesen überhaupt nicht benötigt. Auch nicht sehr elegant.

Records for the win

Mit C# 9 und Records lässt sich das Ganze ohne explizites Mapping umsetzen, da der Copy-Konstuktor vom Compiler automatisch erzeugt wird und in der Child-Klasse aufgerufen werden kann:

public record class InputDto {
    public string Name {get;set;}
    public int Alter {get;set;}
}

public record class InputDtoWithId : InputDto {
    public int Id {get;set;}
    public InputDtoWithId(InputDto inputDto) : base(inputDto) { }
}

Auf die Übergabe des Werts für Id kann im Konstruktor verzichtet werden, wenn man den Objekt-Initalizer verwendet:

var inputDto = new InputDto() {Alter = 23, Name = "Bob"} ;
var inputDtoWithId = new InputDtoWithId(inputDto) { Id = 1337 };

Noch kürzer kann man das Ganze schreiben, wenn man die Positonal Syntax verwendet. Dann klappt der Aufruf mit dem Objekt-Initalizer allerdings nicht mehr beim direkten Aufruf des Konstrukturs mit new, da der Compiler in dem Fall keinen parameterlosen Konstruktor erzeugt.

public record class InputDto(string Name, int Alter);
public record class InputDtoWithId(int Id, InputDto InputDto) : InputDto(InputDto);

var inputDto = new InputDto("Bob", 23);
var inputDtoWithId = new InputDtoWithId(1337, inputDto);