[Web API] Définir des autorisations dans le web.config, pas possible ?!

Lorsque que l’on écrit une API avec les Web API dans un site utilisant la forms authentication, on peut réguler l’accès aux différentes méthodes en plaçant un attribut Authorize sur l’action en question, ou le contrôleur ou même à un niveau plus global, selon le niveau de granularité désiré. Le faire aura maintenant pour effet d’automatiquement lever une erreur 401 aux utilisateurs ne présentant pas les en-têtes d’authentification nécessaires pour accéder à la ressource.

image

Si j’ai ajouté la précision “maintenant” dans la précédente phrase, c’est que pendant la beta de Web API, ce n’était pas le cas. A l’époque, quelqu’un qui tentait d’accéder à une API sans les autorisations nécessaires se prenait une 302 et était redirigé vers la page de login définie. C’est un comportement très bien pour un site web mais c’est quand même un peu limite pour une API. Alors très vite des solutions sont apparues, la plus sexy consistant à déclarer un module qui, pour les requêtes vers l’API, remplaçait le code de retour 302 par une 401. Ce module est d’ailleurs encore visible si on jette un coup d’œil avec reflector dans System.Web.Http.WebHost; il est juste désactivé par rapport à l’existence d’une propriété SuppressFormsAuthenticationRedirect sur la classe HttpResponseBase, classe de base de la propriété Response d’un HttpContext. C’est cette propriété qui a résolu le problème de redirection des appels non autorisés.

 

Cependant, si ce soir j’écris ce post de blog, c’est que cette propriété n’a finalement pas tout résolu. En fait, si on défini les autorisations non plus via des attributs mais plutôt “à l’ancienne” dans le fichier de configuration…

<authorization>
  <deny users="?" />
</authorization>

 

image

 

J’ai eu beau tourner et retourner le problème dans tous les sens, je ne vois pas comment remédier à ce problème si ce n’est enlever la règle du fichier de configuration et la remplacer par un global filter. Attention lors de la conception de vos prochaines applications donc …

 

Si quelqu’un a une idée …

 

A bientôt !


Classé sous

Visual Studio 2012 et la commande “Paste JSON As Classes”

Disponible avec les Web Developer Tools 2012.2 (que l’on peut trouver entre autres sur WebPI), le bouton « Paste JSON As Classes » peut s’avérer extrêmement utile pour la productivité.

En fait, Visual Studio possédait déjà un bouton « Paste XML as Classes », qui est arrivé depuis la beta de Visual Studio 11. Si jamais ça ne vous parle pas, dans Visual Studio, cliquez sur « Edit » puis « Paste Special » et c’est ici que se cachent ces options.

Bref, pour en revenir au sujet de ce post, la commande « Paste JSON As Classes » va donc me permettre de générer (assez intelligemment) une classe à partir d’un morceau de JSON. Le scénario d’utilisation est assez simple : lorsque je développe une application qui consomme une couche de service existante exposée en JSON, je me retrouve très souvent à devoir utiliser Fiddler pour analyser les objets que cette couche de services me renvoie puis, je recrée mes objets métiers avec la même arborescence. Grâce à cette feature donc, je pourrais directement copier la réponse de mes appels dans Fiddler et aller la coller dans Visual Studio et me retrouver avec mes classes générés.

Je commence donc avec un JSON très simple mais qui contient tout de même des types de données mixés (string et entier).

{ "Id":32, "Name":"Léo", "City":"Paris" }

Un petit clic sur l’option en ayant le focus dans un fichier cs…

image

Et le code ci dessous est généré. Je retrouve bien mes différentes propriétés correctement nommée.

public class Rootobject { public int Id { get; set; } public string Name { get; set; } public string City { get; set; } }

Dans un second cas, mon graph est un peu plus complexe et contient une seconde classe.

{ "Id":32, "Name":"Léo", "Country": { "Name":"France" }, "City":"Paris" }

Le code généré contient bien ma nouvelle classe, correctement nommée.

public class Rootobject { public int Id { get; set; } public string Name { get; set; } public Country Country { get; set; } public string City { get; set; } } public class Country { public string Name { get; set; } }

Maintenant, si mon JSON contient une liste de données …

{ "Id":32, "Name":"Léo", "Orders": [ {"Id":1}, {"Id":2}, {"Id":3} ], "City":"Paris" }

Le code généré contient un tableau, et ma propriété Orders a été correctement dépluralisée pour obtenir une classe Order.

public class Rootobject { public int Id { get; set; } public string Name { get; set; } public Order[] Orders { get; set; } public string City { get; set; } } public class Order { public int Id { get; set; } }

Si maintenant je veux m’amuser avec des nullables. D’abord, je repars dans mon item de base et passe mon Id à null (qui est normalement de type int).

{ "Id":null, "Name":"Léo", "City":"Paris" }

Sans surprise, dans le code généré, le type int ne peut être résolu et ma propriété est maintenant de type object.

public class Rootobject { public object Id { get; set; } public string Name { get; set; } public string City { get; set; } }

Si je fais la même chose avec la propriété Id des objets Order de ma liste…

{ "Id":32, "Name":"Léo", "Orders": [ {"Id":1}, {"Id":null}, {"Id":3} ], "City":"Paris" }

Cette fois-ci, comme certaines valeurs de ma liste sont des entiers, il arrive à déduire le type de ma propriété comme étant un entier nullable.

public class Rootobject { public int Id { get; set; } public string Name { get; set; } public Order[] Orders { get; set; } public string City { get; set; } } public class Order { public int? Id { get; set; } }

Et enfin, si je refais le même test en mixant totalement les types pour l’id de mes orders…

{ "Id":32, "Name":"Léo", "Orders": [ {"Id":1}, {"Id":null}, {"Id":"test"} ], "City":"Paris" }

Je me retrouve avec object comme type de ma propriété.

public class Rootobject { public int Id { get; set; } public string Name { get; set; } public Order[] Orders { get; set; } public string City { get; set; } } public class Order { public object Id { get; set; } }

A bientôt !


Classé sous

[Windows 8] Mettre en surbrillance une partie d’un text block

Aujourd’hui j’ai eu besoin d’une fonctionnalité pour mettre en surbrillance (highlighting) une partie du texte d’un de mes TextBlock. L’idée étant de mettre en avant certains items suite à une recherche par exemple.

En gros, la solution consiste juste à prendre le texte du TextBlock et à le remplacer par des Run (contenu dans la collection Inlines).

Ainsi, je n’ai qu’à écrire un morceau de code qui me transformerait par exemple le textblock suivant :

 

<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Width="200"> <TextBlock Text="ceci est un texte pour dire bonjour, et même que mon texte est assez long pour wrapper un peu" FontSize="16" TextWrapping="Wrap" /> </Grid>

Vers ce textblock :

 

<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="1" Width="200"> <TextBlock FontSize="16" TextWrapping="Wrap"> <Run Text="ceci est un " Foreground="White" /><Run Text="texte pour dire bonjour" Foreground="Orange"/><Run Text=", et même que mon texte est assez long pour wrapper un peu" Foreground="White"/> </TextBlock> </Grid>

Pour avoir un résultat comme ceci.

image

Le top étant bien sur de placer le texte à mettre en surbrillance via une attached property.

La première étape consiste à faire une méthode d’extension sur string pour splitter ma chaine de caractères source sur le texte à mettre en surbrillance tout en gardant bien ces éléments dans le tableau en sortie. Un simple appel à la méthode Split va m’enlever les occurences de mon texte à mettre en surbrillance alors qu’un appel à Regex.Split va me permettre de les conserver.

 

public static class StringExtensions { public static IEnumerable<string> Fragmentize(this string str, string fragment) { return Regex.Split(str, "(" + fragment + ")", RegexOptions.IgnoreCase) .ToList() .Where(s => !string.IsNullOrEmpty(s)); } }

J’ai ajouté au passage un sériee de TU pour valider la conformité de la sortie de la méthode par rapport à mes attentes …

[TestClass] public class FragmentizeTests { [TestMethod] public void OriginalTextIsEqualsToTextToHighlight() { const string originalText = "texte"; const string textToHighlight = "texte"; var results = originalText.Fragmentize(textToHighlight).ToList(); var resultOnOneStr = string.Join(string.Empty, results); Assert.AreEqual(results.Count, 1); Assert.AreEqual(resultOnOneStr, originalText); } [TestMethod] public void OriginalTextBeginsWithTextToHighlight() { var originalText = "textesalut"; var textToHighlight = "texte"; var results = originalText.Fragmentize(textToHighlight).ToList(); var resultOnOneStr = string.Join(string.Empty, results); Assert.AreEqual(results.Count, 2); Assert.AreEqual(resultOnOneStr, originalText); originalText = "texte salut"; textToHighlight = "texte"; results = originalText.Fragmentize(textToHighlight).ToList(); resultOnOneStr = string.Join(string.Empty, results); Assert.AreEqual(results.Count, 2); Assert.AreEqual(resultOnOneStr, originalText); } [TestMethod] public void OriginalTextEndsWithTextToHighlight() { var originalText = "saluttexte"; var textToHighlight = "texte"; var results = originalText.Fragmentize(textToHighlight).ToList(); var resultOnOneStr = string.Join(string.Empty, results); Assert.AreEqual(results.Count, 2); Assert.AreEqual(resultOnOneStr, originalText); originalText = "salut texte"; textToHighlight = "texte"; results = originalText.Fragmentize(textToHighlight).ToList(); resultOnOneStr = string.Join(string.Empty, results); Assert.AreEqual(results.Count, 2); Assert.AreEqual(resultOnOneStr, originalText); } [TestMethod] public void OriginalTextBeginsAndEndsWithTextToHighlight() { var originalText = "textesaluttexte"; var textToHighlight = "texte"; var results = originalText.Fragmentize(textToHighlight).ToList(); var resultOnOneStr = string.Join(string.Empty, results); Assert.AreEqual(results.Count, 3); Assert.AreEqual(resultOnOneStr, originalText); originalText = "texte salut texte"; textToHighlight = "texte"; results = originalText.Fragmentize(textToHighlight).ToList(); resultOnOneStr = string.Join(string.Empty, results); Assert.AreEqual(results.Count, 3); Assert.AreEqual(resultOnOneStr, originalText); } [TestMethod] public void OriginalTextContainsMultipleTextToHighlight() { var originalText = "textesaluttextesaluttexte"; var textToHighlight = "texte"; var results = originalText.Fragmentize(textToHighlight).ToList(); var resultOnOneStr = string.Join(string.Empty, results); Assert.AreEqual(results.Count, 5); Assert.AreEqual(resultOnOneStr, originalText); originalText = "texte, salut salut, texte-- salut! texte"; textToHighlight = "texte"; results = originalText.Fragmentize(textToHighlight).ToList(); resultOnOneStr = string.Join(string.Empty, results); Assert.AreEqual(results.Count, 5); Assert.AreEqual(resultOnOneStr, originalText); } }

Cette méthode en main, je peux commencer l’écriture de mon attached property. Je vais d’abord créer deux propriétés attachées pour la valeur de la couleur à utiliser pour mettre en surbrillance le texte et une seconde pour conserver la valeur originale du texte du TextBlock.

public static readonly DependencyProperty HighlightColorProperty = DependencyProperty.RegisterAttached("HighlightColor", typeof (Brush), typeof (TextHighlightingBehavior), new PropertyMetadata(default(Brush))); public static void SetHighlightColor(UIElement element, Brush value) { element.SetValue(HighlightColorProperty, value); } public static Brush GetHighlightColor(UIElement element) { return (Brush) element.GetValue(HighlightColorProperty); } public static readonly DependencyProperty OriginalTextProperty = DependencyProperty.RegisterAttached("OriginalText", typeof(string), typeof(TextHighlightingBehavior), new PropertyMetadata(default(string))); public static void SetOriginalText(UIElement element, string value) { element.SetValue(OriginalTextProperty, value); } public static string GetOriginalText(UIElement element) { return (string) element.GetValue(OriginalTextProperty); }

A partir de là, je peux écrire ma propriété attachée TextToHighlight. Si la valeur du texte à traiter est inexistante ou vide, alors je vais restorer la valeur originale du texte. Sinon je vais convertir le contenu de mon TextBlock vers des objets Run.

public static readonly DependencyProperty TextToHighlightProperty = DependencyProperty.RegisterAttached("TextToHighlight", typeof (string), typeof (TextHighlightingBehavior), new PropertyMetadata(string.Empty, (s, e) => { var textToHighlight = e.NewValue.ToString(); var textBlock = s as TextBlock; if(string.IsNullOrEmpty(textToHighlight)) { RemoveHighlightingFromTextBlock(textBlock); } else { AddHighlightingToTextBlock(textBlock, textToHighlight); } })); public static void SetTextToHighlight(UIElement element, string value) { element.SetValue(TextToHighlightProperty, value); } public static string GetTextToHighlight(UIElement element) { return (string) element.GetValue(TextToHighlightProperty); }

