Affichage des articles dont le libellé est mvc. Afficher tous les articles
Affichage des articles dont le libellé est mvc. Afficher tous les articles

Comment masquer un referer sur un lien sortant avec asp.net mv3

 

Pour diverses raisons il est indispensable de masquer le referer d’un lien sortant dans un site, par exemple lors d’un click dans un extranet ou certains paramètres de l’url peuvent apparaitre et devenir un point d’entrée pour une tentative d’intrusion.

Pour eviter de montrer des informations aux sites distants voici un exemple ce que 'qu’il est possible de faire :

on peut voir ligne 11 ce qu’il faut obtenir , une nouvelle methode d’extension de UrlHelper dans la responsabilité sera d’encoder le lien http://www.google.fr
voici le détail de la methode :
L’idée est de récuperer l’url “http://www.google.fr” et d’encoder la chaine de caractère en base 64 pour qu’elle passe correctement en paramètre meme si elle a elle meme des paramètres, on peut voir que l’url de redirection sera redirect?url=XXX
dans global.asax on va indiquer au gestionnaire de routes comment il doit traiter cette url :

Ligne 5 on indique l’action que doit effectuer le controller home dont voici le code :

on recupere le parametre url encodé en base64, on le décode et on retourne du code html généré dont le but sera d’utiliser la balise meta refresh pour justement masquer la precedente url, voici le resultat :

image

un click sur le lien google.fr, produit ceci :

image

Certe il y a un referer mais il est encodé, le site distant ne peut dont pas voir la page ou etait présent le lien.

Code source de la solution :

Asp.net mvc 2 et RenderAction<> générique

J’ai déjà expliqué ici le peu d’interet que j’accorde aux “magic strings” qui sont souvent sources de bugs, depuis la nouvelle version d’asp.net mvc 2 pour faire du rendu partiel, il n’y à plus besoin d’utiliser les excellentes méthodes de MVCContrib car une nouvelle méthode permet le rendu

Html.RenderAction(ActionName, ControllerName, routeValues);

mais les paramètres ActionName et ControllerName sont de type string , dans le cas d’une refactorisation du nom d’une action ou d’un controlleur , il y a danger. pour éviter ça voici une méthode générique qui permet d’appeler les méthodes du controlleur choisi pour le rendu partiel :

1 public static void RenderAction<T>(this HtmlHelper helper, Expression<Action<T>> expression)
2 where T : Controller
3 {
4 var call = expression.Body as MethodCallExpression;
5 var controllerName = typeof(T).Name.Replace("Controller", string.Empty);
6 var actionName = call.Method.Name;
7
8 var parameters = call.Method.GetParameters();
9 var routeValues = new RouteValueDictionary();
10 if (parameters.Length > 0)
11 {
12 for (int i = 0; i < parameters.Length; i++)
13 {
14 var parameter = call.Arguments[i];
15 object value = null;
16 var constantExpression = parameter as ConstantExpression;
17 if (constantExpression != null)
18 {
19 value = constantExpression.Value;
20 }
21 else
22 {
23 var body = Expression.Convert(parameter, typeof(object));
24 var exp = Expression.Lambda<Func<object>>(body, new ParameterExpression[0]);
25 value = exp.Compile()();
26 }
27 routeValues.Add(parameters[i].Name, value);
28 }
29 }
30 helper.RenderAction(actionName, controllerName, routeValues);
31 }


exemple :


Html.RenderAction<AccountController>(c => c.PartialLoginBox(“login.ascx”));



Bien sur ceci est au détriment de la performance, mais on gagne en robustesse de code.

Validation Asp.Net MVC 2.0 avec Entlib 5.0

Asp.Net 2.0 est maintenant pourvu d’un système de validation des entités du modèle très puissant basé sur des attributs qu’il faut placer sur les propriétés d’une classe pour valider celle-ci.

 

Voir sur le site de Scott Guthrie : http://weblogs.asp.net/scottgu/archive/2010/01/15/asp-net-mvc-2-model-validation.aspx

A priori ce dispositif est très pratique, mais à la moindre modification de règle de validation il faut recompiler le projet et mettre à jour l’application web pour que celle-ci soit prise en compte.

Les “géniaux” développeurs d’Asp.Net MVC 2.0 ont mis en place un Fournisseur de validation d’entité qu’il est possible d’étendre.

Je vais montrer ici comment utiliser le Validation Application Block d’Entlib 5.0 pour valider les entités du modèle et stocker les règles de validation dans un fichier de configuration xml a part.

Dans un premier temps il va falloir indiquer que l’on veut utiliser le Validation Application Block d’Entlib

	public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);

}

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

RegisterRoutes(RouteTable.Routes);

var validationFile = Context.Server.MapPath("/validations.config");

ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new EntLibValidatorProvider(validationFile));
}
}


Il faut donc effacer les validateurs standards, puis ajouter celui d’enlib, j’ai récupéré ce code sur le blog de Brad Wilson à l’adresse suivante :


http://bradwilson.typepad.com/blog/2009/10/enterprise-library-validation-example-for-aspnet-mvc-2.html


	public class EntLibValidatorProvider : ModelValidatorProvider
{
private string m_ConfigFileName;

public EntLibValidatorProvider(string configFileName)
{
m_ConfigFileName = configFileName;
}

public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
{
var configSource = new Microsoft.Practices.EnterpriseLibrary.Common.Configuration.FileConfigurationSource(m_ConfigFileName);
var validator = ValidationFactory.CreateValidatorFromConfiguration(metadata.ModelType, "defaultRuleSets", configSource);

if (validator != null)
{
yield return new EntLibValidatorWrapper(metadata, context, validator);
}
}
}


a noter, la fabrique de validator possède une methode qui permet de charger les règles a partir d’un fichier (le chemin de celui-ci est passé en paramètre du constructeur)



voici le code du wrapper EntLibValidatorWrapper



	public class EntLibValidatorWrapper : ModelValidator
{
private Validator _validator;

public EntLibValidatorWrapper(ModelMetadata metadata, ControllerContext context, Validator validator)
: base(metadata, context)
{
_validator = validator;
}

public override IEnumerable<ModelValidationResult> Validate(object container)
{
return ConvertResults(_validator.Validate(Metadata.Model));
}

private IEnumerable<ModelValidationResult> ConvertResults(IEnumerable<ValidationResult> validationResults)
{
if (validationResults != null)
{
foreach (var validationResult in validationResults)
{
if (validationResult.NestedValidationResults != null)
{
foreach (var result in ConvertResults(validationResult.NestedValidationResults))
{
yield return result;
}
}

yield return new ModelValidationResult
{
Message = validationResult.Message,
MemberName = validationResult.Key
};
}
}
}
}




