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

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);
}
}

Related

Swagger does not recognize WebAPI controller parameter with custom attribute [FromContent]

I want to have a custom attribute to parse data as stream and be testable with Swagger.
So I created controller which reads from POST body:
[SwaggerOperation("Create")]
[SwaggerResponse(HttpStatusCode.Created)]
public async Task<string> Post([FromContent]Stream contentStream)
{
using (StreamReader reader = new StreamReader(contentStream, Encoding.UTF8))
{
var str = reader.ReadToEnd();
Console.WriteLine(str);
}
return "OK";
}
How to define stream so it is visible in Swagger UI?
Here is my implementation of FromContent attribute and ContentParameterBinding binding:
public class ContentParameterBinding : HttpParameterBinding
{
private struct AsyncVoid{}
public ContentParameterBinding(HttpParameterDescriptor descriptor) : base(descriptor)
{
}
public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
HttpActionContext actionContext,
CancellationToken cancellationToken)
{
var binding = actionContext.ActionDescriptor.ActionBinding;
if (binding.ParameterBindings.Length > 1 ||
actionContext.Request.Method == HttpMethod.Get)
{
var taskSource = new TaskCompletionSource<AsyncVoid>();
taskSource.SetResult(default(AsyncVoid));
return taskSource.Task as Task;
}
var type = binding.ParameterBindings[0].Descriptor.ParameterType;
if (type == typeof(HttpContent))
{
SetValue(actionContext, actionContext.Request.Content);
var tcs = new TaskCompletionSource<object>();
tcs.SetResult(actionContext.Request.Content);
return tcs.Task;
}
if (type == typeof(Stream))
{
return actionContext.Request.Content
.ReadAsStreamAsync()
.ContinueWith((task) =>
{
SetValue(actionContext, task.Result);
});
}
throw new InvalidOperationException("Only HttpContent and Stream are supported for [FromContent] parameters");
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public sealed class FromContentAttribute : ParameterBindingAttribute
{
public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
{
if (parameter == null)
throw new ArgumentException("Invalid parameter");
return new ContentParameterBinding(parameter);
}
}
Update
When I create Stream using [FromBody] is shows correctly in Swagger, however Stream is not initiated and ==null
[SwaggerOperation("Create")]
[SwaggerResponse(HttpStatusCode.Created)]
public async Task<string> Post([FromBody]Stream contentStream)
{
using (StreamReader reader = new StreamReader(contentStream, Encoding.UTF8))
{
var str = reader.ReadToEnd();
Console.WriteLine(str);
}
return "OK";
}
So I want to have the same UI but with my custom attribute which let's me have Stream from content.
With my custom attribute it shows without TextArea for the parameter but could be tested using Postman and work correctly and Stream is available
Inherit your binding from FormatterParameterBinding class:
public class ContentParameterBinding : FormatterParameterBinding
{
public ContentParameterBinding(HttpParameterDescriptor descriptor)
: base(descriptor,
descriptor.Configuration.Formatters,
descriptor.Configuration.Services.GetBodyModelValidator())
{
}
//your code
}
Try implementing the interface IValueProviderParameterBinding:
public class ContentParameterBinding
: HttpParameterBinding, IValueProviderParameterBinding
{
public IEnumerable<ValueProviderFactory> ValueProviderFactories
{
get
{
return this.Descriptor.Configuration.Services.GetValueProviderFactories();
}
}
}
In my case it helped.
Also it's generally cleaner as it doesn't inherit FormatterParameterBinding logic, which may not be required.

Trying to call code in my controller but getting Null Reference error

Don't want to over-complicate the issue, but I think I need to post all the code that's hooked into this error.
Using MvcMailer and introduced a separate Send mechanism (for use with Orchard CMS' own EMail).
The MvcMailer Code:
1) AskUsMailer.cs:
public class AskUsMailer : MailerBase, IAskUsMailer
{
public AskUsMailer()
: base()
{
//MasterName = "_Layout";
}
public virtual MvcMailMessage EMailAskUs(AskUsViewModel model)
{
var mailMessage = new MvcMailMessage { Subject = "Ask Us" };
ViewData.Model = model;
this.PopulateBody(mailMessage, viewName: "EMailAskUs");
return mailMessage;
}
}
2) IAskUsMailer.cs:
public interface IAskUsMailer : IDependency
{
MvcMailMessage EMailAskUs(AskUsViewModel model);
}
3) AskUsController.cs: (GETTING NULL REFERENCE ERROR BELOW)
[Themed]
public ActionResult Submitted()
{
//This is the new call (see new code below):
//Note: Debugging steps through eMailMessagingService,
//then shows the null reference error when continuing to
//SendAskUs
eMailMessagingService.SendAskUs(askUsData);
//Below is normal MvcMailer call:
//AskUsMailer.EMailAskUs(askUsData).Send();
return View(askUsData);
}
Note: askUsData is defined in a separate block in the controller:
private AskUsViewModel askUsData;
protected override void OnActionExecuting(ActionExecutingContext
filterContext)
{
var serialized = Request.Form["askUsData"];
if (serialized != null) //Form was posted containing serialized data
{
askUsData = (AskUsViewModel)new MvcSerializer().
Deserialize(serialized, SerializationMode.Signed);
TryUpdateModel(askUsData);
}
else
askUsData = (AskUsViewModel)TempData["askUsData"] ??
new AskUsViewModel();
TempData.Keep();
}
protected override void OnResultExecuted(ResultExecutedContext
filterContext)
{
if (filterContext.Result is RedirectToRouteResult)
TempData["askUsData"] = askUsData;
}
I did not know how to get my EMailMessagingService.cs (see below) call into the controller, so in a separate block in the controller I did this:
private IEMailMessagingService eMailMessagingService;
public AskUsController(IEMailMessagingService eMailMessagingService)
{
this.eMailMessagingService = eMailMessagingService;
}
I think this is part of my problem.
Now, the new code trying to hook into Orchard's EMail:
1) EMailMessagingServices.cs:
public class EMailMessagingService : IMessageManager
{
private IAskUsMailer askUsMailer;
private IOrchardServices orchardServices;
public EMailMessagingService(IAskUsMailer askUsMailer,
IOrchardServices orchardServices)
{
this.orchardServices = orchardServices;
this.askUsMailer = askUsMailer;
this.Logger = NullLogger.Instance;
}
public ILogger Logger { get; set; }
public void SendAskUs(AskUsViewModel model)
{
var messageAskUs = this.askUsMailer.EMailAskUs(model);
messageAskUs.To.Add("email#email.com");
//Don't need the following (setting up e-mails to send a copy anyway)
//messageAskUs.Bcc.Add(AdminEmail);
//messageAskUs.Subject = "blabla";
Send(messageAskUs);
}
....
}
The EMailMessagingService.cs also contains the Send method:
private void Send(MailMessage messageAskUs)
{
var smtpSettings = orchardServices.WorkContext.
CurrentSite.As<SmtpSettingsPart>();
// can't process emails if the Smtp settings have not yet been set
if (smtpSettings == null || !smtpSettings.IsValid())
{
Logger.Error("The SMTP Settings have not been set up.");
return;
}
using (var smtpClient = new SmtpClient(smtpSettings.Host,
smtpSettings.Port))
{
smtpClient.UseDefaultCredentials =
!smtpSettings.RequireCredentials;
if (!smtpClient.UseDefaultCredentials &&
!String.IsNullOrWhiteSpace(smtpSettings.UserName))
{
smtpClient.Credentials = new NetworkCredential
(smtpSettings.UserName, smtpSettings.Password);
}
if (messageAskUs.To.Count == 0)
{
Logger.Error("Recipient is missing an email address");
return;
}
smtpClient.EnableSsl = smtpSettings.EnableSsl;
smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network;
messageAskUs.From = new MailAddress(smtpSettings.Address);
messageAskUs.IsBodyHtml = messageAskUs.Body != null &&
messageAskUs.Body.Contains("<") &&
messageAskUs.Body.Contains(">");
try
{
smtpClient.Send(messageAskUs);
Logger.Debug("Message sent to {0} with subject: {1}",
messageAskUs.To[0].Address, messageAskUs.Subject);
}
catch (Exception e)
{
Logger.Error(e, "An unexpected error while sending
a message to {0} with subject: {1}",
messageAskUs.To[0].Address, messageAskUs.Subject);
}
}
}
Now, in EMailMessagingService.cs I was getting an error that things weren't being implemented, so I auto-generated the following (don't know if this is part of my error):
public void Send(Orchard.ContentManagement.Records.ContentItemRecord recipient, string type, string service, System.Collections.Generic.Dictionary<string, string> properties = null)
{
throw new NotImplementedException();
}
public void Send(System.Collections.Generic.IEnumerable<Orchard.ContentManagement.Records.ContentItemRecord> recipients, string type, string service, System.Collections.Generic.Dictionary<string, string> properties = null)
{
throw new NotImplementedException();
}
public void Send(System.Collections.Generic.IEnumerable<string> recipientAddresses, string type, string service, System.Collections.Generic.Dictionary<string, string> properties = null)
{
throw new NotImplementedException();
}
public bool HasChannels()
{
throw new NotImplementedException();
}
public System.Collections.Generic.IEnumerable<string> GetAvailableChannelServices()
{
throw new NotImplementedException();
}
2) IEMailMessagingServices.cs
public interface IEMailMessagingService
{
MailMessage SendAskUs(AskUsViewModel model);
}
MvcMailer works fine without this addition (outside of Orchard), but I am trying to get everything working within Orchard.
I just cannot figure out what I am doing wrong. Any thoughts?
Sorry for excessive code.
IEmailMessaginService does not implement IDependency, so it can't be found by Orchard as a dependency. That's why it's null.

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 )

