Type-directed serialization in ValueProvider - asp.net-mvc

I was hoping to encapsulate all the serialization logic of my json api using the ValueProvider facility of MS.NET MVC.
However, since my deserialization is type directed (i.e. it needs to know what class to return from deserializing), and I can simply find no reasonable way to get to the type that my ValueProvider is supposed to return.
Suppose we have a controller:
public class MyController : Controller
{
// We want obj to be instantiated by MyValueProvider
public ActionResult MyAction(MyComplexObject obj)
{
}
}
What I would then like to be able to write:
public class MyValueProvider : IValueProvider
{
public bool ContainsPrefix(string prefix)
{
...
}
public ValueProviderResult GetValue(string key)
{
switch (req.HttpMethod)
{
case "POST":
{
req.InputStream.Position = 0;
T retObj = MyDeserialize<T>(req.InputStream);
return new ValueProviderResult(retObj, key, CultureInfo.CurrentCulture);
}
break;
default:
return null;
}
}
}
(Though I realise that without actually binding T in MyValueProvider I would have to use reflection to invoke MyDeserialize with a runtime-provided Type)
I'm almost considering to just derive the runtime type to deserialize using some variable naming scheme keyed by the 'key' argument to GetValue.

After spending half day pondering this, and then reading http://ishwor.cyberbudsonline.com/2012/07/fun-with-aspnet-mvc-3-custom-json-model-binder.html, I have come to the conclusion that what I need is in fact a ModelBinder, as the ModelType is available in the BindingContext.
So, the ValueProvider in the question becomes:
public class MyModelBinder<T> : IModelBinder
{
public object BindModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
JToken resTok;
var key = bindingContext.ModelName;
var req = controllerContext.HttpContext.Request;
if (bindingContext.ModelType != typeof(T)) return null;
switch (req.HttpMethod)
{
case "POST":
req.InputStream.Position = 0;
T retObj = MyDeserialize<T>(req.InputStream);
return retObj;
break;
default:
return null;
}
}
}

Related

What is the best way to prohibit integer value for Enum action's parameter

I use some Enum as parameter in some action. For example we have the following code
public enum SomeEnum { SomeVal1 = 1, SomeVal2 = 2 }
[HttpGet]
public void SomeAction(SomeEnum someParameter) { }
By default asp.net engine allows to use both string and integer values that's why we are able to call it like this 'http://host/SomeController/SomeAction/SomeVal1' or this 'http://host/SomeController/SomeAction/1' or even this 'http://host/SomeController/SomeAction/54'! I would like to stay with the first sample using string value. For this I've implemented the following model binder:
public class RequireStringsAttribute : ModelBinderAttribute
{
public RequireStringsAttribute() : base(typeof(ModelBinder))
{
}
private class ModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.FieldName).FirstValue;
var isValid = Enum.GetNames(bindingContext.ModelType).Any(name =>
name.Equals(value, StringComparison.OrdinalIgnoreCase));
if (isValid)
{
bindingContext.Result = ModelBindingResult.Success(Enum.Parse(bindingContext.ModelType, value, ignoreCase: true));
}
else
{
bindingContext.ActionContext.ModelState.AddModelError(bindingContext.FieldName, $"The value '{value}' is not valid.");
}
return Task.CompletedTask;
}
}
}
And I've applied it:
[HttpGet]
public void SomeAction([RequireStrings]SomeEnum someParameter) { }
It works fine but I just want to know is there a better way to do it?

Receive an array of json objects and validate in the controller

