Serilog HTTP Sink custom formatting for Logstash - json

I am using Serilog HTTP sink for logging to Logstash in my .Net Core Project. In startup.cs I have following code to enable serilog.
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.Http("http://mylogstashhost.com:5000").Enrich.WithProperty("user", "xxx").Enrich.WithProperty("serviceName", "yyy")
.MinimumLevel.Warning()
.CreateLogger();
And this code sends logs to the given http address. I can see on fiddler that following json is being posted to the logstash and logstash returns "ok" message.
{"events":[{"Timestamp":"2018-10-19T18:16:27.6561159+01:00","Level":"Warning","MessageTemplate":"abc","RenderedMessage":"abc","user":"xxx","serviceName":"yyy","Properties":{"ActionId":"b313b8ed-0baf-4d75-a6e2-f0dbcb941f67","ActionName":"MyProject.Controllers.HomeController.Index","RequestId":"0HLHLQMV1EBCJ:00000003","RequestPath":"/"}}]}
But when I checked on Kibana, I can not see this log. I tried to figure out what causes it and i realized that if I send the json as following format I can see the Log.
{"Timestamp":"2018-10-19T18:16:27.6561159+01:00","Level":"Warning","MessageTemplate":"abc","RenderedMessage":"abc","user":"xxx","serviceName":"yyy","Properties":{"ActionId":"b313b8ed-0baf-4d75-a6e2-f0dbcb941f67","ActionName":"MyProject.Controllers.HomeController.Index" ,"RequestId":"0HLHLQMV1EBCJ:00000003","RequestPath":"/"}}
So Logstash doesnt like the event to be in Events{} and also it wants "user" and "ServiceName" tags out of "Properties". Is there a way to format my Json like this?

Ok after some research and help, basically to achieve custom formats, one should implement interfaces like ITextFormatter, BatchFormatter etc.
I could achieve the format i need, by modifying ArrayBatchFormatter a little:
public class MyFormat : BatchFormatter
{
/// <summary>
/// Initializes a new instance of the <see cref="ArrayBatchFormatter"/> class.
/// </summary>
/// <param name="eventBodyLimitBytes">
/// The maximum size, in bytes, that the JSON representation of an event may take before it
/// is dropped rather than being sent to the server. Specify null for no limit. Default
/// value is 256 KB.
/// </param>
public MyFormat(long? eventBodyLimitBytes = 256 * 1024): base(eventBodyLimitBytes)
{
}
/// <summary>
/// Format the log events into a payload.
/// </summary>
/// <param name="logEvents">
/// The events to format.
/// </param>
/// <param name="output">
/// The payload to send over the network.
/// </param>
public override void Format(IEnumerable<string> logEvents, TextWriter output)
{
if (logEvents == null) throw new ArgumentNullException(nameof(logEvents));
if (output == null) throw new ArgumentNullException(nameof(output));
// Abort if sequence of log events is empty
if (!logEvents.Any())
{
return;
}
output.Write("[");
var delimStart = string.Empty;
foreach (var logEvent in logEvents)
{
if (string.IsNullOrWhiteSpace(logEvent))
{
continue;
}
int index = logEvent.IndexOf("{");
string adjustedString = "{\"user\":\"xxx\",\"serviceName\" : \"yyy\"," + logEvent.Substring(1);
if (CheckEventBodySize(adjustedString))
{
output.Write(delimStart);
output.Write(adjustedString);
delimStart = ",";
}
}
output.Write("]");
}
}

I would like to extend #nooaa answer with this variation. Instead of manipulating the string to add new objects, I would suggest using Newtonsoft.Json.Linq. This way you can append, add or remove existing properties of the object itself.
Also, instead of doing output.write after each event, you can combine all the output from the events and do output.write once at the end (a bit of performance)
public override void Format(IEnumerable<string> logEvents, TextWriter output)
{
if (logEvents == null) throw new ArgumentNullException(nameof(logEvents));
if (output == null) throw new ArgumentNullException(nameof(output));
// Abort if sequence of log events is empty
if (!logEvents.Any())
{
return;
}
List<object> updatedEvents = new List<object>();
foreach (string logEvent in logEvents)
{
if (string.IsNullOrWhiteSpace(logEvent))
{
continue;
}
// Parse the log event
var obj = JObject.Parse(logEvent);
// Add New entries
obj["#source_host"] = obj["fields"]["MachineName"].Value<string>().ToLower();
// Remove any entries you are not interested in
((JObject)obj["fields"]).Remove("MachineName");
// Default tags for any log that goes out of your app.
obj["#tags"] = new JArray() { "appName", "api" };
// Additional tags from end points (custom based on routes)
if (obj["fields"]["tags"] != null)
{
((JArray)obj["#tags"]).Merge((JArray)obj["fields"]["tags"]);
((JObject)obj["fields"]).Remove("tags");
}
updatedEvents.Add(obj);
}
output.Write(JsonConvert.SerializeObject(updatedEvents));
}
Update
Release Notes v8.0.0
With latest release, you dont override the method anymore.
namespace Serilog.Sinks.Http.BatchFormatters {
public class MyCustomFormatter: IBatchFormatter {
public void Format(IEnumerable<string> logEvents, TextWriter output) {
...
}
}
}
you don't provide any Contructors for it either.
Add queueLimitBytes along with batchFormatter and textFormatter
WriteTo.Http(new Uri(),
batchFormatter: new MyCustomFormatter(),
queueLimitBytes: 50 * ByteSize.MB,
textFormatter: new ElasticsearchJsonFormatter());

