Archive

Archive for the ‘Entity Framework’ Category

Code First : Entity Framework Reverse POCO Generator

9 décembre 2012 1 commentaire

Dans un précédent article j’ai expliqué comment faire du reverse engeneering Code First en utilisant l’extension Visual Studio Entity Framework Power Tools. Dans ce billet je parlerai d’une autre extension qui permet de faire la même chose : Entity Framework Reverse POCO Generator.

Vous pouvez installer cette extension à partir de la galerie ou via le gestionnaire d’extensions de Visual Studio. Une fois installé vous aurez un template de fichier qui sera ajouté à la liste de templates déjà existants.

Pour utiliser la fonctionnalité de reverse engeneering de cet outil, créer d’abords votre projet de préférence une bibliothèques de classes. Après vous faîtes un clic-droit sur le projet puis « Ajouter un élément ». Dans la boîte de dialogue qui apparaît choisissez le template suivant :
EF Generator
Donnez un nom à votre élément. Dans mon cas vu que j’utilise la base de données AdventureWorksLT2008 j’ai mis AdventureWorksLT.tt. Après validation, vous aurez 3 fichiers de template T4 (fichiers d’extensions .tt) qui seront ajoutés à votre projet :
EF Generator T4

Le seul fichier T4 que vous aurez à toucher et modifier est AdventureWorksLT.tt. Ouvrez ce fichier et suivre les étapes suivantes :

  • Configuration de la chaine de connexion : en début de fichier trouvez le paramètre ConnectionStringName et mettez le nom de la chaîne de connexion à utiliser. Notez que cette chaîne de connexion doit exister dans le fichier de configuration App.config.
    ConnectionStringName = "AdventureWorksLT2008Context";
  • Configuration du nom du contexte : trouvez le paramètre DbContextName et attribuez lui le nom que vous voudriez avoir comme nom de contexte (la clasese dérivant de DbContext)
    DbContextName = "AdventureWorksContext";
  • Rendre vos classes partielles : trouvez le paramètre MakeClassesPartial et attribuez lui la valeur true. Cela à pour but de rendre les classes partielles par défaut elles ne le sont pas.
    MakeClassesPartial = true;

La sauvegarde du fichier vous génère automatiquement le fichier AdventureWorksLT.cs. Ce fichier continent toutes les classes pour utiliser l’approche Code First. Dans ce fichier ces classes sont groupées en 3 catégories. D’abords nous avons le contexte de la base de données (AdventureWorksLT), suivi de toutes les classes POCO et enfin viennent les classes de configuration de nos classes POCO. Les classes de configuration ont pour but, à travers l’API FLuent, de définir les tables, colonnes, clefs et relations entre ces classes et tables existantes dans votre base de données.

Cette extension n’effectue qu’une tâche : faire du reverse engeneering alors que Entity Framework Power Tools en offre d’autres comme visualiser le modèle EDM, le XML de ce dernier ou générer des vues pré-compilées. Mais il faut noter qu’EF Power Tools ne permet pas de filtrer ou exclure les tables qu’on veut ou qu’on ne veut pas utiliser dans notre projet alors Entity Framework Reverse POCO Generator permet de le faire à travers des expressions régulières que vous devez configurer en modifiant les paramètres TableFilterExclude et TableFilterInclude du template T4 AdventureWorksLT.tt.

Bon code ;)

Catégories:Entity Framework Mots-clés :

Faire du Reverse Engineering avec l’outil EF Power Tools

8 décembre 2012 1 commentaire

L’approche Code First est une des 3 approches qu’offre l’ORM de Microsoft : Entity Framework. Cette approche permet au développeur de coder lui-même les classes qui doivent être mappées aux tables de la base de données en suivant l’une des conditions ou un mix de celles-ci :

  • respecter les conventions de l’approche Code First par exemple toute propriété de nom « Id » définie dans une classe sera par défaut considérée comme un champ représentant la clef primaire de la table à laquelle sera mappée cette classe
  • utiliser les annotations : ce sont des attributs .Net se trouvant dans l’espace de nom System.Model.Component.DataAnnotations. Par exemple : Table, Required, Key etc…
  • utiliser l’API FLuent : pour cela vous devez redéfinir la méthode OnModelCreating de votre contexte (la classe dérivant de DbContext).

Quelle que soit la manière utilisée pour faire du Code First bah … comme son nom l’indique vous coderez vous-même les classes à mapper et par la suite vous allez générer votre base de données :) Mais bon il se peut que vous disposez déjà d’une base de données existante avec plus de 100 tables mais vous voulez quand même utiliser cette approche et vous avez la flemme comme moi de vous taper les 100 classes (ou moins s’il y a des tables qui font office uniquement de tables de jointures) qui doivent correspondre à chaque table existante.

Pas d’inquiétudes, deux outils de reverse enginering vous permettent de générer les classes pour le Code First à partir d’une base de données existante :

Utiliser Entity Framework Power Tools
Entity Framework Power Tools est une extension que vous pouvez télécharger via la galerie Visual Studio ou via le gestionnaire d’extensions de Visual Studio.
Une fois l’extension installée, dans le menu contextuel du projet sur lequel vous voulez générer les classes vous verrez le menu Entity Framework avec eux sous menus :
Menus EF Power Tools

  • Reverse Engineer Code First : vous permet de générer du Code First à partir de la base de données existante
  • Customize Reverse Engineer Templates : vous permet de personnaliser le template par défaut utilisé pour la génération des classes ce qui n’est pas l’objet de ce billet.

