logo

log de Julien Corioland (MVP)

Quand les technologies .NET deviennent passion...

all posts in the category : ASP.NET MVC


[Azure] Communication entre instances avec les endpoints internes

by Julien Corioland the 1/14/2012

Il y a quelques temps j’ai posté deux billets décrivant des techniques pour réaliser de l’invalidation de cache sur plusieurs instances d’un rôle Windows Azure :

Dans ce billet, je souhaiterai vous présenter une autre manière de communiquer entres différentes instances d’un même rôle, en utilisant les Endpoints internes.

Les endpoints internes sont des points d’accès sur lesquels chaque instance est en capacité, par exemple, d’hoster un service WCF. Il existe plusieurs avantages dans l’utilisation de ceux-ci :

  • Ils sont internes, donc inaccessibles depuis l’extérieur
  • Il est possible de communiquer via un binding en net.tcp
  • Leur utilisation n’entraîne pas de surcoût sur votre facture Windows Azure

Création d’un endpoint interne

Pour ajouter un endpoint interne à un rôle Windows Azure, rendez-vous dans les propriétés de celui-ci dans votre projet Visual Studio, puis accédez à l’onglet “Endpoints” :

image

Il suffit de cliquer sur le bouton Add Endpoint, de le nommer, de mettre son type à Internal, de choisir le protocole et  enfin de définir le port privé sur lequel le endpoint sera accessible :

image

Toutes les instances du rôles exposeront alors ce endpoint automatiquement !

Héberger un service WCF sur un endpoint interne

Il est possible de faire en sorte qu’un service WCF soit hébergé au démarrage de chaque instance. Celui-ci pourra être appelé par n’importe quelle autre instance du rôle. Par exemple, dans le cas d’un scénario d’invalidation de cache, une instance de rôle doit être capable de notifier toutes les autres de réinitialiser leur cache local. Pour cela, il suffit de créer un canal de communication via WCF entre les instances.

Dans le cas présent, le contrat de service WCF est très simple :

[ServiceContract]
public interface ICacheInvalidationService
{
    [OperationContract]
    void InvalideCache();
}

Son implémentation aussi :

public class CacheInvalidationService : ICacheInvalidationService
{
    public void InvalideCache()
    {
        //invalidation du cache local
    }
}

Le démarrage du service WCF se fait dans la méthode OnStart du WebRole. En premier lieu, il faut récupérer l’adresse du endpoint interne qui a été créé :

if (RoleEnvironment.CurrentRoleInstance.InstanceEndpoints
    .Any(ip => ip.Key == "CacheInvalidationEndpoint"))
{
    IPEndPoint endpoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints
        .First(ip => ip.Key == "CacheInvalidationEndpoint").Value.IPEndpoint;


}

Ensuite, l’hébergement du service WCF se fait très simplement à l’aide d’un ServiceHost :

public class WebRole : RoleEntryPoint
{
    private ServiceHost _serviceHost;

    public override bool OnStart()
    {
        if (RoleEnvironment.CurrentRoleInstance.InstanceEndpoints
            .Any(ip => ip.Key == "CacheInvalidationEndpoint"))
        {
            IPEndPoint endpoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints
                .First(ip => ip.Key == "CacheInvalidationEndpoint").Value.IPEndpoint;

            Uri baseAddress = new Uri(string.Format("net.tcp://{0}", endpoint));
            _serviceHost = new ServiceHost(typeof(CacheInvalidationService), baseAddress);

            NetTcpBinding binding = new NetTcpBinding(SecurityMode.None);
            _serviceHost.AddServiceEndpoint(typeof(ICacheInvalidationService), binding, "CacheInvalidation");

            try
            {
                _serviceHost.Open();
            }
            catch
            {
                Trace.WriteLine("Impossible de démarrer le service d'invalidation de cache");
            }
        }

        return base.OnStart();
    }
}

Il est également possible de surcharger la méthode OnStop du rôle pour arrêter le service WCF proprement :

public override void OnStop()
{
    if (_serviceHost != null)
    {
        try
        {
            _serviceHost.Close();
        }
        catch
        {
            _serviceHost.Abort();
        }
    }
    base.OnStop();
}

A présent, toute instance de ce rôle expose un service WCF permettant l’invalidation de son cache local !

Contacter toutes les instances

La dernière étape consiste à écrire le code qui permet d’appeler le service d’invalidation de cache sur toutes les instances du rôle.

Pour cela, il est nécessaire de récupérer tous les IPEndPoint qui portent le nom “CacheInvalidationEndpoint” :

var ipEndpoints =
    from instance in RoleEnvironment.CurrentRoleInstance.Role.Instances
    from endpoint in instance.InstanceEndpoints
    where endpoint.Key == "CacheInvalidationEndpoint"
    select endpoint.Value.IPEndpoint;

Ensuite, il suffit de parcourir les endpoints pour créer un canal de communication vers chacun d’eux, à l’aide d’une ChannelFactory WCF :

foreach (var ipEndPoint in ipEndpoints)
{
    try
    {
        NetTcpBinding binding = new NetTcpBinding(SecurityMode.None);

        ChannelFactory<ICacheInvalidationService> channelFactory =
            new ChannelFactory<ICacheInvalidationService>(binding);

        string uri = string.Format("net.tcp://{0}/CacheInvalidation", ipEndPoint);
        EndpointAddress address = new EndpointAddress(uri);

        ICacheInvalidationService service = channelFactory.CreateChannel(address);
        if (service != null)
        {
            service.InvalideCache();
        }
    }
    catch
    {
        //log exception
    }
}

Et voilà, toutes les instances ont maintenant été notifiée de vider leur cache local !

Vraiment simple à mettre en place, et super utile !

Pour tester, récupérez le projet d’exemple et mettez un point d’arrêt dans la méthode InvalidateCache du service WCF. Lancez le rôle Azure (configuré pour exécuter deux instances) et appelez ensuite l’URL /Home/InvalidateCache sur l’application MVC. Vous verrez que Visual Studio s’arrête bien deux fois dans le service, une fois pour chaque instance du rôle Smile

Le code source est disponible ici.

A bientôt !


read full post -

[ASP.NET MVC] Multicast dans une application hébergée sur Azure avec AppFabric ServiceBus et NetEventRelayBinding

by Julien Corioland the 11/4/2011

Il y a quelques semaines j’ai posté une astuce pour invalider un cache mémoire sur un changement de configuration d’instance Windows Azure. Bien que cette technique fonctionne, elle nécessite une intervention humaine, puisqu’à chaque fois que l’on souhaite invalider le cache, il faut modifier la configuration du service hébergé sur Azure.

Afin d’éviter de faire cela, je me suis penché sur l’utilisation d’AppFabric ServiceBus dans Azure et notamment du NetEventRelayBinding pour faire en sorte que lorsque je publie / supprime ou modifie un post, toutes les instances de mon blog soit notifiées et invalident leur cache mémoire.

Pour faire simple, j’expose un endpoint sur le service bus Azure, sur lequel toutes les instances vont s’abonner. Dès que l’une d’entre elle contact ce endpoint, toutes les autres sont notifiées.

Le bus des services permet de mettre en place un relai entre un client et un host de service WCF. Par exemple, il est possible d’avoir un service WCF qui soit hébergé sur un serveur IIS dans votre infrastructure et que ce service soit consommé depuis Azure ou une application totalement externe via le bus des services, évitant ainsi des problèmes de sécurité et de configuration de pare-feu.

L’autre scénario (mis en place ici) est le multicast ou le bus des services sert également de relai vers un host WCF, mais en mode multicast.

Pour mettre cela en place, il faut procéder en différentes étapes, décrites ci-après.

1. Activer un domaine sur le bus des services Windows Azure

Connectez-vous au portail d’administration Windows Azure sur http://windows.azure.com puis rendez-vous dans la section Bus des services, Contrôle d’accès et Cache via le menu de gauche :

image

Sélectionnez ensuite Bus des services dans le noeud AppFabric, puis cliquez sur Nouveau dans le ruban. Une popup s’ouvre alors, vous permettant d’ajouter un nouveau domaine sur lequel vous pourrez exposer vos services via AppFabric :

image

Choisissez votre espace de nom, la région dans laquelle exposer le bus des services, l’abonnement sur lequel vous souhaitez l’ajouter… puis valider la popup. Patientez jusqu’à ce que le Bus des services ajouté soit marqué comme “Actif” dans la liste.

image

2. Création du contrat de service et de l’implémentation du service

Comme AppFabric Service Bus sert de relais entre clients et host WCF, on utilise les mêmes mécanismes que WCF classique pour créer contrat de services et implémentations de services.

Dans mon cas, le contrat est ultra simple :

