Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Abonnements

Comment WAQS vous propose d’exprimer vos règles métier ? 5/13

Posts précédents sur WAQS :

WAQS : Introduction

Comment utiliser WAQS?

Comment WAQS vous propose d’exprimer vos règles métier ? 1/13

Comment WAQS vous propose d’exprimer vos règles métier ? 2/13

Comment WAQS vous propose d’exprimer vos règles métier ? 3/13

Comment WAQS vous propose d’exprimer vos règles métier ? 4/13

 

Définition de metadata

Les metadata de l’edmx sont souvent insuffisantes : pas de MinValue, pas de MaxValue, pas de MinLength, pas de Pattern, encore moins de gestion dynamique des metadata.

Dans les spécifications, il est possible d’y remédier en définissant des méthodes pour définir ces metadata.

Pour cela, il suffit d’utiliser des méthodes statiques commençant par Define, sans paramètre et sans type de retour.

public static void DefineOrderDetailMetadata()
{
    Metadata<OrderDetail>.DefineMinValue(od => od.Discount, 0);
    Metadata<OrderDetail>.DefineMaxValue(od => od.Discount, 1);
}

Il est possible de définir des metadata statiques comme précédemment mais également dynamiques comme ceci :

public static void DefineCustomerMetadata()
{
    Metadata<Customer>.DefinePattern(c => c.PostalCode, c => c.Country != null && 
      c.Country.ToUpper() == "FRANCE" ? @"^(\d{2}|(2(A|B)))\d{3}$" : null);
    Metadata<Customer>.IsNullable(c => c.City, c => c.PostalCode == null);
}

WAQS génèrera du code pour la validation des données basées sur ces metadata et sur celles de l’edmx.

La méthode SaveChanges (générée par WAQS) vérifiera les règles métiers avant de persister les modifications dans la base (je vous expliquerai comment ça marche dans un futur post).

Côté client, la validation des metadata est également mise en place par WAQS. Il y a deux façons de les utiliser avec WPF et une avec un projet PCL.

WPF

La première façon consiste à utiliser les DataErrorInfo. WAQS génère la logique des DataErrorInfo. Dès lors, dans votre binding, vous pouvez simplement affecter ValidatesOnDataErrors à true.

<TextBox Text="{Binding OrderDetail.Discount, ValidatesOnDataErrors=true}" />

Si ce que vous tapez dans la TextBox ne correspond pas, le bord du contrôle apparaitra en rouge (par défaut).

Cependant, je ne trouve pas ni le principe du DataErrorInfo ni celui du NotifyDataErrorInfo (pour WPF 4.5 seulement mais non généré par WAQS aujourd’hui) suffisant. En effet, il n’est pas prévu pour avoir plusieurs erreurs et encore moins pour merger des erreurs de criticité différente.

Pour cela WAQS propose son propre fonctionnement à savoir utiliser une collection d’erreurs pour chaque propriété scalaire de l’entité.

WAQS pour WPF définit cette collection en tant que PropertyDescriptor sur le type d’entité (ce qui est mieux pour l’expérience développeur IMO) et WAQS génère également un behavior pour afficher les erreurs.

Dans notre exemple, vous pouvez utiliser le XAML suivant :

<TextBox Text="{Binding OrderDetail.Discount}"
         controls:ErrorsBehaviors.Errors="{Binding OrderDetail.DiscountErrors}" />

De cette façon, la bordure du TextBox prendra la couleur de l’erreur la plus importante (rouge en cas d’erreur, orange pour un warning) avec un tooltip permettant d’afficher les différentes erreurs :

image

A noter que, comme très souvent avec WAQS, une méthode partielle dans la classe ErrorsBehaviors permet de changer le style du contrôle.

static partial void SetControlStyle(Control control, ObservableCollection<Error> errors, Criticity maxCriticity, 
ref bool done);

Il est également possible d’utiliser les metadatas dans le binding (comme MaxLength pour empêcher toute saisie trop longue) :

<TextBox Text="{Binding Customer.CompanyName}" MaxLength="{Binding Customer.CompanyNameMaxLength}" />

PCL

Avec les PCL, on n’a pas de PropertyDescriptors. Donc WAQS génère simplement des propriétés (DiscountErrors sur OrderDetail et CompanyNameMaxLength sur Customer dans notre exemple).

Posté le vendredi 20 décembre 2013 14:03 par Matthieu MEZIL | 0 commentaire(s)

Comment WAQS vous propose d’exprimer vos règles métier ? 4/13

Posts précédents sur WAQS :

WAQS : Introduction

Comment utiliser WAQS?

Comment WAQS vous propose d’exprimer vos règles métier ? 1/13

Comment WAQS vous propose d’exprimer vos règles métier ? 2/13

Comment WAQS vous propose d’exprimer vos règles métier ? 3/13

 

Propriétés calculées sur le serveur

Parfois, on souhaite que les calculs se passent uniquement sur le serveur.

Pour cela, WAQS a un attribut utilisable sur la méthode de spécifications : NotApplicableOnClient.

Parfois il faut effectuer un calcul sur la base pour pouvoir récupérer la valeur de la propriété calculée. Pour cela, il suffit de passer l’interface du contexte à la méthode. Si la méthode est une extension méthode sur une entité et qu’elle prend, en plus le contexte comme paramètre, WAQS génèrera une propriété.

public static int GetOrdersCount(this Customer c, INorthwindWAQSEntities context)
{
    return context.Orders.Where(o => o.CustomerId == c.Id).Count();
}

A noter que si le contexte est passé en paramètre, l’attribut NotApplicableOnClient est inutile. Le calcul ne s’effectuera que sur le serveur.

Dans ce cas, côté client, nous aurons juste le code suivant côté client :

public int OrdersCount
{
    get
    {
        return Specifications != null && Specifications.HasOrdersCount ? Specifications.OrdersCount : default (int);
    }
    set
    {
        throw new System.InvalidOperationException();
    }
}

Posté le mercredi 18 décembre 2013 18:07 par Matthieu MEZIL | 0 commentaire(s)

Comment WAQS vous propose d’exprimer vos règles métier ? 3/13

Posts précédents sur WAQS :

WAQS : Introduction

Comment utiliser WAQS?

Comment WAQS vous propose d’exprimer vos règles métier ? 1/13

Comment WAQS vous propose d’exprimer vos règles métier ? 2/13

 

Propriétés calculées et polymorphisme