En choisissant le sous-menu Reverse Engineer Code First, vous aurez une boîte de dialogue vous invitant à choisir la base de données existante. Vous remarquerez au passage que l’outil ne permet pas de choisir les tables avec lesquelles on aimerait travailler. ça aurait été un plus non négligeable ;)
Après validation, si vous avez utilisé , comme moi, la base de données exemple AdventureWorksLT2008 vous aurez la structure suivante :

  • Un dossier Model à la racine de votre projet contenant les classes générées et le contexte (la classe dérivant de DBContext)
  • un sous-dossier Mapping du dossier Model contenant les configurations (l’outil utilise Fluent API pour la configuration des tables, relations entre elle, clefs etc…)

Le contexte porte par défaut le nom de notre base de données et le contenu est le suivant :

public class AdventureWorksLT2008Context : DbContext
    {
        static AdventureWorksLT2008Context()
        {
            Database.SetInitializer<AdventureWorksLT2008Context>(null);
        }

		public AdventureWorksLT2008Context()
			: base("Name=AdventureWorksLT2008Context")
		{
		}

        public DbSet<BuildVersion> BuildVersions { get; set; }
        public DbSet<ErrorLog> ErrorLogs { get; set; }
        public DbSet<Address> Addresses { get; set; }
        public DbSet<Customer> Customers { get; set; }
        public DbSet<CustomerAddress> CustomerAddresses { get; set; }
        public DbSet<Product> Products { get; set; }
        public DbSet<ProductCategory> ProductCategories { get; set; }
        public DbSet<ProductDescription> ProductDescriptions { get; set; }
        public DbSet<ProductModel> ProductModels { get; set; }
        public DbSet<ProductModelProductDescription> ProductModelProductDescriptions { get; set; }
        public DbSet<SalesOrderDetail> SalesOrderDetails { get; set; }
        public DbSet<SalesOrderHeader> SalesOrderHeaders { get; set; }
        public DbSet<vGetAllCategory> vGetAllCategories { get; set; }
        public DbSet<vProductAndDescription> vProductAndDescriptions { get; set; }
        public DbSet<vProductModelCatalogDescription> vProductModelCatalogDescriptions { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new BuildVersionMap());
            modelBuilder.Configurations.Add(new ErrorLogMap());
            modelBuilder.Configurations.Add(new AddressMap());
            modelBuilder.Configurations.Add(new CustomerMap());
            modelBuilder.Configurations.Add(new CustomerAddressMap());
            modelBuilder.Configurations.Add(new ProductMap());
            modelBuilder.Configurations.Add(new ProductCategoryMap());
            modelBuilder.Configurations.Add(new ProductDescriptionMap());
            modelBuilder.Configurations.Add(new ProductModelMap());
            modelBuilder.Configurations.Add(new ProductModelProductDescriptionMap());
            modelBuilder.Configurations.Add(new SalesOrderDetailMap());
            modelBuilder.Configurations.Add(new SalesOrderHeaderMap());
            modelBuilder.Configurations.Add(new vGetAllCategoryMap());
            modelBuilder.Configurations.Add(new vProductAndDescriptionMap());
            modelBuilder.Configurations.Add(new vProductModelCatalogDescriptionMap());
        }
    }

Pour chaque table, deux classes sont en effet générées : la classe mappée à notre table et une classe ayant pour but de contenir la configuration Fluent API de la classe concernée. Exemple pour la classe Product, sa configuration se trouvera dans la classe ProductMap dans le sous-dossier Mapping.

Le contenu de la classe Product :

public class Product
    {
        public Product()
        {
            this.SalesOrderDetails = new List<SalesOrderDetail>();
        }

        public int ProductID { get; set; }
        public string Name { get; set; }
        public string ProductNumber { get; set; }
        public string Color { get; set; }
        public decimal StandardCost { get; set; }
        public decimal ListPrice { get; set; }
        public string Size { get; set; }
        public Nullable<decimal> Weight { get; set; }
        public Nullable<int> ProductCategoryID { get; set; }
        public Nullable<int> ProductModelID { get; set; }
        public System.DateTime SellStartDate { get; set; }
        public Nullable<System.DateTime> SellEndDate { get; set; }
        public Nullable<System.DateTime> DiscontinuedDate { get; set; }
        public byte[] ThumbNailPhoto { get; set; }
        public string ThumbnailPhotoFileName { get; set; }
        public System.Guid rowguid { get; set; }
        public System.DateTime ModifiedDate { get; set; }
        public virtual ProductCategory ProductCategory { get; set; }
        public virtual ProductModel ProductModel { get; set; }
        public virtual ICollection<SalesOrderDetail> SalesOrderDetails { get; set; }
    }

Le contenu de la classe ProductMap

