I. Avant-Propos

De nos jours, on peut entendre parler de notions comme usine de build, intégration continue. Nous allons tout d'abord voir brièvement quelles sont ces notions.

Dans la gestion de cycle de vie d'un projet, il y'a plusieurs phases comme illustré ci-dessous :

Image non disponible
Cycle de vie d'une application

Cependant, avec les méthodologies agiles on a vu apparaître une gestion plus poussée de la qualité. Ainsi il est courant d'avoir plusieurs choses :

  • un gestionnaire de source (SVN, Visual Source Safe, Mercury, etc.) qui contient toutes les sources du projet avec historique des changements ;
  • une usine de build : un serveur qui est chargé de packager l'application dans les différentes configurations possibles (configuration de test, configuration de production, etc.) ;
  • un serveur d'intégration continue qui contient une version de l'application se basant sur le code source le plus récent (par exemple les Nightly Builds). Ainsi il est possible de tester son application et de voir les impacts du développement en cours. C'est également sur ce serveur que peuvent se jouer les tests unitaires.

Ainsi, la phase d'implémentation est beaucoup plus complexe : il faut en permanence livrer l'application sur un environnement spécifique, lancer les tests unitaires, etc. D'autant plus que les différents environnements ne sont pas les mêmes : bases de données spécifiques, machines différentes, chaînes de connexion différentes : il faut prévoir une version par environnement.

Toutes ces étapes font partie de l'industrialisation d'une application. Bien évidemment, il serait très couteux en temps de faire tout manuellement. C'est pourquoi des solutions automatisées comme Team Foundation Server, Cruise Control.Net existent.

II. Introduction

Il est assez compliqué d'industrialiser une application Silverlight. En effet, lors de la compilation, elle est packagée en un fichier xap qui est difficilement modifiable. Lorsque l'on veut changer les adresses des web services selon l'environnement (dev, qualif, prod) par exemple, il faut décompresser le xap, modifier le fichier ServiceReferences.ClientConfig et recompresser comme il faut. Un peu lourd à mettre en place dans le cas d'une usine de build (comme Team Foundation Server).

Cependant, avec la version 2010 de Visual Studio est apparue une fonctionnalité très intéressante : la possibilité de transformer le Web.Config d'une application web selon la configuration de compilation (debug, release, etc.). Ne serait-il pas possible de s'en inspirer pour nos projets Silverlight ?

III. Les briques à mettre en place

Pour la transformation du Web.Config, on s'aperçoit qu'il faut plusieurs choses :

  • un Web.Config normal ;
  • un fichier de transformation Web.Debug.Config ;
  • trouver comment appliquer la transformation du second sur le premier ;
  • automatiser la chose dans une compilation.

Nous allons donc appliquer la même démarche dans notre compilation de projet Silverlight. Pour cela nous allons nous servir de MSBuild.

IV. Transformation du fichier de configuration

La transformation est très bien documentée sur MSDN : Référence MSDN, je ne vais donc pas la détailler. Nous allons plutôt l'appliquer à un fichier ServiceReferences.ClientConfig d'exemple.

Exemple de ServiceReferecences.ClientConfig
Sélectionnez

<configuration>
	<system.serviceModel>
		<bindings>
			<basicHttpBinding>
				<binding name="BasicHttpBinding_IService1"
					maxBufferSize="2147483647"
					maxReceivedMessageSize="2147483647">
					<security mode="None" />
				</binding>
			</basicHttpBinding>
		</bindings>
		<client>
			<endpoint address=http://localhost:30693/Service1.svc
				binding="basicHttpBinding"
				bindingConfiguration="BasicHttpBinding_IService1"
				contract="ServiceReference1.IService1"
				name="BasicHttpBinding_IService1" />
		</client>
	</system.serviceModel>
</configuration>

Ce fichier a été généré à l'aide Ajouter une référence de Service de Visual Studio. Nous souhaitons que lorsqu'il est compilé en Debug, le service utilise l'adresse http://www.servicedetest.net/Service.svc et http://www.servicedeprod.net/Service.svc en Release. Pour cela, il faut créer deux fichiers de transformation : ServiceReferences.Debug.ClientConfig et ServiceReferences.Release.ClientConfig