Lors de l’écriture des règles métiers, il est possible d’avoir besoin de polymorphisme. Par exemple, on pourrait vouloir appliquer 10% aux clients VIP.

Comment exprimer cela ? Vous ne pouvez pas utiliser les mots clés asbtract, virtual et override sur les méthodes statiques.

Puisque le code des spécifications ne sert que de modèle à la génération d’un autre code, WAQS propose une solution basée sur des attributs pour exprimer cela :

[Virtual]
public static double GetDiscount(this Customer c)
{
    return 0;
}

[Override]
public static double GetDiscount(this VIPCustomer c)
{
    return 0.1;
}

Du fait de ces attributs, WAQS va générer le code approprié.

Dans la classe Customer :

public virtual double Discount
{
    get
    {
        try
        {
            if (Specifications != null && Specifications.HasDiscount)
                return Specifications.Discount;
            return 0;
        }
        catch (System.NullReferenceException)
        {
            return default (double);
        }
        catch (System.InvalidOperationException)
        {
            return default (double);
        }
    }
    set
    {
        throw new System.InvalidOperationException();
    }
}

Dans la classe VIPCustomer :

public override double Discount
{
    get
    {
        try
        {
            if (Specifications != null && Specifications.HasDiscount)
                return Specifications.Discount;
            return 0.1;
        }
        catch (System.NullReferenceException)
        {
            return default (double);
        }
        catch (System.InvalidOperationException)
        {
            return default (double);
        }
    }
    set
    {
        throw new System.InvalidOperationException();
    }
}

Avec ce moyen, nous avons ainsi facilement pu exprimer le polymorphisme.

Posté le mercredi 18 décembre 2013 14:38 par Matthieu MEZIL | 0 commentaire(s)

Comment WAQS vous propose d’exprimer vos règles métier ? 2/13

Posts précédents sur WAQS :

WAQS : Introduction

Comment utiliser WAQS?

Comment WAQS vous propose d’exprimer vos règles métier ? 1/13

 

Les propriétés calculées

Lors de la génération du code côté serveur, WAQS a créé un répertoire Specifications.

Ce répertoire est l’emplacement par défaut des règles métiers. Cet emplacement pourra être modifié ou complété avec d’autres répertoires dans le fichier .waqs qui correspond la configuration de génération du code.

Comme évoqué plus haut, l’idée est d’écrire du code à part. Il ne sera donc pas possible d’utiliser une propriété sur les classes d’entités.

De fait, nous allons utiliser des méthodes statiques.

Afin d’homogénéiser le code écrit par les développeurs, WAQS se base sur des conventions pour l’écriture du code métier.

Dans le cas des propriétés calculées, elles devront commencer par « Get » et être des extension methods sur le type à étendre.

Commençons avec un exemple très simple : une propriété calculées qui fait l’agrégation du CompanyName avec le ContactName.

public static class NorthwindSpecifications
{
    public static string GetCustomerName(this Customer c)
    {
        return string.Concat(c.CompanyName, " - ", c.ContactName);
    }
}

Après une nouvelle génération des T4, nous avons maintenant une propriété CustomerName sur la classe Customer côté client dont le get intègre notre code :

public string CustomerName
{
    get
    {
        try
        {
            if (Specifications != null && Specifications.HasCustomerName)
                return Specifications.CustomerName;
            return string.Concat(this.CompanyName, " - ", this.ContactName);
        }
        catch (System.NullReferenceException)
        {
            return default (string);
        }
        catch (System.InvalidOperationException)
        {
            return default (string);
        }
    }
   
set
    {
        throw new System.InvalidOperationException();
    }

}

Je vous expliquerai dans un prochain post l’intérêt du try, du if et du set.

Posté le mercredi 18 décembre 2013 11:09 par Matthieu MEZIL | 0 commentaire(s)

Comment WAQS vous propose d’exprimer vos règles métier ? 1/13

Posts précédents sur WAQS :

WAQS : Introduction

Comment utiliser WAQS?

 

Où écrire le code métier ?

Le problème de la maintenance

Le code métier est souvent disséminé un peu partout dans l’application : dans les entités côté serveur, dans les entités côté client et dans le service.

On parle parfois de code spaghetti pour décrire cela et ça complique la maintenabilité.

On a souvent une duplication des règles métiers entre le serveur et le client ce qui n’est pas l’idéal et ce qui présente également un risque potentiel lors d’une mise à jour de règle.

De plus, le code métier est souvent mélangé à du code technique.

C’est aussi un problème pour la maintenance car, le code métier étant dilué dans du code technique, on peut facilement perdre l’intention du développeur.

L’approche du code métier avec WAQS

WAQS aborde ce problème très différemment de ce qui se fait d’habitude : avec WAQS, le code métier est écrit à part, sans code technique.

Ce code ne sera jamais exécuté. Il va uniquement servir de modèle aux différents T4 qui, à l’aide de Roslyn, vont le réinjecter dans les différentes couches de l’application en le mélangeant avec le code technique.

Cela a donc un intérêt énorme sur la maintenance mais aussi sur l’écriture de ce code.

Par ailleurs, contrairement à un moteur de règles propriétaires, WAQS n’inclut pas de boîte noire. Le code étant généré, il vous sera très facile de le débugger afin de vous apercevoir des erreurs potentielles dans l’écriture de vos règles métiers.

Enfin, contrairement à une description XML au formalisme limité, nous bénéficions ici de toute la puissance de C# pour écrire ce code.

WAQS identifie trois types de règles métiers :

