mardi 31 mai 2011

Microsoft Days 2011 à Strasbourg

Les Microsoft Days 2011 de Strasbourg auront lieu le 8 Juin prochain. Vous pouvez vous inscrire .

Pour ma part, j’y animerai 2 sessions pendant la matinée :

  • “Du Framework .NET 2.0 au Framework .NET 4.0 - Tout ce que vous avez manqué” avec Stéphanie Hertrich de Microsoft.
  • “Windows Azure - Approche pragmatique d'une révolution en marche”, avec mon collègue Philippe Geiger.

Je donne rendez-vous avec grand plaisir à tout ceux qui souhaitent participer à cet évènement.

Fonction d’exportation simple

Beaucoup d’applications proposent des fonctionnalités d’exportation de grilles de données vers des formats tels que CSV ou Excel. L’objectif de cet article est de proposer la fonction la plus simple et la plus générique possible pour répondre à cette problématique courante.

Ainsi, pour rendre la fonction la plus générique possible :

  • La réflexion est utilisée pour détecter dynamiquement les propriétés
  • Des paramètres optionnels (avec des valeurs par défaut) permettent d’utiliser la même fonction en faisant varier les détails d’implémentation
  • La fonction est une méthode d’extension sur IEnumerable<T>, ce qui la rend applicable dans quasiment tous les cas d’utilisation.

La fonction

Voici la fonction principale d’export :

public static class ExportationExtensions
{
public static string Export<TSource>(this IEnumerable<TSource> items,
bool includeTitleRow = true,
string header = "", string footer = "",
string rowPrefix = "", string rowSuffix = "",
string rowSeparator = "\r\n", string itemSeparator = ",",
Type ignoreFieldHavingAttributeType = null)
{
// serializable properties
var serializableProperties = typeof(TSource).GetProperties().AsEnumerable();
if (ignoreFieldHavingAttributeType != null)
serializableProperties = serializableProperties
.Where(pi => pi.GetCustomAttributes(false)
.All(o => o.GetType().FullName != ignoreFieldHavingAttributeType.FullName));

// result
return items.Aggregate(
string.Concat(header, serializableProperties
.Aggregate(rowPrefix,
(str, sp) => string.Concat(str, sp.Name, itemSeparator),
res => string.Concat(res, rowSuffix, rowSeparator))),
(row, item) => string.Concat(row, serializableProperties
.Aggregate(rowPrefix,
(str, sp) => string.Concat(str, sp.GetValue(item, null).ToString(), itemSeparator),
res => string.Concat(res, rowSuffix)), rowSeparator),
rows => string.Concat(rows, footer));
}
}



L’exportation produit par défaut des données au format CSV.


Utilisation


La fonction prévoit l’écriture éventuel de la ligne de titre, le préfixe et le suffixe de la chaine produite et de chaque ligne, ainsi que le séparateur entre chaque élément. En outre, afin de ne pas sélectionner certaines propriétés à exporter, la fonction élimine les propriétés sur lesquelles sont appliquées un attribut particulier (de la même manière que l’attribut XmlIgnore empêche la sérialisation XML du champ sur lequel l’attribut porte). Voici un exemple d’export d’une liste au format CSV puis sous forme de tableau HTML (ce qui permet l’ouverture directe par Excel par exemple) :

class Person
{
[DebuggerDisplay("")]
public string Nom { get; set; }
public string Prenom { get; set; }
public int Age { get; set; }
}

class Program
{
static void Main(string[] args)
{
List<Person> personnes = new List<Person>() {
new Person(){ Age = 55, Nom = "Gates", Prenom = "Bill"},
new Person(){ Age = 48, Nom = "Obama", Prenom = "Barack"},
new Person(){ Age = 26, Nom = "Damaj", Prenom = "Dany"}
};

Console.WriteLine(personnes.Export());

Console.WriteLine();

Console.WriteLine(personnes.Export(header: "<table>", footer: "</table>",
rowPrefix: "<tr><td>", rowSuffix: "</td></tr>",
itemSeparator: "</td><td>",
ignoreFieldHavingAttributeType: typeof(DebuggerDisplayAttribute)));

Console.ReadLine();
}
}



Champ d’application et perspectives


La fonction présentée ci-dessus a été écrite à l’occasion de la recherche d’une fonction d’Export CSV pour les tables Azure. Comme la plupart des fonctions très (trop?) génériques, ses performances ne sont pas excellentes en raison de l’utilisation de la réflexion et du type String en retour de la méthode (un fonctionnement par buffer serait préférable). D’autre part, le choix d’un séparateur entre éléments et d’un préfixe/suffixe pour chaque entité est celui qui semble être le plus logique, mais il ne convient sans doute pas à toutes les implémentations.

jeudi 19 mai 2011

Utilisation de MsBuild pour automatiser les déploiements

Le déploiement est une étape critique de la vie d’un projet. Cependant, la plupart du temps, il n’existe aucune procédure automatisée, et parfois même aucune documentation. Il existe pourtant de nombreux outils permettant d’automatiser la tâche de déploiement, parmi lesquels figurent NAnt et MSBuild par exemple.

Qu’est-ce que MSBuild?

