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.
Related
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/
I am using a custom Media formatter to read post data for multipartform in webapi. Handling the serialization errors and validation errors in a custom Action Filter Attribute. In the formatter I am mapping the input to a Type Object ImageMedia. I want to add any serliazation errors to the ModelState so that I can handle the those in the CustomFilterAttribute, which recieve an ActionContext. Here is the code:
public class ImageMediaFormatter : MediaTypeFormatter
{
public ImageMediaFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpeg"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpg"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/png"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/gif"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
}
public override bool CanReadType(Type type)
{
return type == typeof(ImageMedia);
}
public override bool CanWriteType(Type type)
{
return false;
}
public override Task<object> ReadFromStreamAsync(
Type type, Stream stream, HttpContent request,
IFormatterLogger formatterContext)
{
return Task.Factory.StartNew<object>(() =>
{
var streamProvider = new MultipartMemoryStreamProvider();
var keys = new Dictionary<string, string>();
var result = request.ReadAsMultipartAsync(streamProvider).Result;
//get and remove random slashes from posted vaues
var categoryid =
streamProvider.Contents.First(x => x.Headers.ContentDisposition.Name.Contains("userGeneratedCategoryId")).ReadAsStringAsync().Result.Replace("\"", string.Empty);
keys.Add(Constants.UserGeneratedCategoryId, categoryid);
var imageBuffer =
streamProvider.Contents.First(x => x.Headers.ContentDisposition.Name.Contains(Constants.ImageFile)).ReadAsByteArrayAsync().Result;
return new ImageMedia(keys, imageBuffer);
});
}
}
public class ImageMedia
{
public ImageMedia(Dictionary<string, string> keys, byte[] imageBuffer)
: this()
{
var keyvaluePair = new KeyValuePair<string, string>();
foreach (var property in GetType().GetProperties())
{
try
{
keyvaluePair = keys.FirstOrDefault(pair => pair.Key.ToLower() == property.Name.ToLower());
if (!string.IsNullOrEmpty(keyvaluePair.Key))
{
property.SetValue(this, keyValuePair.Value, null);
}
}
catch (Exception ex)
{
// Add these serialization errors to the Model State so I can handle these in a Custom Validation Action Attribute
Errors.Add(keyvaluePair.Key, new List<string> { ex.Message });
IsValid = false;
}
}
Buffer = imageBuffer;
}
private ImageMedia()
{
Errors = new Dictionary<string, List<string>>();
}
public int UserGeneratedCategoryId { get; set; }
public byte[] Buffer { get;set;}
}
You could added errors to the supplied IFormatterLogger context that is passed in to ReadFromStreamAsync method:
public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent request,
IFormatterLogger formatterContext)
Example:
formatterLogger.LogError(errorPath: "OrderedDate", errorMessage: "Not a valid date");
I have implemented a custom MediaTypeFormatter in a SelfHosted AspNet WebAPI. I've used Unity.WebApi for dependency resolution. The controller classes only know about the interfaces implemented by the model classes, whereas, repositories give the concrete models as a result of actions.
The custom MediaTypeFormatter is inherited from BufferedMediaTypeFormatter as discussed here: http://www.asp.net/web-api/overview/formats-and-model-binding/media-formatters.
The problem is this media type formatter is not working. Even when I use to debug the code, the ReadFormStream method is never hit. Does someone know:
What could be the possible issue?
Do I need to tell the unity container for mapping of interfaces to model classes?
How would I get the reference to the dependency resolver inside the custom media type formatter?
Below is the code to add formatter:
var config = new SelfHostConfiguration("https://xxx.xxx.xxx.xxx:xxxx/");
config.Formatters.Add(new EntityTypeFormatter());
Below is the code for the EntityController:
public class EntityController : ApiController
{
private readonly IEntitiesRepository repository = null;
public EntityController(IEntitiesRepository repository)
{
if (repository == null)
{
throw new ArgumentNullException("repository");
}
this.repository = repository;
}
public IEnumerable<IEntity> Get()
{
return (IEnumerable<IEntity>)repository.Get();
}
}
Below is the code for the EntityRepository:
public class EntitiesRepository : IEntitiesRepository
{
public IEnumerable<IEntity> Get()
{
return new Entities[]
{
new Entity
{
Prop1 = "value for property 1",
Prop2 = "value for property 2",
Prop3 = "value for property 3"
},
new Entity
{
Prop1 = "value for property 1",
Prop2 = "value for property 2",
Prop3 = "value for property 3"
}
};
}
public IEntity Get(long id)
{
return new Entity
{
Prop1 = Convert.ToString(id),
Prop2 = "value for property 2",
Prop3 = "value for property 3"
};
}
}
Below is the implementation of the EntityMediaTypeFormatter class:
public class EntityMediaTypeFormatter : BufferedMediaTypeFormatter
{
public EntityMediaTypeFormatter()
: base()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
}
public override bool CanReadType(Type type)
{
if (type == null)
{
throw new ArgumentNullException("type");
}
if (type is IEntity)
{
return true;
}
else if (type.IsGenericType)
{
return type.GenericTypeArguments.Single(a => a.GetType().Equals(typeof(IEntity))) != null;
}
return false;
}
public override bool CanWriteType(Type type)
{
if (type == null)
{
throw new ArgumentNullException("type");
}
if (type is IEntity)
{
return true;
}
return false;
}
public override object ReadFromStream(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
{
if (type.IsInterface)
{
type = GetConcreteType(type);
}
return base.ReadFromStream(type, readStream, content, formatterLogger);
}
public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
{
//nothing special for now...
base.WriteToStream(type, value, writeStream, content);
}
private Type GetConcreteType(Type type)
{
//TODO: Need to find a way to DependencyResolver to get the concrete type
return typeof(Entity);
}
}
Thanking in advance for any help.
Looks to me like one of the existing formatters is taking priority over yours. Either clear the existing formatter collection before adding yours, or insert yours at position 0.
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.
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);
}
}