How to unit test modelbinder with ModelMetadata - asp.net-mvc-3

How do I unit test a custom ModelBinder?
Here's the code.
public class MagicBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var boundModelObject = base.BindModel(controllerContext, bindingContext);
var properties = bindingContext.ModelType.GetProperties().Where(a => a.CanWrite);
foreach (var propertyInfo in properties)
{
object outValue = null;
bindingContext.TryGetValue(propertyInfo.Name, propertyInfo.DeclaringType, out outValue);
propertyInfo.SetValue(boundModelObject, outValue, null);
}
return boundModelObject;
}
}
And here is the test script.
[TestMethod]
public void TestFooBinding()
{
var dict = new ValueProviderDictionary(null)
{
{"Number", new ValueProviderResult("2", "2", null)},
{"Test", new ValueProviderResult("12", "12", null)},
};
var bindingContext = new ModelBindingContext() { ModelName = "foo", ValueProvider = dict};
var target = new MagicBinder();
Foo result = (Foo)target.BindModel(null, bindingContext);
}
public class Foo
{
public int Number { get; set; }
public int Test { get; set; }
}
Problem? In the MagicBinder, bindingContext.Model is null. If I try set it with
bindingContext.Model = new Foo(). I get an exception saying it is deprecated, and I should set the ModelMetadata.
So how do I construct a ModelMetadata? It can't even be mocked.

NOTE: This answer is for ASP.NET on .NET Framework and might be outdated.
Try like this:
[TestMethod]
public void TestFooBinding()
{
// arrange
var formCollection = new NameValueCollection
{
{ "Number", "2" },
{ "Test", "12" },
};
var valueProvider = new NameValueCollectionValueProvider(formCollection, null);
var metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(Foo));
var bindingContext = new ModelBindingContext
{
ModelName = "",
ValueProvider = valueProvider,
ModelMetadata = metadata
};
var controllerContext = new ControllerContext();
var sut = new MagicBinder();
// act
Foo actual = (Foo)sut.BindModel(controllerContext, bindingContext);
// assert
// TODO:
}

Incase any of you need this to work for web-api you can use this method which will test's Get Requests, you get the benefit of using the built in provider:
Which will populate the values as the would come in from the web, instead of getting bizarre side effects of creating values that the provider may potentially never return Null etc.
using System;
using System.Globalization;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Metadata.Providers;
using System.Web.Http.ModelBinding;
using System.Web.Http.ValueProviders.Providers;
namespace Apps.API.Web.Tests
{
public class ModelBinderTestRule
{
//This URL is just a place holder for prefixing the query string
public const string MOCK_URL = "http://localhost:8088/";
public TModel BindModelFromGet<TBinder, TModel>(string modelName, string queryString, TBinder binder)
where TBinder : IModelBinder
{
var httpControllerContext = new HttpControllerContext();
httpControllerContext.Request = new HttpRequestMessage(HttpMethod.Get, MOCK_URL + queryString);
var bindingContext = new ModelBindingContext();
var dataProvider = new DataAnnotationsModelMetadataProvider();
var modelMetadata = dataProvider.GetMetadataForType(null, typeof(TModel));
var httpActionContext = new HttpActionContext();
httpActionContext.ControllerContext = httpControllerContext;
var provider = new QueryStringValueProvider(httpActionContext, CultureInfo.InvariantCulture);
bindingContext.ModelMetadata = modelMetadata;
bindingContext.ValueProvider = provider;
bindingContext.ModelName = modelName;
if (binder.BindModel(httpActionContext, bindingContext))
{
return (TModel)bindingContext.Model;
}
throw new Exception("Model was not bindable");
}
}
}

Related

how to test IQueryable of strongly typed using xUnit in .NET CORE