public class ProductMap : EntityTypeConfiguration<Product>
    {
        public ProductMap()
        {
            // Primary Key
            this.HasKey(t => t.ProductID);

            // Properties
            this.Property(t => t.Name)
                .IsRequired()
                .HasMaxLength(50);

            this.Property(t => t.ProductNumber)
                .IsRequired()
                .HasMaxLength(25);

            this.Property(t => t.Color)
                .HasMaxLength(15);

            this.Property(t => t.Size)
                .HasMaxLength(5);

            this.Property(t => t.ThumbnailPhotoFileName)
                .HasMaxLength(50);

            // Table & Column Mappings
            this.ToTable("Product", "SalesLT");
            this.Property(t => t.ProductID).HasColumnName("ProductID");
            this.Property(t => t.Name).HasColumnName("Name");
            this.Property(t => t.ProductNumber).HasColumnName("ProductNumber");
            this.Property(t => t.Color).HasColumnName("Color");
            this.Property(t => t.StandardCost).HasColumnName("StandardCost");
            this.Property(t => t.ListPrice).HasColumnName("ListPrice");
            this.Property(t => t.Size).HasColumnName("Size");
            this.Property(t => t.Weight).HasColumnName("Weight");
            this.Property(t => t.ProductCategoryID).HasColumnName("ProductCategoryID");
            this.Property(t => t.ProductModelID).HasColumnName("ProductModelID");
            this.Property(t => t.SellStartDate).HasColumnName("SellStartDate");
            this.Property(t => t.SellEndDate).HasColumnName("SellEndDate");
            this.Property(t => t.DiscontinuedDate).HasColumnName("DiscontinuedDate");
            this.Property(t => t.ThumbNailPhoto).HasColumnName("ThumbNailPhoto");
            this.Property(t => t.ThumbnailPhotoFileName).HasColumnName("ThumbnailPhotoFileName");
            this.Property(t => t.rowguid).HasColumnName("rowguid");
            this.Property(t => t.ModifiedDate).HasColumnName("ModifiedDate");

            // Relationships
            this.HasOptional(t => t.ProductCategory)
                .WithMany(t => t.Products)
                .HasForeignKey(d => d.ProductCategoryID);
            this.HasOptional(t => t.ProductModel)
                .WithMany(t => t.Products)
                .HasForeignKey(d => d.ProductModelID);

        }
    }

Vous pouvez commencez à utiliser les classes et votre contexte pour toutes opérations qu’offre l’ORM Entity Framework sans problèmes cependant il y a quelques petites fonctionnalités intéressantes lorsque vous faîtes un clic-droit sur le fichier contenant la classe dérivant de DbContext.
EF Power Tools Context Menu

  • View Entity Data Model (read only) : vous permet de visualiser le modèle EDM en lecture seule dans le designer. Le modèle est tout simplement l’EDMX que vous auriez si vous aviez choisi l’approche Model First ou Database First. Le fait de visualiser l’EDM est utile parce que cela vous permet d’avoir une représentation graphique des entités et des relations qu’il y a entre elles.
    EF Power Tools EDM
  • View Entity Data Model XML : vous permet de voir le XML du modèle EDM. EF Power Tools XML
  • View Entity Data Model DDL SQL : vous permet de voir le script SQL qui sera généré à partir de l’EDM pour créer les objets de votre base de données.
  • Generate Views : vous permet de générer des vues pré-compilées. Pour infos l’utilisation des vues pré-compilées permet au runtime d’Entity Framework d’améliorer les performances de démarrage. Utilisée, cette fonctionnalité ajoute le fichier [Le nom de vote contexte].Views.cs dans mon cas AdventureWorksLT2008Context.Views.cs.

Voilà il s’agit d’un outil qui vous permet de gagner de beaucoup de temps si vous disposez déjà d’une base de données mais que vous passer par l’approche Code First pour utiliser l’ORM Entity Framework !

Bon code ;)

Catégories:Entity Framework, Outils

Activer définitivement la « pluralisation » des noms des entités sous EF

4 septembre 2011 1 commentaire

Lorsqu’on ajoute un modèle ADO.Net Entity Framework dans son application, Visual Studio n’active pas la « pluralisation » des noms des entités. Ce comportement est celui pour la version française de Visual Studio alors qu’avec la version anglaise elle est activée par défaut.
Pour ne pas avoir à activer la « pluralisation » des noms à chaque fois qu’on essaie d’ajouter un fichier EDMX ou tout simplement lorsqu’on essaie de mettre à jour notre modèle Entity Framework, il suffit d’aller dans le menu « Outils » puis « Options.. ». Dans la boîte de dialogue qui s’affiche, dans l’arborescence qui se trouve à gauche, déplier le noeud « Outils de base de données », sélectionner l’élément « O/R Designer » et dans la partie de droite activer tout simplement la « pluralisation » en mettant sa valeur à « True » comme dans l’image ci-dessous et cliquer sur « OK ».

Configuration de la pluralisation des entités

Configuration de la pluralisation des entités

Catégories:Entity Framework Mots-clés : ,

101 Exemples de requêtes LINQ

Le site MSDN fournit une ressource 101 exemples de requêtes LINQ. C’est une ressource rassemblant, comme son nom l’indique, 101 exemples de requêtes LINQ est à garder en bookmark dans votre navigateur et y jeter un coup d’œil pour voir la manière d’utiliser certaines clauses en cas d’oubli ;)

Catégories:Entity Framework, LINQ

Partie 3 : WCF Data Services – Création d’une application Silverlight consommant notre service de données OData

31 juillet 2011 1 commentaire

Dans la partie 1 et la partie 2 nous avons respectivement parlé du protocole OData et créé notre service de données en utilisant WCF Data Services. Dans cette partie nous allons créer notre application cliente en utilisant Silverlight mais je rappelle que notre service de données OData est indépendant de toute technologie vous pouvez utiliser les technologies telles que PHP, Java ou tout simplement du javascript pour communiquer avec le service.

