ExtJS的store.sync向Asp.net MVC的Action提交时引发System.Reflection.AmbiguousMatchException异常
ExtJs中的store在sync时,是可以批量提交数据的,所谓的批量是指如果在store中同时记录了create、update、delete的对象时,分三次提交。在提交时,多个对象将被组合成JSON格式的字符串,当要提交的数据只有一条时,JSON的字符串如下所示:
{"ID":"a6d671ca-5480-4b5b-bf7a-b8459c0f598b","MobilePIN":"","Email":"111@ee.com","Password":"","CreateDate":null,"LastLoginDate":null,"LastPasswordChangedDate":null,"Comment":"","UserName":"user1","MobileAlias":"","LastActivityDate":null,"DisplayName":"","RememberMe":false}
如果是多条数据时,JSON的字符串是:
[{"ID":"a6d671ca-5480-4b5b-bf7a-b8459c0f598b","MobilePIN":"","Email":"111@ee.com","Password":"","CreateDate":null,"LastLoginDate":null,"LastPasswordChangedDate":null,"Comment":"","UserName":"user1","MobileAlias":"","LastActivityDate":null,"DisplayName":"","RememberMe":false}]
在Controller的Action中,如果方法的签名是public JsonResult CreateUser(UserDataObject user),则当传入多条数据时,只能接收到第1条;当方法签名是private List<UserDataObject> ProcessUsers(List<UserDataObject> users)时,当多条数据时没有问题,可只有1条数据时,users为null值。
我想当然的想到在Action中直接把两个方法都写上,但结果报了System.Reflection.AmbiguousMatchException,即MVC的ActionSelector已经不能区分应该调哪个方法了。
经过查找MVC的代码,发现在AsyncActionMethodSelector的RunSelectionFilters方法中,将会查找在Action上的Atrribute,像HttpPost这样的Attribute都会做过滤,以判断是否符合当前调用的上下文。
知道了这一点以后,我试想可以通过写一个自定义的Attribute,然后根据当前Request请求的内容来决定哪个方法可用,哪个方法不可用,实现这个的关键点有两个:
1、如何实现这个Attribute。
2、如何得到Request的Post的内容并进行解决。
第1个问题很好解决,因为我还没有系统的看MVC方面的资料,所以查看一下HttpPost可以知道它继承了ActionMethodSelectorAttribute,这个类里有一个抽象的方法IsValidForRequest。
第2个问题可以得到Request后,然后判断当前Post的类型是否为application/json,进行对Post的数据进行JSON的反序列化。经过测试得知,如果是单一对象的话,反序列化化的对象是一个Dictionay<String,Object>对象,而多个对象得到的是一个Object[]。下面给出实现的源码:
public class JsonModelPostAttribute : ActionMethodSelectorAttribute { public enum ParameterType { Single, Multiple } private ParameterType _parameterType; public ParameterType MethodParameterType { get { return _parameterType; } set { _parameterType = value; } } public JsonModelPostAttribute() { _parameterType = ParameterType.Single; } public JsonModelPostAttribute(ParameterType type) { _parameterType = type; } public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo) { var c = GetDeserializedObject(controllerContext); if (c == null) { return true; } if (MethodParameterType == ParameterType.Single) { return c is Dictionary<string, object>; } else { return c is object[]; } } private static object GetDeserializedObject(ControllerContext controllerContext) { if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase)) { // not JSON request return null; } controllerContext.HttpContext.Request.InputStream.Seek(0, SeekOrigin.Begin); StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream); string bodyText = reader.ReadToEnd(); controllerContext.HttpContext.Request.InputStream.Seek(0, SeekOrigin.Begin); if (String.IsNullOrEmpty(bodyText)) { // no JSON data return null; } JavaScriptSerializer serializer = new JavaScriptSerializer(); object jsonData = serializer.DeserializeObject(bodyText); return jsonData; } }
需要注意的时,在读取了InputStream后,要重新将Position置为0,否则后续的JsonValueProviderFactory将得不到反序化实例,这样即使能进入到正确的Action,参数也会为空。
下面是Controller中的测试的代码片段:
[AllowAnonymous] [HttpPost] [JsonModelPost(JsonModelPostAttribute.ParameterType.Multiple)] public JsonResult CreateUser(List<UserDataObject> users) { ProcessUsers(users); return this.Json(new { success = true, user = users }, JsonRequestBehavior.AllowGet); } private List<UserDataObject> ProcessUsers(List<UserDataObject> users) { int i = 1; foreach (var user in users) { user.ID = Guid.Empty.ToString(); user.UserName = "Server Name" + i++; } return users; } [AllowAnonymous] [HttpPost] [JsonModelPost(JsonModelPostAttribute.ParameterType.Single)] public JsonResult CreateUser(UserDataObject user) { var list = new List<UserDataObject>(new UserDataObject[] { user }); ProcessUsers(list); return this.Json(new { success = true, user = list[0] }, JsonRequestBehavior.AllowGet); }