Related

ASP.NET View Components: Remove default path to view

I want to return a View with a specific path to the view like so, return View(~/Views/Home). The type to be returned is an IViewComponentResult.
However, when the site gets rendered, it tries to find the view under: Components/{controller-name}/~/Views/Home.
So I want to ask you guys, if you know a smart way to delete Components/{controller-name}/ from the path. My view component class is deriving from the ViewComponent class.
The problem regarding this is that right now, you have to have a "Components" folder with the name of the controller as a subfolder which contains a Default.cshtml file. I do not like to have all my components inside one folder. I hope you have a solution.
The convention happened in ViewViewComponentResult hence if you don't want the convention, you will have to implement your own IViewComponentResult
since mvc is open source, you could copy all the ViewViewComponentResult and then change the convention bit.
so essentially two things has to be done:
create your own IViewComponentResult - lets call it MyViewViewComponentResult
create a helper in your implemented ViewComponent to replace the original View() - purpose is to returns the custom MyViewViewComponentResult you created at step 1
create your own IViewComponentResult
the convention happened at ExecuteAsync:
public class ViewViewComponentResult : IViewComponentResult
{
// {0} is the component name, {1} is the view name.
private const string ViewPathFormat = "Components/{0}/{1}";
private const string DefaultViewName = "Default";
....
public async Task ExecuteAsync(ViewComponentContext context)
{
....
if (result == null || !result.Success)
{
// This will produce a string like:
//
// Components/Cart/Default
//
// The view engine will combine this with other path info to search paths like:
//
// Views/Shared/Components/Cart/Default.cshtml
// Views/Home/Components/Cart/Default.cshtml
// Areas/Blog/Views/Shared/Components/Cart/Default.cshtml
//
// This supports a controller or area providing an override for component views.
var viewName = isNullOrEmptyViewName ? DefaultViewName : ViewName;
var qualifiedViewName = string.Format(
CultureInfo.InvariantCulture,
ViewPathFormat,
context.ViewComponentDescriptor.ShortName,
viewName);
result = viewEngine.FindView(viewContext, qualifiedViewName, isMainPage: false);
}
....
}
.....
}
so change it to your need
create a helper in your implemented ViewComponent which replace the original View
so base on original
/// <summary>
/// Returns a result which will render the partial view with name <paramref name="viewName"/>.
/// </summary>
/// <param name="viewName">The name of the partial view to render.</param>
/// <param name="model">The model object for the view.</param>
/// <returns>A <see cref="ViewViewComponentResult"/>.</returns>
public ViewViewComponentResult View<TModel>(string viewName, TModel model)
{
var viewData = new ViewDataDictionary<TModel>(ViewData, model);
return new ViewViewComponentResult
{
ViewEngine = ViewEngine,
ViewName = viewName,
ViewData = viewData
};
}
you would do something like
/// <summary>
/// Returns a result which will render the partial view with name <paramref name="viewName"/>.
/// </summary>
/// <param name="viewName">The name of the partial view to render.</param>
/// <param name="model">The model object for the view.</param>
/// <returns>A <see cref="ViewViewComponentResult"/>.</returns>
public MyViewViewComponentResult MyView<TModel>(string viewName, TModel model)
{
var viewData = new ViewDataDictionary<TModel>(ViewData, model);
return new MyViewViewComponentResult
{
ViewEngine = ViewEngine,
ViewName = viewName,
ViewData = viewData
};
}
so in future you would call MyView() instead of View()
more info check out another SO: Change component view location in Asp.Net 5

Asp.net Razor Web Pages Binding Values To Object