[ServiceContract(Name = "IRelayCacheInvalidationEventContract", Namespace = "http://www.juliencorioland.net/Services/")]
public interface IRelayCacheInvalidationEventContract
{
    [OperationContract(IsOneWay = true)]
    void InvalidateCache();
}

Attention toutefois à bien marquer toutes les OperationContract comme étant en mode “One-Way”, une condition nécessaire à l’utilisation du contrat sur le bus des services Azure.

J’ai également prévu une interface pour représenter le canal de communication WCF qui sera utilisé pour invalider le cache :

public interface IRelayCacheInvalidationEventChannel : IRelayCacheInvalidationEventContract, IClientChannel
{
}

A présent l’implémentation du service, une fois encore très simple ici puisqu’il s’agit d’implémenter l’interface IRelayCacheInvalidationEventContract :

public class CacheInvalidator : IRelayCacheInvalidationEventContract
{
    public void InvalidateCache()
    {
        ICacheService cacheService = ServiceLocator.Current.GetInstance<ICacheService>();
        if(cacheService != null)
        {
            cacheService.InvalidateCache();
        }
    }
}

 

3. Ajout du code pour hoster le service dans le Global.asax du rôle web ASP.NET MVC

Dans cette étape, il s’agit d’ajouter le code de création de l’host WCF qui sera exécuté par toutes les instances lors du démarrage de l’application web.

Pour cela, il faut dans un premier temps récupérer 3 informations : le domaine du bus des services, le nom l’émetteur par défaut (issuer name) et la clé partagée (shared secret). Pour récupérer ces informations, sélectionnez votre bus des services dans l’interface d’administration et cliquez sur le bouton Affichage de la zone Clé par défaut, à droite. Une popup s’affiche alors :

image

Personnellement, j’ai choisi de rajouter ces éléments au fichier de configuration du mon instance Azure, ce qui me permet de les récupérer de la sorte :

string serviceNamespace = RoleEnvironment.GetConfigurationSettingValue("CacheInvalidationRelayDomainNamespace");
string issuerName = RoleEnvironment.GetConfigurationSettingValue("CacheInvalidationRelayIssuerName");
string issuerSecret = RoleEnvironment.GetConfigurationSettingValue("CacheInvalidationRelayIssuerSecret");

Ensuite, vous devez créer une EndpointBehavior pour identifier l’appel sur le bus des services. Cela s’effectue à l’aide d’un TokenProvider et d’une instance de la classe TransportClientEndpointBehavior :

TokenProvider tokenProvider = TokenProvider.CreateSharedSecretTokenProvider(issuerName, issuerSecret);
TransportClientEndpointBehavior relayCredentials = new TransportClientEndpointBehavior(tokenProvider);

Pour avoir accès à ces classes, il vous faut référencer la librairie Microsoft.ServiceBus.dll.

A présent, utilisez l’api du service bus pour créer l’Uri qui hostera le relais vers le service :

Uri relayAddress = ServiceBusEnvironment.CreateServiceUri("sb", serviceNamespace, "/CacheInvalidation/");

Puis comme vous l’auriez fait en WCF, crééez le ServiceHost et ajoutez lui l’endpoint behavior comme cela :

_serviceHost = new ServiceHost(typeof(CacheInvalidator), relayAddress);
    foreach (var endpoint in _serviceHost.Description.Endpoints)
{
    endpoint.Behaviors.Add(relayCredentials);
}
_serviceHost.Open();

 

Remarquez que l’endpoint behavior est rajoutée sur tous les endpoints configurés pour le service CacheInvalidator.

A présent, toutes les instances peuvent être atteintes via du multicast et donc pourront invalider leur cache mémoire !

4. Ajout du code pour récupérer un canal WCF et lancer l’invalidation de cache

Comme en WCF, on utilise un ChannelFactory pour récupérer un canal de communication. Pour pouvoir accéder au bus des services, il faut récupérer les mêmes informations que précédemment à savoir espace de noms, émetteur et clé partagé. Il faut également créer la behavior permettant d’authentifier l’appel

string serviceNamespace = RoleEnvironment.GetConfigurationSettingValue("CacheInvalidationRelayDomainNamespace");
string issuerName = RoleEnvironment.GetConfigurationSettingValue("CacheInvalidationRelayIssuerName");
string issuerSecret = RoleEnvironment.GetConfigurationSettingValue("CacheInvalidationRelayIssuerSecret");

TokenProvider tokenProvider = TokenProvider.CreateSharedSecretTokenProvider(issuerName, issuerSecret);
TransportClientEndpointBehavior relayCredentials = new TransportClientEndpointBehavior(tokenProvider);
Uri relayAddress = ServiceBusEnvironment.CreateServiceUri("sb", serviceNamespace, "/CacheInvalidation/");

A partir de là, il est possible de créer la ChannelFactory et de récupérer un canal de communication de type IRelayCacheInvalidationEventChannel pour invalider le cache :

ChannelFactory<IRelayCacheInvalidationEventChannel> channelFactory =
                new ChannelFactory<IRelayCacheInvalidationEventChannel>("CacheInvalidationEndpoint",
                    new EndpointAddress(relayAddress));
channelFactory.Endpoint.Behaviors.Add(relayCredentials);
_cacheInvalidationChannel = channelFactory.CreateChannel();

_cacheInvalidationChannel.Open();
_cacheInvalidationChannel.InvalidateCache();

Pensez à bien fermer le canal lorsque vous n’en n’avez plus besoin :

_cacheInvalidationChannel.Close();

Le code étant en place, il ne reste plus qu’à configurer le service WCF, dans le Web.config du rôle web ASP.NET MVC !

5. Configuration du service WCF

Comme toujours la configuration d’un service WCF se passe dans la section system.serviceModel du fichier de configuration de l’application : ici le Web.config du rôle web !

Tout d’abord, il faut ajouter les extensions propres à l’utilisation du bus des services :

<system.serviceModel>
    <extensions>
      <bindingExtensions>
        <add name="netEventRelayBinding" type="Microsoft.ServiceBus.Configuration.NetEventRelayBindingCollectionElement, Microsoft.ServiceBus, Version=1.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
      </bindingExtensions>
    </extensions>
</system.serviceModel>

Ensuite il faut ajouter la définition du binding netRelayEventBinding (toujours dans system.serviceModel) :

<bindings>
  <netEventRelayBinding>
    <binding name="default" />
  </netEventRelayBinding>
</bindings>

Puis le endpoint du service :

<services>
  <service name="MetroBlog.Bll.CacheInvalidator">
    <endpoint name="CacheInvalidationEndpoint"
            contract="MetroBlog.Interfaces.IRelayCacheInvalidationEventContract"
            binding="netEventRelayBinding"
            bindingConfiguration="default" />
  </service>
</services>

Pour terminer par la configuration du client :

<client>
  <endpoint name="CacheInvalidationEndpoint"
            contract="MetroBlog.Interfaces.IRelayCacheInvalidationEventContract"
            binding="netEventRelayBinding"
            bindingConfiguration="default"
            />
</client>

Et voilà ! Si vous avez bien suivi toutes les étapes, vos instances sont toutes capables d’invalider leur cache grâce au multicast de AppFabric Service Bus !

A bientôt Winking smile


read full post -

[Azure] Invalidation de settings ou de cache à l’aide d’une clé de configuration

by Julien Corioland the 10/15/2011

Dans ce post je vais vous expliquer une technique que j’ai utilisée pour invalider le cache / recharger les settings sur toutes les instances du rôle Web qui héberge mon blog (en vrai il n’y en a qu’une seule, mais je trouve l’astuce sympa Smile)

Le constat est simple : mes posts et les settings du blog (email, smtp, etc…) sont persistés dans le stockage table de Windows Azure. Afin d’éviter les aller-retours vers le stockage par table, je monte tout en cache mémoire (le nombre de transaction est facturé et en plus du coup ça envoi du lourd en terme de perf).

Problème : si je change un settings dans mon storage, comment est-ce que je demande à mon service de recharger les données ? Idem si pour une raison x ou y je souhaite vider le cache de posts, je dois avoir une solution. Si on se place dans un contexte Windows Azure multi-instances (cas très fréquent) c’est une couche de complexité supplémentaire qui se rajoute car il faut notifier chaque instance !

La solution la plus simple est d’utiliser un setting Windows Azure et de faire en sorte que vos instances écoutent les changements de configuration. Dès lors, il suffit de détecter le changement de la bonne clé de settings, “ConfigVersion” dans mon cas (ou “CléBidon” si vous voulez) et de reinitialiser vos différents services (ici mon service de cache et mon service de settings).

Tout cela se passe dans le fichier WebRole.cs du webRole, via l’événement Changed de RoleEnvironment :