Le SDK de Silverlight fournit une bibliothèque cliente nous permettant d’interroger toute source de données OData que cette dernière soit créée avec WCF Data Services ou non. Cette bibliothèque inclut pas mal de classes et de méthodes nous permettant d’accéder et de modifier les ressources.

L’application que nous allons créer doit nous permettre :

  • d’afficher la liste des produits en fonction d’une catégorie,
  • modifier un produit
  • ajouter un produit
  • supprimer un produit : cette fonctionnalité renverra toujours une exception vu que nous avons défini un intercepteur qui refuse tout changement lié à la suppression d’un produit. Nous la mettrons juste pour tester que notre intercepteur fonctionne correctement.

L’application affichera une seule page xaml qui contiendra toutes nos fonctionnalités et ressemblera à l’image ci-dessous :

Interface de notre application

Interface de notre application

Le code XAML de la page MainPage.xaml est le suivant :

<UserControl xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
             x:Class="SilverlightAndWCFDataServices.MainPage"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             d:DesignHeight="600"
             d:DesignWidth="1000">

    <Grid x:Name="LayoutRoot"
          Width="600"
          Background="White"
          HorizontalAlignment="Center">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="400" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <StackPanel Grid.Row="0"
                    Margin="0,5,0,5"
                    Orientation="Horizontal">

            <ComboBox x:Name="cbCategories"
                      ItemsSource="{Binding}"
                      Width="200"
                      Margin="0,0,2,0"
                      DisplayMemberPath="Name" />
            <Button x:Name="btnMaj"
                    Grid.Row="0"
                    Content="Mettre à jour"
                    Margin="0,0,2,0"
                    Click="btnMaj_Click" />
            <Button x:Name="btnSupprimer"
                    Grid.Row="0"
                    Content="Supprimer"
                    Margin="0,0,2,0"
                    Click="btnSupprimer_Click" />
            <Button x:Name="btnAjouter"
                    Grid.Row="0"
                    Content="Nouveau produit"
                    Margin="0,0,2,0"
                    Click="btnAjouter_Click" />
        </StackPanel>

        <toolkit:DataGrid x:Name="myDataGrid"
                          AutoGenerateColumns="False"
                          Grid.Row="1"
                          Margin="0,0,5,0"
                          ItemsSource="{Binding ElementName=cbCategories, Path=SelectedItem.Products, Mode=TwoWay}">
            <toolkit:DataGrid.Columns>
                <toolkit:DataGridTextColumn Header="Numéro du produit"
                                            Binding="{Binding ProductNumber}" />
                <toolkit:DataGridTextColumn Header="Nom"
                                            Binding="{Binding Name}"
                                            Width="300" />
                <toolkit:DataGridTextColumn Header="Prix unitaire"
                                            Binding="{Binding ListPrice}" />
                <toolkit:DataGridTextColumn Header="Couleur"
                                            Binding="{Binding Color}" />
                <toolkit:DataGridTextColumn Header="Taille"
                                            Binding="{Binding Size}" />
            </toolkit:DataGrid.Columns>
        </toolkit:DataGrid>
    </Grid>
</UserControl>

Nous avons 3 boutons et la fonction de chacun se passe de commentaires. Chacun des boutons est abonné à l’évènement Click. Nous avons un contrôle DataGrid qui a sa propriété ItemsSource renseigné grâce à une liaison de données sur la liste des Produits rattachés à la catégorie sélectionnée dans notre ComboxBox prévu à cet effet. Notre ComboBox est abonné à l’évènement SelectionChanged.

Pour nous faciliter la communication avec notre service de donnés, Visual Studio nous permet de générer une classe qui servira de proxy que nous utilisons pour communiquer avec la source de données. Le proxy sera créé dans le projet de l’application Silverlight SilverlightAndWCFDataServices. Pour cela faîtes un clic droit sur le nom du projet puis cliquez sur Ajouter un service de référence et suivre les étapes ci-dessous :

Génération du Proxy

Pour voir ce qui a été généré, dans le dossier Service Reference faîtes un clic droit sur DemoODataServiceReference puis sur Afficher dans l’explorateur d’objets et une boîte de dialogue s’affiche comme dans l’image ci-dessous :

Exploration de la classe servant de proxy

La génération du proxy nous a créé trois classes comme le montre la partie gauche de l’image. La classe AdventureWorksEntites est la plus importante parmi les classes générées. Elle représente notre contexte de données côté client et permettra le suivi de ces données. Elle nous permet grâce à des méthodes asynchrones d’envoyer nos requêtes sous formes d’URI généré à partir d’une chaine de caractères ou à partir d’une instance de classe spéciale DataServiceQuery. Les méthodes d’envoi des requêtes commencent toutes par le préfixe Begin.

Pour ne pas avoir à saisir les requêtes en utilisant les URLs qui peuvent être source d’erreurs nous passerons par la classe DataServiceQuery qui nous générera l’URL correspondante. Cette classe expose une propriété RequestUri qui continent l’URL qui est générée avant l’envoi de la requête à notre service de données grâce aux différentes méthodes asynchrones disponibles dans notre contexte AdventureWorksEntites.

3.1. Chargement de la liste des catégories et des produits
Le code (des commentaires sont insérés pour comprendre ce qui est fait) suivant nous permet de charger les différentes catégories et montre aussi comment sera récupérer les produits associé à une catégorie sélectionnée :

public partial class MainPage : UserControl
{
	private AdventureWorksEntities context;