Pour l’exemple l’idée sera de valider une classe très simple Person



	public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}


On peut voir qu’elle ne possède aucun attribut (pure classe poco).



J’ajoute une règle qui interdit que le nom puisse être vide dans un fichier en racine du site web nommé “validations.config”



<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="validation" type="Microsoft.Practices.EnterpriseLibrary.Validation.Configuration.ValidationSettings, Microsoft.Practices.EnterpriseLibrary.Validation, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="true" />
</configSections>
<validation>
<type name="MvcEntlibValidation.Models.Person" defaultRuleset="defaultRuleSets"
assemblyName="MvcEntlibValidation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<ruleset name="defaultRuleSets">
<properties>
<property name="LastName">
<validator type="Microsoft.Practices.EnterpriseLibrary.Validation.Validators.NotNullValidator, Microsoft.Practices.EnterpriseLibrary.Validation, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
messageTemplate="Vous devez indiquer un nom" name="Not Null Validator" />
</property>
</properties>
</ruleset>
</type>
</validation>
</configuration>


Entlib 5.0 possède un editeur de configuration assez sympa, voici ce que ça donne :



image 


il ne reste plus qu’a mettre en place l’action d’edition d’une personne dans le controller :


		public ActionResult EditPerson()
{
ViewData.Model = new Models.Person();
return View();
}

[HttpPost]
public ActionResult EditPerson(Models.Person person)
{
ViewData.Model = person;

if (!ModelState.IsValid)
{
return View();
}

return View();
}


 



L’appel de la propriété ModelState.IsValid a pour effet de valider l’instance de la classe Person.



image



Voici le contenu de la vue :



    <h2>EditPerson</h2>

<% =Html.ValidationSummary() %>
<% Html.BeginForm(); %>

Nom : <%=Html.EditorFor(m => m.LastName) %><br />
<%=Html.ValidationMessageFor(m => m.LastName) %>
<br />

Prénom : <%=Html.EditorFor(m => m.FirstName) %><br />
<%=Html.ValidationMessageFor(m => m.FirstName) %>

<br />

<input type="submit" name="Valider" />

<% Html.EndForm(); %>





Il ne manque plus que la mise en place de la validation client à la volée pour que ce soit parfait, voici le code source de l’exemple :


Utiliser Unity 2 avec Asp.net MVC 2

Le principe MVC pour Asp.Net est un vraie révolution dans le monde Microsoft par rapport aux WebForms, j’ai vraiment été surpris du virage à 180° qui a été initié par les architectes MS, notamment sur l’abandon total du ViewState et du systeme evenementiel  (pas de regret pour moi ;)),  elle l’est encore plus lorsque l’on peut conjuguer ce systeme avec un inverseur de controle type Unity.

J’ai déjà expliqué sur ce blog qu’est que Unity application block et fait une introduction sur la façon de l’utiliser.

Je vais expliquer ici comment se servir de Unity pour faire de l’injection dans les controllers MVC.

Dans un premier temps il faut mettre en place un container Unity et le configurer, le meilleur endroit pour faire ça est de placer dans l’evenement Application Start dans Global.asax par exemple.

 

	public class MvcApplication : System.Web.HttpApplication
{
public static IUnityContainer IoC { get; private set; }

protected void Application_Start()
{
IoC = new UnityContainer();
}


        }



Il existe différentes techniques pour “stocker” l’instance du container , dans cet exemple pour simplifier, je l’ai placé dans une variable statique de la classe MvcApplication, il sera possible de la retrouver facilement via une appel MvcApplication.IoC.



Asp.Net utilise une Factory pour charger les controlleurs qui “matchent” les routes configurées dans un site MVC, il s’agit de System.Web.Mvc.DefaultControllerFactory, il faut dans un premier temps creer un nouveau Type qui hérite de la fabrique de controlleurs par defaut.



	internal class UnityControllerFactory : DefaultControllerFactory
{
public UnityControllerFactory(IUnityContainer container)
{
this.Container = container;
}

protected IUnityContainer Container { get; private set; }

protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
if (controllerType == null)
{
throw new HttpException(404, String.Format("The controller for path '{0}' could not be found or it does not implement IController.",
requestContext.HttpContext.Request.Path));
}


var controller = Container.Resolve(controllerType) as Controller;
controller.ActionInvoker = new UnityActionInvoker(Container);

return controller;
}
}


Il faut passer le container IoC dans le constructeur de la fabrique, et overrider la methode GetControllerType pour que IoC s’occupe de charger les bons controllers en fonction des routes via :


 var controller = Container.Resolve(controllerType) as Controller; 


la ligne juste en dessous nous permet aussi, si une action d’un controller est marquée par un attribut de type ActionFilterAttribute de pouvoir y injecter aussi des services référencés dans le container, voici le code de l’action Invoker


	public class UnityActionInvoker : ControllerActionInvoker
{
public UnityActionInvoker(IUnityContainer container)
{
this.Container = container;
}

protected IUnityContainer Container { get; private set; }

protected override ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
{
foreach (var filter in filters)
{
Container.BuildUp(filter.GetType(), filter);
}

return base.InvokeActionMethodWithFilters(controllerContext, filters, actionDescriptor, parameters);
}
}


De retour dans notre Application_Start, il suffit alors d’indiquer notre nouvelle fabrique de controller au systeme de la manière suivante :


 


		protected void Application_Start() 



              { 


                   IoC = new UnityContainer(); 


	           var cfactory = new UnityControllerFactory(IoC);
ControllerBuilder.Current.SetControllerFactory(cfactory);


              }



 


Unity prendra alors le control sur l’application web et permettra l’injection, l’interception, ect...


Exemple d’injection d’un service de log pour toutes les actions d’un controller marqué par un Attribut.


voici l’interface du logger :


	public interface ILogger
{
void Write(string message);
}


 



Dans un deuxieme temps nous allons creer un Attribut de log pour marquer les actions du controller :



	public class LoggerActionFilterAttribute : ActionFilterAttribute
{
[Dependency]
public Services.ILogger Logger { get; set; }

public override void OnResultExecuted(ResultExecutedContext filterContext)
{
base.OnResultExecuted(filterContext);
var viewResult = filterContext.Result as ViewResult;
Logger.Write(viewResult.ViewName);
}
}


 



Il faut remarquer l’attribut [Dependency] qui marque la propriété Logger de type ILogger, comme évoqué plus haut nous avons également dans la fabrique de controller intercepté la configuration des ActionInvoker via la ligne suivante



controller.ActionInvoker = new UnityActionInvoker(Container);



Grace au mécanisme d’injection de Unity, il affecte la propriété Logger sans qui soit nécéssaire de chercher ou d’instancier son type .