I'm sending a JSON like this:
[
{col1: 'value', col2: 'value'},
{col1: 'value2', col2: 'value2'},
...
]
The action in my controller has a List parameter that is requiring a custom model binder, like this:
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var contentType = controllerContext.HttpContext.Request.ContentType;
String bodyText;
Stream stream = null;
try
{
stream = controllerContext.HttpContext.Request.InputStream;
stream.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(stream))
{
stream = null;
bodyText = reader.ReadToEnd();
}
}
finally
{
if (stream != null)
stream.Dispose();
}
if (string.IsNullOrEmpty(bodyText))
{
return null;
}
var model = new JavaScriptSerializer().Deserialize<T>(bodyText);
return model;
// return base.BindModel(controllerContext, bindingContext);
}
It's working, except that it is not considering the data annotations for validations (required, range, etc).
How can I get this working with validation?
UPDATE
Controller Action
[HttpPost]
public ActionResult ActionName([ModelBinder(typeof(JsonArrayValidationModelBinder<List<EntityName>>))]List<EntityName> viewModel)
Entity
public class EntityName
{
[Display(Name = "Data Entrada")]
[DataType(DataType.Date)]
[Required]
public DateTime? DataEntrada { get; set; }
// ....
}
I have revised my answer. There were a few problems that I ran into when trying to get this working. Detailed below is the problem and the solution I used.
The json: The json you provided did not match the Model you provided. So I assumed the json string should have included something like this:
`DataEntrada: "1/1/2014"`
The model: Your model describes only EntityName. The deserialized json is a list. These are two different things. So I modified the json to be an object that defines EntityNames (list of EntityName), like this:
`data = { EntityNames: [{ DataEntrada: "1/1/2014" }] };`
and then I implemented this class..this will be the result of deserialization:
public class EntityInfo
{
public EntityName[] EntityNames { get; set; }
}
and finally, modified the ActionMethod like so:
public JsonResult SaveActionName([ModelBinder(typeof(JsonArrayValidationModelBinder<EntityInfo>))]EntityInfo viewModel)
Validation: Validating EntityNames was not as easy to implement as I thought it would be. I could not get the validation attribute for EntityName to fire during model binding (being a member of a list). So, I implemented a custom validator derived from 'ValidationAttribute' like this:
public class EntityNamesValidation : ValidationAttribute
{
public override bool IsValid(object value)
{
EntityName[] list = (EntityName[])value;
foreach (EntityName e in list)
{
if (string.IsNullOrEmpty(e.DataEntrada.ToString()))
return false;
// more checks performed here
}
return true;
}
}
and then I applied EntityNamesValidation attribute to EntityNames and EntityInfo, like so:
[EntityNamesValidation]
public EntityName[] EntityNames { get; set; }
Incorrect model during bind: The JsonArrayValidationModelBinder was using a bindingContext that did not have an instance of anything. If you debug BindModel before base.BindModel you will see that bindingContext.Model is null. So what I did was set bindingContext.ModelMetadata.Model = model after deserialization and before the call to base.BindModel. I also moved base.BindModel in the code to fire just before model is returned...see below
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
[...]
bindingContext.ModelMetadata.Model = model;
base.BindModel(controllerContext, bindingContext);
return model;
}
Verification: I did not unit test this, but I did place a breakpoint in the ActionMethod. I then used the following json:
data = { EntityNames: [{ DataEntrada: "1/1/2014" }, { DataEntrada: null }] };
when the code reached the breakpoint, ModelState.IsValid is false. I then changed json to this:
data = { EntityNames: [{ DataEntrada: "1/1/2014" }, { DataEntrada: "2/19/2014" }] };
when the code reached the breakpoint, ModelState.IsValid is true.
This approach works, but is not ideal. I think you want validation to occur without creating custom code and use MVC to handle this.
I hope this gets you a step further.
ALL THE CODE
javascript
data = { EntityNames: [{ DataEntrada: "1/1/2014" }, { DataEntrada: null }] };
var jsonOfLog = JSON.stringify(data);
$.ajax({
type: 'POST',
dataType: 'text',
url: "/EntityData/SaveActionName",
data: jsonOfLog,
success: function (data) {
alert(data);
},
error: function (result) {
alert(result);
}
,
async: false
});
models
public class EntityInfo
{
[EntityNamesValidation]
public EntityName[] EntityNames { get; set; }
}
public class EntityName
{
[Display(Name = "Data Entrada")]
[DataType(DataType.Date)]
[Required]
public DateTime? DataEntrada { get; set; }
}
custom validator
public class EntityNamesValidation : ValidationAttribute
{
public override bool IsValid(object value)
{
EntityName[] list = (EntityName[])value;
foreach (EntityName e in list)
{
if (string.IsNullOrEmpty(e.DataEntrada.ToString()))
return false;
// more checks performed here
}
return true;
}
}
BindModel
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var contentType = controllerContext.HttpContext.Request.ContentType;
String bodyText;
Stream stream = null;
try
{
stream = controllerContext.HttpContext.Request.InputStream;
stream.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(stream))
{
stream = null;
bodyText = reader.ReadToEnd();
}
}
finally
{
if (stream != null)
stream.Dispose();
}
if (string.IsNullOrEmpty(bodyText))
{
return null;
}
var model = new JavaScriptSerializer().Deserialize<T>(bodyText);
bindingContext.ModelMetadata.Model = model;
base.BindModel(controllerContext, bindingContext);
return model;
}
ActionMethod
[HttpPost]
public JsonResult SaveActionName([ModelBinder(typeof(JsonArrayValidationModelBinder<EntityInfo>))]EntityInfo viewModel)
Deriving from DefaultModelBinder will give you what you are looking for. In your override, call base method, like so
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// base BindModel should validate your model
base.BindModel(controllerContext, bindingContext);
// (optional) Capture validation result
bool ModelIsValid = bindingContext.ModelState.IsValid;
var contentType = controllerContext.HttpContext.Request.ContentType;
[...]
}

