lundi 1 novembre 2010

Rss Converter (Asp.net MVC AsyncController sample)

L’exercice du jour, c’est d’explorer le contrôleur asynchrone ASP.net MVC 2 (et autres nouveautés). Pour cela, le sujet du jour c’est de construire un convertisseur de flux RSS qui transforme les podcasts Channel 9 de base au format wmv, vers d’autres formats, mp4 entre autre, sachant que l’information du nouveau fichier est déjà dans le flux original mais pas dans le bon tag XML. Cette fonctionnalité est déjà fournie par le site Channel 9 lui même en ajoutant /mp3 ou /ipod à l’adresse des flux mais cela fait un bon exercice.

EditorForModel

Commençons par créer un objet conteneur des champs du formulaire de demande de conversion d’un flux.

namespace Channel9Converter.Models
{
public enum TargetType : byte
{
Mp3,
Mp4,
Wmv
}

public class ConverterRequest
{
[DisplayName("Original feed url"), StringLength(200)]
public string OriginalFeedUrl { get; set; }
[DisplayName("Target file format"), UIHint("Enum")]
public TargetType TargetType { get; set; }
}
}

Cette classe porte sur ses propriétés, des attributs qui permettront aux méthodes d'extension du HtmlHelper de générer des labl contenant un texte d’édition, l’attribut UIHint permet de choisir de template qui sera utilisé pour afficher l’éditeur de la propriété TargetType qui est une énumération.
Cet éditeur le voici : un simple fichier Enum.ascx placé dans le répertoire Views\Shared\EditorTemplates du projet.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<object>" %>
<%
if (Model != null)
{
var enumType = Model.GetType();
if (enumType.IsEnum)
{
%>
<%:
Html.DropDownList(
"",
Enum.GetNames(enumType).Select(
n => new SelectListItem() {
Text = n,
Value = n,
Selected = n == Model.ToString()
})
)
%>
<%
}
}
%>

La vue d'index du site affiche un formulaire montrant deux champs : un pour l'adresse du flux à convertir, et un pour le format cible

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<Channel9Converter.Models.ConverterRequest>" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Channel 9 feed converter</title>
</head>
<body>
<h1>
Welcome to the channel 9 feed converter</h1>
<% using (Html.BeginForm("Convert", "Rss", FormMethod.Get))
{%>
Enter a Channel 9 RSS Feed below and click the "Convert !" button<br />
in order to display the same rss feed converted to the file format you selected.<br />
Then copy the new url in the adress bar and use it in any Rss reader you want.
<br /><br />
<%: Html.ValidationSummary(true) %>
<%: Html.EditorForModel() %>
<br /><br />
<input type="submit" value="Convert !" />
<% } %>

<span style="color:red;">You actually only need to add "/ipod" at the end of the original podcast adress</span>
</body>
</html>

Ce qui a pour rendu :

image


AsyncController

On en vient au contrôleur asynchrone, objet principal de ce post. Le but des pages asynchrones, déjà implémentée sous ASP.net depuis le Framework 2, est de ne pas monopoliser des threads IIS (utilisés pour “servir” les requêtes HTTP) à l’attente d’opérations pouvant être longues (dans notre exemple un téléchargement de fichier sur un autre serveur). Cette gestion asynchrone permet, sous certaines conditions (de ne pas rediriger l’attente vers un autre goulot d’étranglement encore plus critique) d’augmenter le nombre de requêtes simultanés que pourra gérer votre site.

Si dessous le code de ce contrôleur asynchrone :

namespace Channel9Converter.Controllers
{
public partial class RssController : AsyncController
{
[OutputCache(Duration = 300, VaryByParam = "*")]
public virtual void ConvertAsync(ConverterRequest request)
{
if (request == null || request.OriginalFeedUrl == null)
return;

AsyncManager.OutstandingOperations.Increment();

var web = new WebClient();
web.OpenReadCompleted += (sender, e) =>
{
AsyncManager.Parameters["stream"] = e.Result;
AsyncManager.Parameters["request"] = request;

AsyncManager.OutstandingOperations.Decrement();
};

web.OpenReadAsync(new Uri(request.OriginalFeedUrl, UriKind.Absolute));
}

string translate_to_content_type(TargetType targetType)
{
switch (targetType)
{
case TargetType.Mp3:
return "audio/mp3";
case TargetType.Mp4:
return "video/mp4";
case TargetType.Wmv:
return "x-ms-wmv";
default:
throw new NotImplementedException();
}
}

public virtual ActionResult ConvertCompleted(Stream stream, ConverterRequest request)
{
var reader = XmlReader.Create(stream, new XmlReaderSettings() { DtdProcessing = System.Xml.DtdProcessing.Parse, ValidationType = ValidationType.None });
var channel9 = SyndicationFeed.Load(reader);

foreach (var item in channel9.Items)
{
var media_group =
item.ElementExtensions.ReadElementExtensions<XElement>("group", @"http://search.yahoo.com/mrss/").FirstOrDefault();
if (media_group == null) continue;

Func<XElement, string, string> attValueOrEmpty = (xe, att_name) =>
{
var att = xe.Attribute(att_name);
if (att == null) return "";
else return att.Value;
};

var media_contents =
from mc in media_group.Descendants()
select new
{
url = attValueOrEmpty(mc, "url"),
expression = attValueOrEmpty(mc, "expression"),
duration = attValueOrEmpty(mc, "duration"),
fileSize = attValueOrEmpty(mc, "fileSize"),
type = attValueOrEmpty(mc, "type"),
medium = attValueOrEmpty(mc, "medium"),
};

var content_type = translate_to_content_type(request.TargetType);
var media = media_contents.Where(obj => obj.type == content_type).FirstOrDefault();
if (media == null) continue;

var link = item.Links.Where(l => l.RelationshipType == "enclosure").FirstOrDefault();
if (link == null) continue;

link.Uri = new Uri(media.url);
link.MediaType = media.type;
link.Length = int.Parse(media.fileSize);
}

return new RssResult(channel9);
}
}
}