I am just wondering is there a way to bind coming request values to an object like we do in MVC in Web Pages.
Web pages has a simpler way to develop smaller websites than mvc. So I want to use razor web pages to develop websites now.
I used MVC for long time and there was automatic model binding based on conventions. And in any Action you can use TryUpdateModel() method to bind request values on complex object for extra cool job.
There is a ModelBinders.Binders.DefaultBinder.BindModel() method I can see in page but it needs two long parameters.
I am wondering is there a simple fast way for bind Request Parameters to C# object :)
Thx for help.
There is no Model Binding in the Web Pages framework. The framework was developed with novice developers primarily in mind and from the comments I saw from members of the project team at the time, they didn't think their target audience would understand or use strongly typed containers for data (business objects).
I needed something like this for a project I worked on, so I built my own very simple model binder. It's rough code that only handles simple types and hasn't been put through any kind of wringer, but it serves my purpose. You can use it for the basis of something more robust:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web;
public static class RequestBinder
{
/// <summary>
/// Generates entity instances from Request values
/// </summary>
/// <typeparam name="TEntity">The Type to be generated</typeparam>
/// <param name="request"></param>
/// <returns>TEntity</returns>
public static TEntity Bind<TEntity>(this HttpRequestBase request) where TEntity : class, new()
{
var entity = (TEntity)Activator.CreateInstance(typeof(TEntity));
var properties = typeof(TEntity).GetProperties();
foreach (var property in properties)
{
object safeValue;
Type t = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
try
{
if (t == typeof(String))
{
safeValue = string.IsNullOrEmpty(request[property.Name]) ? null : request[property.Name];
}
else if (t.IsEnum)
{
safeValue = (request[property.Name] == null) ? null : Enum.Parse(t, request[property.Name]);
}
else
{
Type tColl = typeof(ICollection<>);
if (t.IsGenericType && tColl.IsAssignableFrom(t.GetGenericTypeDefinition()) || t.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == tColl))
{
continue;
}
safeValue = (request[property.Name] == null) ? null : Convert.ChangeType(request[property.Name], t);
}
property.SetValue(entity, safeValue, null);
}
catch (Exception)
{
}
}
return entity;
}
/// <summary>
/// Populates an existing entity's properties from the Request.Form collection
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="request"></param>
/// <param name="entity"></param>
/// <returns></returns>
public static TEntity Bind<TEntity>(this HttpRequestBase request, TEntity entity) where TEntity : class, new()
{
foreach (string item in request.Form)
{
var property = entity.GetType().GetProperty(item);
if (property != null)
{
object safeValue;
Type t = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
try
{
if (t == typeof(String))
{
safeValue = string.IsNullOrEmpty(request[property.Name]) ? null : request[property.Name];
}
else if (t.IsEnum)
{
safeValue = (request[property.Name] == null) ? null : Enum.Parse(t, request[property.Name]);
}
else
{
Type tColl = typeof(ICollection<>);
if (t.IsGenericType && tColl.IsAssignableFrom(t.GetGenericTypeDefinition()) || t.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == tColl))
{
continue;
}
safeValue = (request[property.Name] == null) ? null : Convert.ChangeType(request[property.Name], t);
}
property.SetValue(entity, safeValue, null);
}
catch (Exception)
{
}
}
}
return entity;
}
}
You would use it like this:
var person = Request.Bind<Person>();
It's unclear at the moment how Web Pages will be incorporated into ASP.NET vNext. The ASP.NET team have talked about eliminating duplication across Web Pages, MVC and Web API, so hopefully that will result in Web Pages including Model Binding.

Passing UTC DateTime to Web API HttpGet Method results in local time