Dans le controller Home par exemple , il faut alors marquer une action pour pouvoir la logger



		[ActionFilters.LoggerActionFilter]
public ActionResult Index()
{
ViewData["Message"] = "Welcome to ASP.NET MVC!";

return View();
}


Bien sur il est aussi possible d’injecter l’instance du logger dans le controller toujours en utilisant Unity , une technique serait par exemple d’ajouter un paramètre dans le constructeur.



	[HandleError]
public class HomeController : Controller
{
public HomeController(Services.ILogger logger)
{
this.Logger = logger;
}

protected Services.ILogger Logger { get; private set; }

[ActionFilters.LoggerActionFilter]
public ActionResult Index()
{
ViewData["Message"] = "Welcome to ASP.NET MVC!";

return View();
}

public ActionResult About()
{
return View();
}
}


Et la il n’y a pas besoin de marquer la propriété via un attribut [Dependency] car Unity sait résoudre le paramètre du constructeur s’il a été défini au préalable de la manière suivante :


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

RegisterRoutes(RouteTable.Routes);

var container = new UnityContainer();

container.RegisterType<Services.ILogger, Services.ConcreteLogger>(new ContainerControlledLifetimeManager());

var cfactory = new UnityControllerFactory(container);
ControllerBuilder.Current.SetControllerFactory(cfactory);
}


Voila, avec le couple Asp.Net MVC + Unity il devient vraiment possible de créer de très grandes applications web très agiles, dans un prochain billet, j’expliquerai comment mettre en place un dispositif de plugins toujours basé sur Unity pour asp.net MVC

Comment tester les routes Asp.Net MVC

lors d’une phase de test, j’ai été amené a devoir tester un certain nombre de routes pour savoir quels Controllers et Actions etaient matchés, voici comment j’ai procédé.

Dans un premier temps il faut creer une classe HttpContext “bouchon” qui herite de HttpContextBase

public class MockHttpContext : HttpContextBase
{
private readonly string _relativeUrl;
private readonly Models.UserPrincipal _principal;
private readonly NameValueCollection _formParams;
private readonly NameValueCollection _queryStringParams;
private readonly HttpCookieCollection _cookies;
private readonly SessionStateItemCollection _sessionItems;


public MockHttpContext(string relativeUrl)
: this(relativeUrl, null, null, null, null, null)
{
}

public MockHttpContext(string relativeUrl, Models.UserPrincipal principal, NameValueCollection formParams, NameValueCollection queryStringParams, HttpCookieCollection cookies, SessionStateItemCollection sessionItems)
{
_relativeUrl = relativeUrl;
_principal = principal;
_formParams = formParams;
_queryStringParams = queryStringParams;
_cookies = cookies;
_sessionItems = sessionItems;
}

public override HttpRequestBase Request
{
get
{
return new MockHttpRequest(_relativeUrl, _formParams, _queryStringParams, _cookies);
}
}

public override IPrincipal User
{
get
{
return _principal;
}
set
{
throw new System.NotImplementedException();
}
}

public override HttpSessionStateBase Session
{
get
{
return null;
}
}

}
Et une classe HttpRequest “Bouchon”
	public class MockHttpRequest : HttpRequestBase
{
private readonly string _relativeUrl;
private readonly NameValueCollection _formParams;
private readonly NameValueCollection _queryStringParams;
private readonly HttpCookieCollection _cookies;

public MockHttpRequest(string relativeUrl, NameValueCollection formParams, NameValueCollection queryStringParams, HttpCookieCollection cookies)
{
_relativeUrl = relativeUrl;
_formParams = formParams;
_queryStringParams = queryStringParams;
_cookies = cookies;
}

public override NameValueCollection Form
{
get
{
return _formParams;
}
}

public override NameValueCollection QueryString
{
get
{
return _queryStringParams;
}
}

public override HttpCookieCollection Cookies
{
get
{
return _cookies;
}
}

public override string AppRelativeCurrentExecutionFilePath
{
get { return _relativeUrl; }
}

public override string PathInfo
{
get { return String.Empty; }
}


}
Et pour pouvoir tester n’importe quelle url, il faut commencer par la création du context :

var fakeContext = new Serialcoder.ERPStore.Web.MockHttpContext(“/test”);

puis passer le context au gestionnaire des routes  pour obtenir les valeurs sur la route determinée :

var currentRoute = RouteTable.Routes.GetRouteData(context);

on verifie le “controller” et l’”action” retournés :

var controller =(string) ((KeyValuePair<string, object>)currentRoute.Values["controller"]).Value;

var action = (string) ((KeyValuePair<string, object>)currentRoute.Values["action"]).Value;

il ne reste plus qu’a faire les comparaisons de test
 

Compression GZip d’une vue avec asp.net mvc

Il est possible de compresser très facilement le contenu d’une vue avec asp.net mvc avant son rendu grace à un FilterAttribute, voici comment proceder :

	public class CompressFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var request = filterContext.HttpContext.Request;

string acceptEncoding = request.Headers["Accept-Encoding"];

if (string.IsNullOrEmpty(acceptEncoding))
{
return;
}

acceptEncoding = acceptEncoding.ToUpperInvariant();

var response = filterContext.HttpContext.Response;

if (acceptEncoding.IndexOf("GZIP", StringComparison.InvariantCultureIgnoreCase) != -1)
{
response.AppendHeader("Content-encoding", "gzip");
response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
}
else if (acceptEncoding.IndexOf("DEFLATE", StringComparison.InvariantCultureIgnoreCase) != -1)
{
response.AppendHeader("Content-encoding", "deflate");
response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
}
}
}


puis de placer cet attribut sur la vue à compresser :



		[CompressFilter]
public ActionResult Index()
{
return View();
}


Attention, si le serveur IIS est déjà configuré pour compresser les pages, cela peut creer des problèmes d’affichage.

Asp.net MVC et CatchAll

Je viens d’avoir une attaque sur un site asp.net, du classique /admin/pma/main.php, ect… certains doivent bien connaitre. au passage il ne faut jamais mettre son backoffice sur le meme host que le site front, voir si c’est possible et je prêche pour ma parroisse, ne pas avoir de backoffice du tout mais plutot un vrai logiciel de gestion qui permet de piloter le site front , mais ceci est un autre sujet.

Ces pages provoquaient des erreurs de type 500 qui poluaient le journal d’evenement.

J’ai ajouté un systeme de filtre qui me permet de voir les pages qui ne sont pas catchées par mes routes que je ne détaillerai pas ici et du coup j’ai pu éliminer les attaques *.php





Voici un moyen d’intercepter toutes les routes avec asp.net, dans la déclaration des routes (global.asax) il suffit d’ajouter celle-ci EN DERNIER attention !