	public MainPage()
	{
		InitializeComponent();
		this.Loaded += this.userControlLoaded;
		this.cbCategories.SelectionChanged += this.cbCategoriesSelectionChanged;
		this.context = new AdventureWorksEntities(new Uri("http://localhost:3000/AdventureWorksDataService.svc"));
	}

	private void userControlLoaded(object sender, RoutedEventArgs e)
	{
		this.Loaded -= this.userControlLoaded;

		// Nous créons une requête Linq que nous convertissons en un objet du type DataServiceQuery<ProductCategory>
		DataServiceQuery<ProductCategory> query = (from c in this.context.ProductCategories
												   orderby c.Name
												   where c.ParentProductCategoryID != null
												   select c) as DataServiceQuery<ProductCategory>;
		try
		{
			// Nous utilisons la méthode asynchrone BeginExecute (méthode permettant d'exécuter toute requête avec le verbe HTTP GET) pour exécuter la requête précédemment construite.
			// la méthode réçoit en paramètres :
			// -    une URI en l'occurence celle générée par l'instance query
			// -    une méthode de rappel : la méthode qui sera exécutée une fois que une réponse est réçue de la part de notre service de données
			// -    tout type d'objet que nous voudrions par la suite récupérer dans lorsque la méthode de rappel sera exécutée
			context.BeginExecute<ProductCategory>(query.RequestUri, this.getCategoriesCallback, null);
		}
		catch (Exception ex)
		{
			MessageBox.Show(string.Format("Erreur survenue : {0}", ex.Message));
		}
	}

	private void getCategoriesCallback(IAsyncResult result)
	{
		this.Dispatcher.BeginInvoke(() =>
		{
			try
			{
				// Nous appelons la méthode EndExecute pour pouvoir récupérer le résultat. 
				// Ici le récultat correspond à la liste de catégories
				var categories = this.context.EndExecute<ProductCategory>(result);
				this.cbCategories.DataContext = categories;
			}
			catch (Exception ex)
			{ 
				// Dans ce bloc nous gérons les erreurs rencontrées 
				// Une gestion un tout petit particulier pour récupérer le message exacte de l'erreur 
				// s'il s'agit d'une exception du type DataServiceRequestException
				string message;
				if (ex is DataServiceRequestException)
					message = this.getMessage(ex as DataServiceRequestException);
				else message = ex.Message;
				MessageBox.Show("Erreur : " + message);
			}
		});
	}

	private void cbCategoriesSelectionChanged(object sender, SelectionChangedEventArgs e)
	{
		try
		{
			// Si aucune catégorie n'est sélectionnée on ne fait rien
			if (this.cbCategories.SelectedIndex == -1) return; 

			// On récupère la catégorie concernée
			ProductCategory category = this.cbCategories.SelectedItem as ProductCategory;

			// On appelle la méthode BeginLoadProperty permettant le chargement des données d'une propriéte particulière
			this.context.BeginLoadProperty(category, "Products", getProductByCategoryCallback, null);
		}
		catch (Exception ex)
		{
			MessageBox.Show(string.Format("Erreur survenue : {0}", ex.Message));
		}
	}

	private void getProductByCategoryCallback(IAsyncResult result)
	{
		this.Dispatcher.BeginInvoke(() =>
		{
			try
			{
				// Nous appelons juste la méthode EndLoadProperty. 
				// la liste des produits associées à la catégorie concernée sera bien remplie
				// vu que nous l'avons spécifié lors de l'appel BeginLoadProperty. 
				this.context.EndLoadProperty(result);
			}
			catch (Exception ex)
			{
				string message;
				if (ex is DataServiceRequestException)
					message = this.getMessage(ex as DataServiceRequestException);
				else message = ex.Message;
				MessageBox.Show("Erreur : " + message);
			}
		});
	}

	private string getMessage(DataServiceRequestException dataServiceRequestException)
	{
		if (dataServiceRequestException.InnerException == null) return dataServiceRequestException.Message;

		// Nécessite l'ajout de la DLL System.Xml.Linq
		// Le message exacte à récupérer lorsqu'une exception générée par le service de données est interceptée par 
		// le client. Le type de l'exception sera du type DataServiceRequestException sauf que le message que nous voulons
		// ne se trouve pas dans la propriété Message de l'exception mais dans celle de la propriété innerException. 
		// Le message est par contre cpntenu dans une structure XML d'où le fait que nous utilisons Linq To XML pour récupére le texte
		// de l'élément message
		XDocument xDoc = XDocument.Parse(dataServiceRequestException.InnerException.Message);
		XElement xElement = xDoc.Root;
		XNamespace ns = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata";
		var message = (from elt in xElement.Elements(ns + "message")
					   select elt.Value).FirstOrDefault();
		return message;
	}
}

3.2. Ajout, mise à jour et suppression d’un produit
Dans l’extrait de code suivant nous avons les gestionnaires de l’évènement Click des différents boutons :

private void btnMaj_Click(object sender, RoutedEventArgs e)
{
    try
    {
        // On verifie qu'un produit est bien sélectionné sinon on ne fait rien
        if (this.myDataGrid.SelectedIndex == -1) return;

        // On récupère l'instance du produit sélectionné
        Product product = this.myDataGrid.SelectedItem as Product; 

        // On spécifie à notre contexte que le produit doit être mis à jour grâce 
        // à la méthode UpdateObject de notre contexte 
        this.context.UpdateObject(product);  

        // Une fois le produit marqué comme à mettre à jour alors
        // nous devons valider la modification vers notre service de données
        // en appelant la méthodde BeginSaveChanges
        this.context.BeginSaveChanges(this.saveChangeCallback, null);
    }
    catch (Exception ex)
    {
        MessageBox.Show("Erreur : " + ex.Message);
    }
}