Avant de faire la conversion, je dois d’abord m’assurer que la valeur du texte originale a bien été placée dans l’attached property dédiée.

private static void AddHighlightingToTextBlock(TextBlock textBlock, string textToHighlight) { TrySaveTextBlockText(textBlock); PopulateTextBlockWithInlines(textBlock, textToHighlight); } private static void TrySaveTextBlockText(TextBlock textBlock) { var originalText = GetOriginalText(textBlock); if (string.IsNullOrEmpty(originalText)) { SetOriginalText(textBlock, textBlock.Text); } }

Enfin, dans la méthode PopulateTextBlockWithInlines, je vais vider l’éventuel contenu texte de mon contrôle et ses inlines. Puis je fais appel à ma méthode Fragmentize afin qu’elle génère les instance de Run avec la bonne couleur de Foreground (celle classique ou celle d’accentuation).

private static void PopulateTextBlockWithInlines(TextBlock textBlock, string textToHighlight) { textBlock.Text = string.Empty; textBlock.Inlines.Clear(); var highlightColor = GetHighlightColor(textBlock) ?? new SolidColorBrush(Colors.Red); var fragments = GetOriginalText(textBlock).Fragmentize(textToHighlight); foreach (var fragment in fragments) { textBlock.Inlines.Add(new Run() { Text = fragment, Foreground = fragment == textToHighlight ? highlightColor : textBlock.Foreground }); } }

Il manque juste la partie restauration du texte d’origine …

private static void RemoveHighlightingFromTextBlock(TextBlock textBlock) { RestoreTextBlockText(textBlock); } private static void RestoreTextBlockText(TextBlock textBlock) { var orginalText = GetOriginalText(textBlock); textBlock.Text = orginalText; }

 

C’est finalement très facile à implémenter et l’effet au rendu est assez intéressant et peu commun dans les app Win8.

 

 

A bientôt !


Classé sous ,

[ASP.NET MVC] Héberger des WEB API hors d’un site ASP.NET MVC

Une des fonctionnalités que je trouve particulièrement intéressante dans les ASP.NET WEB API est la possibilité d’héberger et de monter un serveur HTTP qui mettra mes services à disposition de clients, hors d’un site web ASP.NET MVC et même hors d’un serveur IIS. Je peux ainsi héberger cette couche directement dans un service Windows, dans une application console, etc. Bref, je suis extremement proche du développement d’un service WCF classique.

Pour commencer, je vais créer un projet console avec des références vers les éléments suivants :

- System.Net.Http ;

- System.Web.Http ;

- System.Web.Http.SelfHost ;

- Le package Nuget Json.NET (Newtonsoft Json).

 

Je vais utiliser le modèle Customer et le controller ci dessous (qui répond uniquement au verbe GET en renvoyant une liste de clients).

public class Customer { public string Name { get; set; } } public class CustomerController : ApiController { public List<Customer> Get() { return new List<Customer>() { new Customer() { Name = "Customer A"}, new Customer() { Name = "Customer B"}, new Customer() { Name = "Customer C"} }; } }

La prochaine étape consiste à créer un objet de type HttpSelfHostConfiguration. Celui-ci va me permettre de définir l’adresse d’écoute de mon service, le type de credential que les clients devront utiliser et surtout, la configuration des routes à utiliser. Dans mon cas, la constante ServiceAddress est égale à “http://localhost:7612”.

var httpSelfHostConfiguration = new HttpSelfHostConfiguration(ServiceAddress); httpSelfHostConfiguration.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional });

A partir de là, je peux créer un objet HttpSelfHostService en lui passant la configuration précédente, et je démarre l’écoute avec un appel à OpenAsync. Et puis, pour fermer le tout proprement, je fais un appel à CloseAsync et j’englobe bien le tout dans un using.

using (var httpSelfHostServer = new HttpSelfHostServer(httpSelfHostConfiguration)) { httpSelfHostServer.OpenAsync().Wait(); Console.WriteLine("Appuyez sur une touche pour quitter"); Console.ReadLine(); httpSelfHostServer.CloseAsync().Wait(); }

Comme d’habitude, un petit tour dans Fiddler pour valider que le tout fonctionne bien…

image

A savoir, dans mon cas, j’exécute tout le temps Visual Studio en administrateur, donc il n’y a pas eu de soucis pour que mon serveur s’enregistre sur l’adresse spécifiée. Si ce n’est pas le cas, il faut exécuter (avec des droits d’admin) une commande pour donner les droits d’enregistrement à un compte en particulier (important pour le déploiement en prod donc) :

netsh http add urlacl url=http://+:7612/ user=monnomdeuser

 

Enfin, si je veux faire de l’injection de dépendance, avec par exemple Unity (et le package Unity Web Api qui contient un dependencyresolver propre à la stack web api), je peux l’enregistrer facilement dans mon HttpSelfHostConfiguration qui contient une propriété DependencyResolver. Ce qui me donnerait au final, quelque chose du genre …

public interface IMonService { } public class MonService : IMonService { } class Program { private static IUnityContainer BuildUnityContainer() { var container = new UnityContainer(); container.RegisterType<IMonService, MonService>(); return container; } private const string ServiceAddress = "http://localhost:7612"; static void Main(string[] args) { var httpSelfHostConfiguration = new HttpSelfHostConfiguration(ServiceAddress); var container = BuildUnityContainer(); httpSelfHostConfiguration.DependencyResolver = new UnityDependencyResolver(container); httpSelfHostConfiguration.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); using (var httpSelfHostServer = new HttpSelfHostServer(httpSelfHostConfiguration)) { httpSelfHostServer.OpenAsync(); Console.WriteLine("Appuyez sur une touche pour quitter"); Console.ReadLine(); httpSelfHostServer.CloseAsync().Wait(); } } }

A bientôt !


Classé sous ,

Faire le ménage dans IIS Express

Derrière ce titre se cache la dure réalité d'un problème que les développeurs vivent occasionnellement et auquel j'ai encore eu le droit cet après-midi : mon site marchait il y a 5 minutes, mais maintenant, dès que je modifie une page, il ne se passe rien dans le navigateur. Désespérément rien.

Bien sûr, dans ces moments difficiles, on s'énerve, on cherche un coupable...

  • Le cache du navigateur d'abord. On commence par bourriner intelligemment les touches Control et F5. On se dit que c'est la faute d'IE et que ça marchera mieux sur un autre navigateur, en vain… Même le mode p…rivé n'apporte pas de solution.
  • Peut-être que cela vient de Visual Studio ? C'est le moment d'aller trifouiller les options de debug du projet. On essaye en release, sait-on jamais. On vide les répertoires bin et obj. On force Visual Studio à recompiler la solution à chaque F5. Pourtant, toujours rien.
  • Le problème doit venir d'ailleurs… Le cache ? oui, ça ne peut être que le cache. Il nous vient alors une obsession, faire la chasse aux répertoires temporaires. On va voir du côté du Framework (C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files), du côté du navigateur, comme s'il n'avait pas déjà donné…

Et rien, toujours rien.

Nouvelle stratégie : je vais essayer de déployer ma solution sur un autre site, et même un autre port, ça sera forcément la bonne version que je retrouverai dans mon navigateur.

Je fonce dans les propriétés de mon projet, je passe l'adresse de publication sur un port random et je déploie. Toujours rien. J'essaye encore un autre port, je (re)déploie. Pas mieux. J'essaye en décochant la case « Use IIS Express » pour voir ce que ça donne sur mon IIS classique. Impossible : l'adresse est déjà utilisée par IIS Express. Une idée me vint alors : si IIS Express a conservé quelque part l'information sur l'utilisation d'une URL et d'un port en particulier, ne (me) cacherait il pas d'autres choses également ?

Le bougre n'ayant qu'une interface graphique un peu trop épurée, il faut se rendre dans le répertoire mes documents, puis dans « IISExpress ». Là-dedans, je vide le répertoire « Logs », je vide le répertoire « TraceLogFiles », enfin, dans le répertoire « Config », j'ouvre « applicationhost.config ».

Et là, au beau milieu de ce très gros fichier, au cœur d'une balise « sites », je découvre une pléthore de balise « site » qui contiennent effectivement des références vers mes différentes URL et vers les répertoires correspondant. Et dans mon cas précis, il y en avait vraiment … beaucoup. En fait, j'ai la sale habitude de créer un nouveau projet dès que je veux tester un petit truc. Et comme j'aime bien tester pleins de petits trucs, et que je n'ai pas formaté ma machine depuis … depuis un bon moment maintenant. Autant dire que j'ai eu de la chance de tomber sur un port random qui n'était pas utilisé lors que je l'ai modifié à la main. Bref, je décide donc de supprimer la déclaration de tous mes sites dans ce fichier.

Retour dans Visual Studio, je vérifie que ma case « Use IIS Express » est bien cochée, je fais un petit coup de CTRL+S et enfin F5 … C'est bon, mes modifications sont enfin là !

J'ai toujours du mal à comprendre s'il s'agissait finalement d'un problème de cache ou si c'était la configuration de mon IIS Express qui commençait à ne plus tourner rond, en tout cas, je vous invite à vérifier ce point dans votre prochaines prises de tête du vendredi après-midi, lorsque plus rien ne semble marcher comme prévu.

Bon week end !


Classé sous

[ASP.NET MVC] Définir ses bundles dans le web.config

Il n’existe pas de solutions directement dans ASP.NET MVC pour nous permettre de déclarer nos bundles directement dans le fichier web.config.

Pour palier à ce manque, j’ai écris une custom section qui me permet de déclarer mes bundles et de les charger au lancement de mon application Smile

D’abord donc, mon arborescence de configuration désirée :

<bundle enableOptimizations="true"> <bundles> <add type="Script" name="~/bundles/jquery"> <includes> <add path="~/Scripts/jquery-{version}" /> </includes> </add> <add type="Style" name="~/Content/css"> <includes> <add path="~/Content/site.css" /> </includes> </add> </bundles> </bundle>

La déclaration de ma section :

<section name="bundle" type="BundleExtensions.BundleSection, BundleExtensions" />

La partie la plus barbante, c’est donc la création du modèle correspondant à la section custom …

public sealed class BundleSection : ConfigurationSection { [ConfigurationProperty("enableOptimizations", DefaultValue = true, IsRequired = false)] public bool EnableOptimizations { get { return (bool)this["enableOptimizations"]; } set { this["enableOptimizations"] = value; } } [ConfigurationProperty("bundles", IsRequired = true)] public BundleConfigurationCollection Bundles { get { return this["bundles"] as BundleConfigurationCollection; } set { this["bundles"] = value; } } } public sealed class BundleConfigurationItem : ConfigurationElement { [ConfigurationProperty("type")] public BundleType Type { get { return (BundleType)this["type"]; } set { this["type"] = value; } } [ConfigurationProperty("customType")] public string CustomType { get { return (string)this["customType"]; } set { this["customType"] = value; } } [ConfigurationProperty("name", IsRequired = true)] public string Name { get { return (string)this["name"]; } set { this["name"] = value; } } [ConfigurationProperty("includes", IsRequired = true)] public IncludeConfigurationCollection Includes { get { return this["includes"] as IncludeConfigurationCollection; } set { this["includes"] = value; } } } public enum BundleType { None, Script, Style } public sealed class IncludeConfigurationItem : ConfigurationElement { [ConfigurationProperty("path", IsRequired = true)] public string Path { get { return (string)this["path"]; } set { this["path"] = value; } } } public class BundleConfigurationCollection : ConfigurationElementCollection { protected override ConfigurationElement CreateNewElement() { return new BundleConfigurationItem(); } protected override object GetElementKey(ConfigurationElement element) { return ((BundleConfigurationItem)element).Name; } protected override string ElementName { get { return "bundles"; } } } public class IncludeConfigurationCollection : ConfigurationElementCollection { protected override ConfigurationElement CreateNewElement() { return new IncludeConfigurationItem(); } protected override object GetElementKey(ConfigurationElement element) { return ((IncludeConfigurationItem)element).Path; } protected override string ElementName { get { return "includes"; } } }

A noter que j’ai géré via l’enum de le cas des bundles js et css, mais j’ai laissé la possibilité d’en utiliser d’autres (less, etc.) via la propriété customtype.

 

L’initializer qui va avec tout ça pour générer les bundles :

public static class BundleInitializer { public static void Initialize() { var section = ConfigurationManager.GetSection("bundle") as BundleSection; if (section == null) return; foreach (BundleConfigurationItem bundleConfigurationItem in section.Bundles) { Bundle bundle; if (string.IsNullOrEmpty(bundleConfigurationItem.CustomType)) { switch (bundleConfigurationItem.Type) { case BundleType.Style: bundle = new StyleBundle(bundleConfigurationItem.Name); break; case BundleType.Script: bundle = new ScriptBundle(bundleConfigurationItem.Name); break; default: bundle = new Bundle(bundleConfigurationItem.Name); break; } } else { var type = Type.GetType(bundleConfigurationItem.CustomType); bundle = Activator.CreateInstance(type, bundleConfigurationItem.Name) as Bundle; } foreach (IncludeConfigurationItem includeConfigurationItem in bundleConfigurationItem.Includes) { bundle.Include(includeConfigurationItem.Path); } BundleTable.Bundles.Add(bundle); } BundleTable.EnableOptimizations = section.EnableOptimizations; } }