L’attribut OutputCache permet encore d’optimiser la charge serveur et surtout dans ce cas le temps de réponse en mettant en cache le rendu de la page. Ici le cache est rafraichi toutes les 5 minutes (300 secondes).

Le résultat de la deuxième méthode est un RssResult, directement inspiré du RssResult proposé par l’excellent Nerd Dinner (http://nerddinner.codeplex.com/) et dont le code est ci dessous.


namespace Channel9Converter.Controllers
{
public class RssResult : FileResult
{
private Uri currentUrl;
private SyndicationFeed _feed;

public RssResult() : base("application/rss+xml") { }

public RssResult(SyndicationFeed feed)
: this()
{
this._feed = feed;
}

public override void ExecuteResult(ControllerContext context)
{
currentUrl = context.RequestContext.HttpContext.Request.Url;
base.ExecuteResult(context);
}

protected override void WriteFile(System.Web.HttpResponseBase response)
{
Rss20FeedFormatter formatter = new Rss20FeedFormatter(_feed);

using (XmlWriter writer = XmlWriter.Create(response.Output))
{
formatter.WriteTo(writer);
}
}
}
}

Note : T4MVC ne me semble pas compatible avec les contrôleurs asynchrones.

samedi 9 octobre 2010

Entity Framework Code Only configuration from attributes

N'aimant pas particulièrement ni les designer, ni les fichier xml, je m'intéresse de plus en plus à la solution "Code Only" proposée bientôt par Entity Framework. Voyant ce post je me suis un peu trop empressé et j’ai implémenté une petite solution permettant de déduire le mapping depuis les DataAnnotations sur les objets du modèle.  

Ce n’est que quelques heures plus tard, super fier de moi (c’était bien plus difficile que prévu), que je m’apprête à publier ma solution et que je tombe sur cet autre post rendant ma solution complètement inutile et obsolète. C’est la vie. Je publie quand même car cet exemple montre une multitude d’utilisations de la Reflection sur des types et des méthodes génériques ainsi que ma première utilisation du mot clé dynamic.

Si vous souhaitez vous servir de Entity Framework Code Only c’est par ici : http://blogs.msdn.com/b/adonet/archive/2010/07/14/ctp4codefirstwalkthrough.aspx

Quelle aventure ! :-s



/// <summary>
/// Classe d'aide à la configuration Entity Framwork
/// permettant de déduire la configuration des attributs Key, Required et StringLength
/// de System.ComponentModel.DataAnnotations présent sur les POCO du model.
/// </summary>
/// <typeparam name="TContext">Type du context (ObjectContext)</typeparam>
class EntitiesConfigurator<TContext>
where TContext : ObjectContext
{
private readonly ContextBuilder<TContext> builder;

public EntitiesConfigurator(ContextBuilder<TContext> builder)
{
this.builder = builder;
}

public EntityConfiguration<T> TypeConfig<T>()
{
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
var typeConfig = builder.Entity<T>();
foreach (var prop in properties)
{
// instancie un PropertyConfigurator<T, TProperty> avec les bons paramètres de type
// et le place dans une variable dynamique
// pour ne pas avoir à appeller ses méthodes par réflexion
dynamic prop_configurator =
typeof(PropertyConfigurator<,>).MakeGenericType(typeof(T), prop.PropertyType)
.GetConstructor(new Type[] { })
.Invoke(new object[] { });

var propertyAccessExpression = prop_configurator.GetPropertyAccessExpression(prop);
prop_configurator.TypeConfig_Type(typeConfig, prop, propertyAccessExpression);
prop_configurator.TypeConfig_Property(typeConfig, prop, propertyAccessExpression);
}

return typeConfig;
}
}

class PropertyConfigurator<T, TProperty>
{
public Expression GetPropertyAccessExpression(PropertyInfo prop)
{
// d'abord l'expression du paramètres
var paramExpression = Expression.Parameter(typeof(T), "obj");

// puis l'expression entière
// Expression<Func<T, TProperty>> expression = obj => obj.Property
var expression = Expression.Lambda<Func<T, TProperty>>(
Expression.Property(paramExpression, prop.Name),
paramExpression
);

return expression;
}

public void TypeConfig_Type(EntityConfiguration<T> typeConfig, PropertyInfo prop, object propertyAccessExpression)
{
// ici le paramètre propertyAccessExpression à été passé en objet
// pour permettre à la compilation dynamique de trouver la bonne méthode
var propertyAccess = propertyAccessExpression as Expression<Func<T, TProperty>>;

// Si la propriété porte l'attribut [Key]
var isKey = prop.GetCustomAttributes(typeof(KeyAttribute), true).Any();
// On déclare la clé
if (isKey) typeConfig.HasKey<TProperty>(propertyAccess);
}

public void TypeConfig_Property(EntityConfiguration<T> typeConfig, PropertyInfo prop, object propertyAccessExpression)
{
// ici le paramètre propertyAccessExpression à été passé en objet
// pour permettre à la compilation dynamique de trouver la bonne méthode
var propertyAccess = propertyAccessExpression as Expression<Func<T, TProperty>>;

// Valeur (ou pas) de MaximumLength de l'attribut [StringLength(int)]
// s'il est présent sur la propriété
int? stringLenght =
prop.GetCustomAttributes(typeof(StringLengthAttribute), true)
.Select(att => (int?)((StringLengthAttribute)att).MaximumLength)
.FirstOrDefault();

// Si la propriété porte l'attribut [Required]
bool isRequired = prop.GetCustomAttributes(typeof(RequiredAttribute), true).Any();

if (!isRequired && !stringLenght.HasValue) return; // pas la peine d'aller plus loin

// on appel en dynamique la méthode Property() de typeConfig
// (qui possède 26 surcharge définition différentes typées pour des types tels que int, int?, string, bool, bool? ...)
// l'appel dynamique permet d'éviter de faire de la réflexion mais il peut ne pas trouver la bonne définition
// donc l'appel est protégé.
dynamic dyna_typeConfig = typeConfig;
dynamic propConfig;
try { propConfig = dyna_typeConfig.Property(propertyAccess); }
catch (RuntimeBinderException) { return; } // pas de configuration possible pour le type de cette propriété

// Les appels dynamiques suivants sont protégés

if (stringLenght.HasValue) // Configuration de la taille de chaine dans la base de donnée
try { PropertyConfigurationExtensions.HasMaxLength(propConfig, stringLenght.Value); }
catch (RuntimeBinderException) { }

if (isRequired) // Configuration du null dans la base de donnée
try { PropertyConfigurationExtensions.IsRequired(propConfig); }
catch (RuntimeBinderException) { }
}
}

Empêcher une application web de ne pas démarrer correctement

Pour initialiser votre site web ASP.net vous aller peut-être utilisé la méthode Application_Start du fichier Global.asax. L’Application_Start n’est utilisé qu’à la première requête reçu par l’application et si une exception se produit dans le corps de cette méthode, l’utilisateur recevra une page d’erreur … soit. Mais si une deuxième requête arrive, l’Application_Start n’est pas exécuté une seconde fois, et votre site vas exécuter des pages dans une application qui n’est pas correctement initialisé.

L’exemple ci dessous permet de forcer le redémarrage de l’application lors d’une exception de l’Application_Start.

protected void Application_Start(object sender, EventArgs e)
{
try
{
// ...
// corps original de la méthode
// ...
}
catch (Exception ex)
{
var html = "<html>" +
"<h1>Application initialisation error</h1>" +
"Please contact the administrator.";
#if DEBUG
html += "[" + DateTime.Now.ToString() + "-" + ex.GetType().Name + "]" + ex.Message;
#endif
html += "</html>";

this.Context.Response.StatusCode = 500; // Server error code
this.Context.Response.Write(html);

// Si une erreur se produit lors de l'initialisation de l'application
// Il est très important de ne pas permettre à celle ci de tourner quand même
// C'est pourquoi on décharge le domaine.
HttpRuntime.UnloadAppDomain();
}
}

vendredi 5 mars 2010

Je suis certifié Scrum Master

A moi l'agilité, les projets qui se déroulent dans les temps et qui produisent quelque chose de satisfaisant. J'ai maintenant le titre de Scrum Master.