A single instance of controller 'TestController' cannot be used to handle multiple requests

I have Some issues with the life time manager in unity, it uses the object like its singleton, but in the configuration I set it to "PerWebRequest".
The Error is:
A single instance of controller 'TestController' cannot be used to handle multiple requests. If a custom controller factory is in use, make sure that it creates a new instance of the controller for each request.
The PerWebRequest code:
public class UnityPerWebRequestLifetimeManager : LifetimeManager
{
private HttpContextBase _httpContext;
public UnityPerWebRequestLifetimeManager()
: this(new HttpContextWrapper(HttpContext.Current))
{
}
public UnityPerWebRequestLifetimeManager(HttpContextBase httpContext)
{
_httpContext = httpContext;
}
private IDictionary<UnityPerWebRequestLifetimeManager, object> BackingStore
{
get
{
_httpContext = (HttpContext.Current != null) ? new HttpContextWrapper(HttpContext.Current) : _httpContext;
return UnityPerWebRequestLifetimeModule.GetInstances(_httpContext);
}
}
private object Value
{
[DebuggerStepThrough]
get
{
IDictionary<UnityPerWebRequestLifetimeManager, object> backingStore = BackingStore;
return backingStore.ContainsKey(this) ? backingStore[this] : null;
}
[DebuggerStepThrough]
set
{
IDictionary<UnityPerWebRequestLifetimeManager, object> backingStore = BackingStore;
if (backingStore.ContainsKey(this))
{
object oldValue = backingStore[this];
if (!ReferenceEquals(value, oldValue))
{
IDisposable disposable = oldValue as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
if (value == null)
{
backingStore.Remove(this);
}
else
{
backingStore[this] = value;
}
}
}
else
{
if (value != null)
{
backingStore.Add(this, value);
}
}
}
}
[DebuggerStepThrough]
public override object GetValue()
{
return Value;
}
[DebuggerStepThrough]
public override void SetValue(object newValue)
{
Value = newValue;
}
[DebuggerStepThrough]
public override void RemoveValue()
{
Value = null;
}
}
The controller:
public class TestController : Controller
{
//
// GET: /Test/
public TestController()
{
}
public ActionResult Index()
{
return View();
}
public ActionResult RadioButtonList()
{
return View("TestControl");
}
}
The Controller Factory:
public class ControllerFactory : DefaultControllerFactory
{
protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
return (controllerType == null) ? base.GetControllerInstance(requestContext, controllerType) : IoC.Resolve<IController>(controllerType);
}
}
And in one of the views I am trying to use it like this:
...
<% Html.RenderAction<TestController>(c => c.RadioButtonList()); %>
<% Html.RenderAction<TestController>(c => c.RadioButtonList()); %>
...
I don't know what wrong here?
Thanks.
Both unity controller requests are created within the same HTTP request/reply, hence you get the same instance. You need to switch so that the controllers have a Transient lifetime.
I would switch to DependencyResolver instead of using ControllerFactory since you are running MVC3.