RouteTable.Routes.MapRoute(
    "CatchAll"
    , "{*catchall}"
    , new { controller = "Home", action = "CatchAll" }
);

puis d’ajouter la methode CatchAll au controller Home

public ActionResult CatchAll()
{

Response.StatusCode = 404;

return View("404");

}

Récupération de la sortie html d’une vue asp.net mvc

Pour pouvoir templater des emails pour un site web en utilisant asp.net mvc, j’avais besoin de pouvoir recuperer la sortie html d’une vue sans avoir à recrawler une page (ce qui provoque des problèmes de statistiques et autres), en cherchant je suis tombé sur cet article très interessant :

http://jwbs-blog.blogspot.com/2009/08/chaining-aspnet-mvc-actions.html

Je l’ai un peu modifié pour en faire une methode d’extension :

public static string GetActionOutput<T>(this System.Web.Mvc.Controller ctrl, Expression<Action<T>> action)
{
var body = action.Body as MethodCallExpression;

var responseFilter = ctrl.HttpContext.Response.Filter;
var memoryStreamFilter = new MemoryStreamFilter(ctrl.HttpContext.Response.Filter);
ctrl.HttpContext.Response.Filter = memoryStreamFilter;

var routeData = ctrl.ControllerContext.RequestContext.RouteData;
string controllerName = body.Object.Type.Name.Replace("Controller", "");
string actionName = body.Method.Name;
var orgAction = routeData.Values["action"];
var orgController = routeData.Values["controller"];
routeData.Values["action"] = actionName;
routeData.Values["controller"] = controllerName;

var parameters = body.Method.GetParameters();
if (parameters.Length > 0)
{
for (int i = 0; i < parameters.Length; i++)
{
var expression = body.Arguments[i];
object value = null;
var constantExpression = expression as ConstantExpression;
if (constantExpression != null)
{
value = constantExpression.Value;
}
else
{
var lambda = Expression.Lambda<Func<object>>(Expression.Convert(expression, typeof(object)), new ParameterExpression[0]);
value = lambda.Compile()();
}

routeData.Values.Add(parameters[i].Name, value);
}
}

var controllerFactory = ControllerBuilder.Current.GetControllerFactory();
var c = controllerFactory.CreateController(ctrl.ControllerContext.RequestContext, controllerName);
c.Execute(ctrl.ControllerContext.RequestContext);

ctrl.HttpContext.Response.Flush();
memoryStreamFilter.Position = 0;
string result = null;
using (var r = new StreamReader(memoryStreamFilter))
{
result = r.ReadToEnd();
r.Close();
}

memoryStreamFilter.Close();
memoryStreamFilter.Dispose();

ctrl.HttpContext.Response.Filter = responseFilter;
routeData.Values["action"] = orgAction;
routeData.Values["controller"] = orgController;

return result;
}





voici la classe de wrapping pour recuperer la sortie en mémoire :




public class MemoryStreamFilter : MemoryStream
{
public MemoryStreamFilter(Stream stream)
{

}
}





pour utiliser cette methode d’extension, il suffit de faire un appel comme ceci :




public ActionResult Index()
{
ViewData["Message"] = "Welcome to ASP.NET MVC!";

var content = this.GetActionOutput<HomeController>(i => i.About());

return View();
}





il est possible de passer des paramètres, la methode est prevue pour ça.

ValidationSummary designable avec asp.net mvc

La methode d’extension Html.ValidationSummary() est certe pratique mais très vite limitée en terme de webdesign pour un rendu professionnel d’un message d’erreur de validation, pour etre plus precis, voici une solution que j’utilise :

Il faut creer un controle validationsummary.ascx (par exemple) à placer dans le repertoire courrant ou dans /shared

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>

<% if (!ViewData.ModelState.IsValid) { %>
<div class="validation">
<strong><img src="/content/images/icon_error.png" alt=""/>Attention !</strong>
<% foreach (var item in ViewData.ModelState.GetAllErrors()) { %>
<p class="error">
<span><%=item%></span>
</p>
<% } %>
</div>
<% } %>



Ajouter la methode d’extension GetAllErrors dans une classe helper :




/// <summary>
/// Retourne la liste de toutes les erreurs de validation d'un formulaire
/// </summary>
/// <param name="modelState">State of the model.</param>
/// <returns></returns>
public static IEnumerable<string> GetAllErrors(this ModelStateDictionary modelState)
{
var errors = from state in modelState
from error in state.Value.Errors
where state.Value.Errors.Count > 0
select error.ErrorMessage;

return errors;
}





puis dans la page ou se trouve le formulaire il suffit d’appeler un rendu partiel




<%Html.RenderPartial("ValidationSummary"); %>

Favicon.ico, asp.net mvc et 404

Sur un de mes sites j’ai constaté dans les logs IIS qu’un 404 se produisait car les navigateurs veulent obtenir le fichier favicon.ico, voici une solution pour eviter de renvoyer un 404 :

dans global.asax, ajouter une route :

routes.MapRoute(
"FavIcon"
, @"favicon.ico"
, new { controller = "Home", action = "FavIcon" }
);



dans le controller “HomeController” ajouter l’action FavIcon :




public ActionResult FavIcon()
{
var pixel = new byte[35] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61
, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00
, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF
, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00
, 0x02, 0x01, 0x44, 0x01, 0x00
, 0x3B };

return this.File(pixel, "image/gif");
}



le serveur retournera bien une reponse 200 au navigateur avec une image d’un pixel.



A savoir, si le fichier existe, pour eviter de passer par le controller par defaut, il faut ajouter la ligne suivante dans global.asax :




routes.IgnoreRoute("{*favicon}", new { favicon = @"(.*/)?favicon.ico(/.*)?" });



Une variation pour l’action FavIcon() suite à la juste remarque de Dascritch :




[OutputCache(VaryByParam="none", Duration=3600)]
public ActionResult FavIcon()
{
byte[] result = null;
using (var bitmap = new System.Drawing.Bitmap(16, 16, System.Drawing.Imaging.PixelFormat.Format24bppRgb))
{
var backColor = bitmap.GetPixel(0, 0);
bitmap.MakeTransparent(backColor);
using (var ms = new System.IO.MemoryStream())
{
bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
result = ms.GetBuffer();
ms.Close();
}
}
return this.File(result, "image/x-icon");
}