Et donc, dans le global.asax, un simple appel à BundleInitializer.Initialize() suffit Smile

 

Je vais mettre le code au propre et le pousser dans un package nuget à l’occasion. En espérant que cela puisse être utile !


Classé sous ,

[ASP.Net MVC] Extraire facilement les metadata d’un modèle

En me penchant sur une problématique de génération de vues en asp.net mvc à partir d’un modèle correctement décoré avec des data annotations, j’ai été amené à regarder un peu comment fonctionne en interne les HtmlHelper EditorFor, LabelFor, etc.

En fait, classiquement, pour répondre à ma problématique, j’utiliserais la réflexion pour récupérer l’ensemble des propriétés de mon modèle, ensuite je peux regarder le type de données de ces différentes propriétés ou encore, toujours via reflection, récupérer la liste des attributs appliqués sur chacune de ces propriétés et donc, pouvoir formater correctement ma vue.

Pour en revenir aux méthodes HtmlHelper, elles utilisent en fait une série de classe basée autour de la classe ModelMetadata. En plus, pour une fois, cette classe n’est pas restreinte en internal ! Smile Celle-ci va permettre d’inspecter automatiquement un modèle. Elle est accessible au travers de différents types de source :

- Le type de mon modèle ;

- Une MemberExpression (c’est ce que l’on utilise finalement le plus souvent en MVC) ;

- Une chaine de caractère du nom de la propriété qui m’intéresse.

A partir de là je vais donc récupérer un modèle totalement hydraté et qui a donc intelligemment lu mes différents attributs et sait me les restituer aux travers de propriétés simple d’accès. Par exemple, est-ce que ma propriété peut être éditée ? L’information est disponible via l’accesseur ShowForEdit.

Rentrons dans le vif du sujet. Je commence par mon modèle avec mes data annotations.

 

public class Customer { [Display(Name = "Prénom")] public string FirstName { get; set; } [Required] [Display(Name = "Nom", Description = "Lorem ipsum ...")] public string LastName { get; set; } }

Je vais donc pouvoir récupérer mon ModelMetadata à partir d’une expression vers mon modèle et le type de mon modèle et je passe le tout dans le view bag.

 

public ActionResult Index() { var model = new Customer() { FirstName = "John", LastName = "Maclane" }; ViewBag.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof (Customer)); return View(model); }

Enfin, je vais itérer sur les différentes propriétés remontées par mon model metadata et, avantage sur la reflection, je vais gérer plus facilement le cas où une description a été définie via l’attribut Display.

@model object @section scripts { @Scripts.Render("~/bundles/jqueryval") } @{ ViewBag.Title = "Home Page"; } @using(Html.BeginForm("Index", "Home", FormMethod.Post)) { @Html.ValidationSummary() <ol> @foreach (ModelMetadata property in ViewBag.ModelMetadata.Properties) { <li> @Html.Label(property.PropertyName) @Html.Editor(property.PropertyName) @Html.ValidationMessage(property.PropertyName) @if(!string.IsNullOrEmpty(property.Description)) { <p> @property.Description </p> } </li> } </ol> <input type="submit" value="Submit"/> }

Ce qui me donne le résultat suivant.

image

 

En espérant que cela vous aidera dans la construction de vues dynamiques ! Smile


Classé sous ,

[ASP.Net MVC] Afficher des bundles oui, mais qu’une seule fois

Il y a quelque temps, j’avais écrit un article sur les bundles avec MVC 4. A l’usage, je me suis rendu qu’il y avait un comportement que je trouve au final assez gênant.

Imaginons que nous travaillons à plusieurs développeurs sur un même projet web. Dans une de mes vues, j’ai besoin d’utiliser une librairie que j’ai stocké dans un bundle. Mais peut être qu’un des autres développeurs a déjà placé ce bundle dans le layout de base du site, ou dans une vue partiel.

Malheureusement, en l’état, il n’y a pas de control de la duplication du rendu lorsque l’on fait un Scripts.Render. Pour preuve, dans mon layout j’ai ceci.

 

      
@Scripts.Render( " ~/bundles/jquery " ) @RenderSection( " scripts " , required: false )

Dans une de mes vues, j’ai également ce code :

@section scripts { @Scripts.Render("~/bundles/jquery") }

Un petit coup d’oeil au script de ma page lorsque j’ouvre mon site dans le navigateur et je retrouve deux fois l’appel au bundle jquery.

<script src="/Scripts/jquery-1.8.2.js"></script> <script src="/Scripts/jquery-1.8.2.js"></script>

Bien sûr, le navigateur le gère intelligemment et ne va pas aller chercher deux fois la ressource, mais je trouve quand même que nous pourrions faire mieux et écrire un petit helper qui permet d’éviter ce doublon.

En fait, en jetant un coup à comment marche en interne les méthodes Scripts.Render ou Styles.Render, on peut se rendre qu’il y a bien un mécanisme permettant d’éviter des doublons. Le problème étant que celui-ci n’est invoqué que pour le tableau d’arguments reçu par la méthode Render.

En clair, si j’écris l’exemple suivant, il va réussir à éliminer le doublon (ouf !).

 

@Scripts.Render("~/bundles/jquery", "~/bundles/jquery")

Pour en revenir à mon problème initial, ce que je vais faire pour éviter de rendre mon bundle en doublon, c’est tout simplement stocker dans la collection Items du HttpContext le nom des bundles (unique je vous le rappelle) qui ont déjà été rendus. Ce qui nous donne le helper suivant (je l’ai volontairement placé dans le namespace System.Web.Optimization pour pouvoir l’utiliser sans directive using supplémentaire).

 

namespace System.Web.Optimization { public static class BundlesExtensions { public static IHtmlString RenderScripts(this HtmlHelper htmlHelper, params string[] paths) { var toRender = paths.Where(p => !HttpContext.Current.Items.Contains(p)).ToList(); toRender.ForEach(p => HttpContext.Current.Items.Add(p, null)); return Scripts.Render(toRender.ToArray()); } } }

Et à l’usage donc @Html.RenderScripts("~/bundles/jquery"). En esperant que ça peut vous être utile ! Smile


Classé sous ,

Désactiver la minification pour un bundle

Dans un précédent post, je présentais les bundles et la minification des ressources avec asp.net mvc 4. Aujourd’hui, j’ai eu besoin de débugguer un script en particulier alors que la minification était activée pour mon site et que je ne pouvais pas la désactiver.

Rien de plus simple, plutot que de créer un ScriptBundle, je crée un Bundle, il n’a donc pas de IBundleTransform associé et donc pas de minification effectuée Smile

bundles.Add(new Bundle("~/bundles/monbundle") .Include("~/Scripts/script1.js") .Include("~/Scripts/script2.js"));

Classé sous

Changer l’aspect des résultats de l’autocomplete box de JQuery UI

Pour le projet sur lequel je travaille en ce moment, j’ai eu besoin de customiser un peu la popup d’affichage de résultats de l’auto complete de jquery ui. Je me suis donc interoger sur la manière utilisée pour peupler cette popup. En fouillant un peu dans le js de jquery ui, j’ai trouvé la fonction _renderItem.

_renderItem: function( ul, item) { return $( "<li></li>" ) .data( "item.autocomplete", item ) .append( "<a>" + item.label + "</a>" ) .appendTo( ul ); },

J’ai donc pu venir remplacer la définition originale par ma propre méthode (oui c’est un peu barbare …). Dans mon cas, je fais la différence grâce à une propriété type qui est sur les items que j’ai passé à l’auto complete box. Dans mon cas, j’ai donc deux types d’éléments non sélectionnables, des “header” et des “spacing”. Si je ne leur ajoute pas de valeur data pour la propriété “item.autocomplete”, ils ne seront pas sélectionnables dans la popup, ils serviront donc juste pour la décoration Smile

$.ui.autocomplete.prototype._renderItem = function (ul, item) { if (item.type == "Header") { return $('<li style="font-weight:bold;">' + item.label + "</li>") .appendTo(ul); } else if (item.type == "Spacing") { return $('<li style="height:15px;"></li>').appendTo(ul); } else { return $("<li></li>") .data("item.autocomplete", item) .append('<a>' + item.label + "</a>") .appendTo(ul); } };

Le résultat en image :

image

A bientôt !


Classé sous ,

Afficher des données avec JQuery Template

Etant à la base un développeur XAML, j’adore la notion de conteneur de liste d’objets combiné à des item template. Pour retrouver ce genre de fonctionnement, je me suis donc penché sur la librairie JQuery Template.Son principe est simple, on crée un template reposant sur des balises dans le style {{= MaPropriété }}, ce template est ensuite appliqué aux éléments d’une liste, et le contenu généré est ajouté au DOM. Malheureusement elle est toujours au stade de beta et n’a pas été acceptée par l’équipe de JQuery. Personellement, je continue à l’utiliser faute de mieux à l’heure actuelle…

La page officielle du plug in est disponible ici : http://api.jquery.com/category/plugins/templates/

Premièrement, pour les besoins de ce billet, je commence par définir le modèle suivant, une classe représentant un produit, et une classe plus spécialisée pour les produits mis en avant, qui contiennent une description en plus du simple nom du produit.

public class Product { public string Name { get; set; } public Product(string name) { Name = name; } public Product Featured(string description) { return new FeaturedProduct(Name) { Description = description }; } } public class FeaturedProduct : Product { public string Description { get; set; } public FeaturedProduct(string name) : base(name) { } }

A partir de là, je crée mon controller avec deux actions qui retournent du json et qui prennent en paramètres de quoi faire de la pagination sur les données. Une de ces deux actions retournent l’intégralité du catalogue de produits, l’autre retournent les produits mis en avant.

public class ProductController : Controller { public ActionResult Index() { return View(); } private JsonResult Items(int pageSize, int pageIndex) { return Items(d => true, pageSize, pageIndex); } private JsonResult Items(Func<Product, bool> filter, int pageSize, int pageIndex) { return Json(new[] { new Product("Product A"), new Product("Product B").Featured("Aenean sit amet diam ipsum."), new Product("Product C").Featured("Praesent lacinia pulvinar tincidunt."), new Product("Product D"), new Product("Product E"), new Product("Product F"), new Product("Product G").Featured("Nunc venenatis, elit eget luctus adipiscing, ut tempus nulla elit nec erat."), new Product("Product H"), new Product("Product I"), new Product("Product J"), }.Where(filter).Skip(pageSize * pageIndex).Take(pageSize), JsonRequestBehavior.AllowGet); } public JsonResult FeaturedProducts(int pageSize, int pageIndex) { return Items(d => d is FeaturedProduct, pageSize, pageIndex); } public JsonResult AllProducts(int pageSize, int pageIndex) { return Items(pageSize, pageIndex); } }

Venons en au coeur du sujet. Ma page HTML contient basiquement les deux blocs ci dessous (je me suis branché directement sur le template de base d’une appli web MVC). J’ai donc deux sections, une qui vise à accueillir les produits mis en avant et l’autre tous les produits.

<article> <header> <h3>Featured Products</h3> </header> <div id="featured-products-container"></div> <a href="#" id="featured-products-more-link">Load more</a> <img src="~/Images/ajax-loader.gif" id="featured-products-loader" /> </article> <article> <header> <h3>All Products</h3> </header> <div id="all-products-container"></div> <a href="#" id="all-products-more-link">Load more</a> <img src="~/Images/ajax-loader.gif" id="all-products-loader" /> </article>

Il est ensuite temps pour moi de créer le template qui sera appliqué pour afficher mes produits. Je pourrais le passer directement en inline à la méthode tmpl du plug in Jquery template (qui au coeur de son fonctionnement), mais je préfère l’isoler dans un script de type text/html. Dans ce cas, je vais utiliser la balise qui me permet d’afficher la valeur d’une propriété.

<script id="classic-product-template" type="text/html"> <p>{{= Name }}</p> </script>

Pour appliquer ce template à une liste d’objet, il suffit simplement d’appeler la méthode tmpl sur un selecteur du template en question, de lui passer la liste d’objets à templater et de chainer ça avec l’id de l’élément qui doit contenir les éléments générés.

$("#template").tmpl(objets).append("#container");

Dans mon cas, je me suis donc crée un petit plug in jquery qui permet d’aller charger une source de données avec de la pagination et qui applique un template aux éléments retournés par le service.