private void btnSupprimer_Click(object sender, RoutedEventArgs e)
{
    try
    {
        // On verifie qu'un produit est bien sélectionné sinon on ne fait rien
        if (this.myDataGrid.SelectedIndex == -1) return;

        // On récupère l'instance du produit sélectionné
        Product product = this.myDataGrid.SelectedItem as Product;

        // On spécifie à notre contexte que le produit doit être supprimé grâce 
        // à la méthode DeleteObject de notre contexte 
        this.context.DeleteObject(product);

        // Une fois le produit marqué comme à supprimer alors
        // nous devons valider la modification vers notre service de données
        // en appelant la méthodde BeginSaveChanges
        this.context.BeginSaveChanges(this.saveChangeCallback, null);
    }
    catch (Exception ex)
    {
        MessageBox.Show("Erreur : " + ex.Message);
    }
}

private void btnAjouter_Click(object sender, RoutedEventArgs e)
{
    if(this.cbCategories.SelectedIndex == -1) return;
    try
    {
        // On récupère la catégorie à associer au nouveau produit
        ProductCategory category = this.cbCategories.SelectedItem as ProductCategory;

        // On renseigne les différentes propriétés requises 
        Random random = new Random();
        int n = random.Next(999);
        Product newProduct = new Product()
        {
            Name = string.Format("Added product {0}", n),
            Color = "White",
            ListPrice = 150m,
            StandardCost = 99,
            ProductNumber = string.Format("Added product - {0}", n),
            SellStartDate = DateTime.Now,
            rowguid = Guid.NewGuid(),
            ModifiedDate = DateTime.Now,
            Size = "XL" // Mettre cette propriété à null si vous voulez tester si notre intercepteur empêchant l'ajout
                        // d'un produit appartenant à la catégorie parent Clothing Ex : la catgorie Socks est enfant de Clothing.
        };

        // Mettre à jour les propriétes Products et ProductCategory respectivement pour category et newProductCategory
        category.Products.Add(newProduct);
        newProduct.ProductCategory = category;

        // On crée la relation entre la catégorie et le produit. Cela n'est pas fait de façon automatique lorsqu'on 
        // met à jour les propriétés de nos données avec les deux lignes de codes précédentes
        this.context.AddRelatedObject(category, "Products", newProduct);

        // Nous appelons la méthode BeginSaveCahnges pour mettre à jour le service de données.
        this.context.BeginSaveChanges(this.saveChangeCallback, null);
    }
    catch (Exception ex)
    {
        MessageBox.Show("Erreur : " + ex.Message);
    }
}

private void saveChangeCallback(IAsyncResult result)
{
        this.Dispatcher.BeginInvoke(() =>
        {
            try
            {
                // Nous appelons la méthode EndSaveChanges 
                this.context.EndSaveChanges(result);
                MessageBox.Show("Mise à jour effectuée avec succcès");
            }
            catch (Exception ex)
            {
                string message;
                if (ex is DataServiceRequestException)
                    message = this.getMessage(ex as DataServiceRequestException);
                else message = ex.Message;
                MessageBox.Show("Erreur : " + message);
            }
        });
            
}

Nous pouvons tester notre application pour récupérer les données, ajouter, mettre à jour et essayer de supprimer un produit.
La solution Visual Studio 2010 se trouve ici.
Sources :
• Site du protocole OData : http://www.odata.org
• MSDN WCF Data Services : http://msdn.microsoft.com/fr-fr/library/cc668792.aspx
• MSDN WCF Data Services (Silverlight) : http://msdn.microsoft.com/en-us/library/cc838234(VS.95).aspx

Partie 2 : WCF Data Services – Création de notre service de données OData

31 juillet 2011 1 commentaire

Dans la partie 1 , nous avons parlé du protocole OData que chaque technologie (.Net, Java etc) se doit de respecter les spécifications pour mettre en place un service exposant des données suivant ce protocole.

Dans le cas de .Net nous avons WCF Data Services. WCF Data Services anciennement appelé ADO.Net Data Services fournit un ensemble d’APIs clients et serveurs pour permettre respectivement de consommer et créer un service de données OData. Ces bibliothèques sont incluses dans le Framework .Net. Le schéma suivant résume globalement comment fonctionne ces bibliothèques WCF Data Services :

Schéma expliquant de façon globale le fonctionnement de WCF Data Services (image récupérée sur le site MSDN)

Schéma expliquant de façon globale le fonctionnement de WCF Data Services (image récupérée sur le site MSDN)

Comme on le voit sur le schéma le runtime Data Services se base des fournisseurs (Data Services Providers) pour exposer les données. Ces fournisseurs de services de données sont de 3 types :

Entity Framework : ce fournisseur se base sur ADO.Net Entity Framework pour permettre d’exposer nos données issues d’une base de données relationnelle.
Reflection : se base sur la réflexion pour exposer nos données en se basant sur des classes implémentant IQueryable.
Custom : dans ce cas il s’agira d’un fournisseur de données que vous auriez codé en respectant les règles définies par le Framework .Net.

Dans le présent article nous créerons un service qui se basera sur le fournisseur de type Entity Framework qui me semble être le plus facile à manier.