Custom Not-Found Route Fires Only Once

I tend to dislike posting dozens of lines of code and assuming the community at large is interested in untangling my mess. In this case I've exercised everything I can think to search on Google, traced through Glimpse, and Firebug/Fiddler, and what I'm left with is an occasionally working behavior, which is particularly annoying to debug. So, I'm calling out for help.
Here's the gist: I've got a series of classes that handle MVC routes that are otherwise not found (and would produce a 404 error) thanks to #AndrewDavey. I'm attempting to intercept the 404 and show data-driven content where any exists. It all works until I refresh the page. The request works on the first load, but it never fires again after that.
If you're bored or have an itch, the entire code block is below.
Setup goes like this:
Add WebActivator via NuGet
In your AppStart folder add a cs file with the code below
Add a "PageContext" connection string to your web.config
Run the app, the default MVC screen shows up
Now add "/abc" to the end of the url (i.e http://localhost/abc)
A cshtml view, stored in the database, will render.
Change the view's markup in the database and reload the page. Notice no change in your browser.
the /abc route assumes you have a record in the database with the following
Path: "~/abc/index.cshtml"
View: "#{ Layout = null;}<!doctype html><html><head><title>abc</title></head><body><h2>About</h2></body></html>"
I've got no idea why the first request works and subsequent requests don't hit break points and serve up stale content.
My suspicions are:
Some voodoo with the VirtualFile
Something cached (but where?)
A misconfigured handler
Thanks for the help - here's the code (as I shamefully tuck my tail for posting this much code).
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Entity;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;
using Microsoft.Web.Infrastructure.DynamicModuleHelper;
using SomeCms;
[assembly: WebActivator.PreApplicationStartMethod(typeof(Sample.Web.App_Start.cms), "PreStart")]
namespace Sample.Web.App_Start
{
public static class cms
{
public static void PreStart()
{
DynamicModuleUtility.RegisterModule(typeof(InstallerModule));
}
}
}
namespace SomeCms
{
class ActionInvokerWrapper : IActionInvoker
{
readonly IActionInvoker actionInvoker;
public ActionInvokerWrapper(IActionInvoker actionInvoker)
{
this.actionInvoker = actionInvoker;
}
public bool InvokeAction(ControllerContext controllerContext, string actionName)
{
if (actionInvoker.InvokeAction(controllerContext, actionName))
{
return true;
}
// No action method was found.
var controller = new CmsContentController();
controller.ExecuteCmsContent(controllerContext.RequestContext);
return true;
}
}
class ControllerFactoryWrapper : IControllerFactory
{
readonly IControllerFactory factory;
public ControllerFactoryWrapper(IControllerFactory factory)
{
this.factory = factory;
}
public IController CreateController(RequestContext requestContext, string controllerName)
{
try
{
var controller = factory.CreateController(requestContext, controllerName);
WrapControllerActionInvoker(controller);
return controller;
}
catch (HttpException ex)
{
if (ex.GetHttpCode() == 404)
{
return new CmsContentController();
}
throw;
}
}
static void WrapControllerActionInvoker(IController controller)
{
var controllerWithInvoker = controller as Controller;
if (controllerWithInvoker != null)
{
controllerWithInvoker.ActionInvoker = new ActionInvokerWrapper(controllerWithInvoker.ActionInvoker);
}
}
public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
{
return factory.GetControllerSessionBehavior(requestContext, controllerName);
}
public void ReleaseController(IController controller)
{
factory.ReleaseController(controller);
}
}
class InstallerModule : IHttpModule
{
static bool installed;
static readonly object installerLock = new object();
public void Init(HttpApplication application)
{
if (installed)
{
return;
}
lock (installerLock)
{
if (installed)
{
return;
}
Install();
installed = true;
}
}
static void Install()
{
Database.SetInitializer(new CreateDatabaseIfNotExists<PageContext>());
HostingEnvironment.RegisterVirtualPathProvider(new ExampleVirtualPathProvider());
WrapControllerBuilder();
AddNotFoundRoute();
AddCatchAllRoute();
}
static void WrapControllerBuilder()
{
ControllerBuilder.Current.SetControllerFactory(new ControllerFactoryWrapper(ControllerBuilder.Current.GetControllerFactory()));
}
static void AddNotFoundRoute()
{
// To allow IIS to execute "/cmscontent" when requesting something which is disallowed,
// such as /bin or /add_data.
RouteTable.Routes.MapRoute(
"CmsContent",
"cmscontent",
new { controller = "CmsContent", action = "CmsContent" }
);
}
static void AddCatchAllRoute()
{
RouteTable.Routes.MapRoute(
"CmsContent-Catch-All",
"{*any}",
new { controller = "CmsContent", action = "CmsContent" }
);
}
public void Dispose() { }
}
public class CmsContentController : IController
{
public void Execute(RequestContext requestContext)
{
ExecuteCmsContent(requestContext);
}
public void ExecuteCmsContent(RequestContext requestContext)
{
//new CmsContentViewResult().ExecuteResult(new ControllerContext(requestContext, new FakeController()));
new CmsContentViewResult().ExecuteResult(new ControllerContext(requestContext, new FakeController()));
}
// ControllerContext requires an object that derives from ControllerBase.
// NotFoundController does not do this.
// So the easiest workaround is this FakeController.
class FakeController : Controller { }
}
public class CmsContentHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
var routeData = new RouteData();
routeData.Values.Add("controller", "CmsContent");
var controllerContext = new ControllerContext(new HttpContextWrapper(context), routeData, new FakeController());
var cmsContentViewResult = new CmsContentViewResult();
cmsContentViewResult.ExecuteResult(controllerContext);
}
public bool IsReusable
{
get { return false; }
}
// ControllerContext requires an object that derives from ControllerBase.
class FakeController : Controller { }
}
public class CmsContentViewResult : ViewResult
{
public CmsContentViewResult()
{
ViewName = "index";
}
public override void ExecuteResult(ControllerContext context)
{
var request = context.HttpContext.Request;
if (request != null && request.Url != null)
{
var url = request.Url.OriginalString;
ViewData["RequestedUrl"] = url;
ViewData["ReferrerUrl"] = (request.UrlReferrer != null && request.UrlReferrer.OriginalString != url)
? request.UrlReferrer.OriginalString
: null;
}
base.ExecuteResult(context);
}
}
public class ExampleVirtualPathProvider : VirtualPathProvider
{
private readonly List<SimpleVirtualFile> virtualFiles = new List<SimpleVirtualFile>();
public ExampleVirtualPathProvider()
{
var context = new PageContext();
var pages = context.Pages.ToList();
foreach (var page in pages)
{
virtualFiles.Add(new SimpleVirtualFile(page.Path));
}
}
public override bool FileExists(string virtualPath)
{
var files = (from f in virtualFiles
where f.VirtualPath.Equals(virtualPath, StringComparison.InvariantCultureIgnoreCase) ||
f.RelativePath.Equals(virtualPath, StringComparison.InvariantCultureIgnoreCase)
select f)
.ToList();
return files.Count > 0 || base.FileExists(virtualPath);
}
private class SimpleVirtualFile : VirtualFile
{
public SimpleVirtualFile(string filename) : base(filename)
{
RelativePath = filename;
}
public override Stream Open()
{
var context = new PageContext();
var page = context.Pages.FirstOrDefault(p => p.Path == RelativePath);
return new MemoryStream(Encoding.ASCII.GetBytes(page.View), false);
}
public string RelativePath { get; private set; }
}
private class SimpleVirtualDirectory : VirtualDirectory
{
public SimpleVirtualDirectory(string virtualPath)
: base(virtualPath)
{
}
public override IEnumerable Directories
{
get { return null; }
}
public override IEnumerable Files
{
get
{
return null;
}
}
public override IEnumerable Children
{
get { return null; }
}
}
public override VirtualFile GetFile(string virtualPath)
{
var files = (from f in virtualFiles
where f.VirtualPath.Equals(virtualPath, StringComparison.InvariantCultureIgnoreCase) ||
f.RelativePath.Equals(virtualPath, StringComparison.InvariantCultureIgnoreCase)
select f).ToList();
return files.Count > 0
? files[0]
: base.GetFile(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
return IsPathVirtual(virtualPath) ? null : base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
private bool IsPathVirtual(string virtualPath)
{
var checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
return
virtualFiles.Any(f => checkPath.StartsWith(virtualPath, StringComparison.InvariantCultureIgnoreCase)) ||
virtualFiles.Any(f => checkPath.Replace("~", "").StartsWith(virtualPath, StringComparison.InvariantCultureIgnoreCase));
}
public override bool DirectoryExists(string virtualDir)
{
return IsPathVirtual(virtualDir) || Previous.DirectoryExists(virtualDir);
}
public override VirtualDirectory GetDirectory(string virtualDir)
{
return IsPathVirtual(virtualDir)
? new SimpleVirtualDirectory(virtualDir)
: Previous.GetDirectory(virtualDir);
}
}
public class ContentPage
{
public int Id { get; set; }
public string Path { get; set; }
public string View { get; set; }
}
public class PageContext : DbContext
{
public DbSet<ContentPage> Pages { get; set; }
}
}
This question turns out to be a non-issue. My oversight of the cache dependency in the virtual path provider is returning null for virtual paths. As such, the view is cached indefinitely.
The solution is to use a custom cache dependency provider that expires immediately.
public class NoCacheDependency : CacheDependency
{
public NoCacheDependency()
{
NotifyDependencyChanged(this, EventArgs.Empty);
}
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
return IsPathVirtual(virtualPath) ? new NoCacheDependency() : base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}

Resources