(function($) { function getJsonUrl(serviceUri, pageSize, pageIndex) { var jsonUrl = serviceUri + "/?pageSize=" + pageSize + "&pageIndex=" + pageIndex; return jsonUrl; } function loadMore(serviceUri, pageSize, pageIndex, isBusy, hasReachedEnd, template, onLoading, onLoaded) { if (isBusy) return; if (hasReachedEnd) return; $(this).data("isBusy", true); pageIndex++; $(this).data("pageIndex", pageIndex); var that = $(this); onLoading(); $.ajax({ type: 'GET', url: getJsonUrl(serviceUri, pageSize, pageIndex), success: function(o) { $("#" + template).tmpl(o).appendTo("#" + that.attr("id")); $(that).data("isBusy", false); if (o == null || o.length <= 0 || o.length < pageSize) { $(that).data("hasReachedEnd", true); onLoaded(true); } else { onLoaded(false); } } }); } var methods = { init: function(serviceUri, pageSize, template, onLoading, onLoaded) { $(this).data("serviceUri", serviceUri); $(this).data("pageSize", pageSize); $(this).data("pageIndex", -1); $(this).data("template", template); $(this).data("onLoading", onLoading); $(this).data("onLoaded", onLoaded); methods.more.apply(this); }, more: function() { var serviceUri = $(this).data("serviceUri"); var pageSize = $(this).data("pageSize"); var pageIndex = $(this).data("pageIndex"); var isBusy = $(this).data("isBusy"); var hasReachedEnd = $(this).data("hasReachedEnd"); var template = $(this).data("template"); var onLoading = $(this).data("onLoading"); var onLoaded = $(this).data("onLoaded"); loadMore.apply(this, [serviceUri, pageSize, pageIndex, isBusy, hasReachedEnd, template, onLoading, onLoaded]); } }; $.fn.dataPager = function(options) { if (typeof options == "object") { options = $.extend({ }, $.fn.dataPager.defaults, options); return methods.init.apply(this, [options.serviceUri, options.pageSize, options.template, options.onLoading, options.onLoaded]); } else if (typeof options == "string" && methods[options]) { return methods[options].apply(this); } else { $.error("unknown method on plugin datapager"); } }; $.fn.dataPager.defaults = { pageSize: 4 }; })(jQuery);

J’applique ensuite mon plugin au conteneur que j’ai créé plus tôt dans cet article, en lui passent différents paramètres : adresse du service, l’id du template à utiliser pour afficher les éléments et deux callbacks qui vont me permettre d’afficher / cacher un loader et le bouton pour charger plus d’éléments.

$("#featured-products-container").dataPager({ 'serviceUri': '@Url.Action("FeaturedProducts", "Product")', 'template': 'featured-product-template', 'onLoading': function() { $("#featured-products-more-link").hide(); $("#featured-products-loader").show(); }, 'onLoaded': function(completed) { $("#featured-products-loader").hide(); if (!completed) $("#featured-products-more-link").show(); } }); $("#featured-products-more-link").click(function(args) { args.preventDefault(); $("#featured-products-container").dataPager("more"); });

Le champs de possibilité offerte par ce mini langage de balisage est encore plus large puisqu’il me permet d’introduire conditions et sub templating. Dans l’exemple suivant, je me suis donc créé un template selector afin de rediriger vers le template correspondant selon si le produit est mis en avant ou non. Une autre balise intéressante que je n’ai pas utilisée ici, la balise {{each}} permet d’itérer sur une collection et de donc de créer des templates nettement plus complexes.

<script id="featured-product-template" type="text/html"> <p>{{= Name }} - {{= Description }}</p> </script> <script id="classic-product-template" type="text/html"> <p>{{= Name }}</p> </script> <script id="product-template-selector" type="text/html"> {{if Description }} {{tmpl "#featured-product-template" }} {{else}} {{tmpl "#classic-product-template" }} {{/if}} </script>

Je peux donc brancher mon second conteneur en lui passant cette fois ci mon simili data template selector.

$("#all-products-container").dataPager({ 'serviceUri': '@Url.Action("AllProducts", "Product")', 'template': 'product-template-selector', 'onLoading': function() { $("#all-products-more-link").hide(); $("#all-products-loader").show(); }, 'onLoaded': function(completed) { $("#all-products-loader").hide(); if (!completed) $("#all-products-more-link").show(); } }); $("#all-products-more-link").click(function(args) { args.preventDefault(); $("#all-products-container").dataPager("more"); });

Ci-dessous, le résultat de la page avec le data pager et les templates.

image

Pour infos, le projet a été proposé par microsoft. Peut être verrons nous à l’avenir un plug in stable et accepté par l’équipe officielle…

 

A bientôt !


Classé sous ,

ASP.NET MVC 4 : Bundles et minifications

Depuis ASP.Net MVC 4, pour augmenter les performances de son application, il est possible de mettre facilement en place du bundling et de la minification. Qu’est-ce que ces termes signifient ?

Le bundling va permettre de réduire le nombre de requêtes HTTP qui partent pour aller charger différentes ressources. D’autant plus que ce nombre de connexions simultannées peut être bridé sur certains navigateurs (voir http://www.browserscope.org/?category=network).

La capture d’écran ci dessous présente le traffic réseau généré par l’affichage du simple index de la page d’index du template de base d’un site mvc 4. On voit clairement que les css des différents control de jquery ui représentent un gros paquet de fichiers.

image

Le principe du bundling consiste donc à prendre tout ces fichiers et à les grouper dans un seul fichier. Pour nous développeurs, cela ne change rien, les fichiers restent séparés, c’est donc mieux organisé et plus simple à debugger, mais pour le consommateur de nos pages web, il n’y aura qu’un seul fichier à télécharger.

 

L’autre feature don’t je parlais en introduction de cet article concerne la minification. Le principe est simple, un script (javascript, css, ou autre), contient beaucoup de caractères qui ne sont pas nécessaires à sa bonne exécution (des commentaires, des espaces blancs etc.). Lorsque l’on minifie un script, on va donc enlever tout ces caractères et ne garder que l’essentiel, mais on va aussi renommer les différents éléments du code (variables, paramètres, etc.) avec des noms les plus courts possibles. Cela devient donc illisible pour un développeur, mais c’est beaucoup plus léger et donc beaucoup plus rapide à être téléchargé.

 

Pour commencer, il faut savoir que le bundling n’est activé que si l’application fonctionne sans débuggueur. Pour le tester, il va donc falloir faire un tour dans le web.config de l’application et le désactiver (ou faire un run Ctrl-F5).

<compilation debug="false" targetFramework="4.5" />

Ensuite, il faudra activer la propriété EnableOptimizations (personnellement, je le fais au début de mon BundleConfig).

BundleTable.EnableOptimizations = true;

L’appel d’un bundle, se fait ensuite de la manière suivante :

@Styles.Render("~/Content/themes/base/css")

Nous verrons un peu plus tard à quoi correspond cette url. Pour l’heure, il est temps de tester le site avec les outils de profiling d’un browser afin de voir le changement dans les ressources qui sont chargées.

image

Cette fois, on peut voir que le nombre de fichiers chargé a été énormement réduit. On constate notamment que les fichiers css de jquery ui ont été factorisés en un seul fichier css dont le poid fait approximativement le poid d’un seul fichier css non minifié.

Si on regarde le détail de ce fichier bundlé, on peut constater qu’il a été grandement remanié avant d’être retourné au navigateur.

image

Il est temps maintenant de regarder comment cela marche en détails…

Avec le template de base, la gestion des bundles se fait dans la classe BundleConfig de App_Start, sa méthode RegisterBundles sera automatiquement appelée dans le Application_Start. Il suffit alors de peupler la collection de bundle.

Les classes de bundle dépendent du type d’objet à bundler et à minifier. Pour du css, on utilisera un StyleBundle, pour du javascript, on utilisera un objet ScriptBundle. Dans les deux cas, ces classes dérivent de la classe Bundle. Chacun de ces types est lié à une collection de IBundleTransform qui seront appelés au moment de générer la réponse pour le client. Dans le cas de la classe StyleBundle, celle ci utilise une instance de CssMinify. Dans le cas de la classe ScriptBundle, elle utilise une instance de JsMinify. Ainsi, le modèle est totalement extensible, et on peut déjà trouver d’autres types de bundle et de transform liés pour du coffee script, less, etc.

Pour la création d’un bundle, le premier paramètre à passer est donc celui qui sera utilisé comme nom du bundle (et que l’on retrouvera dans @Styles.Render par exemple). Puis, dans la méthode Include, on pourra passer les n fichiers à inclure dans le bundle.

bundles.Add(new StyleBundle("~/Content/themes/base/css").Include( "~/Content/themes/base/jquery.ui.core.css", "~/Content/themes/base/jquery.ui.resizable.css", "~/Content/themes/base/jquery.ui.selectable.css", "~/Content/themes/base/jquery.ui.accordion.css", "~/Content/themes/base/jquery.ui.autocomplete.css", "~/Content/themes/base/jquery.ui.button.css", "~/Content/themes/base/jquery.ui.dialog.css", "~/Content/themes/base/jquery.ui.slider.css", "~/Content/themes/base/jquery.ui.tabs.css", "~/Content/themes/base/jquery.ui.datepicker.css", "~/Content/themes/base/jquery.ui.progressbar.css", "~/Content/themes/base/jquery.ui.theme.css"));

Il est également possible d’utiliser des wildcards, ainsi, l’exemple précédent pour également s’écrire de la manière suivante.

bundles.Add(new StyleBundle("~/Content/themes/base/css").Include( "~/Content/themes/base/*.css"));

Dans le cas de jquery, le token “{version}”, le moteur va donc automatiquement gérer le renvoie de la version actuelle de jquery, les fichiers de docs utilisés par l’intellisense sont automatiquement exclus, et les versions minifiés sont utilisés lorsque le débuggueur n’est pas attaché.

bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js"));

La classe bundle permet également la génération d’une clé uniquement à partir de son contenu. Celle ci sera utilisée pour la mise en cache du bundle côté client (la durée du cache est d’un an, et propre au user agent de chaque browser). A chaque modification d’un des fichiers du bundle, sa clé est modifiée et une nouvelle version pourra être téléchargée.

image

Enfin, le mécanisme de bundle permet de rediriger automatiquement vers un CDN. Il suffit d’activer l’option UseCdn sur la collection de bundle, puis de spécifier l’adresse du CDN pour le contenu concerné. L’exemple ci dessous concerne JQuery vers le CDN de Google. Si le mécanisme d’optimisations est activé, alors JQuery sera téléchargé depuis le CDN, sinon, il sera pris depuis la copie locale.

bundles.UseCdn = true; const string jqueryCdnPath = "http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"; bundles.Add(new ScriptBundle("~/bundles/jquery", jqueryCdnPath).Include( "~/Scripts/jquery-{version}.js"));

En espérant que ce post vous permettra d’améliorer les performances de vos applications web. A bientot !


Classé sous ,

Mais quelle est la différence entre HttpReponseMessage et HttpResponseException ?!

Développer un site web avec ASP.Net MVC c’est déjà bien. Utiliser Web API pour faire une couche de service qui pourra être consommée via jquery, mais aussi par n’importe quel autre client, ça donne au final une superbe web app. En plus, les Web API donnent l’accès à une maitrise avancée de la réponse http renvoyée par vos actions.

Par exemple, si je dois créer un client, je vais faire un POST sur un controller /api/customers. Si dans le design de mon application il a été établi que seuls les administrateurs de l’application peuvent créer de nouveaux clients, alors, il faudrait que je reçoive une réponse avec le code d’erreur 401 (unauthorized) et un beau message personnalisé.

Dans le même contexte, cette fois je fais la même requête avec un compte administrateur, mais, j’ai oublié de saisir un des champs (son nom par exemple), il faudrait donc que la réponse http ait le code 400 (bad request) et là encore, un beau message personnalisé.

La classe HttpResponseMessage permet de répondre à ce besoin : elle contient un code de réponse, éventuellement un message ou tout un objet, il est donc également utilisable sur les verbes GET.

Ci dessous se trouve un controlleur basique supportant le verbe post avec un Customer en entrée et le domaine correspondant aux scénarios décrits plus tôt dans ce post, le type de réponse de l’action étant un HttpResponseMessage.

 

public class User { private readonly IPrincipal _principal; public User(IPrincipal principal) { _principal = principal; } public bool IsAdmin { get { return _principal.IsInRole("Administrators"); } } } public class Customer { public string FirstName { get; set; } public string LastName { get; set; } public bool Validate() { if(string.IsNullOrEmpty(FirstName)) throw new MissingCustomerFirstNameException(); if(string.IsNullOrEmpty(LastName)) throw new MissingCustomerLastNameException(); return true; } } public class MissingCustomerFirstNameException : Exception { } public class MissingCustomerLastNameException : Exception { } public class ValuesController : ApiController { public HttpResponseMessage Post(Customer customer) { var user = new User(HttpContext.Current.User); if (!user.IsAdmin) return Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Only administrators can create new customers"); try { customer.Validate(); } catch(MissingCustomerFirstNameException) { return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "First name should not be empty"); } catch(MissingCustomerLastNameException) { return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Last name should not be empty"); } return Request.CreateResponse(HttpStatusCode.OK); } }