I'm trying to pass a UTC date as a query string parameter to a Web API method. The URL looks like
/api/order?endDate=2014-04-01T00:00:00Z&zoneId=4
The signature of the method looks like
[HttpGet]
public object Index(int zoneId, DateTime? endDate = null)
The date is coming in as 31/03/2014 8:00:00 PM but I'd like it to come in as 01/04/2014 12:00:00 AM
My JsonFormatter.SerializerSettings looks like this
new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
DateFormatHandling = DateFormatHandling.IsoDateFormat
};
EDIT #1:
I've noticed when I POST 2014-04-01T00:00:00Z it will serialize to the UTC DateTime kind in C#. However I've found a work around of doing endDate.Value.ToUniversalTime() to convert it although I find it odd how it works for a POST but not a GET.
The query string parameter value you are sending 2014-04-01T00:00:00Z is UTC time. So, the same gets translated to a time based on your local clock and if you call ToUniversalTime(), it gets converted back to UTC.
So, what exactly is the question? If the question is why is this happening if sent in as query string but not when posted in request body, the answer to that question is that ASP.NET Web API binds the URI path, query string, etc using model binding and the body using parameter binding. For latter, it uses a media formatter. If you send JSON, the JSON media formatter is used and it is based on JSON.NET.
Since you have specified DateTimeZoneHandling.Utc, it uses that setting and you get the date time kind you want. BTW, if you change this setting to DateTimeZoneHandling.Local, then you will see the same behavior as model binding.
If you want the conversion to be transparent, then you could use a custom TypeConverter:
public sealed class UtcDateTimeConverter : DateTimeConverter
{
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return ((DateTime)base.ConvertFrom(context, culture, value)).ToUniversalTime();
}
}
and wire it up using:
TypeDescriptor.AddAttributes(typeof(DateTime), new TypeConverterAttribute(typeof(UtcDateTimeConverter)));
Then the query string parameter will be instantiated as DateTimeKind.Utc.
I ended up just using the ToUniversalTime() method as parameters come in.
So, for those of you who do not wish to override string-to-date conversion in your entire application, and also don't want to have to remember to modify every method that takes a date parameter, here's how you do it for a Web API project.
Ultimately, the general instructions come from here:
https://learn.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api#model-binders
Here's the specialized instructions for this case:
In your "WebApiConfig" class, add the following:
var provider = new SimpleModelBinderProvider(typeof(DateTime),new UtcDateTimeModelBinder());
config.Services.Insert(typeof(ModelBinderProvider), 0, provider);
Create a new class called UtcDateTimeModelBinder:
public class UtcDateTimeModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext,
ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(DateTime)) return false;
var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (val == null)
{
return false;
}
var key = val.RawValue as string;
if (key == null)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName,
"Wrong value type");
return false;
}
DateTime result;
if (DateTime.TryParse(key, out result))
{
bindingContext.Model = result.ToUniversalTime();
return true;
}
bindingContext.ModelState.AddModelError(bindingContext.ModelName,
"Cannot convert value to Utc DateTime");
return false;
}
}
I finally find this code , it's not the main answer but it can be used in some cases :
var dateUtc = TimeZoneInfo.ConvertTimeToUtc(date);
DateTimeOffset
Our versioned API classes are automapped to internal classes. Using DateTimeOffset in the URL parameter model of the API and adding a mapping DateTimeOffset => DateTime is effective at preventing the timezone conversion. I.E.
API Class:
public DateTimeOffset? SomeDateTime{ get; set; }
Internal Class:
public DateTime? SomeDateTime{ get; set; }
Mapping profile:
CreateMap<DateTimeOffset, DateTime>();
[This answer expands on the answer from #SeanFausett]
I wanted to have an ISO 8601 date that could have a "Z" on the and the web api function would receive it as a Utc Kind DateTime. But if there was not a "Z", I did not want the conversion.
I also needed to convert dates from incoming POST JSON payloads. The function below can support converting a string to a DateTime, DateTime?, DateTimeOffset, or DateTimeOffset?
It's handy to have dates parse the same way whether form a JSON post or URL parameter. Feel free to tailor the conversion to suit your needs.
//Register the two converters
var jSettings = new Newtonsoft.Json.JsonSerializerSettings()
jSettings.Converters.Add(new UtcDateTimeConverterJSON());
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings = jSettings;
GlobalConfiguration.Configure(config =>
{
TypeDescriptor.AddAttributes(typeof(DateTime), new TypeConverterAttribute(typeof(UtcDateTimeConverterURI)));
WebApiConfig.Register(config);
}
//Date converter for URI parameters
public class UtcDateTimeConverterURI : DateTimeConverter
{
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
if (value?.GetType() == typeof(string))
{
return StringToDate(typeof(DateTime), (string)value, Path: "URI parameter");
}
else
{
return base.ConvertFrom(context, culture, value);
}
}
/// <summary>
/// Convert String to DateTime, DateTime?, DateTimeOffset, or DateTimeOffset?<br />
/// Used for incoming JSON objects and URI parameters
/// </summary>
/// <param name="targetType">The type (i.e. typeof(DateTime))</param>
/// <param name="sDate">string representation of date to be converted</param>
/// <param name="Path">JSON Path in case of error, so the caller knows which parameter to fix</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static object StringToDate(Type targetType, string sDate, string Path)
{
//if the object is a DateTime, determine if we need to return a UTC or Local date type
bool returnUTC = false;
//DateTime or DateTimeOffset return type
bool isDateTimeOffset;
if (targetType == typeof(DateTime?) || targetType == typeof(DateTime))
{
isDateTimeOffset = false;
}
else
{
isDateTimeOffset = true;
}
DateTimeOffset d;
if (String.IsNullOrEmpty(sDate))
{
//if we have an empty string and the type is a nullable date, then return null... otherwise throw an error
if (targetType == typeof(DateTime?))
{
return null;
}
else
{
throw new Exception(Path + " cannot be an empty Date");
}
}
if (sDate[0] == '/')
{
// /Date(xxxxx)/ format
sDate = sDate.Substring(6, sDate.Length - 8);
var index = sDate.LastIndexOf('-');
if (index == -1) index = sDate.LastIndexOf('+');
if (index >= 0)
{
//lop off timezone offset
sDate = sDate.Substring(0, index);
}
else
{
//no timezone offset, return as UTC
returnUTC = true;
}
if (!Int64.TryParse(sDate, out var l))
{
//can't parse....
throw new Exception(Path + " cannot be parsed as a Date");
}
else
{
d = DateTimeOffset.FromUnixTimeMilliseconds(l);
}
}
else
{
//try and parse ISO8601 string
if (!DateTimeOffset.TryParse(sDate, out d))
{
throw new Exception(Path + " cannot be parsed as a Date");
}
else
{
if (!isDateTimeOffset)
{
//if UTC is specifically requested and we're not returning a DateTimeOffset, then make sure the return is UTC
if (d.Offset == TimeSpan.Zero && sDate[sDate.Length - 1] == 'Z') returnUTC = true;
}
}
}
if (isDateTimeOffset)
{
return d;
}
else
{
if (returnUTC)
{
return d.UtcDateTime;
}
else
{
//return the raw time passed in, forcing it to the "Local" Kind
//for example:
//"2020-03-27T12:00:00" --> use 2020-03-27 12:00:00PM with Kind=Local
//"2020-03-27T12:00:00-05:00" --> use 2020-03-27 12:00:00PM with Kind=Local
return DateTime.SpecifyKind(d.DateTime, DateTimeKind.Local); //this will pull the raw time and force the Kind to "Local"
}
}
}
}
//Date converter for JSON payloads
public class UtcDateTimeConverterJSON : DateTimeConverterBase
{
public override bool CanRead
{
get
{
return true;
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value == null || reader.TokenType == JsonToken.Date) return reader.Value;
if (reader.TokenType != JsonToken.String) throw new Exception("Cannot parse Date");
return UtcDateTimeConverterURI.StringToDate(objectType, (string)reader.Value, reader.Path);
}
}

Store class instance - Windows store application

I'm a bit new to programing a windows store app.So the question is how can I save an instance of a class in to an xml or binary file.I tried some code but it isn't working.
Hope that some one can steer me in the right direction .
You can serialize your instance by using this code
/// <summary>
/// Deserializes the XML.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="xml">The XML.</param>
/// <returns>The instance</returns>
public static T DeserializeXml<T>(this String xml)
{
var bytes = Encoding.UTF8.GetBytes(xml);
using (var stream = new MemoryStream(bytes))
{
var serializer = new DataContractSerializer(typeof(T));
return (T)serializer.ReadObject(stream);
}
}
/// <summary>
/// Serializes the specified instance.
/// </summary>
/// <param name="instance">The instance.</param>
/// <returns>Xml</returns>
public static String SerializeXml(this Object instance)
{
using (var stream = new MemoryStream())
{
var serializer = new DataContractSerializer(instance.GetType());
serializer.WriteObject(stream, instance);
stream.Position = 0;
using (var reader = new StreamReader(stream))
{
var result = "<?xml version='1.0' encoding='UTF-8' ?>";
result += reader.ReadToEnd();
return result;
}
}
}
Next step is to save the serialized instance text to a file.
var filename = "instance.txt";
var file = await ApplicationData.Current.LocalFolder.CreateFileAsync(filename, CreationCollisionOption.OpenIfExists);
var content = yourInstance.SerializeXml();
await FileIO.WriteTextAsync(file, content, Windows.Storage.Streams.UnicodeEncoding.Utf8);
Now there should be a file in your AppPackage-Local-Folder called instance.txt which contains the current instance serialized to xml.
You can use Windows.Storage to store any file, the usage is like IO operation. MSDN
IsolatedStorage is similar to this for Windows Phone apps.

Using sections in Editor/Display templates

I want to keep all of my JavaScript code in one section; just before the closing body tag in my master layout page and just wondering the best to go about it, MVC style.
For example, if I create a DisplayTemplate\DateTime.cshtml file which uses jQuery UI's DateTime Picker than I would embed the JavaScript directly into that template but then it will render mid-page.
In my normal views I can just use #section JavaScript { //js here } and then #RenderSection("JavaScript", false) in my master layout but this doesn't seem to work in display/editor templates - any ideas?
You could proceed with a conjunction of two helpers:
public static class HtmlExtensions
{
public static MvcHtmlString Script(this HtmlHelper htmlHelper, Func<object, HelperResult> template)
{
htmlHelper.ViewContext.HttpContext.Items["_script_" + Guid.NewGuid()] = template;
return MvcHtmlString.Empty;
}
public static IHtmlString RenderScripts(this HtmlHelper htmlHelper)
{
foreach (object key in htmlHelper.ViewContext.HttpContext.Items.Keys)
{
if (key.ToString().StartsWith("_script_"))
{
var template = htmlHelper.ViewContext.HttpContext.Items[key] as Func<object, HelperResult>;
if (template != null)
{
htmlHelper.ViewContext.Writer.Write(template(null));
}
}
}
return MvcHtmlString.Empty;
}
}
and then in your _Layout.cshtml:
<body>
...
#Html.RenderScripts()
</body>
and somewhere in some template:
#Html.Script(
#<script src="#Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
)
Modified version of Darin's answer to ensure ordering. Also works with CSS:
public static IHtmlString Resource(this HtmlHelper HtmlHelper, Func<object, HelperResult> Template, string Type)
{
if (HtmlHelper.ViewContext.HttpContext.Items[Type] != null) ((List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type]).Add(Template);
else HtmlHelper.ViewContext.HttpContext.Items[Type] = new List<Func<object, HelperResult>>() { Template };
return new HtmlString(String.Empty);
}
public static IHtmlString RenderResources(this HtmlHelper HtmlHelper, string Type)
{
if (HtmlHelper.ViewContext.HttpContext.Items[Type] != null)
{
List<Func<object, HelperResult>> Resources = (List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type];
foreach (var Resource in Resources)
{
if (Resource != null) HtmlHelper.ViewContext.Writer.Write(Resource(null));
}
}
return new HtmlString(String.Empty);
}
You can add JS and CSS resources like this:
#Html.Resource(#<script src="#Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>, "js")
#Html.Resource(#<link rel="stylesheet" href="#Url.Content("~/CSS/style.css")" />, "css")
And render JS and CSS resources like this:
#Html.RenderResources("js")
#Html.RenderResources("css")
You could do a string check to see if it starts with script/link so you don't have to explicitly define what each resource is.
I faced the same problem, but solutions proposed here work good only for adding reference to the resource and are not very suitable for inline JS code. I found a very helpful article and wrapped all my inline JS (and also script tags) in
#using (Html.BeginScripts())
{
<script src="#Url.Content("~/Scripts/jquery-ui-1.8.18.min.js")" type="text/javascript"></script>
<script>
// my inline scripts here
<\script>
}
And in the _Layout view placed #Html.PageScripts() just before closing 'body' tag. Works like a charm for me.
The helpers themselves:
public static class HtmlHelpers
{
private class ScriptBlock : IDisposable
{
private const string scriptsKey = "scripts";
public static List<string> pageScripts
{
get
{
if (HttpContext.Current.Items[scriptsKey] == null)
HttpContext.Current.Items[scriptsKey] = new List<string>();
return (List<string>)HttpContext.Current.Items[scriptsKey];
}
}
WebViewPage webPageBase;
public ScriptBlock(WebViewPage webPageBase)
{
this.webPageBase = webPageBase;
this.webPageBase.OutputStack.Push(new StringWriter());
}
public void Dispose()
{
pageScripts.Add(((StringWriter)this.webPageBase.OutputStack.Pop()).ToString());
}
}
public static IDisposable BeginScripts(this HtmlHelper helper)
{
return new ScriptBlock((WebViewPage)helper.ViewDataContainer);
}
public static MvcHtmlString PageScripts(this HtmlHelper helper)
{
return MvcHtmlString.Create(string.Join(Environment.NewLine, ScriptBlock.pageScripts.Select(s => s.ToString())));
}
}
I liked the solution posted by #john-w-harding, so I combined it with the answer by #darin-dimitrov to make the following probably overcomplicated solution that lets you delay rendering any html (scripts too) within a using block.
USAGE
In a repeated partial view, only include the block one time:
#using (Html.Delayed(isOnlyOne: "MYPARTIAL_scripts")) {
<script>
someInlineScript();
</script>
}
In a (repeated?) partial view, include the block for every time the partial is used:
#using (Html.Delayed()) {
<b>show me multiple times, #Model.Whatever</b>
}
In a (repeated?) partial view, include the block once, and later render it specifically by name one-time:
#using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
<b>show me once by name</b>
<span>#Model.First().Value</span>
}
To render:
#Html.RenderDelayed(); // the "default" unidentified blocks
#Html.RenderDelayed("one-time", false); // render the specified block by name, and allow us to render it again in a second call
#Html.RenderDelayed("one-time"); // render the specified block by name
#Html.RenderDelayed("one-time"); // since it was "popped" in the last call, won't render anything
CODE
public static class HtmlRenderExtensions {
/// <summary>
/// Delegate script/resource/etc injection until the end of the page
/// <para>#via https://stackoverflow.com/a/14127332/1037948 and http://jadnb.wordpress.com/2011/02/16/rendering-scripts-from-partial-views-at-the-end-in-mvc/ </para>
/// </summary>
private class DelayedInjectionBlock : IDisposable {
/// <summary>
/// Unique internal storage key
/// </summary>
private const string CACHE_KEY = "DCCF8C78-2E36-4567-B0CF-FE052ACCE309"; // "DelayedInjectionBlocks";
/// <summary>
/// Internal storage identifier for remembering unique/isOnlyOne items
/// </summary>
private const string UNIQUE_IDENTIFIER_KEY = CACHE_KEY;
/// <summary>
/// What to use as internal storage identifier if no identifier provided (since we can't use null as key)
/// </summary>
private const string EMPTY_IDENTIFIER = "";
/// <summary>
/// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
/// </summary>
/// <param name="helper">the helper from which we use the context</param>
/// <param name="identifier">optional unique sub-identifier for a given injection block</param>
/// <returns>list of delayed-execution callbacks to render internal content</returns>
public static Queue<string> GetQueue(HtmlHelper helper, string identifier = null) {
return _GetOrSet(helper, new Queue<string>(), identifier ?? EMPTY_IDENTIFIER);
}
/// <summary>
/// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
/// </summary>
/// <param name="helper">the helper from which we use the context</param>
/// <param name="defaultValue">the default value to return if the cached item isn't found or isn't the expected type; can also be used to set with an arbitrary value</param>
/// <param name="identifier">optional unique sub-identifier for a given injection block</param>
/// <returns>list of delayed-execution callbacks to render internal content</returns>
private static T _GetOrSet<T>(HtmlHelper helper, T defaultValue, string identifier = EMPTY_IDENTIFIER) where T : class {
var storage = GetStorage(helper);
// return the stored item, or set it if it does not exist
return (T) (storage.ContainsKey(identifier) ? storage[identifier] : (storage[identifier] = defaultValue));
}
/// <summary>
/// Get the storage, but if it doesn't exist or isn't the expected type, then create a new "bucket"
/// </summary>
/// <param name="helper"></param>
/// <returns></returns>
public static Dictionary<string, object> GetStorage(HtmlHelper helper) {
var storage = helper.ViewContext.HttpContext.Items[CACHE_KEY] as Dictionary<string, object>;
if (storage == null) helper.ViewContext.HttpContext.Items[CACHE_KEY] = (storage = new Dictionary<string, object>());
return storage;
}
private readonly HtmlHelper helper;
private readonly string identifier;
private readonly string isOnlyOne;
/// <summary>
/// Create a new using block from the given helper (used for trapping appropriate context)
/// </summary>
/// <param name="helper">the helper from which we use the context</param>
/// <param name="identifier">optional unique identifier to specify one or many injection blocks</param>
/// <param name="isOnlyOne">extra identifier used to ensure that this item is only added once; if provided, content should only appear once in the page (i.e. only the first block called for this identifier is used)</param>
public DelayedInjectionBlock(HtmlHelper helper, string identifier = null, string isOnlyOne = null) {
this.helper = helper;
// start a new writing context
((WebViewPage)this.helper.ViewDataContainer).OutputStack.Push(new StringWriter());
this.identifier = identifier ?? EMPTY_IDENTIFIER;
this.isOnlyOne = isOnlyOne;
}
/// <summary>
/// Append the internal content to the context's cached list of output delegates
/// </summary>
public void Dispose() {
// render the internal content of the injection block helper
// make sure to pop from the stack rather than just render from the Writer
// so it will remove it from regular rendering
var content = ((WebViewPage)this.helper.ViewDataContainer).OutputStack;
var renderedContent = content.Count == 0 ? string.Empty : content.Pop().ToString();
// if we only want one, remove the existing
var queue = GetQueue(this.helper, this.identifier);
// get the index of the existing item from the alternate storage
var existingIdentifiers = _GetOrSet(this.helper, new Dictionary<string, int>(), UNIQUE_IDENTIFIER_KEY);
// only save the result if this isn't meant to be unique, or
// if it's supposed to be unique and we haven't encountered this identifier before
if( null == this.isOnlyOne || !existingIdentifiers.ContainsKey(this.isOnlyOne) ) {
// remove the new writing context we created for this block
// and save the output to the queue for later
queue.Enqueue(renderedContent);
// only remember this if supposed to
if(null != this.isOnlyOne) existingIdentifiers[this.isOnlyOne] = queue.Count; // save the index, so we could remove it directly (if we want to use the last instance of the block rather than the first)
}
}
}
/// <summary>
/// <para>Start a delayed-execution block of output -- this will be rendered/printed on the next call to <see cref="RenderDelayed"/>.</para>
/// <para>
/// <example>
/// Print once in "default block" (usually rendered at end via <code>#Html.RenderDelayed()</code>). Code:
/// <code>
/// #using (Html.Delayed()) {
/// <b>show at later</b>
/// <span>#Model.Name</span>
/// etc
/// }
/// </code>
/// </example>
/// </para>
/// <para>
/// <example>
/// Print once (i.e. if within a looped partial), using identified block via <code>#Html.RenderDelayed("one-time")</code>. Code:
/// <code>
/// #using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
/// <b>show me once</b>
/// <span>#Model.First().Value</span>
/// }
/// </code>
/// </example>
/// </para>
/// </summary>
/// <param name="helper">the helper from which we use the context</param>
/// <param name="injectionBlockId">optional unique identifier to specify one or many injection blocks</param>
/// <param name="isOnlyOne">extra identifier used to ensure that this item is only added once; if provided, content should only appear once in the page (i.e. only the first block called for this identifier is used)</param>
/// <returns>using block to wrap delayed output</returns>
public static IDisposable Delayed(this HtmlHelper helper, string injectionBlockId = null, string isOnlyOne = null) {
return new DelayedInjectionBlock(helper, injectionBlockId, isOnlyOne);
}
/// <summary>
/// Render all queued output blocks injected via <see cref="Delayed"/>.
/// <para>
/// <example>
/// Print all delayed blocks using default identifier (i.e. not provided)
/// <code>
/// #using (Html.Delayed()) {
/// <b>show me later</b>
/// <span>#Model.Name</span>
/// etc
/// }
/// </code>
/// -- then later --
/// <code>
/// #using (Html.Delayed()) {
/// <b>more for later</b>
/// etc
/// }
/// </code>
/// -- then later --
/// <code>
/// #Html.RenderDelayed() // will print both delayed blocks
/// </code>
/// </example>
/// </para>
/// <para>
/// <example>
/// Allow multiple repetitions of rendered blocks, using same <code>#Html.Delayed()...</code> as before. Code:
/// <code>
/// #Html.RenderDelayed(removeAfterRendering: false); /* will print */
/// #Html.RenderDelayed() /* will print again because not removed before */
/// </code>
/// </example>
/// </para>
/// </summary>
/// <param name="helper">the helper from which we use the context</param>
/// <param name="injectionBlockId">optional unique identifier to specify one or many injection blocks</param>
/// <param name="removeAfterRendering">only render this once</param>
/// <returns>rendered output content</returns>
public static MvcHtmlString RenderDelayed(this HtmlHelper helper, string injectionBlockId = null, bool removeAfterRendering = true) {
var stack = DelayedInjectionBlock.GetQueue(helper, injectionBlockId);
if( removeAfterRendering ) {
var sb = new StringBuilder(
#if DEBUG
string.Format("<!-- delayed-block: {0} -->", injectionBlockId)
#endif
);
// .count faster than .any
while (stack.Count > 0) {
sb.AppendLine(stack.Dequeue());
}
return MvcHtmlString.Create(sb.ToString());
}
return MvcHtmlString.Create(
#if DEBUG
string.Format("<!-- delayed-block: {0} -->", injectionBlockId) +
#endif
string.Join(Environment.NewLine, stack));
}
}
Install the Forloop.HtmlHelpers nuget package - it adds some helpers for managing scripts in partial views and editor templates.
Somewhere in your layout, you need to call
#Html.RenderScripts()
This will be where any script files and script blocks will be outputted in the page so I would recommend putting it after your main scripts in the layout and after a scripts section (if you have one).
If you're using The Web Optimization Framework with bundling, you can use the overload
#Html.RenderScripts(Scripts.Render)
so that this method is used for writing out script files.
Now, anytime you want to add script files or blocks in a view, partial view or template, simply use
#using (Html.BeginScriptContext())
{
Html.AddScriptFile("~/Scripts/jquery.validate.js");
Html.AddScriptBlock(
#<script type="text/javascript">
$(function() { $('#someField').datepicker(); });
</script>
);
}
The helpers ensure that only one script file reference is rendered if added multiple times and it also ensures that script files are rendered out in an expected order i.e.
Layout
Partials and Templates (in the order in which they appear in the view, top to bottom)
This post really helped me so I thought I would post my implementation of the basic idea. I've introduced a helper function that can return script tags for use in the #Html.Resource function.
I also added a simple static class so that I can use typed variables to identify a JS or CSS resource.
public static class ResourceType
{
public const string Css = "css";
public const string Js = "js";
}
public static class HtmlExtensions
{
public static IHtmlString Resource(this HtmlHelper htmlHelper, Func<object, dynamic> template, string Type)
{
if (htmlHelper.ViewContext.HttpContext.Items[Type] != null) ((List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[Type]).Add(template);
else htmlHelper.ViewContext.HttpContext.Items[Type] = new List<Func<object, dynamic>>() { template };
return new HtmlString(String.Empty);
}
public static IHtmlString RenderResources(this HtmlHelper htmlHelper, string Type)
{
if (htmlHelper.ViewContext.HttpContext.Items[Type] != null)
{
List<Func<object, dynamic>> resources = (List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[Type];
foreach (var resource in resources)
{
if (resource != null) htmlHelper.ViewContext.Writer.Write(resource(null));
}
}
return new HtmlString(String.Empty);
}
public static Func<object, dynamic> ScriptTag(this HtmlHelper htmlHelper, string url)
{
var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
var script = new TagBuilder("script");
script.Attributes["type"] = "text/javascript";
script.Attributes["src"] = urlHelper.Content("~/" + url);
return x => new HtmlString(script.ToString(TagRenderMode.Normal));
}
}
And in use
#Html.Resource(Html.ScriptTag("Areas/Admin/js/plugins/wysiwyg/jquery.wysiwyg.js"), ResourceType.Js)
Thanks to #Darin Dimitrov who supplied the answer in my question here.
The answer given in Populate a Razor Section From a Partial using the RequireScript HtmlHelper follows the same pattern. It also has the benefit that it checks for and suppresses duplicate references to the same Javascript URL, and it has an explicit priority parameter that can be used to control ordering.
I extended this solution by adding methods for:
// use this for scripts to be placed just before the </body> tag
public static string RequireFooterScript(this HtmlHelper html, string path, int priority = 1) { ... }
public static HtmlString EmitRequiredFooterScripts(this HtmlHelper html) { ... }
// use this for CSS links
public static string RequireCSS(this HtmlHelper html, string path, int priority = 1) { ... }
public static HtmlString EmitRequiredCSS(this HtmlHelper html) { ... }
I like Darin's & eth0's solutions though since they use the HelperResult template, which allows for script and CSS blocks, not just links to Javascript and CSS files.
#Darin Dimitrov and #eth0 answers to use with bundle extention usage :
#Html.Resources(a => new HelperResult(b => b.Write( System.Web.Optimization.Scripts.Render("~/Content/js/formBundle").ToString())), "jsTop")