Exemple de ServiceReferecences.Debug.ClientConfig
Sélectionnez

	
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
	<system.serviceModel>
		<client>
			<endpoint address="http://www.servicedetest.net/Service.svc"
				name="BasicHttpBinding_IService1"
				xdt:Transform="SetAttributes"
				xdt:Locator="Match(name)"/>
		</client>
	</system.serviceModel>
</configuration>
Exemple de ServiceReferecences.Release.ClientConfig
Sélectionnez

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
	<system.serviceModel>
		<client>
			<endpoint address="http://www.servicedeprod.net/Service.svc"
				name="BasicHttpBinding_IService1"
				xdt:Transform="SetAttributes"
				xdt:Locator="Match(name)"/>
		</client>
	</system.serviceModel>
</configuration>

Lors de la compilation, le processeur ira trouver le bon noeud endpoint en s'appuyant sur l'attribut name et changera l'adresse.
Intéressons-nous désormais à ce processeur.

V. Analyser le processeur de transformation fourni avec Visual Studio 2010

Lors de la publication d'un site, plusieurs tâches MSBuild sont utilisées dans un certain ordre pour produire le paquet final. L'enchainement de celles-ci est dans un fichier .targets disponible à cet endroit : C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.targets
En l'ouvrant avec un éditeur de texte traditionnel, on aperçoit quelques lignes intéressantes (lignes 17 et 1197) :

Extrait de Microsoft.Web.Publishing.targets
Sélectionnez

<UsingTask TaskName="TransformXml" AssemblyFile="Microsoft.Web.Publishing.Tasks.dll"/>

<TransformXml Source="@(WebConfigsToTransform)"
				Transform="%(TransformFile)"
				Destination="%(TransformOutputFile)"
				Condition="!%(Exclude)"
				StackTrace="$(TransformWebConfigStackTraceEnabled)"
				SourceRootPath="$(WebPublishPipelineSourceRootDirectory)"
				TransformRootPath="$(WebPublishPipelineTransformRootDirectory)"/>

C'est notre processeur de transformation ! Il va donc falloir utiliser ce fichier de targets pour notre projet Silverlight.
C'est ce que nous allons faire de ce pas.

VI. Intégrer la transformation à la compilation Silverlight

Il faut d'abord sauvegarder toute la solution et les projets, puis faire un clic droit sur le projet Silverlight et choisir Décharger le projet. Le projet apparait grisé et il est désormais possible d'éditer le .csproj (ou .vbproj) en faisant un clic droit dessus.

Une compilation Silverlight s'effectue en plusieurs temps : compilation des dll, vérification du xaml, création du manifest et packaging en xap.
Le bon moment pour transformer notre fichier est après la compilation mais obligatoirement avant le packaging.

Voyons ce que cela donne dans notre .csproj (ou .vbproj), nous allons travailler à partir de la ligne suivante déjà existante :

Extrait du fichier projet
Sélectionnez

<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Silverlight\$(SilverlightVersion)\Microsoft.Silverlight.CSharp.targets" />

C'est elle qui inclut les tâches de compilation Silverlight pour compiler notre projet. Comme nous avons vu dans la partie précédente, il faut aussi inclure les tâches de la publication web afin de bénéficier de la tâche TransformXml. Cela se fait en ajoutant les deux lignes suivantes :

Extrait du fichier projet
Sélectionnez

<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.targets" />
<UsingTask TaskName="TransformXml" AssemblyFile="Microsoft.Web.Publishing.Tasks.dll"/>

Nous allons ensuite créer une Target qui se déclenche juste après la tâche ValidateXaml de la compilation Silverlight. Cette Target ne doit être exécutée que si un fichier ServiceReferences.ClientConfig est présent :

Extrait du fichier projet
Sélectionnez

<Target Name="TransformServiceReference" AfterTargets="ValidateXaml" Condition="Exists('ServiceReferences.ClientConfig')">
</Target>

Ensuite, il faut remplir cette Target par plusieurs actions : il faut d'abord ne pas inclure les fichiers ClientConfig dans le xap final ! En effet, ils ne servent à rien et il serait même dangereux de laisser les urls de qualification dans un paquet de production (et vice versa).

Pour cela il faut créer une propriété ExcludedConfigFiles qui contient tous les fichiers ClientConfig du projet :

Extrait du fichier projet
Sélectionnez

<ItemGroup>
	<ExcludedConfigFiles Include="**\*.ClientConfig" />