La requête suivante est exécutée pour tester ce service…

 

$(document).ready(function() { $.ajax({ type: 'POST', url: '/api/values', contentType: 'application/json', data: JSON.stringify({ FirstName: "Pierre", LastName: "" }), success: function () { debugger; }, error: function (data) { debugger; } }); });

Ci dessous, les trames (requête et réponses) vues dans Fiddler, dans un cas où l’utilisateur n’est pas administrateur, puis dans un cas où il l’est mais que les données passées au service sont invalides.

POST http://localhost:60409/api/values HTTP/1.1 Accept: */* Content-Type: application/json X-Requested-With: XMLHttpRequest Referer: http://localhost:60409/ Accept-Language: en-US Accept-Encoding: gzip, deflate User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0) Host: localhost:60409 Content-Length: 36 DNT: 1 Connection: Keep-Alive Pragma: no-cache Cookie: UserOpenedSessionUtcDate=11/29/2012 2:53:27 PM {"FirstName":"Pierre","LastName":""}

La réponse contient donc bien le code 401 si l’utilisateur n’est pas administrateur.

HTTP/1.1 401 Unauthorized Cache-Control: no-cache Pragma: no-cache Content-Type: application/json; charset=utf-8 Expires: -1 Server: Microsoft-IIS/8.0 X-AspNet-Version: 4.0.30319 X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcbGxhYmFfMDAwXERvY3VtZW50c1xWaXN1YWwgU3R1ZGlvIDIwMTJcUHJvamVjdHNcTXZjQXBwbGljYXRpb24yXE12Y0FwcGxpY2F0aW9uMlxhcGlcdmFsdWVz?= X-Powered-By: ASP.NET Date: Thu, 29 Nov 2012 16:20:33 GMT Content-Length: 58 {"Message":"Only administrators can create new customers"}

Dans le second cas, elle contient bien le code 400.

HTTP/1.1 400 Bad Request Cache-Control: no-cache Pragma: no-cache Content-Type: application/json; charset=utf-8 Expires: -1 Server: Microsoft-IIS/8.0 X-AspNet-Version: 4.0.30319 X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcbGxhYmFfMDAwXERvY3VtZW50c1xWaXN1YWwgU3R1ZGlvIDIwMTJcUHJvamVjdHNcTXZjQXBwbGljYXRpb24yXE12Y0FwcGxpY2F0aW9uMlxhcGlcdmFsdWVz?= X-Powered-By: ASP.NET Date: Thu, 29 Nov 2012 16:30:37 GMT Content-Length: 43 {"Message":"Last name should not be empty"}

Et bien sur, grâce aux web api, le type de réponse sait s’adapter au header accept de la requête. Ainsi, si je demande du xml, j’ai en réponse :

HTTP/1.1 400 Bad Request Cache-Control: no-cache Pragma: no-cache Content-Type: application/xml; charset=utf-8 Expires: -1 Server: Microsoft-IIS/8.0 X-AspNet-Version: 4.0.30319 X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcbGxhYmFfMDAwXERvY3VtZW50c1xWaXN1YWwgU3R1ZGlvIDIwMTJcUHJvamVjdHNcTXZjQXBwbGljYXRpb24yXE12Y0FwcGxpY2F0aW9uMlxhcGlcdmFsdWVz?= X-Powered-By: ASP.NET Date: Thu, 29 Nov 2012 16:34:35 GMT Content-Length: 63 <Error><Message>Last name should not be empty</Message></Error>

Enfin, mon controlleur aurait également pu s’écrire en utilisant la classe HttpResponseException, qui prend un HttpResponseMessage en paramètre, et qui donc dérive d’Exception et est donc gérable dans un bloc try… catch, etc.

Pour en revenir au titre de ce post, voici ce que l’on peut lire à ce sujet sur internet, et ma réponse juste en dessous :

  • HttpResponseException permet immédiatement de quitter le flux d’exécution, je peux donc lever mon exception dans mon service et y attacher la réponse HTTP correspondante. En utilisant HttpResponseMessage attachée à la réponse, le flux continue son exécution..

Oui mais non. Si j’essaye de déporter ma logique (et dans la plupart des cas c’est quand même ce que l’on fera), avec une approche DDD comme dans mon exemple ou non, on se retrouve alors à devoir fortement lier sa couche de service au protocole HTTP. Alors qu’en utilisant des exceptions totalement métiers et en constituant la réponse HTTP dans le controlleur comme dans mon cas, on se retrouve exactement avec le même résultat, sauf que la logique métier reste découplée du protocole.

  • HttpResponseException est à utiliser lorsque l’on a des contrôlleurs fortement typé (Customer Get(int id)).

J’en ai parlé en introduction, rien ne m’empêche de lier mon objet HttpResponseMessage à un objet métier, celui a alors un rôle de wrapper. Ce comportement était plus visible durant la beta par la présence d’une classe HttpResponseMessage<T>, mais il reste possible via un return Request.CreateResponse(HttpStatusCode.OK, monObjet).

  • Si je souhaite faire du unit testing sur mes contrôlleurs, est-ce que cela fait sens d’avoir des méthodes qui retournent des HttpResponseMessage dans tous les cas ?

C’est peut être l’argument qui se discute le plus. Partons du principe que j’ai utilisé des HttpResponseException un peu partout dans mon code, si je fais du unit testing, cela va déjà me pourrir un peu mes tests puisque je devrais m’asseoir sur l’attribut ExpectedException. Même si mon HttpResponseMessage peut contenir un objet, celui ci n’est pas facilement accessible (car sérialisé au sein d’un HttpContent). Mais, est-ce normal de faire un test unitaire sur une action dans le cas où un object n’existe pas et de récupérer “null” plutôt qu’un objet HttpResponseMessage avec un code 404 ?

 

Pour conclure, à mon sens, il n’y a donc pas de raisons particulières d’utiliser des HttpResponseException, si ce n’est introduire un lien étroit vers des classes MVC dans des méthodes de service ou de domaine. Mais ce n’est qu’on m’on avis et c’est un choix de design qui reviendra à l’appréciation de tout un chacun… et je suis totalement à l’écoute de vos avis, surtout par rapport au dernier des points évoqués ci dessus.

 

Merci !


Classé sous

Reactive Extensions : Consommer des services avec Rx Partie 3, les pièges à éviter

Une mauvaise utilisation de rx  lors de l’écriture d’une couche d’accès à des services peut conduire à des cas embarassants avec des erreurs mal gérées, des appels qui ne partent lorsqu’ils le devraient, et même des résultats incorrects … le tout nuisant fortement à la qualité de votre application.

Afin d’éviter que vous tapiez bêtement sur la techno, je vous ai préparé un petit meddley de trucs à pas faire vs trucs à faire.

 

Pour commencer, les subscribe imbriqués, c’est vilain. Par exemple, je veux faire une requête sur une url de service, je crée donc une source observable dans laquelle je vais créer ma requête puis je récupère la réponse avec un FromAsyncPattern sur lequel je vais faire un subscribe afin de traiter le résultat et renvoyer mes items à l’observer.

public IObservable<Player> GetPlayers() { return Observable.Create<Player>(observer => { try { var request = WebRequest.CreateHttp("http://localhost/WcfService1/Service1.svc/Players"); Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse)() .Subscribe(response => { using (var responseStream = response.GetResponseStream()) { var dataContractJsonSerializer = new DataContractJsonSerializer(typeof(List<Player>)); var players = dataContractJsonSerializer.ReadObject(responseStream) as List<Player>; foreach (var player in players) observer.OnNext(player); observer.OnCompleted(); } }); } catch (Exception e) { observer.OnError(e); } return () => { }; }); }

Ce qu’il faut savoir, c’est que si une erreur se produit dans la callback du subscribe ci dessus, elle ne sera pas récupérée automatiquement par le onError de l’observer. Essayer l’exemple ci dessous où je fais volontairement un throw…

return Observable.Create<Player>(observer => { try { var request = WebRequest.CreateHttp("http://localhost/WcfService1/Service1.svc/Players"); Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse)() .Subscribe(response => { throw new Exception("test !"); using (var responseStream = response.GetResponseStream()) { var dataContractJsonSerializer = new DataContractJsonSerializer(typeof(List<Player>)); var players = dataContractJsonSerializer.ReadObject(responseStream) as List<Player>; foreach (var player in players) observer.OnNext(player); observer.OnCompleted(); } }); } catch (Exception e) { observer.OnError(e); } return () => { }; });

BIM

image

 

Mon exception n’est pas catchée, l’application crash bêtement …

Deuxième raison qui devrait vous convraincre de ne pas imbriquer des subscribe : la complexité du code. Dans l’exemple précédent, je me contentais de créer une requête pour d’un seul FromAsyncPattern pour récupérer la réponse. Si maintenant je veux faire un appel en POST à mon service, je vais devoir faire un autre appel à FromAsyncPattern afin de récupérer le stream de ma requête. Je vais donc écrire quelque chose du genre…

public IObservable<Player> GetPlayers() { return Observable.Create<Player>(observer => { try { var request = WebRequest.CreateHttp("http://localhost/WcfService1/Service1.svc/Players"); request.Method = "POST"; Observable.FromAsyncPattern<Stream>(request.BeginGetRequestStream, request.EndGetRequestStream)() .Subscribe(stream => { var bytes = Encoding.UTF8.GetBytes("mes données post"); stream.Write(bytes, 0, bytes.Length); stream.Close(); Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse)() .Subscribe(response => { using (var responseStream = response.GetResponseStream()) { var dataContractJsonSerializer = new DataContractJsonSerializer(typeof(List<Player>)); var players = dataContractJsonSerializer.ReadObject(responseStream) as List<Player>; foreach (var player in players) observer.OnNext(player); observer.OnCompleted(); } }); }); } catch (Exception e) { observer.OnError(e); } return () => { }; });

On commence déjà à avoir quelque chose d’illisible. Sachant que si je veux gérer un peu plus proprement mes erreurs et le cas où je dois disposer mes “sous requêtes” car la principale se fait disposer, je me retrouve avec quelque chose du genre…

public IObservable<Player> GetPlayers() { IDisposable getRequestStream = null; IDisposable getResponse = null; return Observable.Create<Player>(observer => { try { var request = WebRequest.CreateHttp("http://localhost/WcfService1/Service1.svc/Players"); request.Method = "POST"; getRequestStream = Observable.FromAsyncPattern<Stream>(request.BeginGetRequestStream, request.EndGetRequestStream)() .Subscribe(stream => { try { var bytes = Encoding.UTF8.GetBytes("mes données post"); stream.Write(bytes, 0, bytes.Length); stream.Close(); } catch (Exception e) { observer.OnError(e); } getResponse = Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse)() .Subscribe(response => { try { using (var responseStream = response.GetResponseStream()) { var dataContractJsonSerializer = new DataContractJsonSerializer(typeof(List<Player>)); var players = dataContractJsonSerializer.ReadObject(responseStream) as List<Player>; foreach (var player in players) observer.OnNext(player); observer.OnCompleted(); } } catch (Exception e) { observer.OnError(e); } }, e => { observer.OnError(e); }); }, e => { observer.OnError(e); }); } catch (Exception e) { observer.OnError(e); } return () => { if (getRequestStream != null) getRequestStream.Dispose(); if (getResponse != null) getResponse.Dispose(); }; }); }

C’est totalement imbuvable et inmaintenable. Et le pauvre développeur qui va passer après vous ne va pas comprendre ce qui a motivé votre choix de Rx. Et franchement, avec un code comme ça, même vous vous devriez remettre en cause votre choix de rx.

Donc, on oublie les subscribe imbriqués, et à la place on va plutôt chainer les appels Rx. Exemple ci dessous, grâce au meilleur des opérateurs Linq, le SelectMany, je peux créer ma requête, récupérer son stream, y écrire des choses, puis récupérer la réponse et enchainer avec un autre Select pour parser mon résultat (cf mon article de blog précédent).

Si une erreur est levée, elle sera remontée correctement à votre observer. Si vous faîtes un dispose sur votre subscription, tout sera proprement arrêté. Et surtout, le code est beaucoup plus concis et lisible.

return Observable.Create<WebRequest>(observer => { try { var r = (HttpWebRequest)WebRequest.CreateHttp(url); r.Method = "POST"; observer.OnNext(r); observer.OnCompleted(); } catch (Exception e) { observer.OnError(e); } return () => { }; }) .SubscribeOn(Scheduler.ThreadPool) .SelectMany(r => Observable.FromAsyncPattern<Stream>(r.BeginGetRequestStream, r.EndGetRequestStream)(), (r, s) => new { request = r, stream = s }) .Do(a => { var bytes = Encoding.UTF8.GetBytes("mes données post"); a.stream.Write(bytes, 0, bytes.Length); a.stream.Close(); }) .Select(a => a.request) .SelectMany(r => Observable.FromAsyncPattern<WebResponse>(r.BeginGetResponse, r.EndGetResponse)());