·         Les propriétés calculées
·         Les méthodes de validation
·         Les méthodes de service

     

    Par défaut, vous devez définir votre code métier dans le répertoire Specifications créé par l’exécution de la commande WAQS du package NuGet.

    Posté le mercredi 18 décembre 2013 10:11 par Matthieu MEZIL | 0 commentaire(s)

    Comment utiliser WAQS?

    Posts précédents sur WAQS :

    WAQS : Introduction

     

    WAQS peut être utiliser dans plusieurs cas. Certains de mes clients l’utilisent déjà dans les cas suivants:

    • Application 3-Tiers
    • Application Web application (ASP.NET MVC)
    • Web service

    Dans ce post, je vais décrire le scénario 3-Tiers.

     

    3-Tiers application:

    Les applications 3-Tiers est le scénario par défaut pour lequel WAQS a été développé.

    Architecture

    La problématique du découpage en couches et de la dépendance

    Un premier parti pris de WAQS, c’est d’adopter une architecture en couche (« layered architecture »), un découpage sain, grand classique en entreprise, dans lequel chaque couche a une responsabilité propre et bien définie. Les abstractions entre chaque couche assurent un couplage lâche, pour la flexibilité dans les implémentations et pour une meilleure testabilité.

    Il faut insister sur le fait qu’en adoptant cette démarche, WAQS cherche également à réduire la dépendance aux technologies utilisées comme Entity Framwork ou WCF par exemple.

    L’architecture de WAQS

    Ainsi, l’architecture d’une application « type » réalisée avec la version actuelle de WAQS sera la suivante (avec toutes les briques en bleu foncé générées par WAQS) :

    image

    Comment générer le code avec WAQS ?

    Comme vous pouvez le voir sur le schéma précédent, pour utiliser la version courante de WAQS vous avez besoin d’un edmx. En général c’est la première chose que nous allons faire.

    Il est important de noter qu’avec la version courante de WAQS, EF6 n’est pas encore supportée. Il faudra donc utiliser un edmx EF5 ou EF4. Par ailleurs, pour utiliser WAQS, il est obligatoire de garder les deux côtés des relations et d’intégrer la FK au modèle (sauf dans le cas des relations many to many bien entendu).

    De plus, malheureusement, le package NuGet actuellement public de Roslyn ne supporte pas les solutions VS 2013. Il faudra donc utiliser VS 2012.

     

    Afin de pouvoir générer le code, il faut installer l’extension Visual Studio de WAQS pour la première utilisation.

    Pour cela, il suffit d’aller dans le menu Tools/Extensions and Updates. Ensuite il faut rechercher WCFAsyncQueryableServices et l’installer.

     

    Une fois cela fait (uniquement la première fois), vous allez pouvoir utiliser les packages NuGet de WAQS.

    Pour la partie serveur, vous devez installer le package NuGet WCFAsyncQueryableServices.Server.

    Pour cela, vous pouvez taper la commande suivante dans la console NuGet (Package Manager Console):

    Install-Package WCFAsyncQueryableServices.Server

    A noter que pour la première installation du package par version de WAQS il faut avoir lancé VS avec les droits administrateur. 

    L’installation du package ne va pas changer votre projet. Elle va seulement ajouter une nouvelle commande PowerShell dans la console NuGet.

    Ensuite, vous pouvez générer le code de WAQS côté serveur en utilisant la commande WCFAsyncQueryableServicesServer.

    Par exemple:

    WCFAsyncQueryableServicesServer '"C:\VS Projects\WAQSDemo\WAQSDemo.Web\Northwind.edmx"' All

    A noter que vous bénéficiez de l’intellisense. Vous pouvez donc vous contentez de taper WCFA<tab> <tab> All.

    Il y a plusieurs autres options que “All”. Nous y reviendrons une autre fois.

     

    Maintenant que nous avons le code serveur, nous allons nous occuper du code client.

     

    Pour le client, il existe deux versions : une version spéciale WPF et une version PCL (avec quelques restrictions mineures) qui est utilisable avec plusieurs types de projets tels que W8, WP8, WPF, SL, mais aussi iOS et Android via Xamarin (certains de mes clients font cela). A noter que WCF n’étant pas complètement supporté par Xamarin il y a des restrictions sur WAQS avec iOS et Android. Néammoins c’est possible.

    WPF

    Pour le tiers client, nous allons procéder de la même façon que sur le serveur.

    Pour commencer nous allons installer le package NuGet :

    Install-Package WCFAsyncQueryableServices.Client.WPF

    Ensuite, il faut faire attention à sélectionner le bon projet dans la console :

    image

     

    Puis, vous pouvez lancer la génération côté client en utilisant la nouvelle commande PowerShell (toujours avec l’intellisense) :

    WCFAsyncQueryableServicesClientWPF '"C:\VS Projects\WAQSDemo\WAQSDemo.Web\Northwind.edmx"' '"C:\VS Projects\WAQSDemo\WAQSDemo.Web\Northwind.svc"' All

    Une fois la génération effectuée, vous pouvez créer une classe pour votre ViewModel.

    Par défaut, elle devrait ressembler à ça:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace WAQSDemo.Client.WPF
    {
        class MainWindowViewModel
        {
        }
    }

    Le package NuGet a également ajoutée deux autres commandes : WCFAsyncQueryableServicesGlobalClientWPF que nous verrons plus tard et WCFAsyncQueryableServicesApplyViewModelWPF.

    Nous allons utiliser cette dernière (toujours avec l’intellisense):

    WCFAsyncQueryableServicesApplyViewModelWPF "Northwind" '"C:\VS Projects\WAQSDemo\WAQSDemo.Client.WPF\MainWindow.xaml"'

    Après l’avoir exécutée, le code du ViewModel a été mis à jour :

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using WpfApplication1;
    using WpfApplication1.ClientContext;
    using WpfApplication1.ClientContext.Interfaces;
    using WpfApplication1.ClientContext.Interfaces.Errors;
    using WCFAsyncQueryableServices.ClientContext;
    using WCFAsyncQueryableServices.ClientContext.Interfaces;
    using WCFAsyncQueryableServices.ClientContext.Interfaces.Errors;
    using WCFAsyncQueryableServices.ClientContext.Interfaces.Querying;
    using WCFAsyncQueryableServices.ComponentModel;
     
    namespace WAQSDemo.Client.WPF
    {
         public class MainWindowViewModel : ViewModelBase
         {
             private INorthwindClientContext _context;
             public MainWindowViewModel(INorthwindClientContext context): base (context)
             {
                 _context = context;
             }
         } }

    Et le constructeur de la classe MainWindow (MainWindow.xaml.cs) intègre maintenant le ViewModel :

    public MainWindow(WAQSDemo.Client.WPF.MainWindowViewModel vm)
    {
         InitializeComponent();
         DataContext = vm; }

    Dès lors, depuis le ViewModel, vous pouvez utiliser le champ de type INorthwindClientContext afin de requêter vos données ou persister vos changements et plusieurs autres choses que nous verrons dans des posts futurs.

     

    A noter que toutes les requêtes supportées par LINQ To Entities sont utilisables en LINQ To WAQS (vous pouvez même faire plus avec LINQ To WAQS).

    LINQ To WAQS est asynchrone. Pour exécuter une requête LINQ To WAQS, vous devez utiliser deux méthodes : AsAsyncQueryable et Execute.

    var customers = await (from c in _context.Customers.AsAsyncQueryable()
                           select c).Execute();

    PCL

    Avec la version courante de WAQS PCL, vous devez obligatoirement sélectionner SL5 et .NET 4.5 (et potentiellement d’autres comme WP8 ou WP7.5 et W8).

    Ensuite, c’est encore le même process :

    Install-Package WCFAsyncQueryableServices.Client.PCL

    WCFAsyncQueryableServicesClientPCL '"C:\VS Projects\WAQSDemo\WAQSDemo.Web\Northwind.edmx"'
    '"C:\VS Projects\WAQSDemo\WAQSDemo.Web\Northwind.svc"' All

    La seule différence par rapport à l’utilisation de WAQS avec WPF est sur la commande ApplyViewModel car, avec les PCL, elle ne peut pas être associée à une vue :

    WCFAsyncQueryableServicesApplyViewModelPCL "Northwind"

    Ensuite, il faut gérer soit-même la configuration WCF (qui avait été généré par WAQS dans la version WPF). Si vous avez un App.config sur votre client, vous pouvez l’utiliser pour définir la configuration WCF sinon vous pouvez également le faire par code. Voici un exemple utilisable avec W8 :

    Ajouter une méthode SetContext sur la MainPage:

    public void SetDataContext(PCLViewModel viewModel)
    {
         DataContext = viewModel; }

    Puis dans l’App.xaml.cs, vous pouvez utiliser le code suivant :

    protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
         Frame rootFrame = Window.Current.Content as Frame;
         if (rootFrame == null)
         {
             rootFrame = new Frame();
             Window.Current.Content = rootFrame;
         }
         if (rootFrame.Content == null)
         {
             if (!rootFrame.Navigate(typeof(MainPage), args.Arguments))
             {
                 throw new Exception("Failed to create initial page");
             }
             ((MainPage)rootFrame.Content).SetDataContext(GetMainPageDataContext());     }
        Window.Current.Activate(); } private PCLViewModel GetMainPageDataContext() {
         IUnityContainer expressionUnityContainer = new UnityContainer();
         expressionUnityContainer.RegisterType<IExpressionTransformer, ExpressionTransformer>();
         ExpressionTransformerFactory.Factory = () => expressionUnityContainer.Resolve<IExpressionTransformer>();
         IUnityContainer unityContainer = new UnityContainer();
         InitWCFAsyncQueryableServicesModules(unityContainer);
         return unityContainer.Resolve<PCLViewModel>(); } private void InitWCFAsyncQueryableServicesModules(IUnityContainer unityContainer) {
         var binding = new CustomBinding();
         var binaryMessageEncodingBindingElement = new BinaryMessageEncodingBindingElement { MaxSessionSize = int.MaxValue };
         binaryMessageEncodingBindingElement.ReaderQuotas.MaxArrayLength = int.MaxValue;
         binaryMessageEncodingBindingElement.ReaderQuotas.MaxBytesPerRead = int.MaxValue;
         binaryMessageEncodingBindingElement.ReaderQuotas.MaxDepth = int.MaxValue;
         binaryMessageEncodingBindingElement.ReaderQuotas.MaxNameTableCharCount = int.MaxValue;
         binaryMessageEncodingBindingElement.ReaderQuotas.MaxStringContentLength = int.MaxValue;
         binding.Elements.Add(binaryMessageEncodingBindingElement);
         var httpBindingElement = new HttpTransportBindingElement { MaxBufferSize = int.MaxValue,
    MaxReceivedMessageSize = int.MaxValue };
         binding.Elements.Add(httpBindingElement);
         var endPointAddress = new EndpointAddress(http://localhost:13117/Northwind.svc);
         unityContainer.RegisterType<INorthwindService, NorthwindServiceClient>(
    new InjectionConstructor(binding, endPointAddress));
         unityContainer.RegisterType<INorthwindClientContext, NorthwindClientContext>(); }

     

    Si vous avez des commentaires / problèmes, vous pouvez utiliser le site de WAQS sur CodePlex : https://waqs.codeplex.com/

    Posté le mardi 17 décembre 2013 10:50 par Matthieu MEZIL | 0 commentaire(s)

    WAQS : Introduction

    // An English version of this post is available here.

    Je suis très heureux de vous annoncer la publication publique d’une beta de WAQS.

     

    J’ai décidé pour l’occasion d’écrire plusieurs posts pour expliquer comment utiliser WAQS et d’autres pour expliquer comment WAQS fonctionne.

    Par conséquent, dans les prochains mois, je vais vous parler d’un bon nombre de technologies comme T4, Roslyn, NuGet, Entity Framework, LINQ, PowerShell, VS, MS IL, WCF, Unity, Rx, etc.

     

    Mais avant tout je pense qu’il était important de commencer par une introduction afin de décrire ce qu’est WAQS.

     

     

    WAQS est un Framework ayant pour but de booster vos développements d’applications .Net « distribuées » et « data centric ».

    Pour être plus précis, WAQS est un générateur de Framework. En ce sens, il est plus juste de le qualifier de « meta Framework ». En effet, WAQS fait un usage intensif de la métaprogrammation, en tirant un maximum de la génération de code avec templates T4, de Roslyn et NuGet, afin de tailler sur mesure le Framework adapté à votre domaine.

    Ce que WAQS vous propose, c’est une meilleure productivité et de meilleures performances, en prenant en charge avec des patterns éprouvés les aspects les plus techniques, ainsi que de la flexibilité et un code plus robuste vis-à-vis de l’implémentation de vos règles métier. Sur ce dernier point, l’approche que vous propose WAQS est une alternative efficace, ou pourquoi pas un complément, à la conception DDD.

    A vous de choisir : WAQS peut être utilisé pour ses briques spécifiques ou de bout en bout. WAQS est un « Opinionated Framework » : il a des partis-pris mais vous laisse de la flexibilité dans vos choix d’architecture.

     

    L’idée initiale de WAQS (WCF Async Queryable Services) consistait à reproduire les fonctionnalités de l’ObjectContext Entity Framework sur le client, c’est-à-dire d’offrir la flexibilité et la puissance d’un requêtage de vos données à distance ainsi que le tracking transparent des modifications dans le but de pouvoir les persister par la suite en une seule transaction, sans avoir à s’en soucier.

    Microsoft a tenté de répondre à ce besoin à travers WCF Data Services et RIA Services mais ces Frameworks présentent des restrictions souvent rédhibitoires.

    Par rapport à ces initiatives et également par rapport à Entity Framework, WAQS a plutôt tendance à étendre les possibilités plutôt que de les restreindre, ce qui lui a valu ce tweet de Jacob Reimers : « WAQS is remote EF on steroids ». 

     

    Aujourd’hui, WAQS couvre un périmètre plus large encore. L’idée est désormais de générer de bout en bout le code technique d’une application afin de se concentrer sur ce qui a de la valeur pour l’utilisateur : les écrans et les règles métiers.

     

    Pour résumer, WAQS apporte :
    ·         Le requêtage à partir du client de façon asynchrone,
    ·         Le tracking transparent des modifications et la persistance de celles-ci,
    ·         Une solution pour éviter la problématique du code spaghetti, particulièrement sur les règles métiers,
    ·         La simplification du pattern MVVM.

    D’un point de vue décideur, WAQS permet :
    ·         D’industrialiser et d’homogénéiser le processus de développement tout en gardant une approche flexible et customisable à souhait,
    ·         D’augmenter considérablement la productivité des équipes,
    ·         D’augmenter la maintenabilité de la solution,
    ·         De réduire les risques et les coûts.

    Et permet cela
    ·         Sans fondamentalement changer les habitudes des développeurs, WAQS ayant avant tout été pensé pour eux,
    ·         Sans utiliser de moteur ou outil propriétaires. WAQS ne fait que générer du code dans la solution en n'utilisant que des outils Microsoft pour assurer un maximum de pérennité.

    Posté le lundi 16 décembre 2013 21:05 par Matthieu MEZIL | 0 commentaire(s)

    Comment tester un rewriter Roslyn ?

    Hier, j’ai codé un SyntaxRewriter qui rajoute les tests de nullité dans le corps de méthodes.

    Ceci peut s’avérer très facile comme ce code:

    public int Foo1(OrderDetail od)
    {
        return od.Product.Category.CategoryID;
    }

    Qui devient:

    public int Foo1(OrderDetail od)
    {
        if (od == null || od.Product == null || od.Product.Category == null)
            return default(int);
        return od.Product.Category.CategoryID;
    }

    Ou un peu plus complexe comme ce code:

    public int Foo2(OrderDetail od)
    {
        if (od.Product.CategoryID == 0 && od.Order.OrderID == 0 && od.Order.Customer.CompanyName.Length == 0)
            return 1;
        return 2;
    }

    Qui devient :

    public int Foo2(OrderDetail od)
    {
        if (od == null || od.Product == null)
            return default(int);
        bool waqsLogicald7a8b2e418d443b48a5202206ad37500 = od.Product.CategoryID == 0;
        if (waqsLogicald7a8b2e418d443b48a5202206ad37500)
        {
            if (od.Order == null)
                return default(int);
            waqsLogicald7a8b2e418d443b48a5202206ad37500 = od.Order.OrderID == 0;
            if (waqsLogicald7a8b2e418d443b48a5202206ad37500)
            {
                if (od.Order.Customer == null || od.Order.Customer.CompanyName == null)
                    return default(int);
                waqsLogicald7a8b2e418d443b48a5202206ad37500 = od.Order.Customer.CompanyName.Length == 0;
            }
        }

        if (waqsLogicald7a8b2e418d443b48a5202206ad37500)
            return 1;
        return 2;
    }

    Ou beaucoup plus complexe…


    J’ao donc décidé d’écrire de tests unitaires pour tester mon Rewriter.

    Mais là, c’est le drâme, comment tester du meta-code?

    Il n’est pas possible de, basiquement, comparer deux string vu que le Rewriter peut rajouter des variables dont le nom comporte un Guid.

    Etant un grand fan de Regex, j’ai tout d’abord opté pour cette solution.

    Ainsi, pour Foo2, j’ai écrit la méthode suivante :

    [TestMethod]
    public void TestMethod2()
    {
        var compilationUnitSyntax = GetCompilationUnitSyntax();
        Assert.IsTrue(Regex.IsMatch(compilationUnitSyntax.ChildNodes().OfType<NamespaceDeclarationSyntax>().First()
            .ChildNodes().OfType<
    ClassDeclarationSyntax>().First().ChildNodes().OfType<MethodDeclarationSyntax>()
            .First(m => m.Identifier.ValueText ==
    "Foo2").NormalizeWhitespace().ToString(),
    @"^\s*public\s+int\s+Foo2\s*\(\s*OrderDetail\s+od\s*\)\s*
    \s*\{\s*
    \s*if\s*\(od\s*==\s*null\s*\|\|\s*od.Product\s*==\s*null\)\s*
    \s*return\s+default\s*\(\s*int\s*\)\s*;\s*
    \s*bool\s+(waqsLogical[\w\d]{32})\s*=\s*od.Product.CategoryID\s*==\s*0\s*;\s*
    \s*if\s*\(\s*\1\s*\)\s*
    \s*\{\s*
    \s*if\s*\(\s*od.Order\s*==\s*null\s*\)\s*
    \s*return\s+default\s*\(\s*int\s*\)\s*;\s*
    \s*\1\s*=\s*od.Order.OrderID\s*==\s*0\s*;\s*
    \s*if\s*\(\s*\1\s*\)\s*
    \s*\{\s*
    \s*if\s*\(\s*od.Order.Customer\s*==\s*null\s*\|\|\s*od.Order.Customer.CompanyName\s*==\s*null\s*\)\s*
    \s*return\s+default\s*\(\s*int\s*\)\s*;\s*
    \s*\1\s*=\s*od.Order.Customer.CompanyName.Length\s*==\s*0\s*;\s*
    \s*\}\s*
    \s*\}\s*
    \s*if\s*\(\s*\1\s*\)\s*
    \s*return\s+1\s*;\s*
    \s*return\s+2\s*;\s*
    \s*\}\s*$", RegexOptions.Multiline));
    }

    Mais bon, c’était un peu pénible à écrire et difficile à relire également.

    Il me fallait donc trouver une autre technique.

    J’ai donc décidé d’analiser mon code en utilisant Roslyn pour comparer le SyntaxTree fournit par le rewriter avec celui attendu.

    Pour me simplifier la vie sur la définition de celui attendu, je décidais d’utiliser les méthodes de Parse de Roslyn.

    Mais maintenant, le test de comparaison de deux SuntaxTree n’est pas du tout trivial... En effet, il y a énormément de types différents qui héritent de SyntaxNode et donc ça risquait de me prendre très longtemps pour tout coder.

    Le truc cool en revanche, c’est que le code est très répétitif :

    public class SyntaxNodeEqualityVisitor
    {
        private readonly Func<SyntaxNode, SyntaxNode, bool?> _syntaxNodeComparer;
        private readonly Func<string, string, bool> _identifierComparer;

        public SyntaxNodeEqualityVisitor(Func<SyntaxNode, SyntaxNode, bool?> syntaxNodeComparer = null,
         Func<string, string, bool> identifierComparer = null)
        {
         _syntaxNodeComparer = syntaxNodeComparer;
            _identifierComparer = identifierComparer;
        }




       
    public virtual bool AreEquals(SyntaxNode node1, SyntaxNode node2)
        {
         if (node1 == null)
             return node2 == null;
            if (node2 == null)
                return false;
            if (_syntaxNodeComparer != null)
            {
                bool? value = _syntaxNodeComparer(node1, node2);
                if (value.HasValue)
                    return value.Value;
            }

            if (node1.GetType() != node2.GetType())
                 return false;

            var accessorDeclaration1 = node1 as AccessorDeclarationSyntax;
            var accessorDeclaration2 = node2 as AccessorDeclarationSyntax
    ;
            if (accessorDeclaration1 != null && accessorDeclaration2 != null
    )
                return
    AreEqualsAccessorDeclaration(accessorDeclaration1, accessorDeclaration2);   
                                               
            var parameterList1 = node1 as ParameterListSyntax
    ;
            var parameterList2 = node2 as ParameterListSyntax
    ;
            if (parameterList1 != null && parameterList2 != null
    )
                return
    AreEqualsParameterList(parameterList1, parameterList2);    
                                               
            var bracketedParameterList1 = node1 as BracketedParameterListSyntax;
            var bracketedParameterList2 = node2 as BracketedParameterListSyntax
    ;
            if (bracketedParameterList1 != null && bracketedParameterList2 != null
    )
                return
    AreEqualsBracketedParameterList(bracketedParameterList1, bracketedParameterList2);



           
    //...
        }


    public virtual bool AreEqualsAccessorDeclaration(AccessorDeclarationSyntax node1, AccessorDeclarationSyntax node2)
        {
            int
    attributeListsCount = node1.AttributeLists.Count;
            if
    (node2.AttributeLists.Count != attributeListsCount)
             return false
    ;
            for (int
    i = 0 ; i < attributeListsCount ; i ++)
             if
    (! AreEquals(node1.AttributeListsIdea, node2.AttributeListsIdea))
    return false
    ;
            int
    modifiersCount = node1.Modifiers.Count;
            if
    (node2.Modifiers.Count != modifiersCount)
                return false
    ;
            for (int
    i = 0 ; i < modifiersCount ; i ++)
                if
    (node1.ModifiersIdea.Kind != node2.ModifiersIdea.Kind)
                    return false
    ;
            if
    (node1.Keyword.Kind != node2.Keyword.Kind)
             return false
    ;
            if
    (! AreEquals(node1.Body, node2.Body))
             return false
    ;
            if
    (node1.SemicolonToken.Kind != node2.SemicolonToken.Kind)
                return false
    ;
            return true
    ;
    }

    public virtual bool AreEqualsParameterList(ParameterListSyntax node1, ParameterListSyntax
    node2)
    {
            if
    (node1.OpenParenToken.Kind != node2.OpenParenToken.Kind)
                return false
    ;
            int
    parametersCount = node1.Parameters.Count;
            if
    (node2.Parameters.Count != parametersCount)
                return false
    ;
            for (int
    i = 0 ; i < parametersCount ; i ++)
                if
    (! AreEquals(node1.ParametersIdea, node2.ParametersIdea))
                    return false
    ;
            if
    (node1.CloseParenToken.Kind != node2.CloseParenToken.Kind)
             return false
    ;
            return true
    ;
    }


       
    //...
    }

    Profitant de cette répétition, j’ai écrit un template T4 qui génère ce code en analysant, par Reflection, la classe SyntaxVisitor.

    Ensuite, j’ai ajouté une classe RoslynSyntaxTreeComparer :

    public class RoslynSyntaxTreeComparer
    {
        public static bool Equals(SyntaxNode node1, SyntaxNode node2,
         Func<SyntaxNode, SyntaxNode, bool?> syntaxNodeComparer = null,
    Func<string, string, bool> identifierComparer = null)
        {
            return new SyntaxNodeEqualityVisitor(syntaxNodeComparer, identifierComparer).AreEquals(node1, node2);
        }
    }

    Puis j’ai buildé le tout et je peux maintenant l’utiliser de façon très simple :

    [TestMethod]
    public void TestMethod2()
    {
        var compilationUnitSyntax = GetCompilationUnitSyntax();
        Assert.IsTrue(AreEquals(
    @"public int Foo2(OrderDetail od)
    {
    if (od == null || od.Product == null)
    return default(int);
    bool waqsLogical1 = od.Product.CategoryID == 0;
    if (waqsLogical1)
    {
    if (od.Order == null)
        return default(int);
    waqsLogical1 = od.Order.OrderID == 0;
    if (waqsLogical1)
    {
        if (od.Order.Customer == null || od.Order.Customer.CompanyName == null)
            return default(int);
        waqsLogical1 = od.Order.Customer.CompanyName.Length == 0;
    }
    }
    if (waqsLogical1)
    return 1;
    return 2;
    }", compilationUnitSyntax.ChildNodes().OfType<NamespaceDeclarationSyntax>().First().ChildNodes()
    .OfType<
    ClassDeclarationSyntax>().First().ChildNodes().OfType<MethodDeclarationSyntax>()
    .First(m => m.Identifier.ValueText ==
    "Foo2")));
    }
     
    private bool AreEquals(string expectedCode, SyntaxNode currentCode)
    {
        var identifierNames = new Dictionary<string, string>();
        var identifierNames2 = new Dictionary<string, string>();

        return RoslynSyntaxTreeComparer.Equals(Syntax.ParseCompilationUnit(expectedCode).Members[0], currentCode,
    identifierComparer: (n1, n2) =>
            {
                if (n1 == n2)
                    return true;
                if (n1.StartsWith("waqs"))
                {
                    string identifierName;
                    if (identifierNames.TryGetValue(n1, out identifierName))
                        return n2 == identifierName;
                    identifierNames.Add(n1, n2);
                    identifierNames2.Add(n2, n1);
    return true;
                }
                return n1 == n2;
            });
    }

    Pour finir, afin que vous puissiez en profiter, j’ai publié un package NuGet qui contient la dll : https://www.nuget.org/packages/Roslyn.UnitTests.Helpers

    Hope that helps

    Posté le mardi 10 septembre 2013 00:26 par Matthieu MEZIL | 0 commentaire(s)

    Regex et nombres premiers

    J’aime bien faire des Regex. Je prends souvent ça comme un jeu comme d’autres font des mots croisés et il arrive régulièrement qu’on me demande d’écrire un pattern pour valider un format ou modifier une chaine de caractères par exemple.

    Cependant, j’ai voulu écrire ce post pour vous parler d’une utilisation hors norme des Regex et totalement hallucinante de mon point de vue que mon ami Gilles a posté sur son wall FB : les Regex permettent de déterminer si un nombre est premier !

    J’ai été plus que septique et j’ai voulu testé avant même la lecture et ça marche bien.

    Vous pouvez également tester avec le code suivant :

    for (; ; )
    {
        var value = int.Parse(Console.ReadLine()); 
        var isPrime = ! Regex.IsMatch(new string('1', value), @"^1?$|^(11+?)\1+$");
        Console.WriteLine(isPrime);
    }

    Voici l’article pointé par Gilles expliquant le fonctionnement de la Regex : http://www.noulakaz.net/weblog/2007/03/18/a-regular-expression-to-check-for-prime-numbers/

    Posté le mercredi 24 avril 2013 21:09 par Matthieu MEZIL | 3 commentaire(s)

    WCF Data Services vs WAQS – Comparatif de performance

    La raison initiale pour laquelle j’ai fait WAQS était la limitation de WCF Data Services et de WCF RIA Services en terme de requêtage.

    Depuis WAQS a beaucoup évolué et son périmètre est devenu beaucoup plus important.

    A ce propos, je vous invite à me contacter ou à passer me voir sur le stand Infinite Square aux TechDays si vous voulez en savoir plus.

    Ayant récemment procédé à quelques optimisations sur WAQS, j’ai voulu faire le test vs WCF Data Services pour voir si le gain de fonctionalités ne se faisait pas au détriment de la performance.

    J’ai donc effectué des requêtes simples sur la base Contoso qui a l’avantage d’être plus fournie que Northwind.

    A noter, que, dans le cas de WAQS comme dans le cas de WCF Data Services, je suis resté sur le mode par défaut.

     

    Voici les résultats obtenus avec WCF Data Services

    • Récupération des 18 869 clients de la base : 6 693 ms
      var query = _context.DimCustomers;
      var customers = (await Task.Factory.FromAsync(query.BeginExecute(null, null),
          ar => query.EndExecute(ar))).ToList();
    • Récupération des CompanyName des clients : 2 652 ms
      var query = (DataServiceQuery<DimCustomer>)_context.DimCustomers
          .Select(c => new DimCustomer { CustomerKey = c.CustomerKey, CompanyName = c.CompanyName });
      var customers = (await Task.Factory.FromAsync(query.BeginExecute(null, null),
          ar => query.EndExecute(ar)))
          .Select(c => c.CompanyName).ToList();
    • Récupération de 100 clients avec leurs ventes en ligne : 9 672 ms
      var query = (DataServiceQuery<DimCustomer>)_context.DimCustomers
          .Expand("FactOnlineSales")
          .Take(100);
      var customers = (await Task.Factory.FromAsync(query.BeginExecute(null, null),
          ar => query.EndExecute(ar))).ToList();
    • Récupération de 100 clients avec leurs ventes en ligne et les produits de celles-ci : 27 021 ms
      var query = (DataServiceQuery<DimCustomer>)_context.DimCustomers
          .Expand("FactOnlineSales/DimProduct")
          .Take(100);
      var customers = (await Task.Factory.FromAsync(query.BeginExecute(null, null),
          ar => query.EndExecute(ar))).ToList();
    • Récupération de 100 clients avec leurs ventes en ligne, les produits de celles-ci et les catégories de ceux-ci : 42 604 ms
      var query = (DataServiceQuery<DimCustomer>)_context.DimCustomers
          .Expand("FactOnlineSales/DimProduct/DimProductSubcategory/DimProductCategory")
          .Take(100);
      var customers = (await Task.Factory.FromAsync(query.BeginExecute(null, null),
          ar => query.EndExecute(ar))).ToList();
    • Récupération de 200 clients avec leurs ventes en ligne, les produits de celles-ci et les catégories de ceux-ci : Exception !
      var query = (DataServiceQuery<DimCustomer>)_context.DimCustomers
          .Expand("FactOnlineSales/DimProduct/DimProductSubcategory/DimProductCategory")
          .Take(200);
      var customers = (await Task.Factory.FromAsync(query.BeginExecute(null, null),
          ar => query.EndExecute(ar))).ToList();

    Maintenant faisons la même chose avec WAQS :

    • Récupération des 18 869 clients de la base : 3 307 ms (au lieu de 6 693 ms)
      var customers = (await _context.DimCustomers.AsAsyncQueryable().Execute()).ToList();
    • Récupération des CompanyName des clients : 187 ms (au lieu de 2 652 ms)
      var customers = (await _context.DimCustomers.AsAsyncQueryable()
          .Select(c => c.CompanyName)
          .Execute()).ToList();
    • Récupération de 100 clients avec leurs ventes en ligne : 3 260 ms (au lieu de 9 672 ms)
      var customers = (await _context.DimCustomers.AsAsyncQueryable()
          .OrderBy(c => c.CustomerKey)
          .Take(100)
          .IncludeFactOnlineSales()
          .Execute()).ToList();
    • Récupération de 100 clients avec leurs ventes en ligne et les produits de celles-ci : 4 336 ms (au lieu de 27 021 ms)
      var customers = (await _context.DimCustomers.AsAsyncQueryable()
          .OrderBy(c => c.CustomerKey)
          .Take(100)
          .IncludeFactOnlineSalesWithExpression(
              onlineSales => onlineSales.IncludeDimProduct())
          .Execute()).ToList();
    • Récupération de 100 clients avec leurs ventes en ligne, les produits de celles-ci et les catégories de ceux-ci : 4 883 ms (au lieu de 42 604 ms)
      var customers = (await _context.DimCustomers.AsAsyncQueryable()
          .OrderBy(c => c.CustomerKey)
          .Take(100)
          .IncludeFactOnlineSalesWithExpression(
              onlineSales => onlineSales.IncludeDimProductWithExpression(
                  products => products.IncludeDimProductSubcategoryWithExpression(
                      productSubcategories => productSubcategories.IncludeDimProductCategory())))
          .Execute()).ToList();
    • Récupération de 200 clients avec leurs ventes en ligne, les produits de celles-ci et les catégories de ceux-ci : 7 029 ms (au lieu d’une exception)
      var customers = (await _context.DimCustomers.AsAsyncQueryable()
          .OrderBy(c => c.CustomerKey)
          .Take(200)
          .IncludeFactOnlineSalesWithExpression(
              onlineSales => onlineSales.IncludeDimProductWithExpression(
                  products => products.IncludeDimProductSubcategoryWithExpression(
                      productSubcategories => productSubcategories.IncludeDimProductCategory())))
          .Execute()).ToList();

    Si on ne prends pas en compte le côté infini du ratio de la dernière requête, WAQS est donc entre 2,02 et 14,18 fois plus rapide que WCF Data Services avec une moyenne sur mes tests de 6,83.

    Ca va, je suis rassuré… :)

    // A noter que la version de WAQS que j’ai utilisé n’est pas encore publique

    Posté le lundi 11 février 2013 01:30 par Matthieu MEZIL | 6 commentaire(s)

    WCF Async Queryable Services features

    J’avais déjà publié deux vidéos sur WCF Async Queryable Services : une sur l’architecture et une sur le tooling.

    Je viens d’en publier une nouvelle sur les fonctionnalités.

    WCF Async Queryable Services Features

    Maintenant vous pouvez jouer avec WAQS !

    N’hésitez pas à me donner vos feedbacks…

    Posté le samedi 30 juin 2012 02:45 par Matthieu MEZIL | 7 commentaire(s)

    WAQSProp snippet

    Dans ma video sur le tooling de WCF Async Queryable Services, je parle d’un snippet que vous pouvez télécharger ici.

    Posté le vendredi 22 juin 2012 01:49 par Matthieu MEZIL | 0 commentaire(s)

    Utiliser le tooling de WAQS

    Utiliser le tooling de WAQS n’est pas quelque chose de très intuitif. Aussi, j’ai publié une vidéo pour vous expliquer comment l’utiliser.

    WCF Async Queryable Services - Tooling

    Maintenant vous pouvez jouer avec WAQS !

    Posté le vendredi 22 juin 2012 01:46 par Matthieu MEZIL | 0 commentaire(s)

    Roslyn fluent APIs: RoslynHelper NuGet package

    Si vous utilisez Roslyn et que vous vous voulez vous simplifier le code du code rewriter, je vous conseille d’installer mon NuGet package RoslynHelper.

    Posté le jeudi 31 mai 2012 01:24 par Matthieu MEZIL | 0 commentaire(s)

    Classé sous : , ,

    Utiliser Roslyn pour améliorer le compilateur C# ou VB

    J’ai publié un POC qui permet d’enrichir le langage C# en utilisant Roslyn.

    Enjoy! :)

    Posté le jeudi 12 avril 2012 11:21 par Matthieu MEZIL | 5 commentaire(s)

    Classé sous : , ,

    Après-midi du dev Roslyn

    J’aurais le plaisir d’animer un après-midi du dev sur Roslyn le jeudi 12 avril avec Mitsu, Jean-Baptiste, Florent, Simon et David.

    Si vous voulez voir du code qui pique, inscrivez-vous !

    Posté le mercredi 14 mars 2012 21:34 par Matthieu MEZIL | 0 commentaire(s)

    Classé sous : ,

    Ma session TechDays sur Roslyn

    Ma session des TechDays 2012 sur Roslyn que j’ai co-animé avec Léonard Labat est disponible ici.

    Posté le mercredi 14 mars 2012 21:25 par Matthieu MEZIL | 0 commentaire(s)

    Classé sous : ,

    Podcast sur T4 pour Visual Studio Talk Show

    J’ai récemment enregistré un podcast sur T4 avec Guy Barrette et Mario Cardinal que vous pouvez retrouver ici.

    Posté le samedi 10 mars 2012 00:32 par Matthieu MEZIL | 0 commentaire(s)

    Classé sous :

    WCF Async Queryable Services : l’architecture

    Si vous me suivez sur twitter, vous devez probablement savoir que je travaille sur un projet que j’ai appelé WCF Async Queryable Services.

    Je viens de faire une première vidéo sur l’architecture de WCF Async Queryable Services.

    WCF Async Queryable Services - Architecture

    Merci d’avance pour vos feedbacks

    Posté le vendredi 9 mars 2012 23:05 par Matthieu MEZIL | 0 commentaire(s)

    Article sur Roslyn

    Si vous êtes intéressé par Roslyn, je vous invite à lire l’article que j’ai écrit sur le sujet.

    Posté le vendredi 25 novembre 2011 08:26 par Matthieu MEZIL | 2 commentaire(s)

    Classé sous : , , ,

    Plus de Messages Page suivante »


    Les 10 derniers blogs postés

    - [ #Office365 ] Pb de connexion du flux Yammer ajouté à un site SharePoint par Le blog de Patrick [MVP SharePoint] le il y a 23 minutes

    - NFluent & Data Annotations : coder ses propres assertions par Fathi Bellahcene le il y a 32 minutes

    - Installer un site ASP.net 32bits sur un serveur exécutant SharePoint 2013 par Blog de Jérémy Jeanson le il y a 10 heures et 52 minutes

    - [ SharePoint Summit 2014 ] Tests de montée en charge SharePoint par Le blog de Patrick [MVP SharePoint] le il y a 20 heures et 42 minutes

    - [ SharePoint Summit 2014 ] Bâtir un site web public avec Office 365 par Le blog de Patrick [MVP SharePoint] le il y a 22 heures et 56 minutes

    - Kinect + Speech Recognition + Eedomus = Dommy par Aurélien GALTIER le 04-16-2014, 17:17

    - [ SharePoint Summit 2014 ] Une méthodologie simple pour concevoir vos applications OOTB SharePoint de A à Z par Le blog de Patrick [MVP SharePoint] le 04-16-2014, 16:51

    - //Lean/ - Apprendre à faire des Apps Windows universelles par Blog de Jérémy Jeanson le 04-16-2014, 12:57

    - Une culture de la donnée pour tous… par Le blog de Patrick [MVP SharePoint] le 04-16-2014, 11:00

    - [ SharePoint Summit 2014 ] L’utilisation de SharePoint 2013 pour la mise en place d’un site Internet Grand Public par Le blog de Patrick [MVP SharePoint] le 04-15-2014, 20:51