How do I pass value to MVC3 master page ( _layout)?

I have a custom modelbinder, its check the authentication cookie and return the value.
public class UserDataModelBinder<T> : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext.RequestContext.HttpContext.Request.IsAuthenticated)
{
var cookie =
controllerContext.RequestContext.HttpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
if (cookie == null)
return null;
var decrypted = FormsAuthentication.Decrypt(cookie.Value);
if (!string.IsNullOrWhiteSpace(decrypted.UserData))
return JsonSerializer.DeserializeFromString<T>(decrypted.UserData);
}
return null;
}
}
if I need to use it, I just need to pass it to the action. everything works.
public ActionResult Index(UserData userData)
{
AccountLoginWidgetVM model = new AccountLoginWidgetVM();
if (null != userData)
model.UserData = userData;
return View(userData);
}
However, I want to use it in my master page, because once user login, i want to display their info on the top on every page. I tried a few things, coudln't get it work
#Html.RenderPartial("LoginPartial", ???model here??)
We did it as follows:
Defined separate viewmodel for masterpages.
public class MasterPageViewModel
{
public Guid CurrentUserId { get; set; }
public string CurrentUserFullName { get; set; }
}
Added injection filter and filter provider.
public class MasterPageViewModelInjectorFilterProvider: IFilterProvider
{
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
return new [] {new Filter(new MasterPageViewModelInjectorFilter(), FilterScope.Action, null), };
}
private class MasterPageViewModelInjectorFilter: IResultFilter
{
public void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewResult = filterContext.Result as ViewResult;
if (viewResult == null)
return;
if (viewResult.ViewBag.MasterPageViewModel != null)
return;
//setup model whichever way you want
var viewModel = new MasterPageViewModel();
//inject model into ViewBag
viewResult.ViewBag.MasterPageViewModel = viewModel;
}
public void OnResultExecuted(ResultExecutedContext filterContext)
{
}
}
}
Configure filter provider:
//in Application_Start
FilterProviders.Providers.Add(new MasterPageViewModelInjectorFilterProvider());
Use in master:
ViewBag.MasterPageViewModel
This way you have fine uncoupled architecture. Of course you can combine it with Dependency Injection (we do, but I left it out for clarity) and configure your action filter for every action whichever way you want.
In this case you can use ViewBag.
public ActionResult Index(UserData userData)
{
AccountLoginWidgetVM model = new AccountLoginWidgetVM();
if (null != userData)
model.UserData = userData;
ViewBag.UserData = userData;
return View(userData);
}
#Html.RenderPartial("LoginPartial", ViewBag.UserData)
You have to make sure that userData is not null. If it'll be null the passed model will be default model of the view.

How to pass ObjectId from MongoDB in MVC.net

I'm starting a new project with Mongo, NoRM and MVC .Net.
Before I was using FluentNHibernate so my IDs were integer, now my IDs are ObjectId. So when I have an Edit link my URL looks like this :
WebSite/Admin/Edit/23,111,160,3,240,200,191,56,25,0,0,0
And it does not bind automaticly to my controller as an ObjectId
Do you have any suggestions/best practices to work with this? Do I need to encode/decode the ID everytime?
Thanks!
Use a custom model binder like this ... (working against the offical C# MongoDB driver)
protected void Application_Start()
{
...
ModelBinders.Binders.Add(typeof(ObjectId), new ObjectIdModelBinder());
}
public class ObjectIdModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (result == null)
{
return ObjectId.Empty;
}
return ObjectId.Parse((string)result.ConvertTo(typeof(string)));
}
}
I Use following
public class ObjectIdModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
string value = controllerContext.RouteData.Values[bindingContext.ModelName] as string;
if (String.IsNullOrEmpty(value)) {
return ObjectId.Empty;
}
return new ObjectId(value);
}
}
and
protected void Application_Start()
{
......
ModelBinders.Binders.Add(typeof(ObjectId), new ObjectIdModelBinder());
}
almost forgot, make URLs from ObjectId.ToString()
I am not familiar with the ObjectId type but you could write a custom model binder that will take care of converting the id route constraint to an instance of ObjectId.
Did you know you can use the [MongoIdentifier] attribute to make any property act as the unique key?
I've been solving this issue by borrowing a technique from WordPress by having every entity also be represented by a "url slug" property and decorating that property with [MongoIdentifier].
So if I had a person named Johnny Walker I'd create a slug of "johnny-walker". You just have to make sure these url slugs stay unique and you get to keep clean urls without ugly object ids.
For Web API you can add Custom parameter binding ule in WebApiConfig:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
//...
config.ParameterBindingRules.Insert(0, GetCustomParameterBinding);
//...
}
public static HttpParameterBinding GetCustomParameterBinding(HttpParameterDescriptor descriptor)
{
if (descriptor.ParameterType == typeof(ObjectId))
{
return new ObjectIdParameterBinding(descriptor);
}
// any other types, let the default parameter binding handle
return null;
}
public class ObjectIdParameterBinding : HttpParameterBinding
{
public ObjectIdParameterBinding(HttpParameterDescriptor desc)
: base(desc)
{
}
public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
{
try
{
SetValue(actionContext, new ObjectId(actionContext.ControllerContext.RouteData.Values[Descriptor.ParameterName] as string));
return Task.CompletedTask;
}
catch (FormatException)
{
throw new BadRequestException("Invalid ObjectId format");
}
}
}
}
And use it Without any additional attributes in controller:
[Route("{id}")]
public IHttpActionResult Get(ObjectId id)