De la même manière, c’est grâce au select many que je vais pouvoir combiner des résultats de mon service. Par exemple, j’ai une méthode GetPlayers qui doit me renvoyer une liste de joueurs, sans score. Une méthode GetPlayerRanks qui me renvoie les scores d’un joueur donné. Je veux faire une méthode GetPlayersWithRanks qui doit faire un appel à GetPlayers puis pour chaque joueur récupérer ses scores et me renvoyer des objets Player correctement hydratés.

La magie réside dans le sélecteur que l’on peut passer comme deuxième argument au selectmany.

public class PlayerService { public IObservable<Player> GetPlayers() { return ObservableExtensions.GetResponseWithRetries("http://ipv4.fiddler/WcfService1/Service1.svc/Players") .SelectMany(response => { using (var responseStream = response.GetResponseStream()) { var dataContractJsonSerializer = new DataContractJsonSerializer(typeof(List<Player>)); return dataContractJsonSerializer.ReadObject(responseStream) as List<Player>; } }); } public IObservable<List<Rank>> GetPlayerRanks(int playerId) { return ObservableExtensions.GetResponseWithRetries("http://ipv4.fiddler/WcfService1/Service1.svc/Ranks/" + playerId) .Select(response => { using (var responseStream = response.GetResponseStream()) { var dataContractJsonSerializer = new DataContractJsonSerializer(typeof(List<Rank>)); return dataContractJsonSerializer.ReadObject(responseStream) as List<Rank>; } }); } public IObservable<Player> GetPlayersWithRanks() { return GetPlayers() .SelectMany(p => GetPlayerRanks(p.Id), (p, r) => new { player = p, ranks = r }) .Do(a => a.player.Ranks = a.ranks) .Select(a => a.player); } }

Avec Fiddler qui reste ouvert dans un coin, je peux vérifier qu’il y a bien le bon nombre de requêtes qui passent.

image

 

 

L’autre gros point a éviter (sauf dans certains cas particuliers mais j’y reviendrais dans un prochain post), c’est l’utilisation de subject plutôt que d’un Observable.Create. Par exemple, testez la méthode GetPlayers ci dessous.

public IObservable<Player> GetPlayers() { var subject = new Subject<Player>(); var request = WebRequest.CreateHttp("http://ipv4.fiddler/WcfService1/Service1.svc/Players"); Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse)() .Subscribe(response => { using (var responseStream = response.GetResponseStream()) { var dataContractJsonSerializer = new DataContractJsonSerializer(typeof(List<Player>)); var players = dataContractJsonSerializer.ReadObject(responseStream) as List<Player>; foreach (var player in players) subject.OnNext(player); subject.OnCompleted(); } }); return subject; }

Là où vous consommez les données, gardez uniquement l’appel à la méthode GetPlayers et commentez le subscribe. Executez le projet et jetez un coup d’oeil à Fiddler.

La requête va partir alors qu’aucun observer ne s’est abonné à la source observable ! ! !

var observable = service.GetPlayers(); //observable.Subscribe(player => // { // }, error => // { // });

Si l’abonnement se fait à posteriori, et que les résultats du service avaient été obtenus avant, alors ils seraient perdus. Il faudrait alors commencer à essayer de contourner le problème avec un ReplaySubject par exemple. mais le comportement ne serait absolument pas logique si jamais on avait souhaité combiner n requête avec un merge par exemple.

Dans la majorité des cas, un observable.create suffira et vous simplifiera la vie lors de la création de votre couche d’accès. Pour bien comprendre la différence entre les deux, je vous renvoie vers ce précédent post : http://blogs.developpeur.org/leo/archive/2012/04/25/reactive-extensions-consommer-des-services-avec-rx-partie-1-cr-er-une-source-observable.aspx

 

En espérant que cet article vous aidera à créer des applications plus solides et plus réactives grâce à rx !

 

A bientôt


Classé sous , ,

Rx pour un BusyMessage plus user-friendly sur Windows Phone 7

Sur Windows Phone, lorsque j’ai des appels à des services qui s’enchainent, j’aime assez voir le status des opérations s’afficher dans le progress indicator pour bien montrer à l’utilisateur qu’il se passe des choses. Et pour que cela soit encore plus joli, j’aimerais que le dernier message, le plus important (ie. “upload terminé avec succès”), reste affiché quelques secondes. C’est le genre de chose que rx permet de faire très simplement, simplifiant également la concurrence.

 

Voici ma page d’exemple : un toggleswitch permet de déterminer si le prochain message sera un message de fin d’opérations, càd qu’il doit rester affihé quelques secondes puis disparaitre, un bouton pour publier une notification.

<shell:SystemTray.ProgressIndicator> <shell:ProgressIndicator IsIndeterminate="{Binding IsBusy}" IsVisible="{Binding IsBusy}" Text="{Binding BusyMessage}" /> </shell:SystemTray.ProgressIndicator> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <toolkit:ToggleSwitch IsChecked="{Binding NextBusy, Mode=TwoWay}" Content="Next publish is busy ?" /> <Button Grid.Row="1" Content="Publish" Click="Button_Click" /> </Grid>

Pour mes notifications, je vais utiliser une classe portant le message et la valeur de IsBusy (les Tuple manquent cruellement sur WP7 !).

public class BusyNotification { public string BusyMessage { get; set;} public bool IsBusy{get; set;} }

Je vais utiliser une source observable hot (pour mémoire http://blogs.developpeur.org/leo/archive/2012/04/25/reactive-extensions-consommer-des-services-avec-rx-partie-1-cr-er-une-source-observable.aspx) puisqu’elle doit continuer à vivre en tout temps.

private Subject<BusyNotification> _notifications;

Au clic sur le bouton, je publie une notification …

private void Button_Click(object sender, RoutedEventArgs e) { _notifications.OnNext(new BusyNotification { BusyMessage = DateTime.Now.ToLongTimeString(), IsBusy = NextBusy }); }

Le plus intéressant maintenant, je vais observer ma source de deux manières :

- la première prend tous les messages et se contente d’afficher le progress indicator et d’y placer le bon contenu ;

- la seconde va faire un throttle sur les messages (ici de 5 secondes, c’est le temps durant lequel le message restera affiché), ainsi elle ne recevra que le dernier à être passé dans la séquence, puis on filtre sur le fait que IsBusy soit false, càd que le message doit faire disparaitre le progress indicator. Grâce au throttle, si une autre opération venait à être lancée entre temps, alors on reprend des messages avec IsBusy a true, la condition du where n’est pas satisfaite et donc le progress indicator ne disparait pas.

_notifications.ObserveOnDispatcher() .Subscribe(n => { if (!IsBusy) IsBusy = true; BusyMessage = n.BusyMessage; }); _notifications.Throttle(TimeSpan.FromSeconds(5)) .Where(n => !n.IsBusy) .ObserveOnDispatcher() .Subscribe(n => { IsBusy = false; BusyMessage = string.Empty; });

Vous aussi, faîtes des applis plus mieux grâce à rx !


Classé sous , ,

Reactive Extensions : Consommer des services avec Rx Partie 2, consommer un service

Dans cet article, je vais montrer un exemple d’utilisation de rx pour la construction d’une couche de service dans une application wp7. Pour l’exemple, j’utilise un service qui expose en json une liste de joueurs et une méthode pour récupérer le score d’un joueur.

Basiquement, dans ce genre de scénario, avec ou sans rx, la première étape consiste à créer une web request.

var request = (HttpWebRequest)WebRequest.Create("http://localhost/WcfService1/Service1.svc/Players");

Le premier opérateur qui nous sera utile est celui qui gère automatiquement le pattern async utilisé par les webrequest. Ainsi, la méthode ci dessous me retournera une séquence observable de webresponse.

Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse)()

A partir de là, je peux utiliser des opérateurs de projection tels que Select ou SelectMany afin d’extraire une liste d’objets depuis le flux json de la réponse. Si une exception était levée à ce stade, elle serait automatiquement captée par l’observer.

Ainsi, je peux écrire la première méthode de ma couche de service.

public IObservable<Player> GetPlayers() { var request = (HttpWebRequest)WebRequest.Create("http://localhost/WcfService1/Service1.svc/Players"); return Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse)() .SelectMany(response => { using (var responseStream = response.GetResponseStream()) { var dataContractJsonSerializer = new DataContractJsonSerializer(typeof(List<Player>)); return dataContractJsonSerializer.ReadObject(responseStream) as List<Player>; } }); }

Rx nous propose des opérateurs de base pour gérer automatiquement timeout et politique de retry. Ainsi, dans l’exemple ci dessous, en cas de congestion réseau, si le temps d’execution de la requête dépasse 20 secondes, une exception sera levée, et un nouvel essai aura lieu, et ainsi de suite 3 fois avant que l’exception soit remontée à mon observer.

public IObservable<Player> GetPlayers() { var request = (HttpWebRequest)WebRequest.Create("http://localhost/WcfService1/Service1.svc/Players"); return Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse)() .Timeout(TimeSpan.FromSeconds(20)) .Retry(3) .SelectMany(response => { using (var responseStream = response.GetResponseStream()) { var dataContractJsonSerializer = new DataContractJsonSerializer(typeof(List<Player>)); return dataContractJsonSerializer.ReadObject(responseStream) as List<Player>; } }); }

Enfin, dans mon view model, mon observer recevra simplement les objets player un par un, et je pourrais gérer propement mon erreur.

playerService.GetPlayers() .Subscribe(player => { _players.Add(player); }, error => { });

Mamheureusement, si on execute l’application à ce stade, l’exception suivante apparaît.

image

En fait, le traitement effectué par la méthode FromAsyncPattern est déporté sur le thread pool. A partir de là, les opérations qui suivent sont executées sur le même thread, y compris les callbacks de l’observer.

Pour preuve, ajoutons quelques infos de debug …

private void Button_Click(object sender, RoutedEventArgs e) { Debug.WriteLine(Thread.CurrentThread.ManagedThreadId); var playerService = new PlayerService(); playerService.GetPlayers() .Subscribe(player => { Debug.WriteLine(Thread.CurrentThread.ManagedThreadId); _players.Add(player); }, error => { }); }
public IObservable<Player> GetPlayers() { var request = (HttpWebRequest)WebRequest.Create("http://localhost/WcfService1/Service1.svc/Players"); return Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse)() .Timeout(TimeSpan.FromSeconds(20)) .Retry(3) .SelectMany(response => { Debug.WriteLine(Thread.CurrentThread.ManagedThreadId); using (var responseStream = response.GetResponseStream()) { var dataContractJsonSerializer = new DataContractJsonSerializer(typeof(List<Player>)); return dataContractJsonSerializer.ReadObject(responseStream) as List<Player>; } }); }

Rx offre la possibilité de refaire passer le traitement sur un autre scheduler via la méthode ObserveOn, et, dans le cas du dispatcher, directement via la méthode ObserveOnDispatcher.

playerService.GetPlayers() .ObserveOnDispatcher() .Subscribe(player => { Debug.WriteLine(Thread.CurrentThread.ManagedThreadId); _players.Add(player); }, error => { });

Il y a un effet que j’aime bien sur WP7, c’est le fait de voir les éléments “popper” un par un dans une liste. Je me suis donc fait mon petit opérateur pour cet effet… La méthode zip renvoie le résullutat d’un selecteur que l’on applique à deux sources observables. L’observer sera alors notifié dès qu’un élément sera dispo dans les deux collections. Ainsi si je fais un zip entre mon observable de player et un timer de n ms, je recevrais bien un player tous les n ms.

public static IObservable<T> Cadence<T>(this IObservable<T> observable, TimeSpan cadence) { return observable.Zip(Observable.Timer(DateTime.Now, cadence), (r, t) => r); }

Je peux appliquer mon nouvel opérateur.

playerService.GetPlayers() .Cadence(TimeSpan.FromMilliseconds(100)) .ObserveOnDispatcher() .Subscribe(player => { _players.Add(player); }, error => { });

Il y a un point important qui peut encore poser problème : la création d’une webrequest peut lever une exception et nous n’avons pas géré ce cas. Par exemple avec …

var request = (HttpWebRequest)WebRequest.Create("htt://localhost/WcfService1/Service1.svc/Players");

Pour remédier à ce problème, nous allons créer une source observable comme vu dans le précédent article, et nous déporterons la création de la requête sur le threadpool. Ainsi la création ne sera plus bloquante et l’erreur pourra être gérée proprement via l’observer.

