Search code examples
c#asp.netasp.net-mvcasp.net-coreasp.net-mvc-5

What is the current ASP.NET MVC replacement for @Html.Serialize and [Deserialize]?


I'm trying to update my ASP.NET MVC 5 web site gradually so I can eventually migrate to ASP.NET MVC Core. I used to put some data in hidden fields and use @Html.Serialize("name", value) it into hidden fields, and then upon html POST deserialize it into an action argument using the [Deserialize] attribute. This used to be part of the Microsoft.AspNet.Mvc.Futures 5.0.0.0 nuget package, which was last updated many years ago now, and I have no idea what it migrated into, or is there not even a successor?


Solution

  • I wrote my own replacements for this using JSON based on the old SerializationExtensions and DeserializeAttribute:

    SerializationExtensions.cs:

    using Microsoft.AspNetCore.Html;
    using Microsoft.AspNetCore.Mvc.Rendering;
    
    namespace My.WebApplication {
        public static class SerializationExtensions {
            public static IHtmlContent JsonSerialize(this IHtmlHelper htmlHelper, string name) {
                return JsonSerializeInternal(htmlHelper, name, null, useViewData: true);
            }
    
            public static IHtmlContent JsonSerialize(this IHtmlHelper htmlHelper, string name, object data) {
                return JsonSerializeInternal(htmlHelper, name, data, useViewData: false);
            }
    
            private static IHtmlContent JsonSerializeInternal(IHtmlHelper htmlHelper, string name, object data, bool useViewData) {
                if (htmlHelper == null) {
                    throw new ArgumentNullException("htmlHelper");
                }
    
                if (string.IsNullOrEmpty(name)) {
                    throw new ArgumentException(nameof(name));
                }
    
                name = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
                if (useViewData) {
                    data = htmlHelper.ViewData.Eval(name);
                }
    
                string value = System.Text.Json.JsonSerializer.Serialize(data);
                TagBuilder tagBuilder = new TagBuilder("input");
                tagBuilder.Attributes["type"] = "hidden";
                tagBuilder.Attributes["name"] = name;
                tagBuilder.Attributes["value"] = value;
                return tagBuilder;
            }
        }
    }
    

    JsonModelBinder.cs:

    using Microsoft.AspNetCore.Mvc.ModelBinding;
    
    namespace My.WebApplication {
        public class JsonModelBinder : IModelBinder {
            public Task BindModelAsync(ModelBindingContext bindingContext) {
                if (bindingContext == null) {
                    throw new ArgumentNullException(nameof(bindingContext));
                }
    
                var modelName = bindingContext.ModelName;
    
                // Try to fetch the value of the argument by name
                var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
    
                if (valueProviderResult == ValueProviderResult.None) {
                    return Task.CompletedTask;
                }
    
                bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
    
                var value = valueProviderResult.FirstValue;
    
                // Check if the argument value is null or empty
                if (string.IsNullOrEmpty(value)) {
                    return Task.CompletedTask;
                }
    
                try {
                    var model = System.Text.Json.JsonSerializer.Deserialize(value, bindingContext.ModelType);
                    bindingContext.Result = ModelBindingResult.Success(model);
                } catch (Exception ex) {
                    bindingContext.ModelState.TryAddModelError(
                        modelName, ex.Message);
    
                    return Task.CompletedTask;
                }
    
                return Task.CompletedTask;
            }
        }
    }
    

    JsonDeserializeAttribute.cs:

    using Microsoft.AspNetCore.Mvc;
    
    namespace My.WebApplication {
        public class JsonDeserializeAttribute : ModelBinderAttribute {
            public JsonDeserializeAttribute() : base(typeof(JsonModelBinder)) {
            }
        }
    }
    

    You can use these exactly like the old @Html.Serialize() and [Deserialize] combo:

    @Html.JsonSerialize("oldValue", ViewData["oldValue"])
    

    And in your controller action:

    public ActionResult Edit([JsonDeserialize] Test oldValue, Test value)
    

    Maybe I'll add it to github so other people can use it too.

    For those curious why I use this: it's handy for object edit pages, where you want to store the old value of an object and send it back to the controller action upon post, so in the SQL UPDATE statement you put the old field values in the SQL WHERE clause to prevent update collisions (so the UPDATE fails if the WHERE clause doesn't match the current database record, so an update by another user doesn't get overwritten silently). I guess this is an old school way of updating database records, but it's solid and I like it.