public class WebRole : RoleEntryPoint
{
    public override bool OnStart()
    {
        // For information on handling configuration changes
        // see the MSDN topic at http://go.microsoft.com/fwlink/?LinkId=166357.
        RoleEnvironment.Changed += OnRoleEnvironmentChanged;
        return base.OnStart();
    }

    private void OnRoleEnvironmentChanged(object sender, RoleEnvironmentChangedEventArgs e)
    {
        if (e.Changes.Any(change => change is RoleEnvironmentConfigurationSettingChange)) {
            foreach (var change in e.Changes.OfType<RoleEnvironmentConfigurationSettingChange>()) {
                if (change.ConfigurationSettingName == "ConfigVersion")
                {
                    var cacheService = UnityRoot.Container.Resolve<ICacheService>();
                    cacheService.InvalidateCache();
                    var settingsManager = UnityRoot.Container.Resolve<ISettingsManager>();
                    settingsManager.Reload();
                }
            }
        }
    }
}

A bientôt Winking smile

Technorati Tags: ,,

read full post -

[EN] [ASP.NET MVC 4] Asynchronous controllers

by Julien Corioland the 9/29/2011

Asynchronous execution is the future of Windows development : it has been largely demonstrated during the //Build conference two weeks ago.

In previous versions of ASP.NET MVC it was possible to create asynchronous controllers by inheriting the AsyncController class and using some conventions :

  • MyActionAsync : method that returns void and launches an asynchronous process
  • MyActionCompleted : method that returns an ActionResult (the result of the MVC action “MyAction”, in this case)

To allow the MVC engine to manage asynchronous operations and pass the result to the view engine, developers had to use the propery AsyncManager of the AsyncController. The “completed” method parameters was passed by the MVC engine through this object.

For example, the controller that is defined bellow allows to get a Json-serialized list of movies – asynchronously – from an OData service :

public class MoviesController : AsyncController
{
    public ActionResult Index()
    {
        return View();
    }

    public void GetJsonMoviesAsync(int? page)
    {
        const int pageSize = 20;
        int skip = pageSize * ((page ?? 1) - 1);
        string url = string.Format("http://odata.netflix.com/[…]&$skip={0}&$top={1}", 
            skip, pageSize);

        //the asynchronous operation is declared
        AsyncManager.OutstandingOperations.Increment();

        var webClient = new WebClient();
        webClient.DownloadStringCompleted += OnWebClientDownloadStringCompleted;
        webClient.DownloadStringAsync(new Uri(url));//the asynchronous process is launched
    }

    private void OnWebClientDownloadStringCompleted(object sender, 
        DownloadStringCompletedEventArgs e)
    {
        //the asynchronous process ends
        //"movies" result is added to the parameters of the AsyncManager
        //NB : it's the name of the parameter that is take by the 
        //GetJsonMoviesCompleted method
        List<Movie> movies = null;
        if (AsyncManager.Parameters.ContainsKey("movies"))
        {
            movies = (List<Movie>)AsyncManager.Parameters["movies"];
            movies.Clear();
        }
        else
        {
            movies = new List<Movie>();
            AsyncManager.Parameters["movies"] = movies;
        }

        movies.AddRange(Movie.FromXml(e.Result));

        //the ends of the asynchronous operation (launches the call of "Action"Completed)
        AsyncManager.OutstandingOperations.Decrement();
    }

    public ActionResult GetJsonMoviesCompleted(List<Movie> movies)
    {
        //on retourne le résultat Json
        return Json(movies, JsonRequestBehavior.AllowGet);
    }
}

It’s not really complicated to create an asynchronous controller but ASP.NET MVC 4 and C# 5 with the new async and await keywords will make it easier !

public class MoviesController : AsyncController
{
	public ActionResult Index()
	{
		return View();
	}

	public async Task<ActionResult> GetJsonMovies(int? page)
	{
		const int pageSize = 20;
		int skip = pageSize * ((page ?? 1) - 1);
		string.Format("http://odata.netflix.com/[…]&$skip={0}&$top={1}", 
            		skip, pageSize);

		var webClient = new WebClient();
		string xmlResult = await webClient.DownloadStringTaskAsync(url);
		return Json(Movie.FromXml(xmlResult), JsonRequestBehavior.AllowGet);
	}
}

As you can see in the previous code snippet, in ASP.NET MVC 4 you always should inherits from AsyncController but there is no more naming conventions, no more Async/Completed methods, no more AsyncManager and the action returns a Task instead of an ActionResult !

It’s awesome !

Hope this helps Winking smile


read full post -

[ASP.NET MVC 4] Contrôleurs asynchrones !

by Julien Corioland the 9/29/2011

On le sait, cela a été largement abordé il y a deux semaines lors de la //Build conférence : le futur du développement est dans l’asynchrone.

Bien entendu, il était déjà possible de réaliser des contrôleur asynchrone dans les versions précédente d’ASP.NET MVC et ce en dérivant nos contrôleurs de la classe AsyncController. Le moteur MVC se basait alors sur des conventions :

  • MonActionAsync : méthode qui retourne void et qui démarre un traitement asynchrone
  • MonActionCompleted : méthode qui retourne un ActionResult (le résultat de l’action MVC “MonAction”, dans ce cas).

Afin de synchroniser le tout, l’AsyncController fournissait une propriété AsyncManager permettant notamment au moteur MVC de connaître le nombre d’opération asynchrone en cours, de gérer celles-ci et de passer les paramètres à la méthode de retour d’action (MonActionCompleted) à la fin du traitement asynchrone, et ce pour retourner le résultat.

Par exemple, le contrôleur ci-dessous expose une action GetJSonMovies asynchrone, qui se charge d’aller récupérer une liste de films via un flux OData :

public class MoviesController : AsyncController
{
    public ActionResult Index()
    {
        return View();
    }

    public void GetJsonMoviesAsync(int? page)
    {
        const int pageSize = 20;
        int skip = pageSize * ((page ?? 1) - 1);
        string url = string.Format("http://odata.netflix.com/[…]&$skip={0}&$top={1}", 
            skip, pageSize);

        //on "déclare" l'opération asynchrone
        AsyncManager.OutstandingOperations.Increment();

        var webClient = new WebClient();
        webClient.DownloadStringCompleted += OnWebClientDownloadStringCompleted;
        webClient.DownloadStringAsync(new Uri(url));//on lance le traitement asynchrone
    }

    private void OnWebClientDownloadStringCompleted(object sender, 
        DownloadStringCompletedEventArgs e)
    {
        //le traitement asynchrone se termine
        //on rajoute le résultat "movies" au paramètre du AsyncManager
        //NB : il s'agit du nom du paramètre de la méthode GetJsonMoviesCompleted !!
        List<Movie> movies = null;
        if (AsyncManager.Parameters.ContainsKey("movies"))
        {
            movies = (List<Movie>)AsyncManager.Parameters["movies"];
            movies.Clear();
        }
        else
        {
            movies = new List<Movie>();
            AsyncManager.Parameters["movies"] = movies;
        }

        movies.AddRange(Movie.FromXml(e.Result));

        //on indique que l'opération se termine (déclenche l'appel du "Action"Completed)
        AsyncManager.OutstandingOperations.Decrement();
    }

    public ActionResult GetJsonMoviesCompleted(List<Movie> movies)
    {
        //on retourne le résultat Json
        return Json(movies, JsonRequestBehavior.AllowGet);
    }
}

Nous sommes d’accord, ce code n’est pas extrêment compliqué, mais ASP.NET MVC 4, par le biais des nouveautés de C# 5 et de l’utilisation des mots clés async et await va grandement le simplifier, comme il est possible de le constater ci-dessous :

public class MoviesController : AsyncController
{
	public ActionResult Index()
	{
		return View();
	}

	public async Task<ActionResult> GetJsonMovies(int? page)
	{
		const int pageSize = 20;
		int skip = pageSize * ((page ?? 1) - 1);
		string.Format("http://odata.netflix.com/[…]&$skip={0}&$top={1}", 
            		skip, pageSize);

		var webClient = new WebClient();
		string xmlResult = await webClient.DownloadStringTaskAsync(url);
		return Json(Movie.FromXml(xmlResult), JsonRequestBehavior.AllowGet);
	}
}

Comme vous pouvez le voir, en ASP.NET MVC 4 on continue à dériver AsyncController, en revanche plus besoin de faire appelle à l’AsyncManager ou même d’utiliser des conventions “ActionAsync/ActionCompleted” ! Notez également que la méthode ne retourne plus un ActionResult mais un Task<ActionResult> (induit par l’utilisation de async et await).

Franchement, ça poutre !

A bientôt Winking smile


read full post -

[EN] [ASP.NET MVC] Handle HTTP errors

by Julien Corioland the 9/27/2011