I am working on a test project for .NET CORE Web API project. I have SchoolService class that implements numbers of methods as some of them below
Service Class
public class SchoolService : ISchoolService
{
private readonly ISchoolEntity schoolEntity;
public SchoolService(ISchoolEntity schoolEntity)
{
this.schoolEntity = schoolEntity;
}
public IQueryable<SchoolDataView> GetAllSchools()
{
var query = this.schoolEntity.GetAllSchool();
return query;
}
public SchoolDataView GetSchoolById(Guid Id)
{
var query = this.schoolEntity.GetSchoolById(Id);
return query;
}
I want to test
1- GetAllSchools return object type is of IQueryable?
2- How I use autofix or by another way for schoolEntity.GetAllSchool() return fake IQueryable?
Service Test
public class SchoolServiceTests
{
private readonly ISchoolService schoolService;
private readonly ISchoolEntity schoolEntity = Substitute.For<ISchoolEntity>();
public SchoolServiceTests()
{
schoolService = new SchoolService(schoolEntity);
}
[Fact]
public void GetAllSchool_ShouldReturn_IQueryableOfSchoolDataView()
{
//Arrange
//Act
var a = schoolEntity.GetAllSchool();
//Assert
Assert.??
}
}
I have written following test to achieve behaviour that I have stated in my question. Open to hearing more feedback and suggestions on it. Thanks
[Fact]
public void GetAllSchool_ShouldReturn_IQueryableOfSchoolDataView()
{
//Arrange
var schoolDataViewList = new List<SchoolDataView>
{
new SchoolDataView { SchoolID = Guid.NewGuid(), Name = "London Primary School"},
new SchoolDataView { SchoolID = Guid.NewGuid(), Name = "Windsor Gramer School"},
new SchoolDataView { SchoolID = Guid.NewGuid(), Name = "Kings College"},
new SchoolDataView { SchoolID = Guid.NewGuid(), Name = "Reading School"}
}.AsQueryable();
schoolEntity.GetAllSchool().Returns(schoolDataViewList);
//Act
var actualSchoolList = sut.GetAllSchools();
//Assert
Assert.IsAssignableFrom<IQueryable<SchoolDataView>>(actualSchoolList);
}
OR Using AutoFixture
[Fact]
public void GetAllSchool_ShouldReturn_IQueryableOfSchoolDataView()
{
//Arrange
var fixture = new Fixture();
var schoolDataViewMock = fixture.CreateMany<SchoolDataView>();
schoolEntity.GetAllSchool().Returns(schoolDataViewMock.AsQueryable());
//Act
var actualSchoolDataList = sut.GetAllSchools();
//Assert
Assert.IsAssignableFrom<IQueryable<SchoolDataView>>(actualSchoolDataList);
}

How to get HttpContext from Controller

I'm using ASP.NET Core (MVC)
If I call an endpoint, then this.HttpContext is not null.
Within the same class as my endpoint, if I put a break point in the controller, this.HttpContext is always null.
How do I get the value of HttpContext from the controller?
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class LoginController : ControllerBase
{
public LoginController()
{
var isNull = this.HttpContext; //always null
}
[HttpGet]
public async Task Get()
{
var isNull = this.HttpContext; //not null
}
}
The purpose for this, is on each end point, I want to access some values (which are from a cookie). In NET Framework, I'd store the cookie values in a base class (from within the constructor).
Whilst I can access HTTPContext on each each end point, doing it in the constructor means code it once per class.
The goal is very much about coding this less. I'm hoping I'm not just being lazy
No, it is not the correct way to do it. you need to use Filter or middleware to do it. HttpContext class is always null in the constructor of a controller
Sample middleware code (for logging)
you can do whatever in this like read cookies or whatnot
public class LoggingMiddleware
{
private static readonly TelemetryConfiguration telemetryConfiguration = TelemetryConfiguration.CreateDefault();
private readonly TelemetryClient telemetryClient;
private IConfiguration configuration;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
private readonly string appName;
private readonly bool loggingEnabled;
private readonly RequestDelegate _next;
public LoggingMiddleware(RequestDelegate next, IConfiguration config)
{
_next = next;
configuration = config;
_recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
telemetryConfiguration.InstrumentationKey = configuration.GetValue<string>("ApplicationInsights:InstrumentationKey");
telemetryClient = new TelemetryClient(telemetryConfiguration);
appName = configuration.GetValue<string>("AppName");
loggingEnabled = configuration.GetValue<bool>("Logging:LogRequestResponse");
}
public async Task Invoke(HttpContext httpContext)
{
if(loggingEnabled)
{
await LogRequest(httpContext);
await LogResponse(httpContext);
}
}
private async Task LogRequest(HttpContext context)
{
context.Request.EnableBuffering();
await using var requestStream = _recyclableMemoryStreamManager.GetStream();
await context.Request.Body.CopyToAsync(requestStream);
string correlationId = context.Request.Headers.Keys.FirstOrDefault(h => h.ToLower() == "correlationid");
if (correlationId == null) correlationId = string.Empty;
if (context.Request.Path != "/")
{
telemetryClient.TrackEvent($"{appName}-RequestMiddleware", new Dictionary<string, string>
{
{ "AppName", appName },
{ "CorrelationId" , correlationId },
{ "Method" , context.Request.Method },
{ "Scheme", context.Request.Scheme},
{ "Host", context.Request.Host.Value },
{ "Path", context.Request.Path },
{ "QueryString", context.Request.QueryString.Value },
{ "Request Body", ReadStreamInChunks(requestStream) }
});
}
context.Request.Body.Position = 0;
}
private static string ReadStreamInChunks(Stream stream)
{
const int readChunkBufferLength = 4096;
stream.Seek(0, SeekOrigin.Begin);
using var textWriter = new StringWriter();
using var reader = new StreamReader(stream);
var readChunk = new char[readChunkBufferLength];
int readChunkLength;
do
{
readChunkLength = reader.ReadBlock(readChunk,
0,
readChunkBufferLength);
textWriter.Write(readChunk, 0, readChunkLength);
} while (readChunkLength > 0);
return textWriter.ToString();
}
private async Task LogResponse(HttpContext context)
{
var originalBodyStream = context.Response.Body;
await using var responseBody = _recyclableMemoryStreamManager.GetStream();
context.Response.Body = responseBody;
await _next(context);
context.Response.Body.Seek(0, SeekOrigin.Begin);
var text = await new StreamReader(context.Response.Body).ReadToEndAsync();
context.Response.Body.Seek(0, SeekOrigin.Begin);
if (context.Request.Path != "/")
{
telemetryClient.TrackEvent($"{appName}-ResponseMiddleware", new Dictionary<string, string> {
{"Scheme", context.Request.Scheme},
{ "AppName", appName },
{"Host", context.Request.Host.Value},
{"Path" , context.Request.Path},
{"QueryString", context.Request.QueryString.Value},
{"Response Body" , text}
});
}
await responseBody.CopyToAsync(originalBodyStream);
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class LoggingMiddlewareExtensions
{
public static IApplicationBuilder UseLoggingMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<LoggingMiddleware>();
}
}
No, you can't do it that way, controller constructors is danger zone (unless you know what you're doing) and should be used for DI only.
Instead, you should look at custom middleware:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write?view=aspnetcore-3.1
More info on asp.net Core life-cycles:
https://www.c-sharpcorner.com/article/asp-net-core-mvc-request-life-cycle/

Render a Razor View containing a URL to a string in ASP.NET Core

I have an Emailer class I am using via Dependency Injection to send emails which gets the contents of a View to send in an email. The process I have works great UNLESS the view contains a call to the underlying URL helper, such as using an A tag like this:
<a asp-controller="Project" asp-action="List">Open</a>
Here is the code I am using to render a view into a string:
private string renderViewAsString<TModel>(string folder, string viewName, TModel model)
{
var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var viewEngineResult = _viewEngine.FindView(actionContext, folder + "/" + viewName, false);
var view = viewEngineResult.View;
var viewData = new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary());
viewData.Model = model;
var tempData = new TempDataDictionary(httpContext, _tempDataProvider);
using (var output = new StringWriter())
{
var viewContext = new ViewContext(actionContext, view, viewData, tempData, output, new HtmlHelperOptions());
var task = view.RenderAsync(viewContext);
task.Wait();
return output.ToString();
}
}
_serviceProvider is of type IServiceProvider and _viewEngine is of type IRazorViewEngine which are both injected in the constructor.
If it references the URL helper it produces this exception at the task.Wait() line:
Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
with this as the call stack:
at System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
at System.Collections.Generic.List`1.get_Item(Int32 index)
at Microsoft.AspNetCore.Mvc.Routing.UrlHelper.get_Router()
at Microsoft.AspNetCore.Mvc.Routing.UrlHelper.GetVirtualPathData(String routeName, RouteValueDictionary values)
at Microsoft.AspNetCore.Mvc.Routing.UrlHelper.Action(UrlActionContext actionContext)
at Microsoft.AspNetCore.Mvc.UrlHelperExtensions.Action(IUrlHelper helper, String action, String controller, Object values, String protocol, String host, String fragment)
at Microsoft.AspNetCore.Mvc.ViewFeatures.DefaultHtmlGenerator.GenerateActionLink(ViewContext viewContext, String linkText, String actionName, String controllerName, String protocol, String hostname, String fragment, Object routeValues, Object htmlAttributes)
at Microsoft.AspNetCore.Mvc.TagHelpers.AnchorTagHelper.Process(TagHelperContext context, TagHelperOutput output)
at Microsoft.AspNetCore.Razor.TagHelpers.TagHelper.ProcessAsync(TagHelperContext context, TagHelperOutput output)
at Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner.<RunAsync>d__0.MoveNext()
How do I get around this without having to resort to hard-coding the A element or email contents?
I was able to get it to work. The call stack mentioned not finding a router, so it was a matter of providing it:
First I added this as a DI object in the constructor parameters:
IHttpContextAccessor accessor
And this in the constructor:
_context = accessor.HttpContext;
Then I changed the function to this:
private string renderViewAsString<TModel>(string folder, string viewName, TModel model)
{
var actionContext = new ActionContext(_context, new RouteData(), new ActionDescriptor());
var viewEngineResult = _viewEngine.FindView(actionContext, folder + "/" + viewName, false);
var view = viewEngineResult.View;
var viewData = new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary());
viewData.Model = model;
var tempData = new TempDataDictionary(_context, _tempDataProvider);
using (var output = new StringWriter())
{
var viewContext = new ViewContext(actionContext, view, viewData, tempData, output, new HtmlHelperOptions());
viewContext.RouteData = _context.GetRouteData(); //set route data here
var task = view.RenderAsync(viewContext);
task.Wait();
return output.ToString();
}
}
This is the one I use in ASP.NET Core 2.0
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
namespace Website
{
public class RazorViewToStringRenderer
{
private readonly IHttpContextAccessor accessor;
private readonly IRazorViewEngine viewEngine;
private readonly IServiceProvider serviceProvider;
private readonly ITempDataProvider tempDataProvider;
public RazorViewToStringRenderer(
IHttpContextAccessor accessor,
IRazorViewEngine viewEngine,
IServiceProvider serviceProvider,
ITempDataProvider tempDataProvider)
{
this.accessor = accessor;
this.viewEngine = viewEngine;
this.serviceProvider = serviceProvider;
this.tempDataProvider = tempDataProvider;
}
public string RenderViewToString<TModel>(string viewLocation, TModel model)
{
HttpContext httpContext = accessor.HttpContext;
httpContext.RequestServices = serviceProvider;
ActionContext actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
IView view = FindView(actionContext, viewLocation);
using (StringWriter stringWriter = new StringWriter())
{
ViewDataDictionary<TModel> viewDataDictionary = new ViewDataDictionary<TModel>(
new EmptyModelMetadataProvider(),
new ModelStateDictionary());
viewDataDictionary.Model = model;
TempDataDictionary tempDataDictionary = new TempDataDictionary(
actionContext.HttpContext,
tempDataProvider);
HtmlHelperOptions htmlHelperOptions = new HtmlHelperOptions();
ViewContext viewContext = new ViewContext(
actionContext,
view,
viewDataDictionary,
tempDataDictionary,
stringWriter,
htmlHelperOptions);
viewContext.RouteData = accessor.HttpContext.GetRouteData();
view.RenderAsync(viewContext).Wait();
return stringWriter.ToString();
}
}
private IView FindView(ActionContext actionContext, string viewLocation)
{
ViewEngineResult getViewResult = viewEngine.GetView(null, viewLocation, true);
if (getViewResult.Success)
{
return getViewResult.View;
}
ViewEngineResult findViewResult = viewEngine.FindView(actionContext, viewLocation, true);
if (findViewResult.Success)
{
return findViewResult.View;
}
IEnumerable<string> searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations);
string message = string.Join(
Environment.NewLine,
new[] { $"Unable to find view '{viewLocation}'. The following locations were searched:" }.Concat(searchedLocations)); ;
throw new Exception(message);
}
}
}
Remember in Startup.cs -> public void ConfigureServices(IServiceCollection serviceCollection) to add this
serviceCollection.AddSingleton<RazorViewToStringRenderer>();
I have no direct answer to this, because i have the exact same problem. But i made a Github issue yesterday. https://github.com/aspnet/Entropy/issues/170
Edit: The solution of Rono works for me
I had the same error, but I couldn't use Rono's solution because it required a real HttpContext, which I did not have in my command-line application. I managed to fix it by adding a RouteCollection to my ActionContext:
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext { RequestServices = serviceProvider };
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
MapRoutes(actionContext);
return actionContext;
}
private void MapRoutes(ActionContext actionContext)
{
var routes = new RouteBuilder(new ApplicationBuilder(serviceProvider))
{
DefaultHandler = new DefaultHandler()
};
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}"
);
actionContext.RouteData.Routers.Add(routes.Build());
}
Full source code with an example project on github: https://github.com/waf/RazorToStringExample/blob/master/RazorToStringExample/Services/RazorViewToStringRenderer.cs

Pass data serialized with Json.net library to the View and bind it with knockout

I am trying to send some serialized data to the view and bind it to the knockout code. I am using json.net library for serialization because I want to pass the constants of an enum to the view ( and not the underlying integers.) I am not sure how my controller returning Json data should look like. Here is the sample code:
My view model that will be serialized:
public class FranchiseInfoViewModel
{
public string FolderName { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public LobbyTemplateOptions LobbyTemplate { get; set; }
public List<LobbyTemplateOptions> LobbyTemplates { get; set; }
public void Initialize()
{
FolderName = "Test";
LobbyTemplate = LobbyTemplateOptions.G;
LobbyTemplates = new List<LobbyTemplateOptions>
{
LobbyTemplateOptions.G,
LobbyTemplateOptions.H,
LobbyTemplateOptions.I
};
Enum:
[JsonConverter(typeof(StringEnumConverter))]
public enum LobbyTemplateOptions
{
G = 7,
H = 8,
I = 9
}
My knockout code:
$(function () {
omega.FranchiseInfo = (function () {
var FolderName = ko.observable();
var LobbyTemplates = ko.observableArray([]);
$.getJSON("FranchiseData", function (data) {
FolderName(data.FolderName);
for (var i = 0; i < data.LobbyTemplate.length; i++) {
LobbyTemplates.push(data.LobbyTemplate[i]);
}
});
return {
folderName: FolderName,
lobbyTemplates: LobbyTemplates
}
} ());
ko.applyBindings(omega.FranchiseInfo);
})
}
I am wondering how my controller that passes serialized Json data to the view should look like as I have not used json.net and I am relatively new to programming:
Controller passing the Json data to the view:
public JsonResult FranchiseData()
{
FranchiseInfoViewModel franchiseInfoViewModel = new FranchiseInfoViewModel();
franchiseInfoViewModel.MapFranchiseInfoToFranchiseInfoViewModel();
string json = JsonConvert.SerializeObject(franchiseInfoViewModel);
// this is how I do it with the default Json serializer
// return Json(franchiseInfoViewModel, JsonRequestBehavior.AllowGet);
}
I would be very gratefull if somebody can post a working example of my controller. Thank You!
You need to implement JsonNetResult.
public class JsonNetResult : ActionResult
{
public Encoding ContentEncoding { get; set; }
public string ContentType { get; set; }
public object Data { get; set; }
public JsonSerializerSettings SerializerSettings { get; set; }
public Formatting Formatting { get; set; }
public JsonNetResult()
{
SerializerSettings = new JsonSerializerSettings();
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = !string.IsNullOrEmpty(ContentType)
? ContentType
: "application/json";
if (ContentEncoding != null)
response.ContentEncoding = ContentEncoding;
if (Data != null)
{
JsonTextWriter writer = new JsonTextWriter(response.Output) { Formatting = Formatting };
JsonSerializer serializer = JsonSerializer.Create(SerializerSettings);
serializer.Serialize(writer, Data);
writer.Flush();
}
}
}
To use it, in your case you need to rewrite controller method in this way:
public ActionResult FranchiseData()
{
FranchiseInfoViewModel franchiseInfoViewModel = new FranchiseInfoViewModel();
franchiseInfoViewModel.MapFranchiseInfoToFranchiseInfoViewModel();
JsonNetResult jsonNetResult = new JsonNetResult();
jsonNetResult.Formatting = Formatting.Indented;
jsonNetResult.Data = franchiseInfoViewModel;
return jsonNetResult;
}
(implementation of JsonNetResult above was taken this blog post
http://james.newtonking.com/archive/2008/10/16/asp-net-mvc-and-json-net.aspx )

Using JSON.NET as the default JSON serializer in ASP.NET MVC 3 - is it possible?

Is it possible to use JSON.NET as default JSON serializer in ASP.NET MVC 3?
According to my research, it seems that the only way to accomplish this is to extend ActionResult as JsonResult in MVC3 is not virtual...
I hoped that with ASP.NET MVC 3 that there would be a way to specify a pluggable provider for serializing to JSON.
Thoughts?
I believe the best way to do it, is - as described in your links - to extend ActionResult or extend JsonResult directly.
As for the method JsonResult that is not virtual on the controller that's not true, just choose the right overload. This works well:
protected override JsonResult Json(object data, string contentType, Encoding contentEncoding)
EDIT 1: A JsonResult extension...
public class JsonNetResult : JsonResult
{
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
var response = context.HttpContext.Response;
response.ContentType = !String.IsNullOrEmpty(ContentType)
? ContentType
: "application/json";
if (ContentEncoding != null)
response.ContentEncoding = ContentEncoding;
// If you need special handling, you can call another form of SerializeObject below
var serializedObject = JsonConvert.SerializeObject(Data, Formatting.Indented);
response.Write(serializedObject);
}
EDIT 2: I removed the check for Data being null as per the suggestions below. That should make newer versions of JQuery happy and seems like the sane thing to do, as the response can then be unconditionally deserialized. Be aware though, that this is not the default behavior for JSON responses from ASP.NET MVC, which rather responds with an empty string, when there's no data.
I implemented this without the need of a base controller or injection.
I used action filters to replace the JsonResult with a JsonNetResult.
public class JsonHandlerAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var jsonResult = filterContext.Result as JsonResult;
if (jsonResult != null)
{
filterContext.Result = new JsonNetResult
{
ContentEncoding = jsonResult.ContentEncoding,
ContentType = jsonResult.ContentType,
Data = jsonResult.Data,
JsonRequestBehavior = jsonResult.JsonRequestBehavior
};
}
base.OnActionExecuted(filterContext);
}
}
In the Global.asax.cs Application_Start() you would need to add:
GlobalFilters.Filters.Add(new JsonHandlerAttribute());
For completion's sake, here is my JsonNetResult extention class that I picked up from somewhere else and that I modified slightly to get correct steaming support:
public class JsonNetResult : JsonResult
{
public JsonNetResult()
{
Settings = new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Error
};
}
public JsonSerializerSettings Settings { get; private set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("JSON GET is not allowed");
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = string.IsNullOrEmpty(this.ContentType) ? "application/json" : this.ContentType;
if (this.ContentEncoding != null)
response.ContentEncoding = this.ContentEncoding;
if (this.Data == null)
return;
var scriptSerializer = JsonSerializer.Create(this.Settings);
scriptSerializer.Serialize(response.Output, this.Data);
}
}
Use Newtonsoft's JSON converter:
public ActionResult DoSomething()
{
dynamic cResponse = new ExpandoObject();
cResponse.Property1 = "value1";
cResponse.Property2 = "value2";
return Content(JsonConvert.SerializeObject(cResponse), "application/json");
}
I know this is well after the question has been answered, but I'm using a different approach as I am using dependency injection to instantiate my controllers.
I have replaced the IActionInvoker ( by injecting the controller's ControllerActionInvoker Property ) with a version that overrides the InvokeActionMethod method.
This means no change to controller inheritance and it can be easily removed when I upgrade to MVC4 by altering the DI container's registration for ALL controllers
public class JsonNetActionInvoker : ControllerActionInvoker
{
protected override ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
{
ActionResult invokeActionMethod = base.InvokeActionMethod(controllerContext, actionDescriptor, parameters);
if ( invokeActionMethod.GetType() == typeof(JsonResult) )
{
return new JsonNetResult(invokeActionMethod as JsonResult);
}
return invokeActionMethod;
}
private class JsonNetResult : JsonResult
{
public JsonNetResult()
{
this.ContentType = "application/json";
}
public JsonNetResult( JsonResult existing )
{
this.ContentEncoding = existing.ContentEncoding;
this.ContentType = !string.IsNullOrWhiteSpace(existing.ContentType) ? existing.ContentType : "application/json";
this.Data = existing.Data;
this.JsonRequestBehavior = existing.JsonRequestBehavior;
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if ((this.JsonRequestBehavior == JsonRequestBehavior.DenyGet) && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
base.ExecuteResult(context); // Delegate back to allow the default exception to be thrown
}
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = this.ContentType;
if (this.ContentEncoding != null)
{
response.ContentEncoding = this.ContentEncoding;
}
if (this.Data != null)
{
// Replace with your favourite serializer.
new Newtonsoft.Json.JsonSerializer().Serialize( response.Output, this.Data );
}
}
}
}
--- EDIT - Updated to show container registration for controllers. I'm using Unity here.
private void RegisterAllControllers(List<Type> exportedTypes)
{
this.rootContainer.RegisterType<IActionInvoker, JsonNetActionInvoker>();
Func<Type, bool> isIController = typeof(IController).IsAssignableFrom;
Func<Type, bool> isIHttpController = typeof(IHttpController).IsAssignableFrom;
foreach (Type controllerType in exportedTypes.Where(isIController))
{
this.rootContainer.RegisterType(
typeof(IController),
controllerType,
controllerType.Name.Replace("Controller", string.Empty),
new InjectionProperty("ActionInvoker")
);
}
foreach (Type controllerType in exportedTypes.Where(isIHttpController))
{
this.rootContainer.RegisterType(typeof(IHttpController), controllerType, controllerType.Name);
}
}
public class UnityControllerFactory : System.Web.Mvc.IControllerFactory, System.Web.Http.Dispatcher.IHttpControllerActivator
{
readonly IUnityContainer container;
public UnityControllerFactory(IUnityContainer container)
{
this.container = container;
}
IController System.Web.Mvc.IControllerFactory.CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
{
return this.container.Resolve<IController>(controllerName);
}
SessionStateBehavior System.Web.Mvc.IControllerFactory.GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
{
return SessionStateBehavior.Required;
}
void System.Web.Mvc.IControllerFactory.ReleaseController(IController controller)
{
}
IHttpController IHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)
{
return this.container.Resolve<IHttpController>(controllerType.Name);
}
}
Expanding on the answer from https://stackoverflow.com/users/183056/sami-beyoglu, if you set the Content type, then jQuery will be able to convert the returned data into an object for you.
public ActionResult DoSomething()
{
dynamic cResponse = new ExpandoObject();
cResponse.Property1 = "value1";
cResponse.Property2 = "value2";
return Content(JsonConvert.SerializeObject(cResponse), "application/json");
}
My Post may help someone.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
namespace MultipleSubmit.Service
{
public abstract class BaseController : Controller
{
protected override JsonResult Json(object data, string contentType,
Encoding contentEncoding, JsonRequestBehavior behavior)
{
return new JsonNetResult
{
Data = data,
ContentType = contentType,
ContentEncoding = contentEncoding,
JsonRequestBehavior = behavior
};
}
}
}
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MultipleSubmit.Service
{
public class JsonNetResult : JsonResult
{
public JsonNetResult()
{
Settings = new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Error
};
}
public JsonSerializerSettings Settings { get; private set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && string.Equals
(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("JSON GET is not allowed");
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = string.IsNullOrEmpty(this.ContentType) ?
"application/json" : this.ContentType;
if (this.ContentEncoding != null)
response.ContentEncoding = this.ContentEncoding;
if (this.Data == null)
return;
var scriptSerializer = JsonSerializer.Create(this.Settings);
using (var sw = new StringWriter())
{
scriptSerializer.Serialize(sw, this.Data);
response.Write(sw.ToString());
}
}
}
}
public class MultipleSubmitController : BaseController
{
public JsonResult Index()
{
var data = obj1; // obj1 contains the Json data
return Json(data, JsonRequestBehavior.AllowGet);
}
}
I made a version that makes web service actions type-safe and simple. You use it like this:
public JsonResult<MyDataContract> MyAction()
{
return new MyDataContract();
}
The class:
public class JsonResult<T> : JsonResult
{
public JsonResult(T data)
{
Data = data;
JsonRequestBehavior = JsonRequestBehavior.AllowGet;
}
public override void ExecuteResult(ControllerContext context)
{
// Use Json.Net rather than the default JavaScriptSerializer because it's faster and better
if (context == null)
throw new ArgumentNullException("context");
var response = context.HttpContext.Response;
response.ContentType = !String.IsNullOrEmpty(ContentType)
? ContentType
: "application/json";
if (ContentEncoding != null)
response.ContentEncoding = ContentEncoding;
var serializedObject = JsonConvert.SerializeObject(Data, Formatting.Indented);
response.Write(serializedObject);
}
public static implicit operator JsonResult<T>(T d)
{
return new JsonResult<T>(d);
}
}

Resources