Retourne une image au format 16x16 transparent au format PNG, IE ne la prends pas en compte, Firefox est ok et Chrome affiche un carré noir :(

Methode d’extension asp.net mvc pour ajouter un paramètre dans l’url

 

Voici une petite methode d’extension bien pratique pour pouvoir ajouter un parametre dans l’url courante

/// <summary>
/// Ajoute un parametre et sa valeur dans une URL en tenant compte de sa presence eventuelle
/// </summary>
/// <example>
/// Exemple d'utilisation
/// <code>
/// <![CDATA[
///
/// une url existante est de la forme
/// http://domain.com/test?param1=valeur1
///
/// <a href='<%=Url.AddParameter("param2", "valeur2")%>'>Mon lien</a>
///
/// Donnera le resultat suivant :
///
/// http://domain.com/test?param1=valeur1&param2=valeur2
///
/// <a href='<%=Url.AddParameter("param1", "valeur2")%>'>Mon lien</a>
///
/// Donnera le resultat suivant :
///
/// http://domain.com/test?param1=valeur2
///
/// ]]>
/// </code>
/// </example>
/// <remarks>
/// "value" est urlencodé par la methode, ne pas passer la valeur deja encodée
/// </remarks>
/// <param name="helper">The helper.</param>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
/// <returns>Le lien avec le couple key=value</returns>
public static string AddParameter(this UrlHelper helper, string key, string value)
{
var link = helper.RequestContext.HttpContext.Request.Url.PathAndQuery;
var pattern = string.Format("&*{0}=[^&]*", key);
link = System.Text.RegularExpressions.Regex.Replace(link, pattern, "");
link = link.Trim('?').Trim('&');
var separator = (link.IndexOf("?") == -1) ? "?" : "&";
link += string.Format("{0}{1}={2}", separator, key, helper.Encode(value));
return link;
}

Comment traduire un site Asp.net MVC ?

 

La traduction d’un site en plusieurs langues est une tache difficile, et les façons de faire sont nombreuses, je vais proposer ici une methode qui permet de traduire “à la volée” un site web réalisé avec la technologie asp.net MVC.

Le problème principal pour un traducteur, est d’obtenir le contexte de traduction (la page dans laquelle se trouve le texte et son ensemble), la traduction mot à mot engendre systematiquement des aproximations. un autre problème est le format d’echange avec le traducteur, puis la réintroduction des textes traduits. Bref la galère…

Le problème principal pour un webdesigner est qu’il doit editer des fichiers de ressources, bien localiser les textes dans les pages puis trouver leurs correspondances dans des fichier généralement en xml. Bref la galère…

Pour resoudre les principaux problèmes du traducteur, je propose qu’il puisse traduire directement dans la page comme un wiki. Ce qui supprime pratiquement le problème du webdesigner et de ses fichiers, il reste néanmoins la definitions des textes dans les pages a mettre en place, pour cela je propose l’ecriture suivante :

Il s’agit de la page index.aspx par defaut lorsque l’on crée un nouveau projet de type asp.net mvc

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="indexTitle" ContentPlaceHolderID="TitleContent" runat="server">
Home Page
</asp:Content>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
<h2><%= Html.Encode(ViewData["Message"]) %></h2>
<p>
<%=Html.Localize("Content1","To learn more about ASP.NET MVC visit")%><a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
</p>
</asp:Content>



Concernant le fichier index.aspx, l’idée est de sauvegarder un fichier par language avec le nom suivant :



index.aspx.fr.resx



index.aspx.en.resx



ect…



le contenu des fichiers est le suivant :




<?xml version="1.0" encoding="utf-8"?>
<localizations>
<localization key="Title"><![CDATA[Mon Application MVC]]></localization>
<localization key="Content1"><![CDATA[Pour en apprendre d'avantage a propos d'ASP.Net MVC allez sur]]></localization>
</localizations>



On retrouve l’identifiant “Content1”



Le but de la methode d’extension Localize(..) est de reconnaitre le language en cours et de retrouver le bon terme.



voici le détail de cette methode :




public static string Localize(this HtmlHelper helper, string contentKey, string defaultContent)
{
string translationLink = null;
var localizationService = new Services.LocalizationService();
var view = helper.ViewContext.View as System.Web.Mvc.WebFormView;
var path = view.ViewPath.Replace("~", "");
if (helper.ViewContext.HttpContext.Request["translate"] == "1")
{
translationLink = helper.RouteLink("translate"
, new { controller = "Localization"
, action = "LocalizeContent"
, id = string.Empty
, path = path
, key = contentKey
});
}
if (view != null)
{
var localization = localizationService.GetContentLocalization(path, contentKey);
if (localization != null)
{
return localization.Value + "&nbsp;" + translationLink + "&nbsp;";
}
}

return defaultContent + "&nbsp;" + translationLink + "&nbsp;";
}



Dans un premier temps elle va instancier le service de traduction, recuperer le nom du fichier en cours et interroger le service de traduction pour savoir s’il existe un terme pour le fichier en cours avec l’identifiant contentKey , si c’est le cas le terme est retourné, sinon on utilise le terme defaultContent.



On peut voir aussi que l’on verifie l’existence d’un parametre translate=1 dans l’url, c’est ce parametre qui permet de traduire les pages à la volée, s’il est la, on ajoute un lien pour la traduction du terme a coté du terme a traduire, de telle sorte que le traducteur retrouve son contexte.



Voici le service de traduction :




public class LocalizationService : ILocalizationService
{
public LocalizationService()
{

}

private List<Models.ContentLocalization> m_LocalizationList;
private static object m_Lock = new object();

public IList<Models.ContentLocalization> LocalizationList
{
get
{
if (m_LocalizationList == null)
{
lock (m_Lock)
{
if (m_LocalizationList == null)
{
m_LocalizationList = new List<Models.ContentLocalization>();
var currentDirectory = System.Web.HttpContext.Current.Server.MapPath("/");
var fileList = from file in System.IO.Directory.GetFiles(currentDirectory, "*.resx", System.IO.SearchOption.AllDirectories)
select file;

foreach (var file in fileList)
{
XDocument doc = null;
string content = null;
System.IO.StreamReader fs = null;
try
{
fs = System.IO.File.OpenText(file);
content = fs.ReadToEnd();
doc = XDocument.Parse(content);
}
catch
{
continue;
}
finally
{
if (fs != null)
{
fs.Close();
}
}

var path = System.IO.Path.GetFileName(file);
var language = path.Split('.')[2];
path = file.Replace(string.Format(".{0}.resx", language), "");
path = path.Replace(currentDirectory, "");
path = '/' + path.Replace('\\', '/');
m_LocalizationList.AddRange(from x in doc.Root.Elements()
select new Models.ContentLocalization()
{
Key = x.Attribute(XName.Get("key")).Value,
Value = x.Value,
Path = path,
Language = language,
});

}
}
}

}
return m_LocalizationList;
}
}

public static string DefaultLanguage
{
get
{
var language = (string)System.Web.HttpContext.Current.Session["language"];
if (language == null)
{
language = "fr";
}
return language;
}
set
{
System.Web.HttpContext.Current.Session["language"] = value;
}
}

#region ILocalizationService Members

/// <summary>
/// Gets the content localization.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="key">The key.</param>
/// <returns></returns>
public Models.ContentLocalization GetContentLocalization(string path, string key)
{
return LocalizationList.FirstOrDefault(i => i.Path.Equals(path, StringComparison.InvariantCultureIgnoreCase)
&& i.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)
&& i.Language.Equals(LocalizationService.DefaultLanguage, StringComparison.InvariantCultureIgnoreCase));
}

public IList<Models.ContentLocalization> GetContentLocalizationList(string path, string key)
{
var list = LocalizationList.Where(i => i.Path.Equals(path, StringComparison.InvariantCultureIgnoreCase)
&& i.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)).ToList();

foreach (var language in GetLanguageList())
{
var item = list.SingleOrDefault(i => i.Language.Equals(language, StringComparison.InvariantCultureIgnoreCase));
if (item == null)
{
list.Add(new Models.ContentLocalization()
{
Key = key,
Path = path,
Language = language,
Value = null,
});
}
}

return list;
}