This subject seems to be really basic when developing a web application but it seems that it’s very discussed on the web too. In a lot of forum threads or blog posts we can find very various methods to handle HTTP exception within an ASP.NET MVC application. This post exposes a way to do that. I’m not pretending that it’s the best way but I think that it’s an elegant one Smile (so don’t hesitate to comment on this post if you’ve remarks or suggestions).

What’s the real need ?

I think that developers should manage HttpException in an other way than other exceptions for 3 main reasons :

  1. Respect of web-standards (a not found error returns a 404 http code, that’s it !)
  2. SEO : search engines use http codes (because it’s standards) to build their indexes.
  3. The user : a user-friendly page is already better than a stack trace…

I think that the most important is that the error code is well returned by the server : for example the URL http://www.mywebsite.com/Pages/some-page-that-does-not-exists is called the server should return a 404 status code and not a 302 (found) followed by a 200 (or 404) from a specific error page like http://www.mywebsite.com/Errors/Error404).

When HttpException are thrown in an ASP.NET MVC application ?

HttpException are thrown by the ASP.NET MVC engine or by the application code, directly in the asp.net controllers of your apps :

throw new HttpException(404, "Not found");

In some cases, they can also be thrown by the server (for internal errors, for example). But all HttpException can be handled in the application.

How to handle these exceptions ?

After a lot of researches and some tests I think that http exceptions should be handled in two places :

  1. In a base controller inherited by all the controllers of the application
  2. In the Global.asax file

Handle http exception in the controller :

Exceptions that are thrown by the developer directly in the application code can be handle in a base controller. For example :

public ActionResult Product(int id)
{
    var product = _unitOfWork.GetProduct(id);
    if(product == null)
        throw new HttpException(404, "Le produit est introuvable");
 
    return View(product);
}

By inheriting the System.Web.Mvc.Controller class it’s possible to override an “OnException” method. This method is called when an exception occurs in a controller method (action) :

protected override void OnException(ExceptionContext filterContext) {
    base.OnException(filterContext);
 
    if (filterContext.Exception != null) {
        filterContext.ExceptionHandled = true;
 
        if (filterContext.Exception is HttpException) {
            if (!ControllerContext.RouteData.Values.ContainsKey("error")) {
                ControllerContext.RouteData.Values.Add("error", filterContext.Exception);
            }
 
            var httpException = (HttpException) filterContext.Exception;
 
            switch (httpException.GetHttpCode()) {
                case 404:
                    filterContext.HttpContext.Response.StatusCode = 404;
                    filterContext.HttpContext.Response.StatusDescription = httpException.Message;
                    View("Error404", null).ExecuteResult(ControllerContext);
                    break;
                case 500:
                    filterContext.HttpContext.Response.StatusCode = 500;
                    filterContext.HttpContext.Response.StatusDescription = httpException.Message;
                    View("Error500", null).ExecuteResult(ControllerContext);
                    break;
                default:
                    filterContext.HttpContext.Response.StatusDescription = httpException.Message;
                    View("GenericError", null).ExecuteResult(ControllerContext);
                    break;
            }
        }
 
        //autre traitement si pas HttpException (log par exemple...)
    }
}

As you can see in this code snippet the first step is to check that the exception is an HttpException. If yes, a redirection to an appropriate error page is done just after setting the status code and status description of the http response. The views are in the “Shared” folder of the application.

Now http exceptions that are thrown in a controller return the good status code and redirect to a user friendly page.

Handle HTTP exceptions in the Global.asax :

It’s in the Global.asax file that all other http exception are handled. To to that you should subscribe to the Error event of the mvc application (in the Init method override) :

public override void Init()
{
    base.Init();
    this.Error += new EventHandler(MvcApplication_Error);
}

In the Error event handler, initialize an errors-dedicated controller and create appropriate route data to work with the error :

var routeData = new RouteData();
routeData.Values.Add("controller", "Errors"); 
 
var lastException = Server.GetLastError();
 
if (lastException is HttpException) {
    var httpException = (HttpException) lastException;
    switch(httpException.GetHttpCode()) {
        case 404:
            routeData.Values.Add("action", "Error404");
            break;
        case 500:
            routeData.Values.Add("action", "Error500");
            break;
        default:
            routeData.Values.Add("action", "GenericError");
            break;
    }
}
else
{
    routeData.Values.Add("action", "GenericError");
}
 
routeData.Values.Add("exception", lastException);
 
Server.ClearError();
 
IController errorController = _unityContainer.Resolve<ErrorsController>();
errorController.Execute(new RequestContext(
new HttpContextWrapper(Context), routeData));

The ErrorsController :

public class ErrorsController : Controller
{
    public ActionResult Error404() {
        Response.StatusCode = 404;
        Exception exception = null;
        if(RouteData.Values.ContainsKey("exception")) {
            exception = (Exception) RouteData.Values["exception"];
        }
        return View(exception);
    }
 
    public ActionResult Error500() {
        Response.StatusCode = 500;
        Exception exception = null;
        if (RouteData.Values.ContainsKey("exception"))
        {
            exception = (Exception)RouteData.Values["exception"];
        }
        return View(exception);
    }
 
    public ActionResult GenericError() {
        Exception exception = null;
        if (RouteData.Values.ContainsKey("exception"))
        {
            exception = (Exception)RouteData.Values["exception"];
        }
        return View(exception);
    }
}

As you can see each action defines itself the http status code of the http response.

Now all http exceptions are handled in a user and SEO friendly way in your ASP.NET MVC applications !

Hope this helps Winking smile


read full post -

[ASP.NET MVC] Gestion des erreurs HTTP

by Julien Corioland the 9/27/2011

Bien que cela soit un sujet qui paraissent essentiel et surtout “basic” lorsque l’on développe une application web (avec ASP.NET MVC ou pas d’ailleurs), ce sujet semble avoir fait couler beaucoup d’encre sur le web ! (si je puis dire…). En effet, il semble qu’il règne une sorte de “flou” autour de ce sujet ou chacun utilise une méthode différente pour gérer les erreurs HTTP. Je ne sais pas si on peut parler ici de “bonne pratique” mais en tout cas, ce post présente un moyen que je juge efficace et suffisament générique pour gérer les Http Exception dans une application ASP.NET MVC ! (n’hésitez pas à me faire par de vos remarques / suggestions en commentaire Smile)

Quel est le réel besoin ?

Au final, pourquoi souhaite-t-on traiter les erreurs HTTP de manière différente des autres erreurs ? A mon humble avis, pour 3 raisons principalement :

  1. Le respect des standards (une page qui n’existe pas c’est un 404, c’est tout !)
  2. La SEO : les moteurs de recherche utilisent tous les codes HTTP (comme c’est standard) pour construire leurs index.
  3. L’utilisateur : on souhaite en général personnaliser la page d’erreur sur laquel arrive l’utilisateur !

Ce qui importe donc, c’est avant tout que le code d’erreur soit bien renvoyé : si j’appelle une URL “http://www.monsite.com/Pages/Page-qui-n-existe-pas” je souhaite que ce soit bien cette page là qui me retourne un code de status 404 et non pas un code 302 (trouvé) suivi d’un 200 (ou 404, à la limite, mais non) sur une URL de type “http://www.monsite.com/Errors/Error404”.

Quand est-ce que des HttpException sont levées en ASP.NET MVC ?

Les exceptions sont levées soit par le moteur ASP.NET MVC, soit dans votre code :

throw new HttpException(404, "Not found");

Elles peuvent également être levées au niveau du serveur (erreur interne 500, par exemple), mais dans tous les cas, il est possible de les gérer !

Comment gérer ces exceptions ?

Après pas mal de recherches et de tests, je pense qu’il y a deux endroits où il faut gérer les exceptions :

  1. Dans un contrôleur de base dérivé par tous les contrôleurs d’une application ASP.NET MVC
  2. Dans le fichier Global.asax

Gestion des erreurs dans un contrôleur de base :

Au sein du contrôleur, on gèrera les exceptions qui ont été levées intentionnellement par le développeur. Par exemple :

public ActionResult Product(int id)
{
    var product = _unitOfWork.GetProduct(id);
    if(product == null)
        throw new HttpException(404, "Le produit est introuvable");

    return View(product);
}

Lorsque l’on dérive la classe System.Web.Mvc.Controller, il est possible de surcharger une méthode “OnException” : celle-ci est appelée lorsqu’une exception est levée dans le contrôleur.