Unit Testing with ControllerActionInvoker to invoke action with parameters

I use ControllerActionInvoker to invoke controller actions form unit tests
var controllerInvoker = new ControllerActionInvoker();
var result = controllerInvoker.InvokeAction(
testController.ControllerContext, "Default" );
How do I use it to call an action that has parameters?
[AcceptVerbs( HttpVerbs.Post )]
[ActionException( SomeAttribute )]
public SomeResult AddMethod( long[] Ids )
{
//some code
}
From the documentation it looks like you want to use the InvokeActionMethod method which allows you to pass parameters in an IDictionary as the third argument.
The ControllerContext actually carries with it additional data that the controller will use for binding (filters, model binders, route data). Your argument will need to be passed through the ControllerContext.
I found an example about unit testing controllers.
You shouldn't use the ControllerActionInvoker from within your unit tests. What are you actually trying to accomplish?
If you're trying to test the behavior of your actions, just call them directly (they are just regular methods). If you're trying to test the behavior of your filters, create a mock context for the filter and call its OnXxx() method.
I use the ControllerActionInvoker because I want to write specification tests around my controllers rather than low level unit tests. What I have found is that my implementation of the ControllerActionInvoker has had to evolve based on what I am testing but so for the following has worked for me.
class ControllerSpecActionInvoker<TResult> : ControllerActionInvoker where
TResult : ActionResult
{
private readonly Expression body;
public ControllerSpecActionInvoker(Expression body)
{
this.body = body;
}
public TResult Result { get; private set; }
protected override void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
=> Result = actionResult as TResult;
protected override IDictionary<string, object> GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
if (body is MethodCallExpression methodCall)
{
return methodCall.Method.GetParameters()
.Zip(methodCall.Arguments.Select(GetArgumentAsConstant), (param, arg) => new { param.Name, Value = ChangeType(arg.Value, param.ParameterType) })
.ToDictionary(item => item.Name, item => item.Value);
}
return base.GetParameterValues(controllerContext, actionDescriptor);
}
private ConstantExpression GetArgumentAsConstant(Expression exp)
{
switch (exp)
{
case ConstantExpression constExp:
return constExp;
case UnaryExpression uranExp:
return GetArgumentAsConstant(uranExp.Operand);
}
throw new NotSupportedException($"Cannot handle expression of type '{exp.GetType()}'");
}
private static object ChangeType(object value, Type conversion)
{
var t = conversion;
if (!t.IsGenericType || t.GetGenericTypeDefinition() != typeof(Nullable<>)) return Convert.ChangeType(value, t);
if (value == null) return null;
t = Nullable.GetUnderlyingType(t);
return Convert.ChangeType(value, t);
}
}
For my purposes this is used in specification fixture base class and auto mock dependencies but the essence of how you use it is like this:
var actionInvoker = new ControllerSpecActionInvoker<ActionResult>(Expression<Func<ActionResult|JsonResult|Etc>>);
actionInvoker.InvokeAction(<controller context>, <name of the action>);
Result = actionInvoker.Result;
So it might look something like this which is not test. Most of the cruft can be hidden away in a base class:
class MyController : Controller
{
JsonResult MyAction(int i) { return Json(new {}); }
}
class MyControllerFixture
{
[Test]
public void ReturnsData()
{
var controller = new MyController();
var controllerContext = new ControllerContext
{
RouteData = new RouteData(),
HttpContext = httpContextBase,
};
controllerContext.Controller = controller;
controller.ControllerContext = controllerContext;
Action<JsonResult> act = controller.MyAction(1);
var actionInvoker = new ControllerSpecActionInvoker<JsonResult>(act.Body);
actionInvoiker.Result.Should().NotBeNull();
}
}

Resources