</ItemGroup>

En regardant dans les tâches de compilation Silverlight du fichier C:\Program Files (x86)\MSBuild\Microsoft\Silverlight\v4.0\Microsoft.Silverlight.Common.targets et notamment autour de la ligne 353, on s'aperçoit que la liste des fichiers à ajouter dans le xap final est contenue dans la propriété ContentWithTargetPath. Il faut donc retirer les fichiers de ExcludedConfigFiles à ContentWithTargetPath :

Extrait du fichier projet
Sélectionnez

<ItemGroup>
	<ContentWithTargetPath Remove="@(ExcludedConfigFiles)" />
</ItemGroup>

Le point le plus important est désormais de transformer notre fichier de config. Pour cela, il faut utiliser le TransformXml qui prend comme paramètre la source (le fichier de configuration original), le fichier de transformation à appliquer et l'emplacement de destination. Afin d'éviter d'écraser notre fichier de configuration original, on peut stocker le résultat dans le répertoire $(IntermediateOutputPath) qui est en fait le répertoire obj. Enfin, cette transformation ne doit être effectuée que si le fichier de transformation existe. Cela nous donne :

Extrait du fichier projet
Sélectionnez

<TransformXml Source="ServiceReferences.ClientConfig"
			Transform="ServiceReferences.$(Configuration).ClientConfig"
			Destination="$(IntermediateOutputPath)\ServiceReferences.ClientConfig"
			Condition="Exists('ServiceReferences.$(Configuration).ClientConfig')" />

Dernier point, il faut inclure le fichier transformé dans notre xap (à l'aide de la propriété ContentWithTargetPath), s'il n'y avait pas de fichier de transformation, il faut remettre l'original.

Extrait du fichier projet
Sélectionnez

<ItemGroup>
	<ContentWithTargetPath Include="$(IntermediateOutputPath)\ServiceReferences.ClientConfig" Condition="Exists('ServiceReferences.$(Configuration).ClientConfig')" />
	<ContentWithTargetPath Include="ServiceReferences.ClientConfig" Condition="!Exists('ServiceReferences.$(Configuration).ClientConfig')" />
</ItemGroup>

Nous voilà opérationnels ! Voyons ce que ça donne :

Extrait du fichier projet
Sélectionnez

<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Silverlight\$(SilverlightVersion)\Microsoft.Silverlight.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.targets" />
<UsingTask TaskName="TransformXml" AssemblyFile="Microsoft.Web.Publishing.Tasks.dll"/>
<Target Name="TransformServiceReference" AfterTargets="ValidateXaml" Condition="Exists('ServiceReferences.ClientConfig')">
	<Message Text="Transforming ServiceReferences.ClientConfig with configuration $(Configuration)" />
	<ItemGroup>
		<ExcludedConfigFiles Include="**\*.ClientConfig" />
	</ItemGroup>
	<ItemGroup>
		<ContentWithTargetPath Remove="@(ExcludedConfigFiles)" />
	</ItemGroup>
	<TransformXml Source="ServiceReferences.ClientConfig"
					Transform="ServiceReferences.$(Configuration).ClientConfig"
					Destination="$(IntermediateOutputPath)\ServiceReferences.ClientConfig"
					Condition="Exists('ServiceReferences.$(Configuration).ClientConfig')"/>
		<ItemGroup>
			<ContentWithTargetPath Include="$(IntermediateOutputPath)\ServiceReferences.ClientConfig" Condition="Exists('ServiceReferences.$(Configuration).ClientConfig')" />
			<ContentWithTargetPath Include="ServiceReferences.ClientConfig" Condition="!Exists('ServiceReferences.$(Configuration).ClientConfig')" />
		</ItemGroup>
</Target>

VII. Conclusion

Essayons de compiler et oh magie ! Les adresses dépendent de la configuration à la compilation. Quel bonheur d'utiliser ça avec une usine de build et un déploiement automatisé comme celui de Team Foundation Server et MSDeploy.
Bien sûr, ces tâches sont soit à répéter dans chaque fichier projet d'application Silverlight ou bien à factoriser dans un fichier .targets qui devra être inclus avec le noeud <Import>.

VIII. Remerciements

Merci à Claude Leloup pour sa relecture attentive. Merci également aux membres de la section .Net qui ont pris le temps de commenter l'article lors de sa rédaction.