protected override void OnException(ExceptionContext filterContext) {
    base.OnException(filterContext);

    if (filterContext.Exception != null) {
        filterContext.ExceptionHandled = true;

        if (filterContext.Exception is HttpException) {
            if (!ControllerContext.RouteData.Values.ContainsKey("error")) {
                ControllerContext.RouteData.Values.Add("error", filterContext.Exception);
            }

            var httpException = (HttpException) filterContext.Exception;

            switch (httpException.GetHttpCode()) {
                case 404:
                    filterContext.HttpContext.Response.StatusCode = 404;
                    filterContext.HttpContext.Response.StatusDescription = httpException.Message;
                    View("Error404", null).ExecuteResult(ControllerContext);
                    break;
                case 500:
                    filterContext.HttpContext.Response.StatusCode = 500;
                    filterContext.HttpContext.Response.StatusDescription = httpException.Message;
                    View("Error500", null).ExecuteResult(ControllerContext);
                    break;
                default:
                    filterContext.HttpContext.Response.StatusDescription = httpException.Message;
                    View("GenericError", null).ExecuteResult(ControllerContext);
                    break;
            }
        }

        //autre traitement si pas HttpException (log par exemple...)
    }
}

Comme vous pouvez le constater, ce code vérifie si l’exception est de type HttpException, si c’est le cas, elle redirige vers une vue d’erreur après avoir renseigner un status code et un status description au niveau de la réponse.

NB : ces vues sont placées dans le dossier “Shared” des vues partagées.

Premier objectif atteind : une erreur HTTP levée dans un contrôleur se traduit par une page d’erreur “user friendly” et une réponse HTTP “SEO/Standards friendly” puisque retournant le bon code HTTP sur la bonne URL !

Gestion des erreurs HTTP dans le Global.asax :

Dans le Global.asax, on va gérer toutes les autres erreurs HTTP qui pourraient être levées dans l’application.

Pour se faire, on surcharge d’abord la méthode Init afin de s’abonner à l’événement Error :

public override void Init()
{
    base.Init();
    this.Error += new EventHandler(MvcApplication_Error);
}

Dans la méthode MvcApplication_Error, on récupère la dernière erreur du serveur, on vérifie si celle-ci est de type HttpException. Si oui, on instancie un contrôleur spécialisé dans la gestion des erreurs (ici ErrorsController, détaillé plus bas) on exécute le rendu de la vue en lui passant les bonnes RouteData :

var routeData = new RouteData();
routeData.Values.Add("controller", "Errors"); 

var lastException = Server.GetLastError();

if (lastException is HttpException) {
    var httpException = (HttpException) lastException;
    switch(httpException.GetHttpCode()) {
        case 404:
            routeData.Values.Add("action", "Error404");
            break;
        case 500:
            routeData.Values.Add("action", "Error500");
            break;
        default:
            routeData.Values.Add("action", "GenericError");
            break;
    }
}
else
{
    routeData.Values.Add("action", "GenericError");
}

routeData.Values.Add("exception", lastException);

Server.ClearError();

IController errorController = _unityContainer.Resolve<ErrorsController>();
errorController.Execute(new RequestContext(
new HttpContextWrapper(Context), routeData));

Voilà le code du contrôleur ErrorsController :

public class ErrorsController : Controller
{
    public ActionResult Error404() {
        Response.StatusCode = 404;
        Exception exception = null;
        if(RouteData.Values.ContainsKey("exception")) {
            exception = (Exception) RouteData.Values["exception"];
        }
        return View(exception);
    }

    public ActionResult Error500() {
        Response.StatusCode = 500;
        Exception exception = null;
        if (RouteData.Values.ContainsKey("exception"))
        {
            exception = (Exception)RouteData.Values["exception"];
        }
        return View(exception);
    }

    public ActionResult GenericError() {
        Exception exception = null;
        if (RouteData.Values.ContainsKey("exception"))
        {
            exception = (Exception)RouteData.Values["exception"];
        }
        return View(exception);
    }
}

NB : notez que c’est dans chaque action que l’on défini le status code de la réponse HTTP !

Je vous passe le code des vues qui n’a pas grand intérêt (mise à part que l’on peut réutiliser ici les vues du dossier “Shared” évidemment)

Deuxième objectif atteind : une erreur HTTP levée autre part que dans un contrôleur se traduit par une page d’erreur “user friendly” et une réponse HTTP “SEO/Standards friendly” puisque retournant le bon code HTTP sur la bonne URL !

A bientôt ! Winking smile


read full post -

[EN] [ASP.NET MVC] Custom output cache provider

by Julien Corioland the 9/13/2011

Since ASP.NET 4 was released it’s possible to write custom output cache providers to change the way data are cached. For example, one of my customer wanted that some actions were cached on the file system to avoid loss of cache during an application pool reboot.

Be careful by using the following code because in many scenarios it can’t be used “as it is” (in a web farm, for example).

A custom output cache provider is a simple class that inherits from OutputCacheProvider and overrides these methods :

  • Add : adds an entry in the cache
  • Get : gets an entry from the cache
  • Remove : removes an entry from the cache
  • Set : updates an entry in the cache

Cached items must be serializable. In this sample, the following class will be used :

[Serializable]
public class CachedItem
{
    public object Item { get; set; }
    public DateTime UtcExpiry { get; set; }
}

In order to save an item on the file system the two following methods will be used. The first allows to get a file path from the cache key (generated by asp.net) and the second allows to save a CachedItem on the disk :

private string GetFilePathFromKey(string key)
{
    foreach (var invalidChar in Path.GetInvalidFileNameChars())
        key = key.Replace(invalidChar, '_');

    return Path.Combine(CacheDirectory, key);
}

private void SaveCachedItem(CachedItem cachedItem, string filePath)
{
    if (File.Exists(filePath))
        File.Delete(filePath);

    using (var stream = File.OpenWrite(filePath))
    {
        var binaryFormatter = new BinaryFormatter();
        binaryFormatter.Serialize(stream, cachedItem);
    }
}

The Add method :

public override object Add(string key, object entry, DateTime utcExpiry)
{
    string filePath = GetFilePathFromKey(key);
    var cachedItem = GetCachedItem(filePath);

    if (cachedItem != null && cachedItem.UtcExpiry.ToUniversalTime() <= DateTime.UtcNow)
    {
        Remove(key);
    }
    else if (cachedItem != null)
    {
        return cachedItem.Item;
    }

    SaveCachedItem(new CachedItem()
    {
        Item = entry,
        UtcExpiry = utcExpiry
    }, filePath);

    return entry;
}

The Get method :

public override object Get(string key)
{
    string filePath = GetFilePathFromKey(key);
    var cachedItem = GetCachedItem(filePath);
    if (cachedItem != null)
    {
        if (cachedItem.UtcExpiry.ToUniversalTime() <= DateTime.UtcNow)
        {
            Remove(key);
        }
        else
        {
            return cachedItem.Item;
        }
    }

    return null;
}

The Remove method :

public override void Remove(string key)
{
    string filePath = GetFilePathFromKey(key);
    if (File.Exists(filePath))
        File.Delete(filePath);
}

The Set Method :

public override void Set(string key, object entry, DateTime utcExpiry) {
    string filePath = GetFilePathFromKey(key);
    var cachedItem = new CachedItem() {Item = entry, UtcExpiry = utcExpiry};
    SaveCachedItem(cachedItem, filePath);
}

Now that the output cache provider is created, it can be registered in the web.config file of the application, in the caching section :

<system.web>
<caching>
  <outputCache>
    <providers>
      <add name="FileSystemOutputCacheProvider"
           type="Samples.FileSystemOutputCacheProvider, Samples"/>
    </providers>
  </outputCache>
</caching>

The last step consists to override the GetOutputCacheProviderName method in the Global.asax file. Actually this method is called by the ASP.NET Framework when an OutputCache attribute (or an OutputCache directive) is used. The method returns the name of the output cache provider to use in the current context. For example, if you want to use the FileSystemOutputCacheProvider for the Details action of the Products controller, you can use this code :

public override string GetOutputCacheProviderName(HttpContext context)
{
    if (context.Request.RawUrl.ToUpper().Contains("PRODUCTS/DETAILS")) {
        return "FileSystemOutputCacheProvider";
    }
    return base.GetOutputCacheProviderName(context);
}

As mentioned previously you just have to set an OutputCache attribute on the action Details of the Products controller to use the custom output cache provider :

public class ProductsController : Controller
{
    [OutputCache(Duration = 3600, VaryByParam = "id")]
    public ActionResult Details(int id)
    {
        //long operation
        Thread.Sleep(2000);

        ViewBag.ProductId = id;
        return View();
    }
}

Download the source code here.

Hope this helps Winking smile


read full post -

[ASP.NET MVC] Fournisseur de cache de sortie personnalisé

by Julien Corioland the 9/13/2011

ASP.NET 4.0 apporte de nombreuses nouveautés et notamment la possibilité de développer son propre fournisseur de cache de sortie.

Pour des besoins bien particulier, un de mes clients avait besoin de générer un grand nombre de page dynamique : le problème étant que ces pages faisaient appel à énormément de ressources et donc la génération prenait un temps fou.

