I'm trying to create an Extension Method for MVC's htmlHelper.
The purpose is to enable or disable an ActionLink based on the AuthorizeAttribute set on the controller/action.
Borrowing from the MVCSitemap
code that Maarten Balliauw created, I wanted to validate the user's permissions against the controller/action before deciding how to render the actionlink.
When I try to get the MvcHandler, I get a null value.
Is there a better way to the the attributes for the controller/action?
Here is the code for the extension method:
public static class HtmlHelperExtensions
{
public static string SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller)
{
//simplified for brevity
if (IsAccessibleToUser(action, controller))
{
return htmlHelper.ActionLink(linkText, action,controller);
}
else
{
return String.Format("<span>{0}</span>",linkText);
}
}
public static bool IsAccessibleToUser(string action, string controller)
{
HttpContext context = HttpContext.Current;
MvcHandler handler = context.Handler as MvcHandler;
IController verifyController =
ControllerBuilder
.Current
.GetControllerFactory()
.CreateController(handler.RequestContext, controller);
object[] controllerAttributes = verifyController.GetType().GetCustomAttributes(typeof(AuthorizeAttribute), true);
object[] actionAttributes = verifyController.GetType().GetMethod(action).GetCustomAttributes(typeof(AuthorizeAttribute), true);
if (controllerAttributes.Length == 0 && actionAttributes.Length == 0)
return true;
IPrincipal principal = handler.RequestContext.HttpContext.User;
string roles = "";
string users = "";
if (controllerAttributes.Length > 0)
{
AuthorizeAttribute attribute = controllerAttributes[0] as AuthorizeAttribute;
roles += attribute.Roles;
users += attribute.Users;
}
if (actionAttributes.Length > 0)
{
AuthorizeAttribute attribute = actionAttributes[0] as AuthorizeAttribute;
roles += attribute.Roles;
users += attribute.Users;
}
if (string.IsNullOrEmpty(roles) && string.IsNullOrEmpty(users) && principal.Identity.IsAuthenticated)
return true;
string[] roleArray = roles.Split(',');
string[] usersArray = users.Split(',');
foreach (string role in roleArray)
{
if (role != "*" && !principal.IsInRole(role)) return false;
}
foreach (string user in usersArray)
{
if (user != "*" && (principal.Identity.Name == "" || principal.Identity.Name != user)) return false;
}
return true;
}
}
Here is the working code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Security.Principal;
using System.Web.Routing;
using System.Web.Mvc;
using System.Collections;
using System.Reflection;
namespace System.Web.Mvc.Html
{
public static class HtmlHelperExtensions
{
public static string SecurityTrimmedActionLink(
this HtmlHelper htmlHelper,
string linkText,
string action,
string controller)
{
return SecurityTrimmedActionLink(htmlHelper, linkText, action, controller, false);
}
public static string SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller, bool showDisabled)
{
if (IsAccessibleToUser(action, controller))
{
return htmlHelper.ActionLink(linkText, action, controller);
}
else
{
return showDisabled ? String.Format("<span>{0}</span>", linkText) : "";
}
}
public static bool IsAccessibleToUser(string actionAuthorize, string controllerAuthorize)
{
Assembly assembly = Assembly.GetExecutingAssembly();
GetControllerType(controllerAuthorize);
Type controllerType = GetControllerType(controllerAuthorize);
var controller = (IController)Activator.CreateInstance(controllerType);
ArrayList controllerAttributes = new ArrayList(controller.GetType().GetCustomAttributes(typeof(AuthorizeAttribute), true));
ArrayList actionAttributes = new ArrayList();
MethodInfo[] methods = controller.GetType().GetMethods();
foreach (MethodInfo method in methods)
{
object[] attributes = method.GetCustomAttributes(typeof(ActionNameAttribute), true);
if ((attributes.Length == 0 && method.Name == actionAuthorize) || (attributes.Length > 0 && ((ActionNameAttribute)attributes[0]).Name == actionAuthorize))
{
actionAttributes.AddRange(method.GetCustomAttributes(typeof(AuthorizeAttribute), true));
}
}
if (controllerAttributes.Count == 0 && actionAttributes.Count == 0)
return true;
IPrincipal principal = HttpContext.Current.User;
string roles = "";
string users = "";
if (controllerAttributes.Count > 0)
{
AuthorizeAttribute attribute = controllerAttributes[0] as AuthorizeAttribute;
roles += attribute.Roles;
users += attribute.Users;
}
if (actionAttributes.Count > 0)
{
AuthorizeAttribute attribute = actionAttributes[0] as AuthorizeAttribute;
roles += attribute.Roles;
users += attribute.Users;
}
if (string.IsNullOrEmpty(roles) && string.IsNullOrEmpty(users) && principal.Identity.IsAuthenticated)
return true;
string[] roleArray = roles.Split(',');
string[] usersArray = users.Split(',');
foreach (string role in roleArray)
{
if (role == "*" || principal.IsInRole(role))
return true;
}
foreach (string user in usersArray)
{
if (user == "*" && (principal.Identity.Name == user))
return true;
}
return false;
}
public static Type GetControllerType(string controllerName)
{
Assembly assembly = Assembly.GetExecutingAssembly();
foreach (Type type in assembly.GetTypes())
{
if (type.BaseType.Name == "Controller" && (type.Name.ToUpper() == (controllerName.ToUpper() + "Controller".ToUpper())))
{
return type;
}
}
return null;
}
}
}
I don't like using reflection, but I can't get to the ControllerTypeCache.
Your ViewPage has a reference to the view context, so you could make it an extension method on that instead.
Then you can just say if Request.IsAuthenticated or Request.User.IsInRole(...)
usage would be like <%= this.SecurityLink(text, demandRole, controller, action, values) %>
I really liked the code from #Robert's post, but there were a few bugs and I wanted to cache the gathering of the roles and users because reflection can be a little time costly.
Bugs fixed: if there is both a Controller attribute and an Action attribute, then when the roles get concatenated, an extra comma doesn't get inserted between the controller's roles and the action's roles which will not get analyzed correctly.
[Authorize(Roles = "SuperAdmin,Executives")]
public class SomeController() {
[Authorize(Roles = "Accounting")]
public ActionResult Stuff() {
}
}
then the roles string ends up being SuperAdmin,ExecutivesAccounting, my version ensures that Executives and Accounting is separate.
My new code also ignores Auth on HttpPost actions because that could throw things off, albeit unlikely.
Lastly, it returns MvcHtmlString instead of string for newer versions of MVC
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Reflection;
using System.Collections;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Security.Principal;
public static class HtmlHelperExtensions
{
/// <summary>
/// only show links the user has access to
/// </summary>
/// <returns></returns>
public static MvcHtmlString SecurityLink(this HtmlHelper htmlHelper, string linkText, string action, string controller, bool showDisabled = false)
{
if (IsAccessibleToUser(action, controller))
{
return htmlHelper.ActionLink(linkText, action, controller);
}
else
{
return new MvcHtmlString(showDisabled ? String.Format("<span>{0}</span>", linkText) : "");
}
}
/// <summary>
/// reflection can be kinda slow, lets cache auth info
/// </summary>
private static Dictionary<string, Tuple<string[], string[]>> _controllerAndActionToRolesAndUsers = new Dictionary<string, Tuple<string[], string[]>>();
private static Tuple<string[], string[]> GetAuthRolesAndUsers(string actionName, string controllerName)
{
var controllerAndAction = controllerName + "~~" + actionName;
if (_controllerAndActionToRolesAndUsers.ContainsKey(controllerAndAction))
return _controllerAndActionToRolesAndUsers[controllerAndAction];
Type controllerType = GetControllerType(controllerName);
MethodInfo matchingMethodInfo = null;
foreach (MethodInfo method in controllerType.GetMethods())
{
if (method.GetCustomAttributes(typeof(HttpPostAttribute), true).Any())
continue;
if (method.GetCustomAttributes(typeof(HttpPutAttribute), true).Any())
continue;
if (method.GetCustomAttributes(typeof(HttpDeleteAttribute), true).Any())
continue;
var actionNameAttr = method.GetCustomAttributes(typeof(ActionNameAttribute), true).Cast<ActionNameAttribute>().FirstOrDefault();
if ((actionNameAttr == null && method.Name == actionName) || (actionNameAttr != null && actionNameAttr.Name == actionName))
{
matchingMethodInfo = method;
}
}
if (matchingMethodInfo == null)
return new Tuple<string[], string[]>(new string[0], new string[0]);
var authAttrs = new List<AuthorizeAttribute>();
authAttrs.AddRange(controllerType.GetCustomAttributes(typeof(AuthorizeAttribute), true).Cast<AuthorizeAttribute>());
var roles = new List<string>();
var users = new List<string>();
foreach(var authAttr in authAttrs)
{
roles.AddRange(authAttr.Roles.Split(','));
users.AddRange(authAttr.Roles.Split(','));
}
var rolesAndUsers = new Tuple<string[], string[]>(roles.ToArray(), users.ToArray());
try
{
_controllerAndActionToRolesAndUsers.Add(controllerAndAction, rolesAndUsers);
}
catch (System.ArgumentException ex)
{
//possible but unlikely that two threads hit this code at the exact same time and enter a race condition
//instead of using a mutex, we'll just swallow the exception when the method gets added to dictionary
//for the second time. mutex only allow single worker regardless of which action method they're getting
//auth for. doing it this way eliminates permanent bottleneck in favor of a once in a bluemoon time hit
}
return rolesAndUsers;
}
public static bool IsAccessibleToUser(string actionName, string controllerName)
{
var rolesAndUsers = GetAuthRolesAndUsers(actionName, controllerName);
var roles = rolesAndUsers.Item1;
var users = rolesAndUsers.Item2;
IPrincipal principal = HttpContext.Current.User;
if (!roles.Any() && !users.Any() && principal.Identity.IsAuthenticated)
return true;
foreach (string role in roles)
{
if (role == "*" || principal.IsInRole(role))
return true;
}
foreach (string user in users)
{
if (user == "*" && (principal.Identity.Name == user))
return true;
}
return false;
}
public static Type GetControllerType(string controllerName)
{
Assembly assembly = Assembly.GetExecutingAssembly();
foreach (Type type in assembly.GetTypes())
{
if (type.BaseType.Name == "Controller" && (type.Name.ToUpper() == (controllerName.ToUpper() + "Controller".ToUpper())))
{
return type;
}
}
return null;
}
}
Related
Suppose I have users with tags being lazy loaded:
public class User
{
public int UserId { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
}
If I get a user this way, I know that the tags aren't loaded:
User myUser;
using (var context = new MyContext())
{
myUser = context.Users.Find(4);
}
How do I test the Tags collection presence outside of the using clause?
if (myUser.Tags == null) // throws an ObjectDisposedException
I could use a try/catch but there must be a better way.
The only way I can think of is to be able to do a non virtual call (similar to when you do base.Something in a derived class) to the class property getter. Since there is no way to do that with pure C# or reflection, I've ended up with the following helper method which utilizes System.Reflection.Emit LCG (lightweight code generation) to emit Call IL instruction instead of the normal Callvirt:
using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
public static class Utils
{
public static TValue GetClassValue<TSource, TValue>(this TSource source, Expression<Func<TSource, TValue>> selector)
where TSource : class
{
Func<TSource, TValue> getValue = null;
if (source.GetType() != typeof(TSource))
{
var propertyAccessor = selector.Body as MemberExpression;
if (propertyAccessor != null)
{
var propertyInfo = propertyAccessor.Member as PropertyInfo;
if (propertyInfo != null)
{
var getMethod = propertyInfo.GetGetMethod();
if (getMethod != null && getMethod.IsVirtual)
{
var dynamicMethod = new DynamicMethod("", typeof(TValue), new[] { typeof(TSource) }, typeof(Utils), true);
var il = dynamicMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.EmitCall(OpCodes.Call, getMethod, null);
il.Emit(OpCodes.Ret);
getValue = (Func<TSource, TValue>)dynamicMethod.CreateDelegate(typeof(Func<TSource, TValue>));
}
}
}
}
if (getValue == null)
getValue = selector.Compile();
return getValue(source);
}
}
It can be used for both single and collection type navigation properties like this:
if (myUser.GetClassValue(x => x.Tags) == null)
Another solution using a naive try/catch, not sure about the performance compared to the other answer:
using System;
public static class EntityFrameworkExtensions
{
public static bool IsCollectionLoaded<TSource, TValue>(this TSource source, Func<TSource, TValue> selector)
where TSource : class
{
try
{
return (selector(source) != null);
}
catch (ObjectDisposedException)
{
return false;
}
}
}
usage:
if (myUser.IsCollectionLoaded(x => x.Tags))
I'm working with MVC custom validation server side and as i have to use several custom attribute.
I'd like to implement the interface ValidatableObject because I think it is easier way then writing several custom attributes.
To force the ValidationContext I've to use a custom model binder and I've following the instructions by David Haney in his article
Trigger IValidatableObject.Validate When ModelState.IsValid is false
so I've put in global.asax
ModelBinderProviders.BinderProviders.Clear();
ModelBinderProviders.BinderProviders.Add(new ForceValidationModelBinderProvider());
and then in a class
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;
/// <summary>
/// A custom model binder to force an IValidatableObject to execute the Validate method, even when the ModelState is not valid.
/// </summary>
public class ForceValidationModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
base.OnModelUpdated(controllerContext, bindingContext);
ForceModelValidation(bindingContext);
}
private static void ForceModelValidation(ModelBindingContext bindingContext)
{
// Only run this code for an IValidatableObject model
IValidatableObject model = bindingContext.Model as IValidatableObject;
if (model == null)
{
// Nothing to do
return;
}
// Get the model state
ModelStateDictionary modelState = bindingContext.ModelState;
// Get the errors
IEnumerable<ValidationResult> errors = model.Validate(new ValidationContext(model, null, null));
// Define the keys and values of the model state
List<string> modelStateKeys = modelState.Keys.ToList();
List<ModelState> modelStateValues = modelState.Values.ToList();
foreach (ValidationResult error in errors)
{
// Account for errors that are not specific to a member name
List<string> errorMemberNames = error.MemberNames.ToList();
if (errorMemberNames.Count == 0)
{
// Add empty string for errors that are not specific to a member name
errorMemberNames.Add(string.Empty);
}
foreach (string memberName in errorMemberNames)
{
// Only add errors that haven't already been added.
// (This can happen if the model's Validate(...) method is called more than once, which will happen when there are no property-level validation failures)
int index = modelStateKeys.IndexOf(memberName);
// Try and find an already existing error in the model state
if (index == -1 || !modelStateValues[index].Errors.Any(i => i.ErrorMessage == error.ErrorMessage))
{
// Add error
modelState.AddModelError(memberName, error.ErrorMessage);
}
}
}
}
}
/// <summary>
/// A custom model binder provider to provide a binder that forces an IValidatableObject to execute the Validate method, even when the ModelState is not valid.
/// </summary>
public class ForceValidationModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
return new ForceValidationModelBinder();
}
}
It works great...
But here come the question..
I've also to add to this binder a specific behaviour in case of double and double? type to validate number in this format 1.000.000,000 so I was looking at these resources by Reilly and Haack
https://gist.github.com/johnnyreilly/5135647
using System;
using System.Globalization;
using System.Web.Mvc;
public class CustomDecimalModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
//Check if this is a nullable decimal and a null or empty string has been passed
var isNullableAndNull = (bindingContext.ModelMetadata.IsNullableValueType &&
string.IsNullOrEmpty(valueResult.AttemptedValue));
//If not nullable and null then we should try and parse the decimal
if (!isNullableAndNull)
{
actualValue = double.Parse(valueResult.AttemptedValue, NumberStyles.Any, CultureInfo.CurrentCulture);
}
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
Then as suggested in the comment by Haney I've substituted the default DecimalModelBinder with the CustomModelBinder in the global.asax this way
ModelBinders.Binders.Remove(typeof(double));
ModelBinders.Binders.Remove(typeof(double?));
ModelBinders.Binders.Add(typeof(double?), new CustomDecimalModelBinder());
ModelBinders.Binders.Add(typeof(double), new CustomDecimalModelBinder());
But I can't understand why.. the CustomDecimalModelBinder doesn't fire...
So at the moment my workarounf has been to comment the 4 row above in the global.asax
And to add in the custom ModelBinder class the override of BindModel in a way to accept the double and double? in it-It culture
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//if (bindingContext.ModelName == "commercialQty")
if (bindingContext.ModelType == typeof(double?) || bindingContext.ModelType == typeof(double))
{
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
//Check if this is a nullable decimal and a null or empty string has been passed
var isNullableAndNull = (bindingContext.ModelMetadata.IsNullableValueType &&
string.IsNullOrEmpty(valueResult.AttemptedValue));
//If not nullable and null then we should try and parse the decimal
if (!isNullableAndNull)
{
actualValue = double.Parse(valueResult.AttemptedValue, NumberStyles.Any, CultureInfo.CurrentCulture);
}
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}
In this way the ValidationContext with my customValidation works and I also manage to validate double types in a custom way
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
var fieldPreliminaryCostNum = new[] { "preliminaryCostNum" };
var fieldPreliminaryCostAmount = new[] { "preliminaryCostAmount" };
var fieldPreliminaryVoucherNum = new[] { "preliminaryVoucherNum" };
var fieldCodiceIva = new[] { "codiceIva" };
var fieldContoRicavi = new[] { "contoRicavi" };
var fieldContoAnticipi = new[] { "contoAnticipi" };
//per la obbligatorietà di preliminary cost num, preliminary voucher num e preliminary cost amount è sufficiente
//il flag additional oppure occorre anche verificare che il voucher type code sia final?
if (flagAdditional == BLCostanti.fAdditional && preliminaryCostNum == null)
{
results.Add(new ValidationResult(BLCostanti.labelCosto + "preliminaryCostNum ", fieldPreliminaryCostNum));
}
if (flagAdditional == BLCostanti.fAdditional && preliminaryCostAmount == null)
{
results.Add(new ValidationResult(BLCostanti.labelCosto + "preliminaryCostAmount ", fieldPreliminaryCostAmount));
}
if (flagAdditional == BLCostanti.fAdditional && preliminaryVoucherNum == null)
{
results.Add(new ValidationResult(BLCostanti.labelCosto + "preliminaryVoucherNum ", fieldPreliminaryVoucherNum));
//inoltre il preliminary deve essere approvato!
if (! BLUpdateQueries.CheckPreliminaryVoucherApproved(preliminaryVoucherNum) )
{
results.Add(new ValidationResult(BLCostanti.labelCosto + "preliminaryVoucherNum non approvato", fieldPreliminaryVoucherNum));
}
}
if (costPayReceiveInd == BLCostanti.attivo && String.IsNullOrWhiteSpace(codiceIva))
{
//yield return new ValidationResult("codiceIva obbligatorio", fieldCodiceIva);
results.Add(new ValidationResult(BLCostanti.labelEditableFields + "codiceIva ", fieldCodiceIva));
}
if ((sapFlowType == BLCostanti.girocontoAcquisto || sapFlowType == BLCostanti.girocontiVendita)
&& String.IsNullOrWhiteSpace(contoRicavi))
{
results.Add(new ValidationResult(BLCostanti.labelEditableFields + "conto Ricavi ", fieldContoRicavi));
}
if ((sapFlowType == BLCostanti.girocontoAcquisto || sapFlowType == BLCostanti.girocontiVendita)
&& String.IsNullOrWhiteSpace(contoAnticipi))
{
results.Add(new ValidationResult(BLCostanti.labelEditableFields + "conto Anticipi ", fieldContoAnticipi));
}
return results;
}
Any better idea is welcome!
I have a problem with my MVC project! The goal is to set a session var in order to pass it to all the controllers:
inside my xUserController,
Session["UserId"] = 52;
Session.Timeout = 30;
string SessionUserId = ((Session != null) && (Session["UserId"] != null)) ? Session["UserId"].ToString() : "";
//SessionUserId ="52"
But within the ChatMessageController
[HttpPost]
public ActionResult AddMessageToConference(int? id,ChatMessageModels _model){
var response = new NzilameetingResponse();
string SessionUserId = ((Session != null) && (Session["UserId"] != null)) ? Session["UserId"].ToString() : "";
//...
}
return Json(response, "text/json", JsonRequestBehavior.AllowGet);
}
SessionUserId = ""
So, Why this ? How to set the session variable to be global within all my controllers ??
There can be only two reasons of such behavior: the first one is that your session is over and the second is that you rewrite you session variable from another place in your application.
Wthout any additional code there is nothing to say more.
Here is how I solved the issue
I know this is not the best way to do it but it helped me:
First I have created a base controller as follows
public class BaseController : Controller
{
private static HttpSessionStateBase _mysession;
internal protected static HttpSessionStateBase MySession {
get { return _mysession; }
set { _mysession = value; }
}
}
then I changed all my controllers' codes in other to let them inherit from the Base Controller class.
Then I overrode the "OnActionExecuting" method as below :
public class xUserController : BaseController
{
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
BaseController.MySession = Session;
base.OnActionExecuting(filterContext);
}
[HttpPost]
public ActionResult LogIn(FormCollection form)
{
//---KillFormerSession();
var response = new NzilameetingResponse();
Session["UserId"] = /*entity.Id_User*/_model.Id_User;
return Json(response, "text/json", JsonRequestBehavior.AllowGet);
}
}
Finally, I've changed the way I call session variables.
string SessionUserId = ((BaseController.MySession != null) && (BaseController.MySession["UserId"] != null)) ? BaseController.MySession["UserId"].ToString() : "";
instead of
string SessionUserId = ((Session != null) && (Session["UserId"] != null)) ? Session["UserId"].ToString() : "";
now it works and my session vars can walk across all controllers.
Here's the setup:
I have some MVC Controllers that are intended to be consumed by jQuery ajax requests. A normal request would seem somewhat like this:
$.ajax("/Solicitor/AddSolicitorToApplication", {
data: putData,
type: "POST", contentType: "application/json",
success: function (result) {
//My success callback
}
}
});
My controller looks like this:
[HttpPost]
public ActionResult InsertLoanApplication(MortgageLoanApplicationViewModel vm)
{
var mortgageLoanDTO = vm.MapToDTO();
return Json(_mortgageLoanService.UpdateMortgageLoanApplication(mortgageLoanDTO), JsonRequestBehavior.DenyGet);
}
This works perfectly fine with most objects passed to the controller, except that in this specific case one of the properties of the object being passed needs to be deserialized in a specific way.
I've added a JsonConverter that I've used previously with the MVC4 Web API, but in this case I need to apply it to regular mvc controllers.
I tried registering the JsonConverter in my global.asax like this:
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new GrizlyStringConverter());
But so far haven't been able to deserialize the object.
You should replace the built-in JsonValueProviderFactory class with a custom one if you want to use Json.NET when binding JSON requests to view models.
You could write one as shown in this gist:
public sealed class JsonDotNetValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
{
return null;
}
using (var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream))
{
var bodyText = reader.ReadToEnd();
return String.IsNullOrEmpty(bodyText)
? null :
new DictionaryValueProvider<object>(
JsonConvert.DeserializeObject<ExpandoObject>(
bodyText,
new ExpandoObjectConverter()
),
CultureInfo.CurrentCulture
);
}
}
}
and then replace the built-in with your custom one in Application_Start:
ValueProviderFactories.Factories.Remove(
ValueProviderFactories
.Factories
.OfType<JsonValueProviderFactory>()
.FirstOrDefault()
);
ValueProviderFactories.Factories.Add(new JsonDotNetValueProviderFactory());
That's it. Now you are using Json.Net instead of the JavaScriptSerializer for the incoming JSON requests.
The modified version:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Globalization;
using System.IO;
using System.Web.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace MvcJsonNetTests.Utils
{
public class JsonNetValueProviderFactory : ValueProviderFactory
{
public JsonNetValueProviderFactory()
{
Settings = new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Error,
Converters = { new ExpandoObjectConverter() }
};
}
public JsonSerializerSettings Settings { get; set; }
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
throw new ArgumentNullException("controllerContext");
if (controllerContext.HttpContext == null ||
controllerContext.HttpContext.Request == null ||
controllerContext.HttpContext.Request.ContentType == null)
{
return null;
}
if (!controllerContext.HttpContext.Request.ContentType.StartsWith(
"application/json", StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (!controllerContext.HttpContext.Request.IsAjaxRequest())
{
return null;
}
using (var streamReader = new StreamReader(controllerContext.HttpContext.Request.InputStream))
{
using (var jsonReader = new JsonTextReader(streamReader))
{
if (!jsonReader.Read())
return null;
var jsonSerializer = JsonSerializer.Create(this.Settings);
Object jsonObject;
switch (jsonReader.TokenType)
{
case JsonToken.StartArray:
jsonObject = jsonSerializer.Deserialize<List<ExpandoObject>>(jsonReader);
break;
default:
jsonObject = jsonSerializer.Deserialize<ExpandoObject>(jsonReader);
break;
}
var backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
addToBackingStore(backingStore, String.Empty, jsonObject);
return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
}
}
}
private static void addToBackingStore(IDictionary<string, object> backingStore, string prefix, object value)
{
var dictionary = value as IDictionary<string, object>;
if (dictionary != null)
{
foreach (var entry in dictionary)
{
addToBackingStore(backingStore, makePropertyKey(prefix, entry.Key), entry.Value);
}
return;
}
var list = value as IList;
if (list != null)
{
for (var index = 0; index < list.Count; index++)
{
addToBackingStore(backingStore, makeArrayKey(prefix, index), list[index]);
}
return;
}
backingStore[prefix] = value;
}
private static string makeArrayKey(string prefix, int index)
{
return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
}
private static string makePropertyKey(string prefix, string propertyName)
{
return (string.IsNullOrWhiteSpace(prefix)) ? propertyName : prefix + "." + propertyName;
}
}
}
Also to register it at the right index:
public static void RegisterFactory()
{
var defaultJsonFactory = ValueProviderFactories.Factories
.OfType<JsonValueProviderFactory>().FirstOrDefault();
var index = ValueProviderFactories.Factories.IndexOf(defaultJsonFactory);
ValueProviderFactories.Factories.Remove(defaultJsonFactory);
ValueProviderFactories.Factories.Insert(index, new JsonNetValueProviderFactory());
}
I have the following helper method in a ViewModelBase class, which is inherited by other view Models:
public string GetEnumName<T>(Enum value)
{
Type enumType = typeof(T);
var enumValue = Enum.GetName(enumType, value);
MemberInfo member = enumType.GetMember(enumValue)[0];
var attrs = member.GetCustomAttributes(typeof(DisplayAttribute), false);
var outString = ((DisplayAttribute)attrs[0]).Name;
if (((DisplayAttribute)attrs[0]).ResourceType != null)
{
outString = ((DisplayAttribute)attrs[0]).GetName();
}
return outString;
}
I then call this from the view like this:
<p>
#{var rel = Model.GetEnumDisplayName<Enums.wheteverEnum>(Model.wheteverEnum); }
#rel
</p>
Question is - can I work this method so I don't have to tell it the type of the enum? Basically I'd like todo this for all enums:
#Model.GetEnumDisplayName(Model.wheteverEnum)
No typeof, no T, no need to add a reference to the Enums namespace in the View...
Possible?
You can simply remove the type parameter and make it an extension method.
public static string DisplayName(this Enum value)
{
Type enumType = value.GetType();
var enumValue = Enum.GetName(enumType, value);
MemberInfo member = enumType.GetMember(enumValue)[0];
var attrs = member.GetCustomAttributes(typeof(DisplayAttribute), false);
var outString = ((DisplayAttribute)attrs[0]).Name;
if (((DisplayAttribute)attrs[0]).ResourceType != null)
{
outString = ((DisplayAttribute)attrs[0]).GetName();
}
return outString;
}
#Model.wheteverEnum.DisplayName()
Could you not write this as an extension method? Something like...
public static class EnumExtensions
{
public static string ToDescription(this Enum e)
{
var attributes = (DisplayAttribute[])e.GetType().GetField(e.ToString()).GetCustomAttributes(typeof(DisplayAttribute), false);
return attributes.Length > 0 ? attributes[0].Description : string.Empty;
}
}
Usage:
#Model.WhateverEnum.ToDescription();
Nice work #jrummell!
I've added a small tweak below which captures the scenario where an enum doesn't have an associated Display attribute (currently it throws an exception)
/// <summary>
/// Gets the DataAnnotation DisplayName attribute for a given enum (for displaying enums values nicely to users)
/// </summary>
/// <param name="value">Enum value to get display for</param>
/// <returns>Pretty version of enum (if there is one)</returns>
/// <remarks>
/// Inspired by :
/// http://stackoverflow.com/questions/9328972/mvc-net-get-enum-display-name-in-view-without-having-to-refer-to-enum-type-in-vi
/// </remarks>
public static string DisplayFor(this Enum value) {
Type enumType = value.GetType();
var enumValue = Enum.GetName(enumType, value);
MemberInfo member = enumType.GetMember(enumValue)[0];
string outString = "";
var attrs = member.GetCustomAttributes(typeof(DisplayAttribute), false);
if (attrs.Any()) {
var displayAttr = ((DisplayAttribute)attrs[0]);
outString = displayAttr.Name;
if (displayAttr.ResourceType != null) {
outString = displayAttr.GetName();
}
} else {
outString = value.ToString();
}
return outString;
}
The answer of #jrummell in VB.NET for the few of us...
Module ModuleExtension
<Extension()>
Public Function DisplayName(ByVal value As System.Enum) As String
Dim enumType As Type = value.GetType()
Dim enumValue = System.Enum.GetName(enumType, value)
Dim member As MemberInfo = enumType.GetMember(enumValue)(0)
Dim attrs = member.GetCustomAttributes(GetType(DisplayAttribute), False)
Dim outString = CType(attrs(0), DisplayAttribute).Name
If (CType(attrs(0), DisplayAttribute).ResourceType IsNot Nothing) Then
outString = CType(attrs(0), DisplayAttribute).GetName()
End If
Return outString
End Function
End Module
for anyone who might reach to this question, I found this a lot easier than any thing else:
https://www.codeproject.com/articles/776908/dealing-with-enum-in-mvc
Just create a folder "DisplayTemplate" under "Views\Shared", and create an empty view (Name it "Enum") in the new folder "DisplayTemplate", and copy this code to it"
#model Enum
#if (EnumHelper.IsValidForEnumHelper(ViewData.ModelMetadata))
{
// Display Enum using same names (from [Display] attributes) as in editors
string displayName = null;
foreach (SelectListItem item in EnumHelper.GetSelectList(ViewData.ModelMetadata, (Enum)Model))
{
if (item.Selected)
{
displayName = item.Text ?? item.Value;
}
}
// Handle the unexpected case that nothing is selected
if (String.IsNullOrEmpty(displayName))
{
if (Model == null)
{
displayName = String.Empty;
}
else
{
displayName = Model.ToString();
}
}
#Html.DisplayTextFor(model => displayName)
}
else
{
// This Enum type is not supported. Fall back to the text.
#Html.DisplayTextFor(model => model)
}
Here is an extension method that I've written to do just this... it has a little extra logic in it to parse Enum names and split by capital letters. You can override any name by using the Display Attribute
public static TAttribute GetAttribute<TAttribute>(this ICustomAttributeProvider parameterInfo) where TAttribute : Attribute
{
object[] attributes = parameterInfo.GetCustomAttributes(typeof(TAttribute), false);
return attributes.Length > 0 ? (TAttribute)attributes[0] : null;
}
public static bool HasAttribute<TAttribute>(this ICustomAttributeProvider parameterInfo) where TAttribute : Attribute
{
object[] attributes = parameterInfo.GetCustomAttributes(typeof(TAttribute), false);
return attributes.Length > 0 ? true : false;
}
public static string ToFriendlyEnum(this Enum type)
{
return type.GetType().HasAttribute<DescriptionAttribute>() ? type.GetType().GetAttribute<DescriptionAttribute>().Description : type.ToString().ToFriendlyEnum();
}
public static string ToFriendlyEnum(this string value)
{
char[] chars = value.ToCharArray();
string output = string.Empty;
for (int i = 0; i < chars.Length; i++)
{
if (i <= 0 || chars[i - 1].ToString() != chars[i - 1].ToString().ToUpper() && chars[i].ToString() != chars[i].ToString().ToLower())
{
output += " ";
}
output += chars[i];
}
return output.Trim();
}
The GetAttribute extension methods could be slightly overkill, but I use them elsewhere in my projects, so they got reused when I wrote my Enum extension. You could easily combine them back into the ToFriendlyEnum(this Enum type) method
The suggested sollutions does not worked for me with MVC3: so the helper below is good.:
public static string GetEnumDescription(this Enum value)
{
Type type = value.GetType();
string name = Enum.GetName(type, value);
if (name != null)
{
FieldInfo field = type.GetField(name);
if (field != null)
{
string attr = field.GetCustomAttributesData()[0].NamedArguments[0].TypedValue.Value.ToString();
if (attr == null)
{
return name;
}
else
{
return attr;
}
}
}
return null;
}