Skip to content

Instantly share code, notes, and snippets.

@b9chris
Last active August 20, 2019 16:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save b9chris/6991b341e89bb0a4e6d801d02dfd7730 to your computer and use it in GitHub Desktop.
Save b9chris/6991b341e89bb0a4e6d801d02dfd7730 to your computer and use it in GitHub Desktop.
Asp.Net MVC BaseController that fixes a number of problems with the default MVC implementation
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Web;
using System.Web.Mvc;
namespace Brass9.Web.Mvc.Controllers
{
/// <summary>
/// Improvements on the standard MVC Controller class:
///
/// 1) By default JSON output is done by MVC's JsonSerializer, not JSON.Net, resulting in non-standard outputs, most
/// notably for dates and nulls. Both break jQuery. JSON.Net's works fine.
///
/// 2) MVC likes to emit Vary: * which should never happen. They have a suppression method just for that bad
/// implementation. Call it.
///
/// 3) Optionally expose the entire Controller, even the public-seeming methods like View and Json, via .Expose()
/// which just passes back a wrapper inner-class, wrapping the protected methods in public ones.
/// </summary>
public 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
};
}
protected override JsonResult Json(object data, string contentType, Encoding contentEncoding)
{
return new JsonNetResult
{
Data = data,
ContentType = contentType,
ContentEncoding = contentEncoding
};
}
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
// Fix an ultra-annoying bug in Asp.Net MVC where setting any OutputCache VaryByParam
// emits a Vary: *, which causes client caches to stop caching entirely, or rather ask for a new version
// for any sort of change, which is equivalent to blowing up the cache.
// http://stackoverflow.com/a/7612239/176877
// TODO: Verify this change. We've shifted to ETags so ideally Vary: * is just never sent, ever.
Response.Cache.SetOmitVaryStar(true);
}
public ControllerPublic Expose()
{
return new ControllerPublic(this);
}
/// <summary>
/// Presents a facade that exposes View, Json, etc as public methods in case a Controller needs to be passed
/// around to helpers
/// </summary>
public class ControllerPublic
{
protected BaseController controller;
public ControllerPublic(BaseController controller)
{
this.controller = controller;
}
public ViewResult View()
{
return controller.View();
}
public ViewResult View(string viewName)
{
return controller.View(viewName);
}
public ViewResult View(object model)
{
return controller.View(model);
}
public JsonResult Json(object model)
{
return controller.Json(model);
}
public RedirectResult Redirect(string path)
{
return controller.Redirect(path);
}
public HttpSessionStateBase Session { get { return controller.Session; } }
public IPrincipal User { get { return controller.User; } }
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Brass9.IO.FileSystemInfoExtension;
namespace Brass9.IO
{
public static class FileHelper
{
public static string ReadFile(FileInfo file)
{
using (var reader = file.OpenText())
{
return reader.ReadToEnd();
}
}
public static IEnumerable<string> ReadLines(string path)
{
using (var stream = File.OpenText(path))
{
// Holy fancy enumerators! http://msdn.microsoft.com/en-us/library/vstudio/9k7k7cf0.aspx
string line = stream.ReadLine();
while (line != null)
{
yield return line;
line = stream.ReadLine();
}
}
yield break;
}
/// <summary>
/// Processes the lines of
/// </summary>
/// <param name="path"></param>
/// <param name="processHeader"></param>
/// <returns></returns>
public static string ReadLinesUntil(string path, Func<string, bool> where)
{
foreach (var line in ReadLines(path))
{
if (where(line))
return line;
}
return null;
}
public static void WriteFile(FileInfo file, string s)
{
WriteFile(file, s, Encoding.UTF8);
}
public static void WriteFile(FileInfo file, string s, Encoding enc)
{
using (var stream = file.OpenWrite())
{
var bytes = enc.GetBytes(s);
stream.Write(bytes, 0, bytes.Length);
}
}
/// <summary>
/// Reads all text in a file, asynchronously. Doesn't throw for missing files - returns null.
/// </summary>
/// <param name="path">Full file path</param>
/// <returns>Contents of the file, or null if the file doesn't exist.</returns>
public static async Task<string> ReadAllTextAsync(string path)
{
// If the path is enclosed in double quotes for DOS etc we'll screw up in the OpenText call below - strip if present.
if (path.StartsWith("\"") && path.EndsWith("\""))
path = path.Substring(1, path.Length - 2);
// http://stackoverflow.com/a/13168006/176877
string s;
// Avoid throwing if not exists, especially in async
if (!File.Exists(path))
return null;
using(var reader = File.OpenText(path))
{
s = await reader.ReadToEndAsync();
}
return s;
}
public static async Task WriteFileAsync(string path, string s)
{
await WriteFileAsync(new FileInfo(path), s);
}
public static async Task WriteFileAsync(FileInfo file, string s)
{
await WriteFileAsync(file, s, Encoding.UTF8);
}
public static async Task WriteFileAsync(FileInfo file, string s, Encoding enc)
{
using (var stream = file.OpenWrite())
{
var bytes = enc.GetBytes(s);
await stream.WriteAsync(bytes, 0, bytes.Length);
}
}
/// <summary>
/// Checks for the directory and its parent directories to ensure they exist; creates it and as many parents as necessary
/// if they don't.
/// </summary>
/// <param name="path">Path for directory to ensure exists.</param>
/// <returns>True if the entire path already existed. False if any directories were created.</returns>
public static bool EnsureDirectory(string path)
{
var dir = new DirectoryInfo(path);
bool existed = dir.EnsureDirectory();
return existed;
}
/// <summary>
/// Exactly like File.Move(), but overwrites if the file exists.
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
public static void Move(string from, string to)
{
if (File.Exists(to))
File.Delete(to);
File.Move(from, to);
}
public static void Copy(string from, string to)
{
if (from == null)
throw new ArgumentNullException("from");
if (from.Equals(to))
return;
if (File.Exists(to))
File.Delete(to);
File.Copy(from, to);
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using Brass9.IO;
namespace Brass9.Web.Script.JsonSerialization
{
public class JsonNetHelper
{
public const string IsoFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffK";
public static JsonNetHelper Create()
{
return new JsonNetHelper();
}
/// <summary>
/// Serialize to a TextWriter, using the fixed Iso DateTime converter, instead of Json.Net's built-in.
/// </summary>
/// <param name="writer"></param>
/// <param name="o"></param>
public void Serialize(TextWriter writer, object o)
{
var jsonSerializer = getFixedSerializer();
jsonSerializer.Serialize(writer, o);
}
/// <param name="contractResolver">Used to whitelist properties, or otherwise limit what's serialized</param>
public void Serialize(TextWriter writer, object o, IContractResolver contractResolver)
{
var jsonSerializer = getFixedSerializer();
jsonSerializer.Serialize(writer, o);
}
public string SerializeObject(object o)
{
var sb = new StringBuilder();
using(var writer = new StringWriter(sb))
{
Serialize(writer, o);
}
var s = sb.ToString();
return s;
}
/// <param name="contractResolver">Used to whitelist properties, or otherwise limit what's serialized</param>
public string SerializeObject(object o, IContractResolver contractResolver)
{
var sb = new StringBuilder();
using(var writer = new StringWriter(sb))
{
Serialize(writer, o, contractResolver);
}
var s = sb.ToString();
return s;
}
protected JsonConverter getFixedIsoDateTimeConverter()
{
return new IsoDateTimeConverter
{
DateTimeFormat = IsoFormat
};
}
protected JsonSerializer getFixedSerializer()
{
var settings = new JsonSerializerSettings
{
Converters = new[] { getFixedIsoDateTimeConverter() },
//#if MIN
Formatting = Formatting.None
//#endif
};
var jsonSerializer = JsonSerializer.Create(settings);
return jsonSerializer;
}
/// <param name="contractResolver">Used to whitelist properties, or otherwise limit what's serialized</param>
protected JsonSerializer getFixedSerializer(IContractResolver contractResolver)
{
var settings = new JsonSerializerSettings
{
Converters = new[] { getFixedIsoDateTimeConverter() },
ContractResolver = contractResolver,
//#if MIN
Formatting = Formatting.None
//#endif
};
var jsonSerializer = JsonSerializer.Create(settings);
return jsonSerializer;
}
/// <summary>
/// Write any object out to a file as JSON, UTF-8 encoded
/// </summary>
/// <param name="obj"></param>
/// <param name="fullFilePath"></param>
public void WriteFile(object obj, string fullFilePath)
{
using (var writer = new FileStreamWriter(fullFilePath))
{
var jsonEncoder = getFixedSerializer();
jsonEncoder.Serialize(writer, obj);
}
}
public async Task WriteFileAsync(object obj, string fullFilePath)
{
string json = JsonConvert.SerializeObject(obj);
await FileHelper.WriteFileAsync(fullFilePath, json);
}
/// <summary>
/// Read a UTF-8 encoded JSON file in as an object
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="fullFilePath"></param>
/// <returns></returns>
public T ReadFile<T>(string fullFilePath)
{
using (var reader = File.OpenText(fullFilePath))
{
using (var jsonReader = new JsonTextReader(reader))
{
var jsonEncoder = new JsonSerializer();
return jsonEncoder.Deserialize<T>(jsonReader);
}
}
}
public async Task<T> ReadFileAsync<T>(string fullFilePath)
{
string json = await FileHelper.ReadAllTextAsync(fullFilePath);
T obj = JsonConvert.DeserializeObject<T>(json);
return obj;
}
public T ReadAndDisposeStream<T>(Stream stream)
{
// http://stackoverflow.com/a/17788118/176877
var jsonSerializer = new JsonSerializer();
using(var sr = new StreamReader(stream))
{
using(var jsonReader = new JsonTextReader(sr))
{
var result = jsonSerializer.Deserialize<T>(jsonReader);
return result;
}
}
}
/// <summary>
/// Consumes a WebResponse's content as JSON, and returns the resulting object
/// </summary>
/// <param name="re">Any WebResponse, often the result of await request.GetResponseAsync()</param>
public T ReadResponse<T>(System.Net.WebResponse re)
{
return ReadAndDisposeStream<T>(re.GetResponseStream());
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Brass9.Web.Script.JsonSerialization;
namespace Brass9.Web.Mvc.Controllers
{
// http://stackoverflow.com/questions/7109967/using-json-net-as-default-json-serializer-in-asp-net-mvc-3-is-it-possible/7150912#7150912
public class JsonNetResult : JsonResult
{
// To use, add this to a base Controller
// protected override JsonResult Json(object data, string contentType, Encoding contentEncoding)
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;
JsonNetHelper.Create().Serialize(response.Output, Data);
}
public static JsonNetResult JsonNet(object o)
{
return new JsonNetResult { Data = o };
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment