I want a customize modelbinder for .net core mvc action parameters, but I not sure DefaultModelBinder is available in .net core. this is .net standard example for customize modelbinder.
public class EncryptDataBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object result = 0;
if (bindingContext.ModelType == typeof(int))
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != null)
{
// decryption code
}
}
return base.BindModel(controllerContext, bindingContext);
}
}
In ASP.NET Core, you need implements IModelBinder.
A simple demo below about custom model binder to change the bound Id:
Model
public class Author
{
[ModelBinder(BinderType = typeof(EncryptDataBinder))]
public int Id { get; set; }
public string Name { get; set; }
}
Custom IModelBinder
public class EncryptDataBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
if (bindingContext.ModelType == typeof(int))
{
// Try to fetch the value of the argument by name
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != null)
{
int id = 3;
//bind the new id....
bindingContext.Result = ModelBindingResult.Success(id);
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}
}
Controller:
[HttpPost]
public IActionResult Post([FromForm]Author model)
{
return Ok(model);
}
This is my action the ModelState checks only the bookId parameter. The other one even if it is null, no error is raised.
Is there any way to make it check the ModelState of all parameters?
[HttpPut]
[Route("{bookId}")]
public IHttpActionResult Edit([FromBody] EditBookBindingModel model, int bookId)
{
if (!this.service.ExistsBook(bookId))
{
return this.NotFound();
}
if (!this.ModelState.IsValid)
{
return this.StatusCode(HttpStatusCode.BadRequest);
}
this.service.EditBook(bookId, model);
return this.Ok();
}
You could define an ActionFilterAttribute that protects you from null arguments:
public class CheckModelForNullAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ActionArguments.ContainsValue(null))
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "The argument cannot be null");
}
}
Then use this:
[HttpPut]
[Route("{bookId}")]
[CheckModelForNull]
public IHttpActionResult Edit([FromBody] EditBookBindingModel model, int bookId)
{
// model canĀ“t be null now
...
I wrote a custom filter so DataAnnotations works with parameters also.
Here is the filter.
public class ModelValidationFilter : FilterAttribute, IActionFilter, IFilter
{
public Task<HttpResponseMessage> ExecuteActionFilterAsync(HttpActionContext actionContext,
CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
var parameters = actionContext.ActionDescriptor.GetParameters();
if (parameters.Any())
{
var validationParams = parameters.Where(x => x.GetCustomAttributes<ValidationAttribute>().Any());
if (validationParams.Any())
{
foreach (var item in validationParams)
{
var val = actionContext.ActionArguments[item.ParameterName];
foreach (var attr in item.GetCustomAttributes<ValidationAttribute>())
{
if (!attr.IsValid(val))
{
actionContext.ModelState.AddModelError(item.ParameterName, attr.FormatErrorMessage(item.ParameterName));
}
}
}
}
if (!actionContext.ModelState.IsValid)
{
return Task.FromResult(actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState));
}
}
return continuation();
}
}
Usage (I have'nt tested completely.):
add it to global filters.
config.Filters.Add(new ModelValidationFilter());
public Student Post([Required] addStudentDTO)
{
//Your logic
}
public Student Patch([FromBody,Required] addStudentDTO, [Required,EmailAddress]string emailAddress])
{
//Your logic
}
Here's the situation. I've got a single action filter that I'm using in two different controllers. The action filter is defined as:
public class ValidSubmissionAttribute : FilterAttribute, IActionFilter
{
public void OnActionExecuted(ActionExecutedContext filterContext)
{
}
public void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = filterContext.Controller;
var session = filterContext.HttpContext.Session;
var isValid = controller.TempData["IsValid"];
if (isValid == null || !(bool)isValid)
{
SharedUtilities.LogOutUser(session, controller.ViewData.ModelState);
filterContext.Result = SharedUtilities.GetThankYouRedirect();
}
}
}
When I invoke the Attribute in one controller, like this:
[HttpPost]
public ActionResult DoSomething(string button, Model data)
{
try
{
if (ModelState.IsValid)
{
TempData["IsValid"] =
Request.Form["ValidRequest"] == Session.SessionID;
Session["VerifyDoingSomethingData"] = data;
return RedirectToAction("VerifyDoingSomething");
}
}
catch (Exception ex)
{
}
}
[ValidSubmission]
public ActionResult VerifyDoingSomething()
{
ViewData.Model = Session["VerifyDoingSomethingData"];
return View("VerifyDoingSomething");
}
it functions as expected. However, when I call it from a different controller, like this:
[HttpPost]
public ActionResult Index(string button, Model data)
{
try
{
if (ModelState.IsValid)
{
TempData["IsValid"] =
Request.Form["ValidRequest"] == Session.SessionID;
Session["ViewModel"] = data;
return RedirectToAction("VerifyCancellation");
}
}
catch (Exception ex)
{
}
}
[ValidSubmission]
public ActionResult VerifyCancellation()
{
ViewData.Model = Session["ViewModel"];
return View("VerifyCancellation");
}
the attribute doesn't run at all. My breakpoint in the OnActionExecuting method doesn't get hit.
If I had to guess, I'd say there was some difference in the controllers or in the action methods, but they appear to be functionally similar. Any insights? Why would I be seeing such different behavior?
Aaaaand, I'm a schmuck.
Turns out there's a completely different execution path that I'd forgotten about. That path didn't have the TempData information to use in the ValidSubmisionAttribute. Everything is functioning correctly now.
I have searched and tried many localization approaches but all is not exactly what I want. Basically, I want to have my url like this
www.myweb.com <== default language (which is English)
www.myweb.com/promotion
www.myweb.com/th/promotion <== local language (Thai)
www.myweb.com/cn/promotion <== local language (Chinese)
Then I want these url to map with different View structure as below
/Views
/_Localization
/cn
/Home
/About.cshtml
/Index.cshtml
/Shared
/_Layout.cshtml
/Error.cshtml
/th
/Home
/About.cshtml
/Shared
/Home
/About.cshtml
/Index.cshtml
/Shared
/_Layout.cshtml
/_LogOnPartial.cshtml
/Error.cshtml
_ViewStart.cshtml
Web.config
As you can seen, Thai doesn't have it own Index.cshtml, _Layout.cshtml and Error.cshtml. So, I would like this to fallback to use the default instead. But chinese will use it own.
I have tried to MapRoute like this
routes.MapRoute(
"DefaultLocal",
"{lang}/{controller}/{action}/{id}",
new { lang = "th", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
but I don't know how to point to different View structure. And in this example, Brian Reiter, it use Cookie not url.
So, how can I achieve this. note that I use RazorViewEngine.
Thank you for any help and thought.
Due to large amount of code needed i will only illustrate an idea of how it could be done.
You can subclass from RazorViewEngine like this:
public class I18NRazorViewEngine : RazorViewEngine
{
public I18NRazorViewEngine() : this(null)
{ }
protected string[] I18NAreaViewLocationFormats;
protected string[] I18NAreaMasterLocationFormats;
protected string[] I18NAreaPartialViewLocationFormats;
protected string[] I18NViewLocationFormats;
protected string[] I18NMasterLocationFormats;
protected string[] I18NPartialViewLocationFormats;
public I18NRazorViewEngine(IViewPageActivator viewPageActivator)
: base(viewPageActivator)
{
this.I18NAreaViewLocationFormats = new string[]
{
"~/Areas/{3}/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{3}/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{3}/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{3}/{2}/Views/Shared/{0}.vbhtml"
};
this.I18NAreaMasterLocationFormats = new string[]
{
"~/Areas/{3}/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{3}/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{3}/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{3}/{2}/Views/Shared/{0}.vbhtml"
};
this.I18NAreaPartialViewLocationFormats = new string[]
{
"~/Areas/{3}/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{3}/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{3}/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{3}/{2}/Views/Shared/{0}.vbhtml"
};
this.I18NViewLocationFormats = new string[]
{
"~/Views/{2}/{1}/{0}.cshtml",
"~/Views/{2}/{1}/{0}.vbhtml",
"~/Views/{2}/Shared/{0}.cshtml",
"~/Views/{2}/Shared/{0}.vbhtml"
};
this.I18NMasterLocationFormats = new string[]
{
"~/Views/{2}/{1}/{0}.cshtml",
"~/Views/{2}/{1}/{0}.vbhtml",
"~/Views/{2}/Shared/{0}.cshtml",
"~/Views/{2}/Shared/{0}.vbhtml"
};
this.I18NPartialViewLocationFormats = new string[]
{
"~/Views/{2}/{1}/{0}.cshtml",
"~/Views/{2}/{1}/{0}.vbhtml",
"~/Views/{2}/Shared/{0}.cshtml",
"~/Views/{2}/Shared/{0}.vbhtml"
};
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
var langValue = controllerContext.Controller.ValueProvider.GetValue("lang");
if (langValue == null || String.IsNullOrEmpty(langValue.AttemptedValue))
return base.FindView(controllerContext, viewName, masterName, useCache);
//Code here
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
var langValue = controllerContext.Controller.ValueProvider.GetValue("lang");
if (langValue == null || String.IsNullOrEmpty(langValue.AttemptedValue))
return base.FindPartialView(controllerContext, partialViewName, useCache);
//Code here
}
}
The next what you should do is to look inside VirtualPathProviderViewEngine on FindView and FindPartialView inplementations. The reflected code is like this:
public virtual ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (string.IsNullOrEmpty(partialViewName))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName");
}
string requiredString = controllerContext.RouteData.GetRequiredString("controller");
string[] searchedLocations;
string path = this.GetPath(controllerContext, this.PartialViewLocationFormats, this.AreaPartialViewLocationFormats, "PartialViewLocationFormats", partialViewName, requiredString, "Partial", useCache, out searchedLocations);
if (string.IsNullOrEmpty(path))
{
return new ViewEngineResult(searchedLocations);
}
return new ViewEngineResult(this.CreatePartialView(controllerContext, path), this);
}
and
public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewName");
}
string requiredString = controllerContext.RouteData.GetRequiredString("controller");
string[] first;
string path = this.GetPath(controllerContext, this.ViewLocationFormats, this.AreaViewLocationFormats, "ViewLocationFormats", viewName, requiredString, "View", useCache, out first);
string[] second;
string path2 = this.GetPath(controllerContext, this.MasterLocationFormats, this.AreaMasterLocationFormats, "MasterLocationFormats", masterName, requiredString, "Master", useCache, out second);
if (string.IsNullOrEmpty(path) || (string.IsNullOrEmpty(path2) && !string.IsNullOrEmpty(masterName)))
{
return new ViewEngineResult(first.Union(second));
}
return new ViewEngineResult(this.CreateView(controllerContext, path, path2), this);
}
both methods rely on private GetPath method:
private string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations)
{
searchedLocations = VirtualPathProviderViewEngine._emptyLocations;
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
string areaName = AreaHelpers.GetAreaName(controllerContext.RouteData);
List<VirtualPathProviderViewEngine.ViewLocation> viewLocations = VirtualPathProviderViewEngine.GetViewLocations(locations, (!string.IsNullOrEmpty(areaName)) ? areaLocations : null);
if (viewLocations.Count == 0)
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, MvcResources.Common_PropertyCannotBeNullOrEmpty, new object[]
{
locationsPropertyName
}));
}
bool flag = VirtualPathProviderViewEngine.IsSpecificPath(name);
string text = this.CreateCacheKey(cacheKeyPrefix, name, flag ? string.Empty : controllerName, areaName);
if (useCache)
{
return this.ViewLocationCache.GetViewLocation(controllerContext.HttpContext, text);
}
if (!flag)
{
return this.GetPathFromGeneralName(controllerContext, viewLocations, name, controllerName, areaName, text, ref searchedLocations);
}
return this.GetPathFromSpecificName(controllerContext, name, text, ref searchedLocations);
}
What you should do is to reimplement it. Most of the code you can reuse, but you should create your own method instead of VirtualPathProviderViewEngine.GetViewLocations. Here its reflected code:
private static List<VirtualPathProviderViewEngine.ViewLocation> GetViewLocations(string[] viewLocationFormats, string[] areaViewLocationFormats)
{
List<VirtualPathProviderViewEngine.ViewLocation> list = new List<VirtualPathProviderViewEngine.ViewLocation>();
if (areaViewLocationFormats != null)
{
for (int i = 0; i < areaViewLocationFormats.Length; i++)
{
string virtualPathFormatString = areaViewLocationFormats[i];
list.Add(new VirtualPathProviderViewEngine.AreaAwareViewLocation(virtualPathFormatString));
}
}
if (viewLocationFormats != null)
{
for (int j = 0; j < viewLocationFormats.Length; j++)
{
string virtualPathFormatString2 = viewLocationFormats[j];
list.Add(new VirtualPathProviderViewEngine.ViewLocation(virtualPathFormatString2));
}
}
return list;
}
You can also reuse most of the code but instead of VirtualPathProviderViewEngine.ViewLocation and VirtualPathProviderViewEngine.AreaAwareViewLocation you should use your own classes. They could be like this:
class ViewLocation
{
protected string _virtualPathFormatString;
public ViewLocation(string virtualPathFormatString)
{
this._virtualPathFormatString = virtualPathFormatString;
}
public virtual string Format(string viewName, string controllerName, string areaName, string lang)
{
return string.Format(CultureInfo.InvariantCulture, this._virtualPathFormatString, new object[]
{
viewName,
controllerName,
lang
});
}
}
and:
class AreaAwareViewLocation : VirtualPathProviderViewEngine.ViewLocation
{
public AreaAwareViewLocation(string virtualPathFormatString) : base(virtualPathFormatString)
{
}
public override string Format(string viewName, string controllerName, string areaName, string lang)
{
return string.Format(CultureInfo.InvariantCulture, this._virtualPathFormatString, new object[]
{
viewName,
controllerName,
areaName,
lang
});
}
}
and then when you will call Format methods you should pass langValue.AttemptedValue (it is from scope of FindView and FindPartialView in the first code block) to lang parameter. Normally it's called in VirtualPathProviderViewEngine.GetPathFromGeneralName.
The main advice is to use ILSpy or another disassembler to explore the code of System.Web.Mvc (or even better - download its sources). The goal is to reimplement FindView and FindPartialView. The rest code provided is to illustrate how it's already done in mvc framework.
It's also important to to seek through arrays declared in our new view engine instead of those without I18N prefix which are already there and used by default classes
Hope it will help despite answer is indirect. You can ask additional questions if you will face any difficulties.
P.S. Don't foreget to register you view engine in global.asax.cs after it will be developed.
protected virtual void Application_Start()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new I18NRazorViewEngine());
}
UpdateModel fails because arcm.Notes was null coming into this method and I want it to be an empty string.
Maybe I need to refresh the ValueProvider (?) after setting Notes to "", so I can use UpdateModel.
public ActionResult Edit1(Guid id, ActivityResponseConsumerMobile arcm) {
if (arcm.Notes == null)
arcm.Notes = "";
if (!ModelState.IsValid) {
SetupDropDowns();
return View(arcm);
}
ActivityResponseConsumerMobile arcmDb = uow.ActivityResponseConsumerMobiles.Single(a => a.Id == id);
try {
UpdateModel(arcmDb, null, null, new[] { "Id" });
This is the default behavior I believe since MVC2.
note one workaround here:
http://brianreiter.org/2010/09/16/asp-net-mvc-2-0-undocumented-model-string-property-breaking-change/
public sealed class EmptyStringModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;
return base.BindModel(controllerContext, bindingContext);
}
}
And register the binder
protected void Application_Start()
{
ModelBinders.Binders.DefaultBinder = new EmptyStringModelBinder();
RegisterRoutes( RouteTable.Routes );
}