Pour résoudre le problème, j’ai proposé de faire un fournisseur de cache de sortie personnalisé permettant de stocker la réponse HTTP sur le FileSystem afin de pouvoir la renvoyer plus rapidement. Cette solution est assez intéressante car le cache reste disponible même si le pool d’application ASP.NET est coupé. En revanche, elle n’est pas optimale dans un cas multi-serveur avec un répartiteur de charge en amont. Bref, à ne pas utiliser “les yeux fermés” Smile

Création du fournisseur de cache de sortie personnalisé

Pour créer un fournisseur de cache de sortie personnalisé, il suffit de dériver de la classe abstraite OutputCacheProvider et de redéfinir les méthodes suivantes :

public class FileSystemOutputCacheProvider : OutputCacheProvider
{
    public override object Add(string key, object entry, DateTime utcExpiry)
    {
        throw new NotImplementedException();
    }

    public override object Get(string key)
    {
        throw new NotImplementedException();
    }

    public override void Remove(string key)
    {
        throw new NotImplementedException();
    }

    public override void Set(string key, object entry, DateTime utcExpiry)
    {
        throw new NotImplementedException();
    }
}

Voilà un bref descriptif des méthodes :

  • Add : permet d’ajouter un élément identifié par une clé unique dans le cache
  • Get : permet de récupérer un élément dans le cache
  • Remove : permet de supprimer un élément du cache
  • Set : permet de mettre à jour un élément dans le cache

Les éléments mis en cache seront de type “CachedItem” :

[Serializable]
public class CachedItem
{
    public object Item { get; set; }
    public DateTime UtcExpiry { get; set; }
}

Nous utiliserons également deux méthodes permettant de générer un chemin de fichier à partir de la clé passé par le moteur ASP.NET MVC et de sauvegarder un CachedItem sur le disque :

private void SaveCachedItem(CachedItem cachedItem, string filePath)
{
    if (File.Exists(filePath))
        File.Delete(filePath);

    using (var stream = File.OpenWrite(filePath))
    {
        var binaryFormatter = new BinaryFormatter();
        binaryFormatter.Serialize(stream, cachedItem);
    }
}

private string GetFilePathFromKey(string key)
{
    foreach (var invalidChar in Path.GetInvalidFileNameChars())
        key = key.Replace(invalidChar, '_');

    return Path.Combine(CacheDirectory, key);
}

Méthode Add :

public override object Add(string key, object entry, DateTime utcExpiry)
{
    string filePath = GetFilePathFromKey(key);
    var cachedItem = GetCachedItem(filePath);

    if (cachedItem != null && cachedItem.UtcExpiry.ToUniversalTime() <= DateTime.UtcNow)
    {
        Remove(key);
    }
    else if (cachedItem != null)
    {
        return cachedItem.Item;
    }

    SaveCachedItem(new CachedItem()
    {
        Item = entry,
        UtcExpiry = utcExpiry
    }, filePath);

    return entry;
}

Méthode Get :

public override object Get(string key)
{
    string filePath = GetFilePathFromKey(key);
    var cachedItem = GetCachedItem(filePath);
    if (cachedItem != null)
    {
        if (cachedItem.UtcExpiry.ToUniversalTime() <= DateTime.UtcNow)
        {
            Remove(key);
        }
        else
        {
            return cachedItem.Item;
        }
    }

    return null;
}

Méthode Remove :

public override void Remove(string key)
{
    string filePath = GetFilePathFromKey(key);
    if (File.Exists(filePath))
        File.Delete(filePath);
}

Méthode Set :

public override void Set(string key, object entry, DateTime utcExpiry) {
    string filePath = GetFilePathFromKey(key);
    var cachedItem = new CachedItem() {Item = entry, UtcExpiry = utcExpiry};
    SaveCachedItem(cachedItem, filePath);
}

Enregistrement du fournisseur de cache de sortie personnalisé

L’enregistrement d’un fournisseur de cache de sortie se fait en deux étapes : dans le Web.config et dans le Global.asax.

Fichier Web.config :

<system.web>

<caching>
  <outputCache>
    <providers>
      <add name="FileSystemOutputCacheProvider" 
	type="Samples.FileSystemOutputCacheProvider, Samples"/>
    </providers>
  </outputCache>
</caching>

Global.asax :

Il faut surcharger la méthode GetOutputCacheProviderName qui est en charge de retourner le fournisseur contextuel à la requête. Par exemple, si l’on souhaite utiliser le FileSystemCacheProvider que sur le contrôleur “Products”, on écrira :

public override string GetOutputCacheProviderName(HttpContext context)
{
    if (context.Request.RawUrl.ToUpper().Contains("PRODUCTS/DETAILS")) {
        return "FileSystemOutputCacheProvider";
    }
    return base.GetOutputCacheProviderName(context);
}

Utilisation du fournisseur de cache de sortie personnalisé en ASP.NET MVC

Pour utiliser le nouveau fournisseur de cache, il suffit d’utiliser l’attribut "OutputCache” comme à son habitude. C’est le Framework ASP.NET qui se chargera d’instancier le bon fournisseur de cache au bon moment Smile

public class ProductsController : Controller
{
    [OutputCache(Duration = 3600, VaryByParam = "id")]
    public ActionResult Details(int id)
    {
        //long operation
        Thread.Sleep(2000);

        ViewBag.ProductId = id;
        return View();
    }
}

NB : le fait de préciser ici le paramètre VaryByParam = “id” permet de faire en sorte que le moteur MVC génère une clé différente par id, est donc implicitement que le FileSystemOutputCacheProvider génère un fichier par id !

Sources de l'exemple

A bientôt Winking smile


read full post -

[En][ASP.NET MVC] Remote attribute and asynchronous validation

by Julien Corioland the 9/5/2011

As you probably know ASP.NET MVC supports .NET Framework 4 Data Annotations to validate user’s inputs (Required, StringLength, Range, RegularExpression…)

Another attribute exists and allows asynchronous client side validation. It’s the Remote attribute. For example, it can be used to validate e-mail and username in a registration form and alert the user before the form is posted.

Here is a sample model for a registration form :

public class UserModel
{
    [Required]
    [Remote("CheckUsername", "RemoteValidation", ErrorMessage = "This username is already used.")]
    public string Username { get; set; }
 
    [Required]
    [Remote("CheckEmail", "RemoteValidation", ErrorMessage = "This e-mail is already used.")]
    public string Email { get; set; }
     
    [StringLength(80)]
    public string FirstName { get; set; }
 
    [StringLength(80)]
    public string LastName { get; set; }
     
    public DateTime BirthDate { get; set; }
}

As you can see in the previous code snippet, the Remote attribute takes two parameters :

  • the MVC action that should validate the input
  • the controller that defines the action

The controller :

public class RemoteValidationController : Controller 
{
    private readonly string[] _existingUsernames = {"beedoo", "julien", "jcorioland"};
    private readonly string[] _existingEmails = { "contact@juliencorioland.net" };
 
    public JsonResult CheckUsername(string username) {
        bool userIsAvailable = !_existingUsers.Contains(username);
        return Json(userIsAvailable, JsonRequestBehavior.AllowGet);
    }
 
    public JsonResult CheckEmail(string email)
    {
        bool emailIsAvailable = !_existingEmails.Contains(email);
        return Json(emailIsAvailable, JsonRequestBehavior.AllowGet);
    }
}

These actions are very simple : they take the input to validate as a string parameter and return a Json-serialized Boolean that indicates if the input is valid or not. For this article I’ve used two arrays to represent users and e-mails databases but it’s possible to execute any code here.

The view :

@using (Html.BeginForm())
{
    <div class="editor-label">@Html.LabelFor(m => m.Username)</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.Username)
        @Html.ValidationMessageFor(m => m.Username)
    </div>
 
    <div class="editor-label">@Html.LabelFor(m => m.Email)</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.Email)
        @Html.ValidationMessageFor(m => m.Email)
    </div>
 
    <div class="editor-label">@Html.LabelFor(m => m.FirstName)</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.FirstName)
        @Html.ValidationMessageFor(m => m.FirstName)
    </div>
 
    <div class="editor-label">@Html.LabelFor(m => m.LastName)</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.LastName)
        @Html.ValidationMessageFor(m => m.LastName)
    </div>
 
    <div class="editor-label">@Html.LabelFor(m => m.BirthDate)</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.BirthDate)
        @Html.ValidationMessageFor(m => m.BirthDate)
    </div>
     
    <input type="submit" value="S'enregistrer" />
}

To activate the client side validation the following jQuery plugins should be loaded :

<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery-ui-1.8.11.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
</head>

The validation actions are automatically called (asynchronously of course) when the user is completing the register form :

image

Hope this helps Winking smile


read full post -

[ASP.NET MVC] Attribut Remote et validation asynchrone