Dans tout ce qui suit, nous utiliserons les outils suivants :
• Visual Studio 2010
• .Net 4.0
• Silverlight 4.0 SDK
• Silverlight Tools pour Visual Studio 2010
• La base de données AdventureWorks : vous pouvez la base de données ici

1. Ajout des fichiers utiles
Nous allons mettre en place notre premier service de données pour cela créons un projet type Application Silverlight et donnez lui le nom de SilverlightAndWCFDataServices et choisissez le répertoire de destination comme dans l’image ci-dessous :

Création de la solution

Après avoir cliqué sur OK, une boîte de dialogue s’affiche, configurez-la comme définit dans l’image suivante :

Paramétrage de l'application Silverlight

Après avoir cliqué sur le bouton OK, Visual Studio nous crée 2 projets :
SilverlightAndWCFDataServices : qui représente notre application Silverlight.
SilverlightAndWCFDataServices.Web : qui consiste en une application web ASP.Net qui aura pour objectif non seulement d’héberger notre application Silverlight mais aussi héberger notre service de données créé avec WCF Data Services.

Nous allons suivre les étapes suivantes pour ajouter le modèle Entity Framework :

Ajout du fichier EDM


Assistant ajout d'un fichier EDM


Assistant ajout fichier EDM


Choix des tables à mapper

Nous venons de créer notre modèle de données Entity Framework. Pour finaliser renommons les propriétés ProductCateegory1 et ProductCategory2 de l’entité ProductCategory respectivement en SubCategories et ParentCategory. Au final notre modèle ressemblera à ce qui suit :

Modèle final EF

Nous allons poursuivre en ajoutant, toujours dans notre projet web, un nouveau fichier Service de données WCF et nommez ce fichier AdventureWorksDataService.svc comme dans l’image ci-dessous :

Ajout du fichier de Services de données

Une fois le fichier ajouté à notre projet web, vous pouvez accéder au fichier AdventureWorksDataService.svc.cs et visualiser le code ci-dessous :

Contenu du fichier AdventureWorksDataService.svc.cs

On remarque des commentaires avec des TODO qui représentent les deux tâches à effectuer pour que notre service de données soit fonctionnel. Nous devons spécifier le fournisseur de données que le service devra utiliser : pour cela nous devons définir le type de notre contexte de données Entity Framework comme étant le type générique de la classe de base DataService dont dérive notre service de données soit DataService. Reste maintenant à définir les jeux d’entités et les opérations qui seront exposés en configurant les règles d’accès et de modifications.

2. Configuration
Les règles d’accès se configurent dans la méthode statique InitializeService de notre service de données. Cette méthode reçoit un paramètre appelé config qui est une instance de la classe DataServiceConfiguration. Cette classe expose 2 méthodes nous permettant de configurer les règles d’accès de nos jeux d’entités (grâce à la méthode SetEntityAccessRule) et opérations (grâce à la méthode SetOperationAccessRule). Dans notre cas d’utilisation nous n’exposons pas d’opérations alors nous n’allons pas configurer de règles pour celles-ci mais il faut noter qu’elles se configurent avec le même principe que pour les jeux d’entités.
La méthode SetEntityAccessRule reçoit en paramètres :

• Une chaîne de caractère représentant le nom du jeu d’entités Entity Framework dont on veut configurer les règles d’accès. N.B : On peut spécifier le caractère « * » pour spécifier que la configuration s’applique sur tous les jeux d’entités.
• Une énumération qui est du type EntitySetRights (ServiceOperationRights pour les opérations). Ce paramètre définit les droits d’accès sur la ou les ressources concernées. Les valeurs possibles d’EntitySetRights sont :

  • None : Aucun droit d’accès n’est accordé. Alors les clients ne pourront pas accéder à la ressource encore moins la modifier ou la supprimer.
  • ReadSingle : On n’aura pas le droit d’exécuter une requête renvoyant plusieurs éléments d’un jeu d’entités mais uniquement un seul élément de ce jeu.
  • ReadMultiple :On a le droit d’exécuter une requête renvoyant un groupe de données provenant d’un jeu d’entités
  • WriteAppend : Permettre la création d’éléments dans un groupe de données
  • WriteReplace : On pourra remplacer des éléments
  • WriteDelete : On pourra supprimer des éléments
  • WriteMerge : On pourra fusionner les données d’un élément
  • AllRead : Autorisation de lecture des données
  • AllWrite : Autorisation d’écriture de données
  • All : Tous les droits (lecture, écriture, modification et suppression) sont permis

Il faut noter que l’enum EntitySetRights est décoré avec l’attribut FlagsAttribute ce qui nous permet de combiner les différentes valeurs.

Mis à part la configuration des droits d’accès et de modifications sur les jeux d’entités, nous pouvons aussi limiter le nombre d’éléments qui peuvent être inclus dans la réponse d’une requête grâce à la méthode SetEntityPageSize. Cette méthode reçoit en paramètre le nom du jeu d’entités concerné et le nombre d’éléments maximum qu’on pourra récupérer.
Dans notre cas d’utilisation nous autoriserons tous les droits sur les jeux d’entités sans limite sur le nombre d’éléments. Notre code ressemblera à ceci :

public class AdventureWorksDataService : DataService<AdventureWorksEntities>
{
	public static void InitializeService(DataServiceConfiguration config)
	{
		config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
		config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
	}
}

