I got an MVC 5 application that i'm porting to asp.net Core.
In the MVC application call to controller we're made using AngularJS $resource (sending JSON) and we we're POSTing data doing :
ressource.save({ entries: vm.entries, projectId: vm.project.id }).$promise...
that will send a JSON body like:
{
entries:
[
{
// lots of fields
}
],
projectId:12
}
the MVC controller looked like this :
[HttpPost]
public JsonResult Save(List<EntryViewModel> entries, int projectId) {
// code here
}
How can I replicate the same behaviour with .NET Core since we can't have multiple [FromBody]
you cannot have multiple parameter with the FromBody attibute in an action method. If that is need, use a complex type such as a class with properties equivalent to the parameter or dynamic type like that
[HttpPost("save/{projectId}")]
public JsonResult Save(int projectId, [FromBody] dynamic entries) {
// code here
}
As pointed out in the comment, one possible solution is to unify the properties you're posting onto a single model class.
Something like the following should do the trick:
public class SaveModel
{
public List<EntryViewModel> Entries{get;set;}
public int ProjectId {get;set;}
}
Don't forget to decorate the model with the [FromBody] attribute:
[HttpPost]
public JsonResult Save([FromBody]SaveViewModel model)
{
// code here
}
Hope this helps!
It's still rough but I made a Filter to mimic the feature.
public class OldMVCFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.Request.Method != "GET")
{
var body = context.HttpContext.Request.Body;
JToken token = null;
var param = context.ActionDescriptor.Parameters;
using (var reader = new StreamReader(body))
using (var jsonReader = new JsonTextReader(reader))
{
jsonReader.CloseInput = false;
token = JToken.Load(jsonReader);
}
if (token != null)
{
var serializer = new JsonSerializer();
serializer.DefaultValueHandling = DefaultValueHandling.Populate;
serializer.FloatFormatHandling = FloatFormatHandling.DefaultValue;
foreach (var item in param)
{
JToken model = token[item.Name];
if (model == null)
{
// try to cast the full body as the current object
model = token.Root;
}
if (model != null)
{
model = this.RemoveEmptyChildren(model, item.ParameterType);
var res = model.ToObject(item.ParameterType, serializer);
context.ActionArguments[item.Name] = res;
}
}
}
}
}
private JToken RemoveEmptyChildren(JToken token, Type type)
{
var HasBaseType = type.GenericTypeArguments.Count() > 0;
List<PropertyInfo> PIList = new List<PropertyInfo>();
if (HasBaseType)
{
PIList.AddRange(type.GenericTypeArguments.FirstOrDefault().GetProperties().ToList());
}
else
{
PIList.AddRange(type.GetTypeInfo().GetProperties().ToList());
}
if (token != null)
{
if (token.Type == JTokenType.Object)
{
JObject copy = new JObject();
foreach (JProperty jProp in token.Children<JProperty>())
{
var pi = PIList.FirstOrDefault(p => p.Name == jProp.Name);
if (pi != null) // If destination type dont have this property we ignore it
{
JToken child = jProp.Value;
if (child.HasValues)
{
child = RemoveEmptyChildren(child, pi.PropertyType);
}
if (!IsEmpty(child))
{
if (child.Type == JTokenType.Object || child.Type == JTokenType.Array)
{
// nested value has been checked, we add the object
copy.Add(jProp.Name, child);
}
else
{
if (!pi.Name.ToLowerInvariant().Contains("string"))
{
// ignore empty value when type is not string
var Val = (string)child;
if (!string.IsNullOrWhiteSpace(Val))
{
// we add the property only if it contain meningfull data
copy.Add(jProp.Name, child);
}
}
}
}
}
}
return copy;
}
else if (token.Type == JTokenType.Array)
{
JArray copy = new JArray();
foreach (JToken item in token.Children())
{
JToken child = item;
if (child.HasValues)
{
child = RemoveEmptyChildren(child, type);
}
if (!IsEmpty(child))
{
copy.Add(child);
}
}
return copy;
}
return token;
}
return null;
}
private bool IsEmpty(JToken token)
{
return (token.Type == JTokenType.Null || token.Type == JTokenType.Undefined);
}
}
Related
I just need some opinions on implementing a logic to centrally check if there is no scripts added to the inputs.
I am planning to use antiXSS (Sanitizer.GetSafeHtmlFragment("value")) and check the output if it is null, meaning it could contain script and handle the error.
I could come up with an logic to through the model properties and check the value and if there is anything suspicious throws an error.
I wonder if there is a better way of handling this injections for all input fields in one go rather than adding validations for each field.
Lets say if I have a model like:
public class Login {
public string Email {get; set;}
public string Password {get; set;}
}
Can I just add some sort of filtering to check if non of the inputs contain any scripts before hitting the action rather than adding some attributes to the model or validation express and then do html encode individually for each field and then throw an error.
I want something very at top so I don't go through each actions or model and make some changes.
I used filter action and added such a code to check for string type of any model in the request and encode it. It works perfectly fine for us.
public static class HttpEncode
{
public static void ParseProperties(this object model)
{
if (model == null) return;
if (IsPropertyArrayOrList(model.GetType()))
{
ParsePropertiesOfList(model);
}
else
{
GetAllProperties(model).ForEach(t => EncodeField(t, model));
}
}
private static void ParsePropertiesOfList(object model)
{
foreach (var item in (IEnumerable) model)
{
ParseProperties(item);
}
}
private static List<PropertyInfo> GetAllProperties(object value) => value?.GetType()?.GetProperties()?.ToList();
private static void EncodeField(PropertyInfo p, object arg)
{
try
{
if (p.GetIndexParameters().Length != 0 || p.GetValue(arg) == null)
return;
if (IsUserDefinedClass(p.PropertyType) && p.CanWrite)
{
ParseProperties(p.GetValue(arg));
}
else if (IsPropertyArrayOrList(p.PropertyType) && p.CanWrite)
{
ParseArrayOrListProperty(p, arg);
}
else if (p.PropertyType == typeof(string) && p.CanWrite)
{
var encodedValue = HtmlEncode(p.GetValue(arg)?.ToString());
SetPropertyValue(p, arg, encodedValue);
}
}
catch (Exception ex)
{
// ignored
}
}
private static void ParseArrayOrListProperty(PropertyInfo p, object arg)
{
if (p.GetValue(arg) is string[] || p.GetValue(arg) is List<string>)
{
SetPropertyValueOfStaringArrayType(p, arg);
}
else
{
ParsePropertiesOfList(p.GetValue(arg));
}
}
private static void SetPropertyValueOfStaringArrayType(PropertyInfo propertyInfo, object arg)
{
if (propertyInfo.GetValue(arg) is string[] stringValue)
{
var result = new List<string>();
stringValue.ToList().ForEach(l => result.Add(HtmlEncode(l)));
SetPropertyValue(propertyInfo, arg, result.Any() ? result.ToArray() : null);
}
else if (propertyInfo.GetValue(arg) is List<string> listValue)
{
var result = new List<string>();
listValue.ForEach(l => result.Add(HtmlEncode(l)));
SetPropertyValue(propertyInfo, arg, result.Any() ? result : null);
}
}
private static bool IsUserDefinedClass(Type type) =>
type.IsClass &&
!type.FullName.StartsWith("System.");
private static bool IsPropertyArrayOrList(Type type) =>
type.IsArray && type.GetElementType() == typeof(string) ||
(type != typeof(string) && type.GetInterface(typeof(IEnumerable<>).FullName) != null);
private static void SetPropertyValue(PropertyInfo propertyInfo, object allValue, object value)
{
propertyInfo.SetValue(allValue, value);
}
private static string HtmlEncode(string value) => HttpUtility.HtmlEncode(value);
}
public class EncodeInputsActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
ProcessHtmlEncoding(context);
var resultContext = await next();
// do something after the action executes; resultContext.Result will be set
}
private static void ProcessHtmlEncoding(ActionExecutingContext context)
{
context.ActionArguments.ToList().ForEach(arg => { arg.Value.ParseProperties(); });
}
}
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 use Entity Framework to deal with my database, and as I googled a lot, I ran into some solution around updating my entities, here is extension method for updating entity the code :
public static void AttachUpdated(this ObjectContext context, EntityObject objectDetached)
{
if (objectDetached.EntityState == EntityState.Detached)
{
object currentEntityInDb = null;
if (context.TryGetObjectByKey(objectDetached.EntityKey, out currentEntityInDb))
{
context.ApplyPropertyChanges(objectDetached.EntityKey.EntitySetName, objectDetached);
//(CDLTLL)Apply property changes to all referenced entities in context
//Custom extensor method
context.ApplyReferencePropertyChanges((IEntityWithRelationships)objectDetached,
(IEntityWithRelationships)currentEntityInDb);
}
else
{
throw new ObjectNotFoundException();
}
}
}
public static void ApplyReferencePropertyChanges(this ObjectContext context,
IEntityWithRelationships newEntity,
IEntityWithRelationships oldEntity)
{
foreach (var relatedEnd in oldEntity.RelationshipManager.GetAllRelatedEnds())
{
var oldRef = relatedEnd as EntityReference;
if (oldRef != null)
{
// this related end is a reference not a collection
var newRef = newEntity.RelationshipManager.GetRelatedEnd(oldRef.RelationshipName, oldRef.TargetRoleName) as EntityReference;
oldRef.EntityKey = newRef.EntityKey;
}
}
}
And here is the code for saving or adding new object :
public void save (Category obj)
{
OurWebSiteEntities en = new OurWebSiteEntities();
if (obj.CategoryID == -1)
{
en.AddToCategories(obj);
}
else
en.AttachUpdated(obj);
en.SaveChanges();
}
And finally this the code I use to update my category entity in asp.net mvc :
if (cat.Category.CategoryID == -1)
{
// it is a new category
// and needs some property initialization
}
else
catItem = _categoryRepo.Items.FirstOrDefault(x => x.CategoryID == cat.Category.CategoryID);
TryUpdateModel(catItem, "Category");
_categoryRepo.Save(catItem);
But unfortunately it does not work for updating any category, and I am only able to create new one.
In your repository, you could create an update function and set entity state to modified.
public abstract class Repository<T>
{
public void Update(T entity) {
Context.Entry(entity).State = EntityState.Modified;
}
}
Given:
request.UrlReferrer.LocalPath = "/MyApp/MyHome/List";
and I have a Route Mapping that handles this where MyHome is my controller and List is an action that takes a ViewModel. Other variations of this Route include paging and sorting but these are captured by the ViewModel.
My question is this:
How can I use the above URL to generate an instance of the related ViewModel?
EDIT: I have an JQuery Dialog that is adding/updating/deleting an item in a list that is shown by the url in the urlreferrer- the example given is the most basic. When the dialog sends the data to be a/u/d, I want to return the updated body of the list and display that. This information is handled by a different ViewModel than what is instantiated on the POST from the dialog (the url posted to is "/MyApp/MyHome/Edit/True" - for creating a new whatever). This piece follows the standard MVC process and of course works. What I want to do is create a second ViewModel based on the ViewModel for the list action and return this as a partial view containing the updated paged list.
Ok... I think I have this figured out. This is not pretty but it works. I welcome anybody's input to actually feed this through a ModelBinder or any other MVC artifact but here's what I came up with:
First we need to fake a request using the UrlReferrer instead of the actual url being requested:
public class FakeHttpContext : HttpContextBase
{
public FakeHttpContext(HttpContextBase currentContext)
{
_request = new FakeHttpRequest(currentContext.Request);
}
HttpRequestBase _request;
public override HttpRequestBase Request
{
get
{
return _request;
}
}
HttpResponseBase _response = new FakeHttpResponse();
public override HttpResponseBase Response
{
get
{
return _response;
}
}
class FakeHttpRequest : HttpRequestBase
{
HttpRequestBase _request;
public FakeHttpRequest(HttpRequestBase currentRequest)
{
if(currentRequest == null)
throw new ArgumentNullException();
this._request = currentRequest;
}
public override string ApplicationPath
{
get
{
return this._request.ApplicationPath;
}
}
public override string AppRelativeCurrentExecutionFilePath
{
get
{
return "~" + this._request.UrlReferrer.AbsolutePath.Remove(0, this._request.ApplicationPath.Length);
}
}
public override string PathInfo
{
get
{
return this._request.PathInfo;
}
}
}
class FakeHttpResponse : HttpResponseBase
{
}
}
Next, we feed the fake call through the RouteTable to get it broken down. and match up properties to the RouteData.Values.
public static class RouteAndModelBinder
{
public static void BuildViewModel<TViewModel>(ControllerContext context, TViewModel model)
{
FakeHttpContext fake = new FakeHttpContext(context.HttpContext);
RouteData test = RouteTable.Routes.GetRouteData(fake);
PropertyInfo[] properties = typeof(TViewModel).GetProperties();
string value;
foreach(PropertyInfo info in properties)
{
if(test.Values.ContainsKey(info.Name))
{
value = (string)test.Values[info.Name];
if(value == null)
{
continue;
}
if(info.PropertyType.IsGenericType &&
info.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
Type[] nullables = info.PropertyType.GetGenericArguments();
if(nullables.Length > 0)
{
Type nullableType = nullables[0];
if(nullableType.BaseType == typeof(Enum))
{
object o = Enum.Parse(nullableType, value);
info.SetValue(model, o, null);
}
else if(nullableType == typeof(Int32))
{
info.SetValue(model, int.Parse(value), null);
}
else
{
info.SetValue(model, Convert.ChangeType(value, info.PropertyType), null);
}
}
}
else
{
if(info.PropertyType.BaseType == typeof(Enum))
{
object o = Enum.Parse(info.PropertyType.BaseType, value);
info.SetValue(model, o, null);
}
else if(info.PropertyType == typeof(Int32))
{
info.SetValue(model, int.Parse(value), null);
}
else
{
info.SetValue(model, value, null);
}
}
}
}
}
}
Again, I welcome anybody's suggestions on how I can do this with already established MVC code (ie, ModelBinders, etc). I took some ideas and probably code from here (for the nullable type) and here.
I got this error on my unit test:
Assert.AreEqual failed. Expected:<ShizoMe.Web.ViewModel.AccountViewModel>. Actual:<ShizoMe.Web.ViewModel.AccountViewModel>.
This is the code for my test:
[TestMethod]
public void Register_Prevents_Duplicate_Users()
{
var controller = GetAccountController();
var model = new AccountViewModel
{
Register = new RegisterModel
{
EmailAddress = "testUser#test.com"
}
};
var result = (ViewResult) controller.Register(model.Register);
Assert.AreEqual("A user with this email address already exists.",
controller.ModelState["UserExists"].Errors[0].ErrorMessage);
Assert.AreEqual(model, result.ViewData.Model);
}
This is my Register method:
[HttpPost]
public virtual ActionResult Register([Bind(Prefix = "Register")]RegisterModel model)
{
var accountModel = new AccountViewModel();
if (ModelState.IsValid)
{
if (_accountRepository.GetUser(model.EmailAddress) != null)
{
ModelState.AddModelError("UserExists", "A user with this email address already exists.");
return View(accountModel);
}
var newUser = new User
{
EmailAddress = model.EmailAddress,
Password = model.Password,
CreatedDate = DateTime.UtcNow
};
if (_accountRepository.RegisterUser(newUser))
{
_formsService.SignIn(newUser);
return RedirectToAction(MVC.Home.Index());
}
}
return View(accountModel);
}
Any idea why the last Assert.AreEqual failed? When I debug, the ViewModels (model and result.ViewData.Model) are the same.
Thank you very much.
Is AccountViewModel IEquatable? If not, C# will just use Object.ReferenceEquals to compare them. That will only be true if they both reference to the exactly same object (have the same property values is not good enough).
Did you override the Equals() and GetHashCode() methods for your AccountViewModel class?
example:
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
if (Object.ReferenceEquals(this, obj))
{
return true;
}
if (this.GetHashCode() == obj.GetHashCode())
{
return true;
}
return false;
}
public override int GetHashCode()
{
return Id.GetHashCode();
}