by Julien Corioland the 9/5/2011

Comme vous le savez peut-être déjà, ASP.NET MVC 3 supporte les DataAnnotations du .NET Framework 4 pour tout ce qui est validation des entrées utilisateurs : Required, StringLength, Range, RegularExpression…

Il existe un attribut un peu moins connu : Remote. Celui-ci permet de lancer une validation asynchrone côté client. Par exemple, sur un formulaire d’inscription il est fréquent de vouloir valider de manière asynchrone qu’un nom d’utilisateur / e-mail n’existe pas déjà en base utilisateur.

Voilà un exemple de modèle pour parvenir à cela :

public class UserModel
{
    [Required]
    [Remote("CheckUsername", "RemoteValidation", ErrorMessage = "Ce nom d'utilisateur est déjà utilisé")]
    public string Username { get; set; }

    [Required]
    [Remote("CheckEmail", "RemoteValidation", ErrorMessage = "Cet e-mail est déjà utilisé")]
    public string Email { get; set; }
    
    [StringLength(80)]
    public string FirstName { get; set; }

    [StringLength(80)]
    public string LastName { get; set; }
    
    public DateTime BirthDate { get; set; }
}

Comme il est possible de le constater dans l’extrait de code ci-dessus, l’attribut remote prend deux paramètres :

  • L’action MVC en charge de la validation
  • Le contrôlleur dans lequel l’action est défini (ici, RemoteValidationController)

Voilà le code de ce contrôlleur :

public class RemoteValidationController : Controller 
{
    private readonly string[] _existingUsernames = {"beedoo", "julien", "jcorioland"};
    private readonly string[] _existingEmails = { "contact@juliencorioland.net" };

    public JsonResult CheckUsername(string username) {
        bool userIsAvailable = !_existingUsers.Contains(username);
        return Json(userIsAvailable, JsonRequestBehavior.AllowGet);
    }

    public JsonResult CheckEmail(string email)
    {
        bool emailIsAvailable = !_existingEmails.Contains(email);
        return Json(emailIsAvailable, JsonRequestBehavior.AllowGet);
    }
}

Les actions sont ultra simples : elles prennent en paramètre le terme à valider et retourne un booléean, sérialisé en Json.

NB : ici la source d’emails/usernames existants a été simplifiée, mais un appel à une base de données est tout à fait possible.

La vue pour tester :

@using (Html.BeginForm())
{
    <div class="editor-label">@Html.LabelFor(m => m.Username)</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.Username)
        @Html.ValidationMessageFor(m => m.Username)
    </div>

    <div class="editor-label">@Html.LabelFor(m => m.Email)</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.Email)
        @Html.ValidationMessageFor(m => m.Email)
    </div>

    <div class="editor-label">@Html.LabelFor(m => m.FirstName)</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.FirstName)
        @Html.ValidationMessageFor(m => m.FirstName)
    </div>

    <div class="editor-label">@Html.LabelFor(m => m.LastName)</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.LastName)
        @Html.ValidationMessageFor(m => m.LastName)
    </div>

    <div class="editor-label">@Html.LabelFor(m => m.BirthDate)</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.BirthDate)
        @Html.ValidationMessageFor(m => m.BirthDate)
    </div>
    
    <input type="submit" value="S'enregistrer" />
}

Enfin, assurez vous de charger les plugins jQuery UI et jQuery validate pour que cela fonctionne :

<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery-ui-1.8.11.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
</head>

Dès lors, les actions de validation sont appelées en asynchrone lors de la saisie du formulaire, permettant ainsi une aide à la saisie avancée pour l’utilisateur !

image

NB : et comme toujours, la validation côté client ne doit absolument pas remplacer la validation côté serveur !

A bientôt Winking smile


read full post -

[ASP.NET MVC] Authentification et utilisateurs anonymes

by Julien Corioland the 8/22/2011

Lorsque l’on développe une application web nécessitant de l’authentification, plusieurs stratégies sont possibles, par exemple :

  • On autorise l’accès anonyme partout, sauf sur les pages nécessitant que l’utilisateur soit authentifié
  • On refuse l’accès anonyme partout sauf sur les pages ne nécessitant pas que l’utilisateur soit authentifié

Depuis ASP.NET MVC 2 il est possible de réaliser la première option très facilement. En effet, il suffit de placer un filtre d’authorisation sur les méthodes des contrôleurs (entendre action) pour lesquelles il faut que l’utilisateur soit authentifié. Il s’agit de l’attribut AuthorizeAttribute :

[Authorize]
public ActionResult Admin()
{
    //code
    return View();
}

Sur certains projets, seules quelques pages ne nécessitent pas que l’utilisateur soit activée, il devient donc contraigant d’avoir à placer l’attribut Authorize sur TOUTES les méthodes de contrôleur. ASP.NET MVC 3 introduit un nouveau concept : les filtres globaux. Il s’agit en fait de la possibilité d’appliquer des filtres sur toutes les actions de tous les contrôleurs, par exemple.

Pour cela, il suffit de venir ajouter le filtre en question dans la liste des filtres globaux. Ceci se fait dans le fichier Global.asax. Depuis la version 3 d’ASP.NET MVC, une nouvelle méthode est générée : RegisterGlobalFilters :

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());
    filters.Add(new AuthorizeAttribute());
}

Cette méthode est ensuite appelée dans le Application_Start du même fichier :

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}

Si vous testez ce code, vous verrez que plus rien ne s’affiche, même pas la page d’authentification puisque vous demandez que l’utilisateur soit authentifié partout.

Pour rendre le process un peu plus permissif, il suffit de créer un nouvel attribut AnonymousAttribute qui servira à marquer les actions ne nécessitant pas que l’utilisateur soit authentifié :

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class AnonymousAttribute : Attribute
{
}

A présent, il est possible de dériver le filtre AuthorizeAttribute et de surcharger la méthode OnAuthorization de celui-ci. Dans cette surcharge, il est possible de vérifier sur le contexte du filtre si l’action possède ou non l’attribut Anonymous décrit ci-dessus :

public class AuthenticationRequiredAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        bool anonymousAllowed = filterContext.ActionDescriptor.IsDefined(typeof (AnonymousAttribute), false);

        if (!anonymousAllowed)
            base.OnAuthorization(filterContext);
    }
}
Du coup, il suffit d’enregister ce filtre plutôt que le AuthorizeAttribute dans le Global.asax
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());
    filters.Add(new AuthenticationRequiredAttribute());
}

Puis de placer l’attribut Anonymous partout ou vous souhaitez conserver une accès anonyme (par exemple les actions LogOn) :

[Anonymous]
public ActionResult LogOn()
{
    return View();
}

[HttpPost]
[Anonymous]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    //code
}

Du coup, le comportement désiré est bien obtenu : toutes les actions nécessitent que l’utilisateur soit authentifié, sauf celles où pour lesquelles l’accès anonyme est autorisé explicitement.

En espérant que cela serve à certains d’entre vous !

A bientôt Winking smile


read full post -

[ASP.NET MVC] Validation de formulaire avec jQuery

by Julien Corioland the 8/1/2011

ASP.NET MVC 3 supporte de nouvelle fonctionnalités en terme de validation de formulaire côté client, à l’aide de la bibliothèque de jQuery et de son plugin dédié à la validation.

Dans ce post, nous verrons comment valider un formulaire côté client. Il faut garder à l’esprit que la validation côté client n’est là que pour rendre le formulaire plus ergonomique (il s’agit plutôt d’aide à la saisie) : il est impératif de toujours valider un modèle côté serveur !

Dans le fichier de configuration de votre application ASP.NET MVC 3, vous devriez voir les deux clés suivantes dans la section <appSettings /> :

<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />

La première, ClientValidationEnabled, permet de préciser que vous souhaitez que la validation des formulaires côté client (en Javascript donc) soit activée.

La seconde, UnobtrusiveJavaScriptEnabled, permet de préciser que vous souhaitez que du javascript “non intrusif” soit utilisé pour la validation. Le monteur ASP.NET MVC 3 rajoutera alors des attributs supplémentaires lors de la génération des champs d’un formulaire par l’intermédiaire d’un HtmlHelper (TextBox, TextBoxFor, CheckBox, CheckBoxFor…).

Ces attributs pourront ensuite être interprétés par un framework de validation JQuery : jQuery Validate (et jQuery Validate Unobtrusive).

Lorsque vous ajoutez une nouvelle vue à votre projet via l’interface Visual Studio prévue à cet effet, il est possible de cocher une case “Reference script libraries” afin que les scripts nécessaires à la mise en place de la validation soit ajoutés à la page :

image_thumb1

Cela aura pour effet d’ajouter les lignes suivantes à la vue :

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