Nous pouvons maintenant tester notre service de données sans passer par un client mais juste en utilisant un navigateur Web. Pour cela faîtes un clic droit sur le fichier AdventureWorksDataService.svc et puis cliquez sur Afficher dans le navigateur dans le menu contextuel. Dans le navigateur nous avons le Service Document qui s’affiche nous montrant la liste des jeux d’entités accessibles. Testons les requêtes suivantes :

3. Aller plus loin dans la configuration : les intercepteurs
Dans la configuration de notre service de données OData nous avons autorisé tous les droits sur le jeu d’entités Products donc on pourra exécuter une requête d’ajout, de modification et de suppression d’un produit. Nous voudrions ne pas renvoyer la liste des produits en arrêt de commercialisation et aussi ne valider la modification ou l’ajout d’un produit à la catégorie Clothing (Vêtement) que si la taille du vêtement est renseignée. Pour mettre en place ces deux logiques nous devons intercepter les demandes du client.

WCF Data Services intègre ce qu’on appelle des intercepteurs. Les intercepteurs sont définis par entité et dans notre cas d’utilisations ils seront définis sur l’entité Products. Pour définir un intercepteur nous avons à notre disposition deux attributs et chacun reçoit comme paramètre de son constructeur le nom du jeu d’entité concerné :

  • QueryInterceptorAttribute : Cet attribut permettra d’intercepter toutes les requêtes utilisant le verbe http GET et agissant sur le jeu d’entité donné. Ce type d’intercepteur nous permettra d’implémenter notre propre logique de récupération des produits. Cet attribut doit être défini sur une méthode qui ne reçoit aucun paramètre et doit avoir comme type de retour une expression lambda comme le montre le code ci-dessous :
    [QueryInterceptor("Products")]
    public Expression<Func<Product, bool>> OnGetProducts()
    {
         return p => !p.SellEndDate.HasValue;
    }
    
  • ChangeInterceptorAttribute : Cet intercepteur nous permettra de décider si on valide ou non les modifications sur un jeu d’entités. La méthode sur laquelle s’applique cet attribut doit renvoyer aucune valeur et doit recevoir en premier paramètre un objet du même type que les données du jeu d’entités et en deuxième paramètre un enumérateur de type UpdateOptions qui contient les infos sur les modifications qui seront appliquées sur notre donnée et les valeurs possibles sont : Add, Change, Delete et None qui se passent de commentaires. La méthode sera appelée par toute requête utilisant les verbes HTTP autre que GET.
    [ChangeInterceptor("Products")]
    public void OnChangeProducts(Product product, UpdateOperations operations)
    {
    	if (operations == UpdateOperations.Add || operations == UpdateOperations.Change)
    	{
    		// La catégorie est obligatoire sinon nous générons une exception
    		if (!product.ProductCategoryID.HasValue)
    		{
    			// DataServiceException est le type d'exception à lancer lorsqu'on travaille avec WCF Data Services.
    			// Nous utilisons ici la surcharge du constructeur qui reçoit en paramètre le code de l'erreur et
    			// la description du message qui sera lu côté client.
    			throw new DataServiceException(400, "La catégorie du produit doit être renseignée");
    		}
    
    		// Nous avons accès à notre contexte de données grâce à la propriété CurrentDataSource de notre service de données
    		ProductCategory category = this.CurrentDataSource.ProductCategories
    										.Include("ParentCategory")
    										.Where(x => x.ProductCategoryID == product.ProductCategoryID && x.ParentCategory.ProductCategoryID == 3).FirstOrDefault();
    		
    		if (category != null && string.IsNullOrWhiteSpace(product.Size))
    		{
    			throw new DataServiceException(400, 
    				"Tout produit dans la catégorie parent Clothing doit avoir une taille");
    		}
    	}
    	else if (operations == UpdateOperations.Delete)
    	{
    		// On ne peut supprimer un produit mais mettre à jour la date de fin de commercialisation ou 
    		throw new DataServiceException(400, 
    			"Un produit ne peut être supprimé mais à la place mettez à jour la date de fin de commercialisation ou spécifiez qu'il est en rupture de stock.");
    	}
    }
    

    Nous pouvons tester que l’intercepteur de requête marche en utilisant la requête http://localhost:3000/AdventureWorksDataService.svc/Products et nous pouvons constater que pas mal de produits ne sont pas présents dans la liste renvoyée parce qu’ils ne sont plus commercialisés. Pour l’intercepteur de changement nous le testerons une fois que notre application client sera mise en place.

    Nous venons de créer notre service de données en utilisant la bibliothèque serveur fournit par WCF Data Services. Nous allons poursuivre dans la partie 3 en mettant en place une application web Silverlight pour consommer ce service.

La version finale RTW (Release To Web) d’EF 4.1 est sortie !

12 avril 2011 2 commentaires

L’approche Code First et l’abstraction des divers approches à travers l’API DbContext sont les deux fonctionnalités qui ont vu le jour avec l’arrivée d’EF 4.1. Une version finale RTW d’EF 4.1 est accessible par téléchargement depuis hier en version stand-alone ou via NuGet avec l’installation du package EntityFramework. Il faut noter que l’installation via NuGet ne vous permet pas d’installer des templates Visual Studio.

Pas de grands changements mis à part la correction de bugs notamment le bug lié à l’utilisation d’EF avec SQL Compact. Les améliorations telles que le support des enums, des procédures stockées et d’autres ne sont pas encore apportées à cette version.
Pour plus d’informations voir ici.

Suivre

Recevez les nouvelles publications par mail.

%d blogueurs aiment cette page :