MSBuild est le système de Build de Visual Studio, ce qui veut notamment dire que tous les fichiers projet (.csproj par exemple) utilisent ce système. Par exemple, si on crée une application console par Visual Studio et qu’on ouvre directement le fichier .csproj avec un éditeur de texte, on obtient :

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>04c1eb93-daac-4486-8781-7c0e8dd5967c</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>ConsoleApplication1</RootNamespace>
<AssemblyName>ConsoleApplication1</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<TargetFrameworkProfile>Client</TargetFrameworkProfile>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<PlatformTarget>x86</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>



On voit que MSBuild est en fait basé sur XML, et que les fichiers MSBuild respectent un schéma bien précis. Ce dernier permet de définir notamment :



  • Des Target identifiés par leur nom, permettant de regrouper des tâches et d’organiser leur exécution.

  • Des Task représentant une action unitaire pour le système MSBuild. La liste des tâches disponible permet de donner rapidement une vue d’ensemble de ce qu’on peut faire avec MSBuild (on y retrouve les tâches associées à la compilation notamment). Le système prévoit une extensibilité par l’implémentation d’une interface ITask et par l’utilisation de la balise Import.

  • Des ItemGroup contenant des Item représentant des fichiers, la plupart du temps en vue d’une compilation.

  • Des PropertyGroup représentant de définir des propriétés personnalisées.

Un véritable mini-langage pour le déploiement


MSBuild ne s’arrête pas là. A l’aide des éléments When, Choose et Otherwise et des attributs Condition et DependsOn (pour les Target), on peut très rapidement développer une procédure de déploiement complexe qui répond aux problématiques courantes :



  • Création automatique de backups estampillés à la date du jour.

  • Notification automatique de l’équipe projet lorsqu’un nouveau déploiement a lieu.

  • Prise en compte des différences d’environnement entre déploiements en intégration/préproduction/production (notamment pour prendre les bons fichiers de configuration et les bons identifiants pour les serveurs de destination).

  • Lancement automatique de tests unitaires avant tout déploiement.

  • Tout autre traitement automatisé à l’aide de la tâche Exec.

En outre, de nombreuses tâches récurrentes (envoi de mails, compression, archivage sur le contrôleur de code source, copie sur serveur FTP, etc.) sont déjà développées dans le MSBuild Community Tasks Project et viennent largement consolider les tâches MSBuild de base.


On dispose finalement d’un véritable mini-langage compréhensible et extensible pour la réalisation de tâches de déploiement. Pour exécuter le fichier MSBuild et lancer un déploiement, il suffit d’utiliser l’outil msbuild du Framework .NET. Par exemple, en créant un .bat avec le code suivant, où BuildAll.xml est le fichier MSBuild et “Package” un Target à l’intérieur du fichier MSBuild qu’on souhaite exécuter explicitement :

call "%VS100COMNTOOLS%vsvars32.bat"
msbuild BuildAll.xml /t:Package



Un exemple concret


Pour finir, voici un exemple concret d’utilisation d’MSBuild, où la cible Package permet de :



  • Compiler un projet en utilisant comme répertoire de sortie Build\

  • D’exécuter des tests unitaires

  • De créer un fichier Release.txt contenant la date et l’heure courante dans le répertoire Build\

  • De compresser le répertoire Build\

  • De supprimer le répertoire temporaire Build\
<Project DefaultTargets="Package" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<PropertyGroup>
<MSBuildCommunityTasksPath>chemin de la dll…</MSBuildCommunityTasksPath>
</PropertyGroup>
<Import Project="Dependencies\MSBuild.Community.Tasks.Targets"/>

<PropertyGroup>
<SourceDir>répertoire contenant les sources…</SourceDir>
<BuildDir>Build\</BuildDir>
</PropertyGroup>

<Target Name="Build">
<RemoveDir Directories="$(BuildDir)" ContinueOnError="true" />
<MakeDir Directories="$(BuildDir)" ContinueOnError="false" />

<MSBuild Projects="$(SourceDir)\ExempleDeDossier\ExempleDeProjet.csproj"
Properties="Configuration=Release;DebugSymbols=false;DebugType=none;OutDir=$(MSBuildProjectDirectory)\$(BuildDir)"
BuildInParallel="True">
</MSBuild>

<Prompt Text="Build Terminated. Hit [ENTER] ..." />
</Target>


<Target Name="Test" DependsOnTargets="Build">
<Exec Command="MsTest /noresults /testmetadata:&quot;$(SourceDir)\ExempleDeFichierDeTests.vsmdi&quot;" />

<Prompt Text="Tests Terminated. Hit [ENTER] ..." />
</Target>

<Target Name="Package" DependsOnTargets="Build;Test">
<Time Format="yyyyMMddHHmmss">
<Output TaskParameter="FormattedTime" PropertyName="CurrentDate" />
</Time>

<WriteLinesToFile File="$(BuildDir)\Release.txt" Lines="$(CurrentDate)" Overwrite="true" />

<ItemGroup>
<ZipFiles Include="$(BuildDir)\**\*.*" />
</ItemGroup>

<Zip Files="@(ZipFiles)"
ZipFileName="$(MSBuildProjectDirectory)\LeFichierDeBackup.zip" ZipLevel="9" />

<RemoveDir Directories="$(BuildDir)" ContinueOnError="false" />

<Prompt Text="Package Terminated. Hit [ENTER] ..." />
</Target>
</Project>