Screencast
Einleitung
Sieht man sich den einen oder anderen Screencast im Netz an, kann schnell der Eindruck entstehen, eine Solution entsteht durch File-New-Project und die Auswahl eines Projektes (meist einer Konsolenanwendung). Für die schnelle Sample-App reicht das sicherlich auch vollkommen aus. Die Frage ist nun, was man eigentlich tun sollte, um eine saubere, für die Zukunft gerüstete und am besten auch für den TFS geeignete Solution zu bauen. Ich werde hier versuchen, eine schrittweise Anleitung zu erstellen, weil ich im täglichen Zusammenarbeiten mit Entwicklern bemerkt habe, dass doch das ein oder andere “Aha!” vorkommt.
Solution vs. Project
Zunächst einmal gilt es - wie fast immer - ein wenig Trennschärfe in vermeintlich einfache Begriffe zu bringen. Eine Solution ist kein Projekt. Dieser einfache Satz enthält bereits alles wesentliche. Das Problem ist, dass Microsoft hier einiges beim Wording falsch gemacht hat. Um eine Solution zu erstellen, benutzt man nämlich File -> New -> Project.
Gehen wir das einmal durch und machen es ruhig erst einmal falsch. Der folgende Screenshot zeigt, wie ich im Dialog “New Project” fröhlich auf Console Application gehe und dann nach einer Namensgebung gleich ungehemmt auf “OK” klicke:
Das Ergebnis dieses Schnellschusses ist im Solution Explorer:
Das Problem hier ist nun, dass die Solution genau so heißt, wie das Projekt. Die Solution ist aber eigentlich eine Sammlung von beliebig vielen Projekten und i.d.R. entspricht ihr Name eher einer Produktberzeichnung. Würden wir z.B. Microsoft-Entwickler sein, die eine Büro-Anwendungs-Suite erstellen sollen, würde unsere Solution wahrscheinlich “Office” und eines unserer Projekte z.B. “Word” heißen.
Etwas besser
Am unteren Rand in Abb. 1 hätte ich nun im Feld “Solution name” bereits Rücksicht darauf nehmen können:
Der Solution-Explorer sieht nun auch schon logicher aus:
Aha! Alles klar. Beitrag zu Ende? Mitnichten!
Ordnung schaffen
Es hat sich in der Vergangenheit als eher ungünstig erwiesen, Projekte direkt unter die Solution selbst zu hängen. Ein Grund dafür ist, dass selbst vermeintlich kleine Solutions schnell anwachsen können und dann die Übersichtlichkeit im Solution Explorer stark leidet. Auch hilft es einem Entwickler, der zum ersten Mal in ein Projekt gerät merklich, wenn sich die Projekte nicht alle gleichrangig unter der Solution versammeln, sondern noch mindestens eine Schicht aus sog. Solution Foldern genutzt wird.
Kurz gesagt ist es, wie im Dateisystem auch. Teile und herrsche usw. Ich kann nur dringend empfehlen, sich eine Standard-Ordnerstruktur als Konvention zu überlegen. Über die Jahre hat es sich für mich und mein Team als erfolgreich erwiesen, die Solution Folder nach den Schichten der Anwendung zu benennen. Folgende Ordner haben wir eigentlich immer am Start:
- _Shared: Ein Ordner, der immer ganz oben hängt (daher das ”_”) und alles mögliche aufnimmt, nur keine Projekte (z.B. Textdateien mit Kennwörtern usw.).
- Data: Hierunter kommen alle SSDT-Projekte (SQL Server Data Tools) und Klassenbibliotheken, die meist EF-Code oder sonstigen Datenzugriffs-Kram enthalten.
- Logic: Nicht weiter verwunderlich kommen hier meist Klassenbibliotheken (gerne auch Portable) rein, die zentrale Logik bereit stellen.
- Services: MEist ebstehend aus WCF-Projekten.
- Test: Alles, was irgendwie nach Testing riecht, kommt hier rein.
- Ui: Natürlich benötigen wir meist irgendwas visuelles am Anwender. Hier versammeln sich Web-, XAML und z.B. Consolen-Projekte.
- Setup: Hier kommt in letzter Zeit immer wieder ein Projekt vom Typ WiX zu Einsatz.
Wenn man nun aber diese Regel befolgt, dann hilft der Ansatz aus Abb. 3 nur bedingt weiter, weil man das neue Projekt gleich fröhlich verschieben müsste. Daher hier nun die aus meiner Sicht beste Vorgehensweise.
Die Solution entsteht
Wie oben bereits erwähnt, hat sich MS einen kleinen Wording-Fauxpas erlaubt, wie die Hervorhebungen im folgenden Screenshot zeigen:
Mit unserem neuen Wissen über die Begriffe “Solution” und “Project” ist das fast so lustig, wie der berüchtigte Hinweis: “Klicken Sie auf Start, um zu Beenden.”. Naja, man gewöhnt sich dran. Jedenfalls erzeugt ein Klick auf “OK” nun folgendes Bild im Solution Explorer:
So gefällt mir das schon besser. Ich habe genau das bekommen, was ich im ersten Schritt auch nur wollte. Die Solution als Container für meine Projekte und Dateien steht bereit und nun kann ich in aller Ruhe die Solution-Ordner anlegen.
Nach ein paar mal Rechtsklick auf die Solution und dann “Add -> New Solution Folder” sieht meine Solution dann so aus:
Das Dateisystem
An dieser Stelle wird es höchste Zeit, sich das Ergebnis unseres Tuns mal im Windows Explorer zu vergegenwärtigen:
Ich habe hier einfach mal den Ordner über den Solution Explorer gelegt, um zu zeigen, dass Visual Studio zwar meine Solution artig in einem eigenen Ordner angelegt, in diesem aber keine Unterordner, wie z.B. “_Shared” oder “Ui” zu finden sind.
Hier ist wieder wichtig, Begriffe und in diesem Fall Symbole des Studios zu unterscheiden. Was wir angelegt haben, sind “Solution Folder”. Diese werden mit einem “leichteren” Symbol im Solution Explorer gezeigt, als ihre Vettern, die “Project Folder”. Der wesentliche Unterschied aber ist nun der, dass bei Anlage eines Project Folder immer auch automatisch ein entsprechender Ordner im Dateisystem erzeugt wird. Bei Solution Foldern ist das nicht der Fall. Sie sind als rein logische Ordnungsinstrumente gedacht.
Wer jetzt wissen will, wo denn nun die Information über diese Ordner liegt, der sei auf die in Abb. 8 zu erkennende “*.sln”-Datei verwiesen. Diese beinhaltet alle Solution-weit geltenden Einstellungen und sieht in unserem Fall ca. so aus:
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.30723.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Shared", "_Shared", "{877FA3AD-0796-4826-A997-A34C436B2391}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Logic", "Logic", "{E2066926-7F1A-45CB-AEF4-574ED532FC00}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Ui", "Ui", "{587171AD-4E2D-47CF-B3C4-F62A9563A0E2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{7BC92E6A-E54F-4EA2-A812-259643BF46C1}"
EndProject
Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
In unserem Fall sind vor allem die mit “Project” beginnenden Zeilen interessant. Ein Solution Folder ist einfach ein spezieller Projekt-Typ innerhalb einer Solution. Diese Stelle - also die sln-Datei - wird übrigens dann immens wichtig, wenn man sich irgendwann mal fragt, wie man 6 Projekte aus dem Ordner “Ui”, die da irgendwie doch nicht mehr rein sollen flugs mal eben in einen anderen Ordner bewegt!
Doch eigentlich wollten wir uns ja dem Dateisystem widmen. Wenn also Solution Folder (in deutschen Versionen übrigens “Projektmappenordner”) rein logisch sind, dann ist mit Abb. 8 ja alles ok, oder?
Viele werden “Ja” antworten. Ich denke jedoch anders und würde immer empfehlen, manuell die gleiche Ordnerstruktur im Windows Explorer zu erstellen, wie in der Solution auch. Ja, das geht wirklich nur manuell! Dafür habe ich 2 Gründe. Erstens kommt es in laufenden Projekten immer wieder vor, dass man sich durch die Ordnerstruktur im Windows Explorer hangeln muss und irgendwoher eine der Dateien des Projektes (meist aus dem bin-Ordner) braucht. Es hilft dabei ungemein, wenn die Struktur des Dateisystems bereits auf dem obersten Level der der Solution entspricht. Innerhalb der Solution Folder werden die Projekte z.B. alphabetisch sortiert. Hat man aber physisch alle Projekte in einem Ordner liegen, ergibt sich eine ganz andere Sortierung.
Der weit gewichtigere Grund für den Extra-Schritt ist aber der TFS bzw. überhaupt eine Quellcodeverwaltung. Der Team Foundation Server - wenn er denn genutzt wird - arbeitet bei der Verwaltung des Source Code mit einem Mix aus Solution- und Dateisystem-Logik. Ist beides nicht zueinander konsistent, kann es zu verwirrenden Ergebnissen kommen. Ein Beispiel hierfür sind Berechtigungen. Ohne zu sehr ins Detail gehen zu wollen, hier ein kleines Beispiel. Es kommt ein UI-Entwickler an Bord, der in der Middleware und den anderen Layern außer UI nix verloren hat. Ein Solution Folder ist ein prima Einstiegspunkt, um ihm den Zugriff auf einen Solution Folder zu verwehren. Geht bloß nicht, weil der TFS Solution Folder gar nicht sieht. Hat man keinen entsprechenden Ordner im Dateisystem, sieht der TFS absolut keine Struktur auf Solution Folder Ebene.
Nach meinen Anpassungen im Windows Explorer verwandelt sich Abb. 8 in:
An dieser Stelle sei noch ein Hinweis auf die versteckte “.suo”-Datei erlaubt. Hier handelt es sich um eine Binärdatei, die eine Art lokalen Cache für die Solution darstellt. Wenn man heftige Änderungen an seiner Projektstruktur vornimmt (z.B., weil man in einer alten Solution aufräumen will), sollte man öfter mal diese Datei löschen und dann das VS inkl. Projekt neu laden. Das hilft Wunder gegen einen Haufen unlogisch erscheinender Fehler.
Die vermaledeite AssemblyInfo.cs
Noch haben wir kein einziges Projekt in der Solution. Trotzdem gehe ich bereits jetzt auf ein weithin bekanntes Problem großer Solutions ein. Eine Standard-AssenblyInfo-Datei sieht wie folgt aus:
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("ConsoleApplication1")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ConsoleApplication1")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("a5842883-d212-497b-acc0-3343534be465")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
Eine erste Verschlankungsmaßnahme entfernt ComVisible, AssemblyCulture, Guid alle Kommentare und überflüssigen Leerzeilen sowie usings:
using System.Reflection;
[assembly: AssemblyTitle("ConsoleApplication1")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ConsoleApplication1")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("1.0.0.0")]
Sieht man sich das jetzt einmal genauer an, so würden in einer Solution für ein Produkt eigentlich nur noch die Elemente Zeilen 3 und 4 aus Listing 3 übrig bleiben. Der Rest der Zeilen ist in so einem Fall für die ganze Solution gleich. Diese Aussage hängt nun natürlich auch noch davon ab, wie die Versionierung innerhalb der Projekte erfolgen soll und könnte ganze Blog-Serien füllen. Wer mir aber nun zustimmt, würde sich doch wünschen, dass diese redundanten Teile der AssemblyInfo nicht immer mitgeschleppt und - noch schlimmer - getrennt gepflegt werden müssen.
Die gute Nachricht: Es geht. Die schlechte: Es ist umständich. Die mittelgute: Man muss es pro Solution nur einmal einrichten. Aber eins nach dem anderen.
Der erste Schritt zum Glück besteht darin, eine einfache Textdatei mit dem Namen “SharedAssemblyInfo.cs” im Windows-Ordner “_Shared” (oder wie auch immer der Shared-Ordner heißen soll) anzulegen. Da rein kommt der ganze redundante Teil aus Listing 3 und wir geben ein paar sinnvolle Werte an. Also z.B. so:
Hier gilt es zu beachten, dass die ganze Aktion nicht im Visual Studio durchgeführt wurde! Als nächstes wechseln wir nun ins VS und klicken rechts auf den Solution Folder “_Shared” dann auf “Add” und “Existing Item…“. Jetzt in den Ordner “_Shared” wechseln und die Datei hinzufügen. Das Ergebnis sieht nun so im VS aus:
Nachdem das erledigt ist, kommen wir nun zu unserem ersten Projekt. Keine Angst: Das Thema AssemblyInfo ist noch nicht abgeschlossen.
Hier ein Beispiel für eine SharedAssemblyInfo zur Übersicht:
using System.Reflection;
[assembly: AssemblyCompany("codingfreaks")]
[assembly: AssemblyProduct("MySolution")]
[assembly: AssemblyCopyright("Copyright © codingfreaks 2014")]
[assembly: AssemblyTrademark("MySolution")]
[assembly: AssemblyVersion("0.0.1.*")]
[assembly: AssemblyInformationalVersion("0.0.1")]
Man beachte, dass ich am Schluss keine FileVersion verwende sondern diese zugunsten der AssemblyInformationalVersion eingetauscht habe (Details bei MSDN) und dann einen * bei der AssemblyVersion am Ende verwende (siehe Abb. 16).
Das erste Projekt
Es wird Zeit, endlich mal was codierbares in der Solution zu bekommen. Mit anderen Worten: Rechtsklick auf den Ordner “Logic” dann “Add” und “New Project”. Wichtig hier ist vor allem der Rechtsklick auf den entsprechenden Solution Folder!!!
An dieser Stelle sind nun (wie immer) 2 Dinge extrem wichtig:
- Eine Namenskonvention für Projekte.
- Die Auswahl des zuvor erstellten Dateisystemsordners (siehe Abb. 12 unten)
Wenn man Punkt 2 vergisst, ist es vorbei mit der schönen Ordnung im Dateisystem und gerade TFS-User werden irre durch die über die gesamte Ordnerstruktur verstreuten Projekte.
Punkt 1 ist vor allem wegen der obligatorischen Wartbarkeit und dieses Artikels wichtig. Wartbarkeit, Übersicht usw. muss ich eigentlich nicht näher erläutern. In meinem Fall werden die Projekte inzwischen mit dem Solution-Folder als Präfix versehen und dann kommt ein möglichst prägnanter Suffix. Im Beispiel ergibt sich also ein zentrales Kernlogik-Projekt der Business-Logic.
Der bereits erwähnte Artikel macht nun darauf aufmerksam, dass es seit VS 2013 keine gute Idee mehr ist, in 2 Solution Foldern den gleichen Projektnamen zu verwenden (also “Core” unter Logic und unter Services z.B.). So habe ich das halt früher gemacht, weil ich mir dachte, dass der Solution- und Datei-Ordner genügend aussagen. Seit 2013 kommt allerdings dann VS nicht mehr mit Solution-internen Referenzen klar.
Nachdem ich die obsolute “Class1.cs” aus meiner Klassenbibliothek entfernt habe, sieht mein Solution Explorer so aus:
Zurück zur AssemblyInfo
Natürlich richtet die im Shared-Ordner rumdümpelnde SharedAssemblyInfo noch gar nichts an. Das kommt jetzt. Das komplizierte an der Angelegenheit ist, dass man leider ein paar Schritte machen muss, um VS zu überlisten. Wir wollen die SharedAssemblyInfo nämlich in unser neues Projekt aufnehmen. Das aber nicht irgendwo, sondern neben die AssemblyInfo im Ordner "Properties". Das wiederum ist aber kein stinknormaler Ordner und verweigert folgerichtig ein einfaches Rechtsklick und dann "Add...". Die Lösung ist folgende:- Rechtsklick auf das Projekt “Logic.Core” und dann “Add” und “Existing Item…“.
- Navigieren in den Ordner “_Shared” und anklicken der “SharedAssemblyInfo.cs” (KEIN DOPPELKLICK BITTE!)
- Wie in Abb. 14 gezeigt nun den kleinen Pfeil neben der “Add”-Schaltfläche wählen und “Add As Link” anklicken.
Das Ergebnis im Solution Explorer ist nun
Wie der traurige Pfeil anzeigt, befindet sich der Link nun aber leider nicht im Ordner “Properties”. Das kann man nun mit einem beherzten Drag&Drop aber glücklicherweise lösen (zur not den Screencast dazu konsultieren).
Diese Hinzufügen-dann-aber-Verschieben”-Kavalkade kann man sich spätestens ab dem zweiten Projekt schenken. Jetzt kann man nämlich einfach den Link von dem einen Properties-Ordner in den des anderen Projektes kopieren. Man muss nur noch dran denken (auch das am besten im Screencast verdauen).
Der letzte Schritt zu unserem Glück besteht nun noch im Löschen von Ballast aus der eigentlichen AssemblyInfo.cs in Logic.Core. Diese sieht bei mir zum Schluss dann so aus:
using System.Reflection;
[assembly: AssemblyTitle("Logic.Core")]
[assembly: AssemblyDescription("Core logic of my sample project.")]
Wenn ich nun Logic.Core
baue und dann in die Eigenschaften der entstehenden DLL im bin-Ordner schaue, habe ich erreicht, was ich wollte:
Mit anderen Worten: Wenn ich nun z.B. mein Minor-Release für die gesamte Solution um 1 erhöhen möchte, brauche ich nur noch die SharedAssemblyInfo.cs einmal bearbeiten und gut. Es spielt übrigens dabei keine Rolle, ob man die im “_Shared”-Ordner verwendet oder direkt im Projekt auf den Link doppel klickt, weil es ja nur eine Referenz ist.
nuget
Nuget wird mehr und mehr zu einer meiner Lieblingsbestandteile der VS-Welt. Damit es aber so richtig sauber funz (vor allem im Team), sollte man dafür Sorge tragen, dass jedem Build eine Aktion vorangestellt wird, die sicherstellt, dass alle Pakete auf dem neuesten Stand sind. Das erledigt ein Rechtsklick auf die Solution und Auswahl von “Enable NuGet Package Restore”. Nach einer Sicherheitsabfrage und einem Abschluss-Bestätigungs-Dialog sieht mein Projekt so aus:
Ich werde hier nicht das gesamte Feature erschöpfend behandeln können. Nur so viel: Wir erhalten die Nuget.exe, deren Config sowie die targets für die Integration in den Build-Process. Diese Integration findet nur statt, wenn die entsprechende Option im VS angehakt ist:
VS kümmert sich dann um die Integration der targets-Datei und dann führt jeder Build zu einem vorherigen Update der in der Solution enthaltenen Pakete (dafür auch die nuget.exe).
Manchmal schlägt dieser Build beim ersten Mal fehl, weil die Paket-Abhängigkeiten nicht in einem Rutsch aufgelöst werden konnten. Ein zweiter Versuch hilft dann Wunder. Das Output-Fenster ist in solchen Fenster übrigens Gold wert.
Am besten gefällt mir an dem ganzen natürlich, dass die nuget-Macher meine Konvention berücksichtigen und dem Solution Folder “.nuget” auch gleich noch den passenden Dateisystemordner automatisch spendieren. Bravo!
Noch ein Projekt
Der Rest der Geschichte ist nun denkbar trivial. Ein weiteres Projekt unter Tests (Tests.Core) und ein UI-Projekt (Ui.TestConsole) ergänzen meine Solution:
Ich habe in der Abbildung markiert, an welchen Dateien ich jeweils Änderungen vorgenommen habe. Es ist immer das gleiche Vorgehen:
- Neues Projekt durch Rechtsklick auf den richtigen Solution Folder hinzufügen.
- Beim Hinzufügen darauf achten, dass das Projekt im richtigen Dateisystemordner landet und der Konvention folgend benannt wird.
- Link zu SharedAssemblyInfo.cs aus einem bereits bestehenden Properties-Ordner in den neu hinzugefügten kopieren (Drag & Drop).
- AssemblyInfo.cs des neuen Projektes anpassen.
Das wars, könnte man denken, aber eine Sache haben wir da noch.
Namespaces
Es erschreckt mich immer wieder, wie wenig Acht die Leute auf ihre Namespaces legen. Microsoft hat dazu ein paar Konventionen, die unserer ganzen bisherigen Arbeit so richtig die Abrundung geben. Vereinfacht ausgedrückt könnte man sagen:
Der Namespace einer Klasse ergibt sich aus einem beliebigen Suffix gefolgt von {SolutionName}.{SolutionFolder}.{ProjectName ohne SolutionFolder-Teil}.{ProjectFolder1}.{ProjectFolderx}.Hä?, könnte es nun erschallen. Daher ein kleines Beispiel. In Abb. 19 gilt es, der Program.cs des Projektes "Ui.TestConsole" den korrekten Namespace zu verpassen. Gesetzt den Fall, ich verwende für alle meine Projekte "de.codingfreaks" als Präfix für Namespaces (was ich tatsächlich tue), dann ergäbe das
namespace de.codingfreaks.MySolution.Ui.TestConsole"
Damit hier nun nichts schief geht, trage ich diesen Standard-Namespace gleich beim Anlegen neuer Projekte in die Eigenschaften ein. Hier ein Beispiel für die Console:
Hat man sich einmal an diesen 5. Schritt zu jedem neuen Projekt gewöhnt, tut es schon fast körperlich weh, ihn weg zu lassen.
Netter Nebeneffekt aus Abb. 20 für alle Tools-Freunde (ich nutze hier z.B. ReSharper). Code-Analyse-Werkzeuge erkennen die Intention und helfen meist mit Automatiken:
Auch das sieht man sich besser noch mal im Screencast an. Meine Screenshot-Techniken reichen für Besseres nicht aus.
Die Solution kann mehr
Was auch viele nicht wissen: Ein Rechtsklick auf die Solution im Solution Explorer und Auswahl von “Properties” birgt so manche Erleichterung im Umgang mit großen Solutions. Wer z.B. immer schon 2 Consolen auf einmal Starten und Debuggen wollte, der braucht dafür mitnichten 2 geöffnete Studios:
Durch “Multiple Startup projects” kommt man hier einfach zum Ziel. Auch das Überprüfen und Verwalten der viel gehassten Configurations wird vereinfacht:
Unvernünftige Klick-Orgien durch alle Projekte können hier schonmal entfallen.
Fazit
So alltäglich der Begriff Solution auch ist, oftmals hat man gerade wegen der scheinbaren Einfachheit des Konzeptes kaum Muße und Lust, sich damit auseinander zu setzen. Ich hoffe, dieser Exkurs konnte das zeigen und sorgt für weniger Frust im Umgang mit den Tools.