Il s’agit des scripts nécessaires pour faire fonctionner l’API jQuery Validate. L’ajout de ces deux références de scripts ainsi que les deux clés du fichier Web.config suffise à permettre la validation côté client :

image_thumb3

NB : pour que cela fonctionne, le modèle doit porter des attributs de validation ! (cf. exemple en bas de page)

Le markup HTML généré lors de l’appel du formulaire est le suivant :

<div class="editor-label">
    <label for="Title">Title</label>
</div>
<div class="editor-field">
    <input class="text-box single-line" data-val="true" data-val-required="Le champ titre est obligatoire" id="Title" name="Title" type="text" value="" />
    <span class="field-validation-valid" data-valmsg-for="Title" data-valmsg-replace="true"></span>
</div>

<div class="editor-label">
    <label for="Description">Description</label>
</div>
<div class="editor-field">
    <input class="text-box single-line" data-val="true" data-val-required="Le champ description est obligatoire" id="Description" name="Description" type="text" value="" />
    <span class="field-validation-valid" data-valmsg-for="Description" data-valmsg-replace="true"></span>
</div>

Comme il est possible de le constater, un certain nombre d’attribut / class ont été rajoutés sur les input afin de permettre la valdidation “non intrusive” :

  • data-val : indique que la validation doit avoir lieu sur cet élément
  • data-val-required : indique que le champ est requis et fournit le message d’erreur à afficher
  • data-val-message-for : indique que la balise span affiche le message d’erreur du champ description

Du coup, si JavaScript est activé sur la machine de l’utilisateur, les champs doivent être saisis convenablement avant le post du formulaire.

Il faut cependant valider le modèle côté serveur, dans le contrôleur :

if (ModelState.IsValid)
{
    using (var repo = new BlogRepository())
    {
        repo.AddBlog(blog);
        return RedirectToAction("Index", "Home");
    }
}

Si vous désactivez la validation non intrusive (via la clé de configuration UnobtrusiveJavaScriptEnabled vue précédemment) vous obtiendrez alors le markup suivant à la suite du formulaire :

<script type="text/javascript">
//<![CDATA[
if (!window.mvcClientValidationMetadata) { window.mvcClientValidationMetadata = []; }
window.mvcClientValidationMetadata.push({"Fields":[{"FieldName":"Title","ReplaceValidationMessageContents":true,"ValidationMessageId":"Title_validationMessage","ValidationRules":[{"ErrorMessage":"Le champ titre est obligatoire","ValidationParameters":{},"ValidationType":"required"}]},{"FieldName":"Description","ReplaceValidationMessageContents":true,"ValidationMessageId":"Description_validationMessage","ValidationRules":[{"ErrorMessage":"Le champ description est obligatoire","ValidationParameters":{},"ValidationType":"required"}]},{"FieldName":"IsPublished","ReplaceValidationMessageContents":true,"ValidationMessageId":"IsPublished_validationMessage","ValidationRules":[{"ErrorMessage":"The IsPublished field is required.","ValidationParameters":{},"ValidationType":"required"}]},{"FieldName":"PublishDate","ReplaceValidationMessageContents":true,"ValidationMessageId":"PublishDate_validationMessage","ValidationRules":[{"ErrorMessage":"The PublishDate field is required.","ValidationParameters":{},"ValidationType":"required"}]}],"FormId":"form0","ReplaceValidationSummary":false,"ValidationSummaryId":"validationSummary"});
//]]>
</script>

Projet web d’exemple : BlogSample.Mvc.zip

A bientôt Winking smile


read full post -

[ASP.NET MVC] Repository Entity Framework dans un contexte stateless

by Julien Corioland the 7/27/2011

Les mappeurs objets relationnels comme Entity Framework apportent de nombreux avantages lors du développement d’application utilisant des bases de données comme système de stockage. Au delà d’effectuer un mapping entre le monde objet et le monde relationnel, permettre la génération de requête SQL à partir du langage .NET LINQ, Entity Framework fournit également un système de détection de changement sur les entités afin de générer uniquement le nécessaire lorsque l’on demande un commit sur la base de données.

L’idée est assez simple au final : toute entité chargée depuis un contexte Entity Framework possède un état :

  • Unchanged : l’entité n’est pas modifiée, aucune requête SQL ne sera générée pour celle-ci lors du commit
  • Added : l’entité a été ajoutée au contexte, un ordre INSERT sera généré lors du commit
  • Modified : l’entité a été modifiée depuis qu’elle a été chargée via le contexte, un ordre UPDATE sera généré lors du commit
  • Deleted : l’entité a été supprimée du contexte, un ordre DELETE sera généré lors du commit

Il existe un dernier état que peut prendre une entité : Detached, c’est à dire qu’elle n’a pas été récupérée via le contexte Entity Framework.

L’état d’une entité est maintenue au sein du contexte par l’ObjectStateManager. Dès lors que le contexte Entity Framework est disposé, toutes les informations relatives au suivi des changements sont perdues.

ASP.NET MVC de part sa nature stateless force une très courte durée de vie pour un contexte Entity Framework : j’envoie une requête HTTP au serveur, le contexte Entity Framework est créé, la récupération de données est effectuée, le contexte est libéré et la connection à la base de données fermée, impliquant que le suivi des changements soit impossible dans ce cas : comment replacer mon objet dans le contexte lors que je post un formulaire HTML ?

Pour l’exemple, nous allons travailler avec l’entity data model suivant :

image_thumb1

Créeons à présent la classe repository qui va permettre d’effectuer les opérations de CRUD sur un item de type Blog :

public class BlogRepository : IDisposable
{
    private readonly BlogsContainer _blogsContainer = new BlogsContainer();

    public void AddBlog(Blog blog)
    {
        _blogsContainer.Blogs.AddObject(blog);
        _blogsContainer.SaveChanges();
    }

    public Blog GetBlog(int blogId)
    {
        return _blogsContainer.Blogs.SingleOrDefault(b => b.Id == blogId);
    }

    public IEnumerable<Blog> GetBlogs()
    {
        return _blogsContainer.Blogs.ToList();
    }

    public void DeleteBlog(Blog blog)
    {
        
    }

    public void UpdateBlog(Blog blog)
    {
        
    }

    public void Dispose()
    {
        _blogsContainer.Dispose();
    }
}

Comme il est possible de le constater dans l’extrait de code ci-dessus, les opérations Add/Get sont réalisées de manière tout à fait classique sur l’object context Entity Framework. Cela va en revanche différer pour les méthodes Delete et Update, volontairement laissées vides pour le moment.

En effet, dans un contexte stateless, il n’est pas possible de bénéficier directement du mécanisme de change tracking proposé par Entity Framework : il faut auparavant attacher l’objet au contexte s'il ne l’est pas déjà.

Pour cela, l’object context expose une propriété ObjectStateManager qui va permettre de récupérer une ObjectStateEntry associée a une entité : si celle-ci existe, l’élément est connu du contexte, sinon il est inconnu :

ObjectStateEntry stateEntry;
if (_blogsContainer.ObjectStateManager.TryGetObjectStateEntry(blog, out stateEntry))
{
    //l'objet Blog est connu du change tracker
}
else
{
    //l'objet blog est inconnu du change tracker
}

Du coup, dans le cas d’un delete, on vérifie si l’objet est connu du change tracker : si oui, pas de souci, sinon on attache l’objet au contexte avant de demander sa suppression :

public void DeleteBlog(Blog blog)
{
    ObjectStateEntry stateEntry;
    if (!_blogsContainer.ObjectStateManager.TryGetObjectStateEntry(blog, out stateEntry))
    {
        _blogsContainer.Blogs.Attach(blog);
    }
    
    _blogsContainer.Blogs.DeleteObject(blog);
    _blogsContainer.SaveChanges();
}

De la même manière, pour la mise à jour il faut vérifier si l’objet est connu du change tracker. Si oui, on va pouvoir utiliser la méthode “ApplyCurrentValues” de l’object set Blogs pour appliquer les changements et forcer le passage de l’entité dans l’état “Modified”. Sinon, il faut forcer le changement d’état via l’object state manager, après avoir attacher l’entité blog :

public void UpdateBlog(Blog blog)
{
    ObjectStateEntry stateEntry;
    if (!_blogsContainer.ObjectStateManager.TryGetObjectStateEntry(blog, out stateEntry))
    {
        _blogsContainer.Blogs.Attach(blog);
        _blogsContainer.ObjectStateManager.ChangeObjectState(blog, System.Data.EntityState.Modified);
    }
    else
    {
        _blogsContainer.Blogs.ApplyCurrentValues(blog);
    }

    _blogsContainer.SaveChanges();
}

Et voilà un repository ultra simple, fonctionnel et adapté au développement d’applications stateless avec ASP.NET MVC.

A bientôt Winking smile


read full post -

  • prev
  • next