public static IObservable<WebResponse> GetResponseWithRetries(string url) { return Observable.Create<WebRequest>(observer => { Scheduler.ThreadPool.Schedule(() => { try { observer.OnNext((HttpWebRequest)WebRequest.Create(url)); observer.OnCompleted(); } catch (Exception e) { observer.OnError(e); } }); return () => { }; }) .SelectMany(r => Observable.FromAsyncPattern<WebResponse>(r.BeginGetResponse, r.EndGetResponse)()) .Timeout(TimeSpan.FromSeconds(20)) .Retry(3); }

La méthode de notre couche de service peut donc s’écrire de la manière suivante …

public IObservable<Player> GetPlayers() { return ObservableExtensions.GetResponseWithRetries("http://localhost/WcfService1/Service1.svc/Players") .SelectMany(response => { using (var responseStream = response.GetResponseStream()) { var dataContractJsonSerializer = new DataContractJsonSerializer(typeof(List<Player>)); return dataContractJsonSerializer.ReadObject(responseStream) as List<Player>; } }); }

Ou dans le cas d’une autre méthode…

public IObservable<int> GetPlayerRankValue(int id) { return ObservableExtensions.GetResponseWithRetries("http://localhost/WcfService1/Service1.svc/PlayerRank/" + id) .Select(response => { using (var responseStream = response.GetResponseStream()) { var dataContractJsonSerializer = new DataContractJsonSerializer(typeof(Rank)); return dataContractJsonSerializer.ReadObject(responseStream) as Rank; } }) .Select(rank => rank.Value); }

Enfin, je peux même combiner très facilement le résultat de mes requêtes …

public IObservable<Player> GetPlayersWithRank() { return from player in GetPlayers() select new Player { Id = player.Id, Name = player.Name, Rank = GetPlayerRankValue(player.Id).Single() }; }

Voila, j’espère que cet article vous aura aidé à construire des applications plus réactives grâce à Rx !


Classé sous ,

Reactive Extensions : Consommer des services avec Rx Partie 1, créer une source observable

Pour créer une source obsevable sans implémenter IObservable à la main, il y a deux principales solutions :

- utiliser la méthode Observable.Create<T> pour créer une séquence cold;

- utiliser l’instance d’une classe qui implémente ISubject<T>, et qui implémentera donc aussi IObservable<T> et IObserver<T> pour créer une séquence hot.

 

Une séquence cold signifie qu’elle est passive, c’est à dire qu’elle n’est exécutée que lorsque quelqu’un s’y abonne. Dans le cas d’un Observable.Create, la func que l’on passe en paramètre à create sera appelée à chaque subscribe. C’est typiquement ce genre de flux que l’on utilisera pour une requête asynchrone vers un web service.

Cette func reçoit en paramètre un observer et doit renvoyer une action qui sera éxécutée lors du désabonnement.

class Program { static IObservable<int> GetColdStream() { return Observable.Create<int>(observer => { observer.OnNext(1); observer.OnNext(2); observer.OnCompleted(); return () => { Console.WriteLine("unsubscribed"); }; }); } static void ConsumeStream(IObservable<int> observable) { var disposable = observable.Subscribe(n => { Console.WriteLine(n); }, exp => { }, () => { Console.WriteLine("completed"); }); disposable.Dispose(); } static void Main(string[] args) { var coldStream = GetColdStream(); ConsumeStream(coldStream); ConsumeStream(coldStream); Console.ReadLine(); } }

donne

1 2 completed unsubscribed 1 2 completed unsubscribed

 

A l’inverse une séquence hot est active, c’est à dire qu’elle vit sa vie et qu’un subscribe agit un peu comme l’abonnement à un évènement et on ne sera notifié que de ce qui se passe après s’être abonné (sauf cas particuliers ci dessous). C’est le genre de flux que l’on utiliserait pour réagir à des situations “continues” du type mouvement de la souris ou traffic UDP.

Un moyen simple de mettre votre propre séquence hot en place est d’utiliser un Subject. En fait, il y a plusieurs implémentations de ISubject<T> dans l’espace de noms System.Reactive.Subjects, mais elles ont toutes un comportement différent. Ainsi il ne faut surtout pas tomber dans  le piège du bien mal nommé AsyncSubject<T>

 

D’abord, Subject<T>, c’est l’implémentation basique de ISubject<T>. On ne sera notifié que de l’arrivée de nouveaux éléments après avoir fait un subscribe.

static void Main(string[] args) { var subject = new Subject<int>(); subject.OnNext(0); subject.Subscribe(n => { Console.WriteLine(n); }, error => { Console.WriteLine("error"); }, () => { Console.WriteLine("completed"); }); subject.OnNext(1); subject.OnNext(2); subject.OnCompleted(); Console.ReadLine(); }

… donne en sortie …

1 2 completed

ReplaySubject<T> notifiera automatiquement tout nouveau subscriber de l’intégralité des éléments qui sont passés dans le flux, y compris ceux antérieurs au subscribe.

static void Main(string[] args) { var subject = new ReplaySubject<int>(); subject.OnNext(0); subject.Subscribe(n => { Console.WriteLine(n); }, error => { Console.WriteLine("error"); }, () => { Console.WriteLine("completed"); }); subject.OnNext(1); subject.OnNext(2); subject.OnCompleted(); Console.ReadLine(); }

… donne en sortie …

0 1 2 completed

Un peu dans le même principe, BehaviorSubject<T> va conserver la dernière valeur ou une valeur par défaut (qu’il faut obligatoirement passer au constructeur).

static void Main(string[] args) { var subject = new BehaviorSubject<int>(-1); subject.Subscribe(n => { Console.WriteLine(n); }, error => { Console.WriteLine("error"); }, () => { Console.WriteLine("completed"); }); subject.OnNext(1); subject.OnNext(2); subject.OnCompleted(); Console.ReadLine(); }

… donne …

-1 1 2 completed

Enfin, AsyncSubject<T> n’a rien d’asynchrone : il ne retournera qu’une seule valeur, la dernière passée au flux, et la retournera que suite à un appel à OnCompleted();

static void Main(string[] args) { var subject = new AsyncSubject<int>(); subject.OnNext(0); subject.Subscribe(n => { Console.WriteLine(n); }, error => { Console.WriteLine("error"); }, () => { Console.WriteLine("completed"); }); subject.OnNext(1); subject.OnNext(2); subject.OnCompleted(); Console.ReadLine(); }

… donne …

2 completed

Maintenant que nous savons comment créer une source, la prochaine étape sera d’exposer le résultat d’un service au travers de cette source.


Classé sous ,

Factoriser des requêtes Linq To Entities avec LinqKit

Afin d’améliorer la lisibilité de mes requêtes et de faciliter la maintenance du code des application sur lesquelles je suis amené à travailler, j’aimerais avoir la possibilité de factoriser du code au sein de ces requêtes.

L’exemple ci dessous est un cas simplifié de ce que je trouve “moche”.

 

var ordersWithTotal = from order in dataContext.Orders select new { Id = order.Id, Total = order.OrderDetails.Sum(od => od.Quantite * od.Article.Price) }; var shippingWithTotal = from shipping in dataContext.Shippings select new { Id = shipping.Id, Total = shipping.OrderDetails.Sum(od => od.Quantite * od.Article.Price) };

En fait, je préfèrerais pouvoir écrire quelque chose comme ci dessous.

var ordersWithTotal = from order in dataContext.Orders select new { Id = order.Id, Total = order.OrderDetails.Sum(od => od.GetTotal()) }; var shippingWithTotal = from shipping in dataContext.Shippings select new { Id = shipping.Id, Total = shipping.OrderDetails.Sum(od => od.GetTotal()) };

Le problème c’est qu’au moment où Entity Framework va vouloir traduire cette requête en SQL, il ne va pas savoir ce qu’est ce GetTotal ce qui fera crasher mon application au runtime.

Si je fais un appel à ToString sur ma requête, voici ce qui est retourné, pour ma version où mon calcul est dans la requête …

 

System.Collections.Generic.List`1[ConsoleApplication1.Order].Select(order => new <>f__AnonymousType0`2(Id = order.Id, Total = order.OrderDetails.Sum(od => (od.Quantite * od.Article.Price))))

… et pour la version où le calcul est dans une méthode externe.

System.Collections.Generic.List`1[ConsoleApplication1.Order].Select(order => new <>f__AnonymousType0`2(Id = order.Id, Total = order.OrderDetails.Sum(od => od.GetTotal())))

Pour faire fonctionner ma requête, je dois donc trouver un moyen de remplacer l’appel à GetTotal par le contenu de la méthode. Ceci est possible grâce au projet LinqKit.

LinqKit propre une méthode d’extension Expand utilisable sur les expressions. Cette méthode va utiliser un visitor afin d’analyser le contenu d’une expression et de remplacer les éventuelles invocations d’autres expressions par leur contenues.

Ainsi l’exemple ci dessous …

Expression<Func<int, int>> e1 = i => i + 1; Expression<Func<int, int>> e2 = i => e1.Invoke(i) * i; Console.WriteLine(e1.ToString()); Console.WriteLine(e2.ToString()); Console.WriteLine(e2.Expand().ToString());

… donnera la sortie ci dessous. Sur la troisième ligne, on voit bien que dans e2, l’invocation de e1 a été remplacée par son contenu.

i => (i + 1) i => (value(ConsoleApplication1.Program+<>c__DisplayClass0).e1.Invoke(i) * i) i => ((i + 1) * i)

Pour en revenir à mon problème initial avec ma requête L2E, LinqKit dispose d’une méthode d’extension pour les IQueryable<T> appelée AsExpendable. Cette méthode va créer une instance de ExpendableQuery<T>, un simple wrapper autour de l’IQueryable de base. Elle va fournir son propre IQueryProvider de type ExpendableQueryProvider<T> qui se chargera ensuite d’appeler automatiquement la méthode Expand au moment d’exécuter la requête.

Mon exemple de départ peut donc s’écrire de la manière suivane.

Expression<Func<OrderDetail, int>> getTotal = od => od.Quantite * od.Article.Price; var ordersWithTotal = from order in dataContext.Orders.AsExpandable() select new { Id = order.Id, Total = order.OrderDetails.Sum(od => getTotal.Invoke(od)) }; var shippingWithTotal = from shipping in dataContext.Shippings.AsExpandable() select new { Id = shipping.Id, Total = shipping.OrderDetails.Sum(od => getTotal.Invoke(od)) };

Et si on jette un coup d’oeil à l’expression, on constate qu’on a bien quelque chose de compréhensible par EF.

System.Collections.Generic.List`1[ConsoleApplication1.Order].Select(order => new <>f__AnonymousType0`2(Id = order.Id, Total = order.OrderDetails.Sum(od => (od.Quantite * od.Article.Price))))

Et si j’ai des requêtes encore plus complexes, je peux tout à fait imbriquer les invocations.

Expression<Func<OrderDetail, int>> getOrderDetailsTotal = od => od.Quantite * od.Article.Price; Expression<Func<Order, int>> getOrderTotal = o => o.OrderDetails.Sum(od => getOrderDetailsTotal.Invoke(od)); var ordersWithTotal = from order in dataContext.Orders.AsExpandable() select new { Id = order.Id, Total = getOrderTotal.Invoke(order) };

 

A bientôt !


Classé sous ,

Azure et les shared access signature

Aujourd’hui, je souhaite télécharger des données confidentielles des utilisateurs d’une de mes applications depuis le blob storage d’azure.

Pour répondre à cette problémaque, l’approche que je met en place consiste à créer un blob container privé par utilisateur et à générer une clé d’accès temporaire à la demande qui devra être utilisée dans les requêtes de téléchargement de mes blobs. Dans Azure, ce mécanisme s’appelle les Shared Access Signature.

Côté service de mon application, je crée donc une méthode pour générer une shared access signature sur le blob container de l’utilisateur concerné. Ici, je lui donne uniquement l’autorisation de lecture sur le container. Ma clé d’accès aura une durée de vie paramétrable, par exemple 1 heure. Mon service renvoie donc la clé ainsi que sa durée de validité.

var sas = cloudBlobContainer.GetSharedAccessSignature(new SharedAccessPolicy() { Permissions = SharedAccessPermissions.Read, SharedAccessStartTime = DateTime.UtcNow, SharedAccessExpiryTime = DateTime.UtcNow + leaseDuration });

Côté client , par exemple une application Silverlight, je peux faire un petit gestionnaire de clé qui possédera uniquement une méthode . le but de cette méthode étant de récupérer la clé depuis mon service et de la mettre en cache pendant toute sa durée de validité afin de ne pas regénérer d’autres clés inutilement.

L’implémentation ci desous est un exemple rapide, elle n’est pas thread safe et ne gère par non plus le fait que la clé doit être stockée dans le storage relativement à l’utilisateur courant de l’application.

 

