POST json dictionary

后端 未结 10 737
猫巷女王i
猫巷女王i 2020-11-27 05:52

I\'m trying the following : A model with a dictionary inside send it on the first ajax request then take the result serialize it again and send it back to the controller.

相关标签:
10条回答
  • 2020-11-27 06:20

    Grab the following NuGet package for System.Json, which includes the new JsonValue type. JsonValue is a flexible new JSON representative type that fully supports C# 4 dynamic, and is also an IEnumerable<KeyValuePair<string, JsonValue>> in the event you wish to treat a payload as a dictionary/associative array.

    You can pick up System.Json (Beta) with NuGet here. It seems System.Json will be included natively in .NET 4.5, as indicated by the documentation pages here.

    You might also want to read the following article to assist in getting JSON HTTP bodies to properly deserialize into JsonValue objects in your Action method parameters:

    JSON, ASP.NET MVC and JQuery - Working with Untyped JSON made easy

    The two relevant pieces of code from the article above would be the DynamicJsonBinder and DynamicJsonAttribute, replicated here for posterity:

    public class DynamicJsonBinder : IModelBinder  
    {  
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)  
        { 
            if (!controllerContext.HttpContext.Request.ContentType.StartsWith  
                  ("application/json", StringComparison.OrdinalIgnoreCase))  
            {  
                // not JSON request  
                return null;  
            }  
    
            var inpStream = controllerContext.HttpContext.Request.InputStream;  
            inpStream.Seek(0, SeekOrigin.Begin);  
    
            StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);  
            string bodyText = reader.ReadToEnd();  
            reader.Close();  
    
    
            if (String.IsNullOrEmpty(bodyText))  
            {  
                // no JSON data  
                return null;  
            }  
    
            return JsonValue.Parse(bodyText);  
        }  
    } 
    
    public class DynamicJsonAttribute : CustomModelBinderAttribute
    {
        public override IModelBinder GetBinder()
        {
            return new DynamicJsonBinder();
        }
    }
    

    A relevant sample use case would be:

    public class HomeController : Controller
    {
        public ActionResult Index (T a)
        {
          return View();
        }
    
        public JsonResult A([DynamicJson] JsonValue value)
        {
          dynamic t = value.AsDynamic();
    
          if (t.Name.IsEmpty())
          {
            t = new // t is dynamic, so I figure just create the structure you need directly
            {
                Name = "myname",
                D = new // Associative array notation (woot!): 
                {
                    a = "a",
                    b = "b",
                    c = "c" 
                }
            };
          }
    
          return Json(t);
        }
    }
    
    0 讨论(0)
  • 2020-11-27 06:22

    I got it to work with a custom model binder, and changing the way the data is sent; without using Stringify and setting the contenttype.

    JavaScript:

        $(function() {
            $.ajax({
                url: '/home/a',
                type: 'POST',
                success: function(result) {
                    $.ajax({
                        url: '/home/a',
                        data: result,
                        type: 'POST',
                        success: function(result) {
    
                        }
                    });
                }
            });
        });
    

    Custom model binder:

    public class DictionaryModelBinder : IModelBinder
    {          
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
                throw new ArgumentNullException("bindingContext");
    
            string modelName = bindingContext.ModelName;
            IDictionary<string, string> formDictionary = new Dictionary<string, string>();
    
            Regex dictionaryRegex = new Regex(modelName + @"\[(?<key>.+?)\]", RegexOptions.CultureInvariant);
            foreach (var key in controllerContext.HttpContext.Request.Form.AllKeys.Where(k => k.StartsWith(modelName + "[")))
            {
                Match m = dictionaryRegex.Match(key);
                if (m.Success)
                {
                    formDictionary[m.Groups["key"].Value] = controllerContext.HttpContext.Request.Form[key];
                }
            }
            return formDictionary;
        }
    }
    

    And by adding the model binder in Global.asax:

    ModelBinders.Binders[typeof(IDictionary<string, string>)] = new DictionaryModelBinder();
    
    0 讨论(0)
  • 2020-11-27 06:26

    Just use a better deserializer. That first line where I set the position is because the JsonValueProvider leaves the stream at the end. More MS JSON fail.

    Request.InputStream.Position = 0;
    var reader = new StreamReader(Request.InputStream);
    
    var model = Newtonsoft.Json.JsonConvert.DeserializeObject<CreativeUploadModel>(reader.ReadToEnd());
    

    So somewhere in that CreativeUploadModel object graph there is a prop like this:

    public Dictionary<string, Asset> Assets { get; set; }
    

    Which is deserialized from (for example):

    "assets":{"flash":{"type":"flash","value":"http://1234.cloudfront.net/1234.swf","properties":"{\"clickTag\":\"clickTAG\"}"}
    

    Newtonsoft JSON is the default JSON provider for WebAPI... so it's not going anywhere.

    0 讨论(0)
  • 2020-11-27 06:27

    I came across the same issue today and came up with a solution which doesn't require anything but registering a new model binder. It's a bit hacky but hopefully it helps someone.

        public class DictionaryModelBinder : IModelBinder
        {
            /// <summary>
            /// Binds the model to a value by using the specified controller context and binding context.
            /// </summary>
            /// <returns>
            /// The bound value.
            /// </returns>
            /// <param name="controllerContext">The controller context.</param><param name="bindingContext">The binding context.</param>
            public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
            {
                if (bindingContext == null)
                    throw new ArgumentNullException("bindingContext");
    
                string modelName = bindingContext.ModelName;
                // Create a dictionary to hold the results
                IDictionary<string, string> result = new Dictionary<string, string>();
    
                // The ValueProvider property is of type IValueProvider, but it typically holds an object of type ValueProviderCollect
                // which is a collection of all the registered value providers.
                var providers = bindingContext.ValueProvider as ValueProviderCollection;
                if (providers != null)
                {
                    // The DictionaryValueProvider is the once which contains the json values; unfortunately the ChildActionValueProvider and
                    // RouteDataValueProvider extend DictionaryValueProvider too, so we have to get the provider which contains the 
                    // modelName as a key. 
                    var dictionaryValueProvider = providers
                        .OfType<DictionaryValueProvider<object>>()
                        .FirstOrDefault(vp => vp.ContainsPrefix(modelName));
                    if (dictionaryValueProvider != null)
                    {
                        // There's no public property for getting the collection of keys in a value provider. There is however
                        // a private field we can access with a bit of reflection.
                        var prefixsFieldInfo = dictionaryValueProvider.GetType().GetField("_prefixes",
                                                                                          BindingFlags.Instance |
                                                                                          BindingFlags.NonPublic);
                        if (prefixsFieldInfo != null)
                        {
                            var prefixes = prefixsFieldInfo.GetValue(dictionaryValueProvider) as HashSet<string>;
                            if (prefixes != null)
                            {
                                // Find all the keys which start with the model name. If the model name is model.DictionaryProperty; 
                                // the keys we're looking for are model.DictionaryProperty.KeyName.
                                var keys = prefixes.Where(p => p.StartsWith(modelName + "."));
                                foreach (var key in keys)
                                {
                                    // With each key, we can extract the value from the value provider. When adding to the dictionary we want to strip
                                    // out the modelName prefix. (+1 for the extra '.')
                                    result.Add(key.Substring(modelName.Length + 1), bindingContext.ValueProvider.GetValue(key).AttemptedValue);
                                }
                                return result;
                            }
                        }
                    }
                }
                return null;
            }
        }
    

    The binder is registered in the Global.asax file under application_start

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
    
            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);
    
            ModelBinders.Binders.Add(typeof(Dictionary<string,string>), new DictionaryModelBinder());
        }
    
    0 讨论(0)
  • 2020-11-27 06:27

    Here is my solution to similar problem:

    using System.Collections.Generic;
    using System.IO;
    using System.Web.Mvc;
    using System.Web.Script.Serialization;
    
    namespace Controllers
    {
        public class DictionaryModelBinder : IModelBinder
        {
            public object BindModel(ControllerContext context, ModelBindingContext bindingContext)
            {
                context.HttpContext.Request.InputStream.Seek(0, SeekOrigin.Begin);
                using (TextReader reader = new StreamReader(context.HttpContext.Request.InputStream))
                {
                    string requestContent = reader.ReadToEnd();
                    var arguments = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(requestContent);
                    return arguments[bindingContext.ModelName];
                }
            }
        }
    }
    
    using Controllers;
    using Moq;
    using NUnit.Framework;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;
    using System.Text;
    using System.Web;
    using System.Web.Mvc;
    
    namespace ControllersTest
    {
        [TestFixture]
        public class DictionaryModelBinderTest
        {
            private ControllerContext controllerContext;
    
            [Test]
            public void ReturnsDeserializedPrimitiveObjectsAndDictionaries()
            {
                string input =
    @"{
        arguments: {
            simple: 1,
            complex: { a: 2, b: 3 },
            arrayOfSimple: [{ a: 4, b: 5 }],
            arrayOfComplex: [{ a: 6, b: 7 }, { a: 8, b: 9 }]},
        otherArgument: 1
    }";
                SetUpRequestContent(input);
    
                var binder = new DictionaryModelBinder();
                var bindingContext = new ModelBindingContext();
                bindingContext.ModelName = "arguments";
    
                var model = (Dictionary<string, object>)binder.BindModel(controllerContext, bindingContext);
    
                Assert.IsFalse(model.ContainsKey("otherArgument"));
                Assert.AreEqual(1, model["simple"]);
                var complex = (Dictionary<string, object>)model["complex"];
                Assert.AreEqual(2, complex["a"]);
                Assert.AreEqual(3, complex["b"]);
                var arrayOfSimple = (ArrayList)model["arrayOfSimple"];
                Assert.AreEqual(4, ((Dictionary<string, object>)arrayOfSimple[0])["a"]);
                Assert.AreEqual(5, ((Dictionary<string, object>)arrayOfSimple[0])["b"]);
                var arrayOfComplex = (ArrayList)model["arrayOfComplex"];
                var complex1 = (Dictionary<string, object>)arrayOfComplex[0];
                var complex2 = (Dictionary<string, object>)arrayOfComplex[1];
                Assert.AreEqual(6, complex1["a"]);
                Assert.AreEqual(7, complex1["b"]);
                Assert.AreEqual(8, complex2["a"]);
                Assert.AreEqual(9, complex2["b"]);
            }
    
            private void SetUpRequestContent(string input)
            {
                var stream = new MemoryStream(Encoding.UTF8.GetBytes(input));
                stream.Seek(0, SeekOrigin.End);
    
                var controllerContextStub = new Mock<ControllerContext>();
                var httpContext = new Mock<HttpContextBase>();
                httpContext.Setup(x => x.Request.InputStream).Returns(stream);
                controllerContextStub.Setup(x => x.HttpContext).Returns(httpContext.Object);
                this.controllerContext = controllerContextStub.Object;
            }
        }
    }
    
    using Controllers;
    using PortalApi.App_Start;
    using System.Collections.Generic;
    using System.Web.Http;
    using System.Web.Mvc;
    using System.Web.Routing;
    
    namespace PortalApi
    {
        public class MvcApplication : System.Web.HttpApplication
        {
            protected void Application_Start()
            {
                AreaRegistration.RegisterAllAreas();
    
                WebApiConfig.Register(GlobalConfiguration.Configuration);
                RouteConfig.RegisterRoutes(RouteTable.Routes);
                FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
                ModelBinders.Binders.Add(typeof(Dictionary<string, object>), new DictionaryModelBinder());
            }
        }
    }
    

    Have fun! :-P Greetings Łukasz Duda

    0 讨论(0)
  • 2020-11-27 06:28

    For anyone who's coming upon this problem recently still, as long as you don't need your controller to specifically accept a dictionary, you can do the following:

    HttpResponseMessage SomeMethod([FromBody] IEnumerable<KeyValuePair<Key, Value>> values)
    {
        Dictionary<Key, Value> dictionary = values.ToDictionary(x => x.Key, x = x.Value);
    }
    

    Though it is a bit hacky.

    0 讨论(0)
提交回复
热议问题