/// <summary>
/// Sauvegarde une liste de traduction.
///
/// le principe est de sauvegarder un fichier par langue pour une page ou un controle donné
///
/// par exemple si le path est :
///
/// /views/home/index.aspx
///
/// la liste des traductions est fr, en alors, il y a creation ou append de 2 fichiers
///
/// /views/home/index.aspx.fr.resx
/// /views/home/index.aspx.en.resx
///
/// </summary>
/// <param name="list">The list.</param>
public void Save(IList<Models.ContentLocalization> list)
{
var rootPath = System.Web.HttpContext.Current.Server.MapPath("/");

foreach (var localization in list)
{
if (string.IsNullOrEmpty( localization.Value))
{
continue;
}
var path = System.IO.Path.Combine(rootPath, localization.Path.Trim('
/').Replace('/', '\\'));
path = string.Format("{0}.{1}.resx", path, localization.Language);

XDocument doc = null;
if (System.IO.File.Exists(path))
{
using (var fs = System.IO.File.OpenText(path))
{
doc = XDocument.Parse(fs.ReadToEnd());
fs.Close();
}
var match = doc.XPathSelectElement(string.Format("//localization[@key='
{0}']", localization.Key));
if (match != null)
{
match.RemoveAll();
match.Add(new XCData(localization.Value));
match.Add(new XAttribute("
key", localization.Key));
}
else
{
var elem = new XElement("
localization", new XCData(localization.Value));
elem.Add(new XAttribute("
key", localization.Key));
doc.Root.Add(elem);
}
}
else
{
doc = new XDocument(new XDeclaration("
1.0", "UTF-8", null));
var root = new XElement("
localizations");
var elem = new XElement("
localization", new XCData(localization.Value));
elem.Add(new XAttribute("
key", localization.Key));
root.Add(elem);
doc.Add(root);
}
doc.Save(path);
}
m_LocalizationList = null;
}

public IEnumerable<string> GetLanguageList()
{
yield return "
fr";
yield return "
en";
yield return "
es";
yield break;
}

#endregion
}







il implemente l’interface suivante :




public interface ILocalizationService
{
/// <summary>
/// Retourne une traduction en fonction du chemin de la vue et de la clé de traduction
/// </summary>
/// <param name="path">The path.</param>
/// <param name="key">The key.</param>
/// <returns></returns>
Models.ContentLocalization GetContentLocalization(string path, string key);
/// <summary>
/// Retourne la liste de toutes les traductions possible d'un contenu
/// </summary>
/// <param name="path">The path.</param>
/// <param name="key">The key.</param>
/// <returns></returns>
IList<Models.ContentLocalization> GetContentLocalizationList(string path, string key);

/// <summary>
/// Sauvegarde une liste de traduction.
/// </summary>
/// <param name="list">The list.</param>
void Save(IList<Models.ContentLocalization> list);

/// <summary>
/// Liste des language de traduction
/// </summary>
/// <returns></returns>
IEnumerable<string> GetLanguageList();
}





Comme je l’ai évoqué , si le parametre translate=1 est present, des liens son générés dans la page, ces liens utilisent le controlleur de traduction dont voici le code :




public class LocalizationController : Controller
{
public LocalizationController()
{
this.LocalizationService = new Services.LocalizationService();
}

protected Services.ILocalizationService LocalizationService { get; set; }

public ViewResult LocalizeContent(string path, string key)
{
var list = LocalizationService.GetContentLocalizationList(path, key);
ViewData.Model = list;
ViewData["Key"] = key;
ViewData["Path"] = path;
return View();
}

public ViewResult SaveLocalizedContent(FormCollection form)
{
string key = form["Key"];
string path = form["Path"];
var list = LocalizationService.GetContentLocalizationList(path, key);
var languages = form["lg"].Split(',');
var values = form["value"].Split(',');

for (int i = 0; i < languages.Length; i++)
{
var language = languages[i];
var value = values[i];
var localization = list.Single(l => l.Language.Equals(language, StringComparison.InvariantCultureIgnoreCase));
localization.Value = value;
}

LocalizationService.Save(list);

ViewData.Model = list;
ViewData["Key"] = key;
ViewData["Path"] = path;

return View("LocalizeContent");
}

}





Je n’ai pas évoqué la securité, mais bien sur il ne faut pas utiliser cette solution telle qu’elle, en effet il ne faut pas qu’un internaute puisse retraduire le site, mais on peut s’appuyer facilement sur les membership asp.net, ajouter un attribut “Authorize” sur les methodes du controlleur de traduction.



Pour la demo, il faut que le user sous lequel tourne le site web ai les droits d’ecriture dans le repertoire et sous-repertoire views.



pour voir plus en détail cette solution , voici le code source :


Comment faire un handler d’image en asp.net mvc

 

ASP.NET MVC

Lors de la création d’un site web un peu compliqué ou par exemple les images se trouvent sous forme de blob dans une base de données, ou les fichiers images existent à differents endroits, et pas mal d’autre cas encore, il faut utiliser un handler d’image, qui permet d’intercepter la requete de de produire un traitement avant de rendre l’image demandée.

Avec asp.net version webforms il fallait declarer un handler, souvent une url de la forme http://www.monsite.com/image.ashx?fileName=image.gif, ou plus seo compliant http://www.monsite.com/image.ashx/image.gif (malheureusement cette forme d’url ne fonctionnant pas avec le server web de dev cassini).

Avec asp.net mvc l’idée est de faire passer toutes les requetes via le “ControllerFactory”, il faut ajouter une route de la forme :

routes.MapRoute(
"Images"
,"images/{*fileName}"
, new { controller = "Images", action = "Index" }
);

L’url peut etre de la forme http://www.monsite.com/images/image.gif, attention il ne faut pas que le repertoire images existe à la racine du site.


 


A savoir, {*imageFilename} permet d’ajouter ce que l’on veut comme nom de fichier et par exemple /folder1/folder2/image.gif


 


Le controller est codé de la manière suivante :


 



public class ImagesController : Controller
{
public ActionResult Index(string fileName)
{
fileName = System.IO.Path.Combine(@"c:\images", fileName);
var mimeType = "image/gif";
return this.File(fileName, mimeType);
}
}



Pour ne pas compliquer le discours, je suis parti du principe que les images se trouvent dans le repertoire c:\images et qu’il n’y a que des fichiers gif, l’interception est la combinaison du nom de fichier et son chemin.



Le problème ici est qu’il manque le type mime, voici ci dessous un petit dictionnaire qui permet de retrouver un mimeType via une extension ou vice/versa.




public static Dictionary<string, string> MIMETypeDictionary
= new Dictionary<string, string>()
{
{"323", "text/h323"},
{"3gp", "video/3gpp"},
{"3gpp", "video/3gpp"},
{"acp", "audio/x-mei-aac"},
{"act", "text/xml"},
{"actproj", "text/plain"},
{"ade", "application/msaccess"},
{"adp", "application/msaccess"},
{"ai", "application/postscript"},
{"aif", "audio/aiff"},
{"aifc", "audio/aiff"},
{"aiff", "audio/aiff"},
{"asf", "video/x-ms-asf"},
{"asm", "text/plain"},
{"asx", "video/x-ms-asf"},
{"au", "audio/basic"},
{"avi", "video/avi"},
{"bmp", "image/bmp"},
{"bwp", "application/x-bwpreview"},
{"c", "text/plain"},
{"cat", "application/vnd.ms-pki.seccat"},
{"cc", "text/plain"},
{"cdf", "application/x-cdf"},
{"cer", "application/x-x509-ca-cert"},
{"cod", "text/plain"},
{"cpp", "text/plain"},
{"crl", "application/pkix-crl"},
{"crt", "application/x-x509-ca-cert"},
{"cs", "text/plain"},
{"css", "text/css"},
{"csv", "application/vnd.ms-excel"},
{"cxx", "text/plain"},
{"dbs", "text/plain"},
{"def", "text/plain"},
{"der", "application/x-x509-ca-cert"},
{"dib", "image/bmp"},
{"dif", "video/x-dv"},
{"dll", "application/x-msdownload"},
{"doc", "application/msword"},
{"dot", "application/msword"},
{"dsp", "text/plain"},
{"dsw", "text/plain"},
{"dv", "video/x-dv"},
{"edn", "application/vnd.adobe.edn"},
{"eml", "message/rfc822"},
{"eps", "application/postscript"},
{"etd", "application/x-ebx"},
{"etp", "text/plain"},
{"exe", "application/x-msdownload"},
{"ext", "text/plain"},
{"fdf", "application/vnd.fdf"},
{"fif", "application/fractals"},
{"fky", "text/plain"},
{"gif", "image/gif"},
{"gz", "application/x-gzip"},
{"h", "text/plain"},
{"hpp", "text/plain"},
{"hqx", "application/mac-binhex40"},
{"hta", "application/hta"},
{"htc", "text/x-component"},
{"htm", "text/html"},
{"html", "text/html"},
{"htt", "text/webviewhtml"},
{"hxx", "text/plain"},
{"i", "text/plain"},
{"iad", "application/x-iad"},
{"ico", "image/x-icon"},
{"ics", "text/calendar"},
{"idl", "text/plain"},
{"iii", "application/x-iphone"},
{"inc", "text/plain"},
{"infopathxml", "application/ms-infopath.xml"},
{"inl", "text/plain"},
{"ins", "application/x-internet-signup"},
{"iqy", "text/x-ms-iqy"},
{"isp", "application/x-internet-signup"},
{"java", "text/java"},
{"jnlp", "application/x-java-jnlp-file"},
{"jpg", "image/jpeg"},
{"jpe", "image/jpeg"},
{"jpeg", "image/jpeg"},
{"jfif", "image/jpeg"},
{"jsl", "text/plain"},
{"kci", "text/plain"},
{"la1", "audio/x-liquid-file"},
{"lar", "application/x-laplayer-reg"},
{"latex", "application/x-latex"},
{"lavs", "audio/x-liquid-secure"},
{"lgn", "text/plain"},
{"lmsff", "audio/x-la-lms"},
{"lqt", "audio/x-la-lqt"},
{"lst", "text/plain"},
{"m1v", "video/mpeg"},
{"m3u", "audio/mpegurl"},
{"m4e", "video/mpeg4"},
{"MAC", "image/x-macpaint"},
{"mak", "text/plain"},
{"man", "application/x-troff-man"},
{"map", "text/plain"},
{"mda", "application/msaccess"},
{"mdb", "application/msaccess"},
{"mde", "application/msaccess"},
{"mdi", "image/vnd.ms-modi"},
{"mfp", "application/x-shockwave-flash"},
{"mht", "message/rfc822"},
{"mhtml", "message/rfc822"},
{"mid", "audio/mid"},
{"midi", "audio/mid"},
{"mk", "text/plain"},
{"mnd", "audio/x-musicnet-download"},
{"mns", "audio/x-musicnet-stream"},
{"MP1", "audio/mp1"},
{"mp2", "video/mpeg"},
{"mp2v", "video/mpeg"},
{"mp3", "audio/mpeg"},
{"mp4", "video/mp4"},
{"mpa", "video/mpeg"},
{"mpe", "video/mpeg"},
{"mpeg", "video/mpeg"},
{"mpf", "application/vnd.ms-mediapackage"},
{"mpg", "video/mpeg"},
{"mpg4", "video/mp4"},
{"mpga", "audio/rn-mpeg"},
{"mpv2", "video/mpeg"},
{"NMW", "application/nmwb"},
{"nws", "message/rfc822"},
{"odc", "text/x-ms-odc"},
{"odh", "text/plain"},
{"odl", "text/plain"},
{"p10", "application/pkcs10"},
{"p12", "application/x-pkcs12"},
{"p7b", "application/x-pkcs7-certificates"},
{"p7c", "application/pkcs7-mime"},
{"p7m", "application/pkcs7-mime"},
{"p7r", "application/x-pkcs7-certreqresp"},
{"p7s", "application/pkcs7-signature"},
{"PCT", "image/pict"},
{"pdf", "application/pdf"},
{"pdx", "application/vnd.adobe.pdx"},
{"pfx", "application/x-pkcs12"},
{"pic", "image/pict"},
{"PICT", "image/pict"},
{"pko", "application/vnd.ms-pki.pko"},
{"png", "image/png"},
{"pnt", "image/x-macpaint"},
{"pntg", "image/x-macpaint"},
{"pot", "application/vnd.ms-powerpoint"},
{"ppa", "application/vnd.ms-powerpoint"},
{"pps", "application/vnd.ms-powerpoint"},
{"ppt", "application/vnd.ms-powerpoint"},
{"prc", "text/plain"},
{"prf", "application/pics-rules"},
{"ps", "application/postscript"},
{"pub", "application/vnd.ms-publisher"},
{"pwz", "application/vnd.ms-powerpoint"},
{"qt", "video/quicktime"},
{"qti", "image/x-quicktime"},
{"qtif", "image/x-quicktime"},
{"qtl", "application/x-quicktimeplayer"},
{"qup", "application/x-quicktimeupdater"},
{"r1m", "application/vnd.rn-recording"},
{"r3t", "text/vnd.rn-realtext3d"},
{"RA", "audio/vnd.rn-realaudio"},
{"RAM", "audio/x-pn-realaudio"},
{"rat", "application/rat-file"},
{"rc", "text/plain"},
{"rc2", "text/plain"},
{"rct", "text/plain"},
{"rec", "application/vnd.rn-recording"},
{"rgs", "text/plain"},
{"rjs", "application/vnd.rn-realsystem-rjs"},
{"rjt", "application/vnd.rn-realsystem-rjt"},
{"RM", "application/vnd.rn-realmedia"},
{"rmf", "application/vnd.adobe.rmf"},
{"rmi", "audio/mid"},
{"RMJ", "application/vnd.rn-realsystem-rmj"},
{"RMM", "audio/x-pn-realaudio"},
{"rms", "application/vnd.rn-realmedia-secure"},
{"rmvb", "application/vnd.rn-realmedia-vbr"},
{"RMX", "application/vnd.rn-realsystem-rmx"},
{"RNX", "application/vnd.rn-realplayer"},
{"rp", "image/vnd.rn-realpix"},
{"RPM", "audio/x-pn-realaudio-plugin"},
{"rqy", "text/x-ms-rqy"},
{"rsml", "application/vnd.rn-rsml"},
{"rt", "text/vnd.rn-realtext"},
{"rtf", "application/msword"},
{"rul", "text/plain"},
{"RV", "video/vnd.rn-realvideo"},
{"s", "text/plain"},
{"sc2", "application/schdpl32"},
{"scd", "application/schdpl32"},
{"sch", "application/schdpl32"},
{"sct", "text/scriptlet"},
{"sd2", "audio/x-sd2"},
{"sdp", "application/sdp"},
{"sit", "application/x-stuffit"},
{"slk", "application/vnd.ms-excel"},
{"sln", "application/octet-stream"},
{"SMI", "application/smil"},
{"smil", "application/smil"},
{"snd", "audio/basic"},
{"snp", "application/msaccess"},
{"spc", "application/x-pkcs7-certificates"},
{"spl", "application/futuresplash"},
{"sql", "text/plain"},
{"srf", "text/plain"},
{"ssm", "application/streamingmedia"},
{"sst", "application/vnd.ms-pki.certstore"},
{"stl", "application/vnd.ms-pki.stl"},
{"swf", "application/x-shockwave-flash"},
{"tab", "text/plain"},
{"tar", "application/x-tar"},
{"tdl", "text/xml"},
{"tgz", "application/x-compressed"},
{"tif", "image/tiff"},
{"tiff", "image/tiff"},
{"tlh", "text/plain"},
{"tli", "text/plain"},
{"torrent", "application/x-bittorrent"},
{"trg", "text/plain"},
{"txt", "text/plain"},
{"udf", "text/plain"},
{"udt", "text/plain"},
{"uls", "text/iuls"},
{"user", "text/plain"},
{"usr", "text/plain"},
{"vb", "text/plain"},
{"vcf", "text/x-vcard"},
{"vcproj", "text/plain"},
{"viw", "text/plain"},
{"vpg", "application/x-vpeg005"},
{"vspscc", "text/plain"},
{"vsscc", "text/plain"},
{"vssscc", "text/plain"},
{"wav", "audio/wav"},
{"wax", "audio/x-ms-wax"},
{"wbk", "application/msword"},
{"wiz", "application/msword"},
{"wm", "video/x-ms-wm"},
{"wma", "audio/x-ms-wma"},
{"wmd", "application/x-ms-wmd"},
{"wmv", "video/x-ms-wmv"},
{"wmx", "video/x-ms-wmx"},
{"wmz", "application/x-ms-wmz"},
{"wpl", "application/vnd.ms-wpl"},
{"wprj", "application/webzip"},
{"wsc", "text/scriptlet"},
{"wvx", "video/x-ms-wvx"},
{"XBM", "image/x-xbitmap"},
{"xdp", "application/vnd.adobe.xdp+xml"},
{"xfd", "application/vnd.adobe.xfd+xml"},
{"xfdf", "application/vnd.adobe.xfdf"},
{"xla", "application/vnd.ms-excel"},
{"xlb", "application/vnd.ms-excel"},
{"xlc", "application/vnd.ms-excel"},
{"xld", "application/vnd.ms-excel"},
{"xlk", "application/vnd.ms-excel"},
{"xll", "application/vnd.ms-excel"},
{"xlm", "application/vnd.ms-excel"},
{"xls", "application/vnd.ms-excel"},
{"xlt", "application/vnd.ms-excel"},
{"xlv", "application/vnd.ms-excel"},
{"xlw", "application/vnd.ms-excel"},
{"xml", "text/xml"},
{"xpl", "audio/scpls"},
{"xsl", "text/xml"},
{"z", "application/x-compress"},
{"zip", "application/x-zip-compressed"}
};





Le code devient alors :




public ActionResult Index(string fileName)
{
fileName = System.IO.Path.Combine(@"c:\images", fileName);
var ext = System.IO.Path.GetExtension(fileName);
var mime = MIMETypeDictionary.FirstOrDefault(i => i.Key.Equals(ext, StringComparison.InvariantCultureIgnoreCase));
return this.File(fileName, mime.Value);
}

A noter il est possible de renvoyer un tableau de byte à la place d’un fichier, d’autre part si IIS6 est utilisé pour héberger le site il faut soit declarer chaque extension d’application possible par type d’image, ou ajouter un wildcard de la manière suivante :


 


image