public class SharedAccessSignatureManager { private readonly Service _service; private Func<IObservable<Signature>> _getSharedAccessSignature; public SharedAccessSignatureManager(Service service) { _service = service; _getSharedAccessSignature = Observable.FromAsyncPattern<Signature>(_service.BeginGenerateSharedAccessSignatureForUser, _service.EndGenerateSharedAccessSignatureForUser); } private string LoadFromIsolatedStorage() { using (var isolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication()) { if (isolatedStorageFile.FileExists("sharedAccessSignature")) { using (var stream = isolatedStorageFile.OpenFile("sharedAccessSignature", System.IO.FileMode.Open)) { var buffer = new byte[stream.Length]; stream.Read(buffer, 0, buffer.Length); var startIndex = 0; var ticks = BitConverter.ToInt64(buffer, 0); if (ticks >= DateTime.Now.Ticks) { startIndex += sizeof(long); return Encoding.UTF8.GetString(buffer, startIndex, buffer.Length - startIndex); } } } return string.Empty; ; } } private void WriteToIsolatedStorage(string signature, TimeSpan leaseDuration) { using (var isolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication()) { if (isolatedStorageFile.FileExists("sharedAccessSignature")) isolatedStorageFile.DeleteFile("sharedAccessSignature"); using (var file = isolatedStorageFile.CreateFile("sharedAccessSignature")) { var now = DateTime.Now; now += leaseDuration; var ticks = now.Ticks; var bTicks = BitConverter.GetBytes(ticks); var bSignature = Encoding.UTF8.GetBytes(signature); foreach (var b in bTicks) file.WriteByte(b); foreach (var b in bSignature) file.WriteByte(b); } } } private void RetrieveFromService(Action<string> onCompleted) { _getSharedAccessSignature().Subscribe(result => { WriteToIsolatedStorage(result.Signature, result.LeaseDuration); onCompleted(result.Signature); }); } private bool _busy; private readonly List<Action<string>> _toNotify = new List<Action<string>>(); public void GetSignature(Action<string> onCompleted) { _toNotify.Add(onCompleted); if (!_busy) { _busy = true; var fromLocal = LoadFromIsolatedStorage(); if (fromLocal != string.Empty) { FinishGetSignature(fromLocal); } else { RetrieveFromService(s => { FinishGetSignature(s); }); } } } private void FinishGetSignature(string signature) { foreach (var n in _toNotify) { n(signature); } _busy = false; _toNotify.Clear(); } }

Enfin, je peux utiliser mon manager simplement de la manière suivante et je n’ai qu’à concatener ma clé à l’adresse du blob que je veux télécharger.

 

_sharedAccessSignatureManager.GetSignature(s => { var request = HttpWebRequest.CreateHttp(blob.Uri.ToString() + s); request.BeginGetResponse(cb => { var response = request.EndGetResponse(cb); using (var stream = response.GetResponseStream()) { // ... } }, null); });

Notez que pour que le téléchargement fonctionne depuis une application Silverlight, il faut aussi penser à uploader un ClientAccessPolicy.xml à la racine du storage.

 

A bientôt !


Classé sous ,

Roslyn : De l’auto complétion pour le nommage de mes variables

Quand j’écris du code, je suis un peu maniaque dans ma manière de nommer mes variables, propriétés, méthodes etc. Si je dois reprendre du code écrit par quelqu’un d’autre et que ce code ne respecte pas les conventions de nommage que j’ai l’habitude d’utiliser, ça me stresse (attitude totalement égocentrique et j’imagine que ça agace mes collègues… Smile).

Je ne sais jamais si cela s’appelle du pascal case, du camel case ou autre, toujours est-il que le bout de code ci-dessous reprend les conventions que j’utilise.

class Program { private readonly IEnumerable<ProductDetail> _productDetails = new List<ProductDetail>(); public IEnumerable<ProductDetail> ProductDetails { get { return _productDetails; } } static void Main(string[] args) { Program program; } public void AddProductDetail(ProductDetail productDetail) { } } class ProductDetail { public string Name { get; set; } }

Grâce à Roslyn, je vais pouvoir me développer un add in qui m’aidera à automatiquement nommer les éléments de mon code comme ci dessus.

La CTP de roslyn est livrée avec plusieurs template de projet, j’utilise ici un Completion Provider. L’idée étant d’avoir une classe qui implémente l’interface ICompletionProvider, et donc une méthode GetItems qui retourne une collection de ICompletionItem.

Voici déjà quelques helpers pour formatter correctement mes chaines de caractères et pour les passer facilement au pluriel.

public static class StringExtensions { private static readonly IList<string> Unpluralizables = new List<string> { "equipment", "information", "rice", "money", "species", "series", "fish", "sheep", "deer" }; private static readonly IDictionary<string, string> Pluralizations = new Dictionary<string, string> { { "person", "people" }, { "ox", "oxen" }, { "child", "children" }, { "foot", "feet" }, { "tooth", "teeth" }, { "goose", "geese" }, { "(.*)fe?", "$1ves" }, { "(.*)man$", "$1men" }, { "(.+[aeiou]y)$", "$1s" }, { "(.+[^aeiou])y$", "$1ies" }, { "(.+z)$", "$1zes" }, { "([m|l])ouse$", "$1ice" }, { "(.+)(e|i)x$", @"$1ices"}, { "(octop|vir)us$", "$1i"}, { "(.+(s|x|sh|ch))$", @"$1es"}, { "(.+)", @"$1s" } }; public static string Pluralize(this string str) { if (Unpluralizables.Contains(str)) return str; var plural = ""; foreach (var pluralization in Pluralizations) { if (Regex.IsMatch(str, pluralization.Key)) { plural = Regex.Replace(str, pluralization.Key, pluralization.Value, RegexOptions.IgnoreCase); break; } } return plural; } public static string ToLowerCamelCase(this string str) { var chars = str.ToCharArray(); chars[0] = char.ToLower(chars[0]); return new string(chars); } }

Tous les éléments de syntaxe dans mon code sont des SyntaxNode (j’y reviendrais plus tard dans ce billet). Lorsque l’élément représente un paramètre ou une propriété par exemple, il est donc lié à un type représenté par la classe TypeSyntax. Attention, le TypeSyntax représente un type dans l’arbre syntaxique, en gros, juste une chaine de caractères, pas un Type tel qu’on manipule habituellement en .net. Pour pouvoir faire le lien entre ce TypeSyntax et le Type qu’il représente, on peut utiliser une instance de ISemanticModel. Etant liée au fichier de code source courant, et donc à a son contexte, elle saura retrouver le bon type correspondant.

Voici donc une méthodes d’extension pour les TypeSyntax qui me renverra la chaine de caractère adéquat. Grâce au semantic model, je peux vérifier si le type implémente IEnumrable<T> et donc pluraliser le type T en conséquence.

public enum CompletionFormat { UpperCase, LowerCase, UnderscoreWithLowerCase } public static class TypeSyntaxExtensions { public static IEnumerable<CompletionItem> ToCompletionItems(this TypeSyntax typeSyntax, ISemanticModel semanticModel, CompletionFormat completionFormat) { var completionItems = new List<CompletionItem>(); var typeSymbol = semanticModel.GetSemanticInfo(typeSyntax).Type as NamedTypeSymbol; // doest type reppresent a generic list ? if (typeSymbol != null && typeSymbol.AllInterfaces.Any(i => i.Name == typeof(IEnumerable).Name) && typeSymbol.TypeArguments.Count > 0) { var firstTypeArgument = typeSymbol.TypeArguments.First(); var pluralizedName = firstTypeArgument.Name.Pluralize(); switch (completionFormat) { case CompletionFormat.UpperCase: completionItems.Add(new CompletionItem(pluralizedName)); break; case CompletionFormat.LowerCase: completionItems.Add(new CompletionItem(pluralizedName.ToLowerCamelCase())); break; case CompletionFormat.UnderscoreWithLowerCase: completionItems.Add(new CompletionItem(string.Format("_{0}", pluralizedName.ToLowerCamelCase()))); break; } } else { switch (completionFormat) { case CompletionFormat.UpperCase: completionItems.Add(new CompletionItem(typeSyntax.PlainName)); break; case CompletionFormat.LowerCase: completionItems.Add(new CompletionItem(typeSyntax.PlainName.ToLowerCamelCase())); break; case CompletionFormat.UnderscoreWithLowerCase: completionItems.Add(new CompletionItem(string.Format("_{0}", typeSyntax.PlainName.ToLowerCamelCase()))); break; } } return completionItems; } }

Pour terminer, il ne me reste plus que la méthode GetItems à compléter. Je récupère le syntax tree du document courant, je récupère le semantic model, je récupère l’élément courant dans mon syntax tree et je regarde son type. Le syntax tree est composé de syntax node qui peuvent être des FieldDeclarationSyntax, des PropertyDeclaractionSyntax, des VariableDeclarationSyntax etc. Selon ce type, j’appelle la méthode d’extension ToCompletionItems sur le TypeSyntax associé en lui passant la valeur de mon énumération CompletionFormat qui correspond à mes attentes en métière de nommage.

[Order(After = PredefinedCompletionProviderNames.Keyword)] [ExportCompletionProvider("CompletionProvider1", LanguageNames.CSharp)] class CompletionProvider : ICompletionProvider { public IEnumerable<ICompletionItem> GetItems(IDocument document, int position, CancellationToken cancellationToken) { var syntaxTree = (SyntaxTree)document.GetSyntaxTree(cancellationToken); var semanticModel = document.Project.GetCompilation().GetSemanticModel(syntaxTree); var token = (SyntaxToken)syntaxTree.Root.FindToken(position); var ancestors = token.Parent.AncestorsAndSelf(); if (ancestors.OfType<FieldDeclarationSyntax>().Any()) { var fieldDeclarationSyntax = ancestors.OfType<FieldDeclarationSyntax>().First(); if (fieldDeclarationSyntax.Modifiers.Any(c => c.Kind == SyntaxKind.PrivateKeyword)) return fieldDeclarationSyntax.Declaration.Type.ToCompletionItems(semanticModel, CompletionFormat.UnderscoreWithLowerCase); else return fieldDeclarationSyntax.Declaration.Type.ToCompletionItems(semanticModel, CompletionFormat.UpperCase); } else if (ancestors.OfType<PropertyDeclarationSyntax>().Any()) { var propertyDeclarationSyntax = ancestors.OfType<PropertyDeclarationSyntax>().First(); return propertyDeclarationSyntax.Type.ToCompletionItems(semanticModel, CompletionFormat.UpperCase); } else if (ancestors.OfType<VariableDeclarationSyntax>().Any()) { var variableDeclarationSyntax = ancestors.OfType<VariableDeclarationSyntax>().First(); return variableDeclarationSyntax.Type.ToCompletionItems(semanticModel, CompletionFormat.LowerCase); } else if (ancestors.OfType<ParameterSyntax>().Any()) { var parameterSyntax = ancestors.OfType<ParameterSyntax>().First(); return parameterSyntax.TypeOpt.ToCompletionItems(semanticModel, CompletionFormat.LowerCase); } else if (ancestors.OfType<ParameterListSyntax>().Any()) { var parameterListSyntax = ancestors.OfType<ParameterListSyntax>().First(); var parameterSyntax = parameterListSyntax.ChildNodes().OfType<ParameterSyntax>().LastOrDefault(); if (parameterSyntax != null) { return parameterSyntax.TypeOpt.ToCompletionItems(semanticModel, CompletionFormat.LowerCase); } } return Enumerable.Empty<ICompletionItem>(); } }

En espérant que ça puisse vous être utile Smile


Classé sous ,
Plus de Messages Page suivante »

Les 10 derniers blogs postés

- Etes-vous yOS compatible ? (2/3) : la nouvelle plateforme Yammer–Office 365–SharePoint par Le blog de Patrick [MVP SharePoint] le 04-22-2014, 09:27

- [ #Yammer ] [ #Office365 ] Quelques précisions sur l’activation de Yammer Entreprise par Le blog de Patrick [MVP SharePoint] le 04-22-2014, 09:03

- Après Montréal, ce sera Barcelone, rendez-vous à la European SharePoint Conference 2014 ! par Le blog de Patrick [MVP SharePoint] le 04-19-2014, 09:21

- Emportez votre sélection de la MSDN dans la poche ? par Blog de Jérémy Jeanson le 04-17-2014, 22:24

- [ #Office365 ] Pb de connexion du flux Yammer ajouté à un site SharePoint par Le blog de Patrick [MVP SharePoint] le 04-17-2014, 17:03

- NFluent & Data Annotations : coder ses propres assertions par Fathi Bellahcene le 04-17-2014, 16:54

- Installer un site ASP.net 32bits sur un serveur exécutant SharePoint 2013 par Blog de Jérémy Jeanson le 04-17-2014, 06:34

- [ SharePoint Summit Montréal 2014 ] Tests de montée en charge SharePoint par Le blog de Patrick [MVP SharePoint] le 04-16-2014, 20:44

- [ SharePoint Summit Montréal 2014 ] Bâtir un site web public avec Office 365 par Le blog de Patrick [MVP SharePoint] le 04-16-2014, 18:30

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