how to include static content with version info - asp.net-mvc

I am having trouble with my static content being cached on the client (by static I mean js,css,jpeg,gif, etc). (and by client I mean its my development machine most of the time).
As a result, page is either throwing a script error, or not displaying correctly. I am not a Rails developer but I read a few books about it back in time. One thing I remember well is that it appends some magic version number to the end of the included file, so it becomes
<script src="~/Scripts/Invoice.js?201112091712" type="text/javascript"></script>
and if you modify that content file it generates a new version number, so it generates a different include statement, as a result, client thinks it is a new content, and it loads it without checking its cache.
Does asp.net-mvc 3 & IIS 7 support this, or do you know any tools that mimic this behaviour?
Thanks, Hazım

I have this done already in one of my projects, feel free to use my helpers if you like them :
public static class VersionedContentExtensions
{
public static MvcHtmlString VersionedScript(this HtmlHelper html, string file)
{
return VersionedContent(html, "<script src=\"{0}\" type=\"text/javascript\"></script>", file);
}
public static MvcHtmlString VersionedStyle(this HtmlHelper html, string file)
{
return VersionedContent(html, "<link href=\"{0}\" rel=\"stylesheet\" type=\"text/css\">", file);
}
private static MvcHtmlString VersionedContent(this HtmlHelper html, string template, string file)
{
string hash = HttpContext.Current.Application["VersionedContentHash_" + file] as string;
if (hash == null)
{
string filename = HttpContext.Current.Server.MapPath(file);
hash = GetMD5HashFromFile(filename);
HttpContext.Current.Application["VersionedContentHash_" + file] = hash;
}
return MvcHtmlString.Create(string.Format(template, file + "?v=" + hash));
}
private static string GetMD5HashFromFile(string fileName)
{
FileStream file = new FileStream(fileName, FileMode.Open);
MD5 md5 = new MD5CryptoServiceProvider();
byte[] retVal = md5.ComputeHash(file);
file.Close();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < retVal.Length; i++)
{
sb.Append(retVal[i].ToString("x2"));
}
return sb.ToString();
}
}
use them like this :
#Html.VersionedScript("/Scripts/sccat.core.js")

I did something like this:
<script src="<%= GenerateScriptUrl("~/Scripts/Invoide.js") %>"></script>
In GenerateScriptUrl method, I write the content of the file, calculate the md5 value then get the url with version number. The url would be cached so it will be calculated twice. I also create an handler (not exposed to the user) to clear the cache. So the process doesn't need to be restarted when the file content is changed.
You can also get the version number with the last modified or thing. You can even monitor the file's change by FileSystemWatcher, etc.
Hope it helps.

Try this which adds the file modified time
public static class UrlHelperExtentention
{
public static string VersionedContent(this UrlHelper urlHelper,
string contentPath)
{
string versionedContent= urlHelper.Content(contentPath);
string modified= File.GetLastWriteTime(
HostingEnvironment.MapPath(contentPath))
.ToString("yyyyMMddhhmm");
if (result.Contains('?'))
versionedContent += "&" + modified;
else
versionedContent += "?" + modified;
return versionedContent;
}
}
Then
<script src="#Url.VersionedContent("~/js/Home.js")" type="text/javascript"/>

Related

Change asp.net mvc bundles fingerprinting

Is there a possibility how to change the way how System.Web.Optimization renders bundles?
From:
<script src="/bundles/js/bundlename?v=GMFuN8gzKMcwk5BwaMfgjUlieAXKThyQd8twrVplJ8A1"></script>
To something custom like this:
<script src="/bundles/js/v-GMFuN8gzKMcwk5BwaMfgjUlieAXKThyQd8twrVplJ8A1/bundlename"></script>
UPDATE: Not ideal but small nasty workaround:
public static class BundlesHelper
{
public static IHtmlString RenderScripts(params string[] paths)
{
#if DEBUG
return System.Web.Optimization.Scripts.Render(paths);
#endif
// Get raw string
var rawString = System.Web.Optimization.Scripts.Render(paths).ToHtmlString();
// Get version value
var version = Regex.Match(rawString, #"\?v=([0-9a-zA-Z_-])+").Value;
// Remove old hash
rawString = rawString.Replace(version, "");
// Remove script end tag
rawString = rawString.Replace("</script>", "");
// Get last index of "/"
var index = rawString.LastIndexOf('/');
// Return new string
return new HtmlString(rawString.Insert(index, "/v-" + version.Replace("?v=", "")) + "</script>");
}
}
No. That's not the point. The query string portion is a cache buster. The file is located where the script src says it is, and that doesn't change. In your desired version the actual physical location of the file would have to change.

Using an HtmlHelper inside a custom HtmlHelper

Is there a way to do something like the following:
public static IHtmlString TableFor(this HtmlHelper helper, IEnumerable<MaterialGroup> groups, Func<HtmlHelper, MaterialGroup, int, string> tableContentsFunc)
{
return MvcHtmlString.Create("#Html.TextBoxFor(x => this.Model.Something)");
}
Obviously this is a trivial example, but when ever I try something of the sort it renders the Helpers i.e. "#Html.TextBoxFor(x => this.Model.Something)" as text on the page instead of processing them as helpers.
Is there a way to achieve this?
public static MvcHtmlString TableFor(this HtmlHelper<IEnumerable<MaterialGroup>> helper, IEnumerable<MaterialGroup> groups, Func<HtmlHelper<MaterialGroup>, MaterialGroup, int, string> tableContentsFunc)
{
String html = "<table class='materials joists'>";
String endHtml = "</table>";
for (int i = 0; i < groups.Count(); ++i)
{
HtmlHelper<MaterialGroup> groupHelper = new HtmlHelper<MaterialGroup>(helper.ViewContext, helper.ViewDataContainer); // Crashes here with cannot convert IEnumerable<MaterialGroup> to MaterialGroup.
html += TbodyFor(groupHelper , groups.ElementAt(i), i);
html += tableContentsFunc(groupHelper , groups.ElementAt(i), i);
}
return MvcHtmlString.Create(html + endHtml);
}
public static string TbodyForJoists(this HtmlHelper<MaterialGroup> helper, MaterialGroup group, int index)
{
string html = string.Empty;
MvcHtmlString markTextbox = InputExtensions.TextBoxFor<MaterialGroup, String>(helper, x => group.Joists.ElementAt(i).Mark, new { Name = "MaterialGroups[" + index + "].Joists[" + i + "].Mark", Class = "auto-size first-column" });
html += martTextbox;
.
.
.
return html;
}
When I attempt the above I get issues with the HtmlHelper<> Types.
If I leave it with just HtmlHelper I get an error telling me to explicity state since it doesn't know what I doing with it. If I explicitly state is have conversion? issues I guess you could say.
How can I simply just use the TextBoxFor in this way?
Because that is very literally what you are telling it to do. The output of the helper itself is not processed by Razor. Whatever you return is what's going on the page.
However, you could always do:
MvcHtmlString textBox = Html.TextBoxFor(expression);
And, you'd have to feed the helper the expression to use. It perhaps would be easier in this situation to use Html.TextBox instead, but then you're going to have to do more work to try to figure out the right names and such for the fields.
It's going to be far easier and less convoluted to just use editor templates for this type of thing.

Bundling CSS files depending on domain of request?

I have a multi-tenant application and I'm trying to determine the simplest means of controlling which CSS files are bundled based on the url of any incoming request.
I'm thinking I can have some conditional logic inside RegisterBundles() that takes the Url as a string, and bundles accordingly:
public static void RegisterBundles(BundleCollection bundles, string tenant = null) {
if (tenant == "contoso"){
bundles.Add(new StyleBundle("~/contoso.css")
}
}
But I don't know how to pass the string into RegisterBundles, nor even if it's possible, or the right solution. Any help here would be awesome.
It is not possible to do it in RegisterBundles right now. Dynamically generating the bundle content per request will prevent ASP.net from caching the minified CSS (it's cached in HttpContext.Cache).
What you can do is create one bundle per tenant in RegisterBundles then select the appropriate bundle in the view.
Example code in the view:
#Styles.Render("~/Content/" + ViewBag.TenantName)
Edit:
As you said, setting the TenantName in a ViewBag is problematic since you have to do it per view. One way to solve this is to create a static function like Styles.Render() that selects the correct bundle name based from the current tenant.
public static class TenantStyles
{
public static IHtmlString Render(params string[] paths)
{
var tenantName = "test"; //get tenant name from where its currently stored
var tenantExtension = "-" + tenantName;
return Styles.Render(paths.Select(i => i + tenantExtension).ToArray());
}
}
Usage
#TenantStyles.Render("~/Content/css")
The bundle names will need to be in the this format {bundle}-{tenant} like ~/Content/css-test. But you can change the format ofcourse.
I think you are after a solution that allows you to dynamically control the BundleCollection. As far as I know this is currently not possible.
The bundles are configured during app start/configured per the application domain.
A future version of ASP.NET may support this feature i,e using VirtualPathProvider.
Here is some discussion.
See also this SO question.
i'm not good in english, but if you mean you need to handle which CSS file load when you run any URL in your page, i can handle css file in a controler.
First, create a controller name : ResourceController
// CREATE PATH TO CSS FOLDER, I store in webconfig <add key="PathToStyles" value="/Content/MyTheme/" />
private static string _pathToStyles = ConfigurationManager.AppSettings["PathToStyles"];
public void Script(string resourceName)
{
if (!String.IsNullOrEmpty(resourceName))
{
var pathToResource = Server.MapPath(Path.Combine(_pathToScripts, resourceName));
TransmitFileWithHttpCachePolicy(pathToResource, ContentType.JavaScript.GetEnumDescription());
}
}
public void Style(string resourceName)
{
if (!String.IsNullOrEmpty(resourceName))
{
var pathToResource = Server.MapPath(Path.Combine(_pathToStyles, resourceName));
TransmitFileWithHttpCachePolicy(pathToResource, ContentType.Css.GetEnumDescription());
}
}
private void TransmitFileWithHttpCachePolicy(string pathToResource, string contentType)
{
//DO WHAT YOU WANT HERE;
Response.ContentType = contentType;
Response.TransmitFile(pathToResource);
}
//You can handle css or js file...
private enum ContentType
{
[EnumDescription("text/css")]
Css,
[EnumDescription("text/javascript")]
JavaScript
}
In file Global.asax.cs, make sure in application start medthod, in contain the route config
protected void Application_Start()
{
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
Go to routeConfig, add below map to this file (must be add in top of this file) :
routes.MapRoute(
name: "Resource",
url: "resource/{action}/{resourceName}",
defaults: new { controller = "Resource" }
);
Now, create a UrlHelperExtensions class, same path with webconfig file
public static class UrlHelperExtensions
{
public static string Style(this UrlHelper urlHelper, string resourceName)
{
return urlHelper.Content(String.Format("~/resource/style/{0}", resourceName));
}
}
And from now, you can define css file in your view like :
..."<"link href="#Url.Style("yourcss.css")" rel="stylesheet" type="text/css"
/>
Hope this help

Stream file using ASP.NET MVC FileContentResult in a browser with a name?

Is there a way to stream a file using ASP.NET MVC FileContentResult within the browser with a specific name?
I have noticed that you can either have a FileDialog (Open/Save) or you can stream the file in a browser window, but then it will use the ActionName when you try to save the file.
I have the following scenario:
byte[] contents = DocumentServiceInstance.CreateDocument(orderId, EPrintTypes.Quote);
result = File(contents, "application/pdf", String.Format("Quote{0}.pdf", orderId));
When I use this, I can stream the bytes, but a OPEN/SAVE file dialog is given to the user. I would like to actually stream this file in a browser window.
If I just use the FilePathResult, it shows the file in a browser window, but then when I click on "Save" button to save the file in PDF, it shows me the Action Name as the name of the file.
Has anyone encountered this?
public ActionResult Index()
{
byte[] contents = FetchPdfBytes();
return File(contents, "application/pdf", "test.pdf");
}
and for opening the PDF inside the browser you will need to set the Content-Disposition header:
public ActionResult Index()
{
byte[] contents = FetchPdfBytes();
Response.AddHeader("Content-Disposition", "inline; filename=test.pdf");
return File(contents, "application/pdf");
}
Actually, the absolutely easiest way is to do the following...
byte[] content = your_byte[];
FileContentResult result = new FileContentResult(content, "application/octet-stream")
{
FileDownloadName = "your_file_name"
};
return result;
This might be helpful for whoever else faces this problem. I finally figured out a solution. Turns out, even if we use the inline for "content-disposition" and specify a file name, the browsers still do not use the file name. Instead browsers try and interpret the file name based on the Path/URL.
You can read further on this URL:
Securly download file inside browser with correct filename
This gave me an idea, I just created my URL route that would convert the URL and end it with the name of the file I wanted to give the file. So for e.g. my original controller call just consisted of passing the Order Id of the Order being printed. I was expecting the file name to be of the format Order{0}.pdf where {0} is the Order Id. Similarly for quotes, I wanted Quote{0}.pdf.
In my controller, I just went ahead and added an additional parameter to accept the file name. I passed the filename as a parameter in the URL.Action method.
I then created a new route that would map that URL to the format:
http://localhost/ShoppingCart/PrintQuote/1054/Quote1054.pdf
routes.MapRoute("", "{controller}/{action}/{orderId}/{fileName}",
new { controller = "ShoppingCart", action = "PrintQuote" }
, new string[] { "x.x.x.Controllers" }
);
This pretty much solved my issue.
Previous answers are correct: adding the line...
Response.AddHeader("Content-Disposition", "inline; filename=[filename]");
...will causing multiple Content-Disposition headers to be sent down to the browser. This happens b/c FileContentResult internally applies the header if you supply it with a file name. An alternative, and pretty simple, solution is to simply create a subclass of FileContentResult and override its ExecuteResult() method. Here's an example that instantiates an instance of the System.Net.Mime.ContentDisposition class (the same object used in the internal FileContentResult implementation) and passes it into the new class:
public class FileContentResultWithContentDisposition : FileContentResult
{
private const string ContentDispositionHeaderName = "Content-Disposition";
public FileContentResultWithContentDisposition(byte[] fileContents, string contentType, ContentDisposition contentDisposition)
: base(fileContents, contentType)
{
// check for null or invalid ctor arguments
ContentDisposition = contentDisposition;
}
public ContentDisposition ContentDisposition { get; private set; }
public override void ExecuteResult(ControllerContext context)
{
// check for null or invalid method argument
ContentDisposition.FileName = ContentDisposition.FileName ?? FileDownloadName;
var response = context.HttpContext.Response;
response.ContentType = ContentType;
response.AddHeader(ContentDispositionHeaderName, ContentDisposition.ToString());
WriteFile(response);
}
}
In your Controller, or in a base Controller, you can write a simple helper to instantiate a FileContentResultWithContentDisposition and then call it from your action method, like so:
protected virtual FileContentResult File(byte[] fileContents, string contentType, ContentDisposition contentDisposition)
{
var result = new FileContentResultWithContentDisposition(fileContents, contentType, contentDisposition);
return result;
}
public ActionResult Report()
{
// get a reference to your document or file
// in this example the report exposes properties for
// the byte[] data and content-type of the document
var report = ...
return File(report.Data, report.ContentType, new ContentDisposition {
Inline = true,
FileName = report.FileName
});
}
Now the file will be sent to the browser with the file name you choose and with a content-disposition header of "inline; filename=[filename]".
I hope that helps!
The absolute easiest way to stream a file into browser using ASP.NET MVC is this:
public ActionResult DownloadFile() {
return File(#"c:\path\to\somefile.pdf", "application/pdf", "Your Filename.pdf");
}
This is easier than the method suggested by #azarc3 since you don't even need to read the bytes.
Credit goes to: http://prideparrot.com/blog/archive/2012/8/uploading_and_returning_files#how_to_return_a_file_as_response
** Edit **
Apparently my 'answer' is the same as the OP's question. But I am not facing the problem he is having. Probably this was an issue with older version of ASP.NET MVC?
I adapted it in ASP.NET Core with REST API.
public class FileContentWithFileNameResult : FileContentResult
{
public FileContentWithFileNameResult(byte[] fileContents, string contentType, string fileName)
: base(fileContents, contentType)
{
FileName = fileName;
}
public string FileName { get; private set; }
public override Task ExecuteResultAsync(ActionContext context)
{
var response = context.HttpContext.Response;
response.Headers.Append("Content-Disposition", $"inline; filename={FileName}");
response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition");
response.Headers.Append("X-Content-Type-Options", "nosniff");
return base.ExecuteResultAsync(context);
}
}
public FileContentResult GetImage(int productId) {
Product prod = repository.Products.FirstOrDefault(p => p.ProductID == productId);
if (prod != null) {
return File(prod.ImageData, prod.ImageMimeType);
} else {
return null;
}
}

Recommended way to create an ActionResult with a file extension

I need to create an ActionResult in an ASP.NET MVC application which has a .csv filetype.
I will provide a 'do not call' email list to my marketing partners and i want it to have a .csv extension in the filetype. Then it'll automatically open in Excel.
http://www.example.com/mailinglist/donotemaillist.csv?password=12334
I have successfully done this as follows, but I want to make sure this is the absolute best and recommended way of doing this.
[ActionName("DoNotEmailList.csv")]
public ContentResult DoNotEmailList(string username, string password)
{
return new ContentResult()
{
Content = Emails.Aggregate((a,b)=>a+Environment.NewLine + b),
ContentType = "text/csv"
};
}
This Actionmethod will respond to the above link just fine.
I'm just wondering if there is any likelihood of any unexpected conflict of having the file extension like this with any different version of IIS, any kind of ISAPI filter, or anything else I cant think of now.
I need to be 100% sure because I will be providing this to external partners and don't want to have to change my mind later. I really cant see any issues, but maybe theres something obscure - or another more "MVC" like way of doing this.
I used the FileContentResult action to also do something similar.
public FileContentResult DoNotEmailList(string username, string password)
{
string csv = Emails.Aggregate((a,b)=>a+Environment.NewLine + b);
byte[] csvBytes = ASCIIEncoding.ASCII.GetBytes( csv );
return File(csvBytes, "text/csv", "DoNotEmailList.csv");
}
It will add the content-disposition header for you.
I think your Response MUST contain "Content-Disposition" header in this case. Create custom ActionResult like this:
public class MyCsvResult : ActionResult {
public string Content {
get;
set;
}
public Encoding ContentEncoding {
get;
set;
}
public string Name {
get;
set;
}
public override void ExecuteResult(ControllerContext context) {
if (context == null) {
throw new ArgumentNullException("context");
}
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = "text/csv";
if (ContentEncoding != null) {
response.ContentEncoding = ContentEncoding;
}
var fileName = "file.csv";
if(!String.IsNullOrEmpty(Name)) {
fileName = Name.Contains('.') ? Name : Name + ".csv";
}
response.AddHeader("Content-Disposition",
String.Format("attachment; filename={0}", fileName));
if (Content != null) {
response.Write(Content);
}
}
}
And use it in your Action instead of ContentResult:
return new MyCsvResult {
Content = Emails.Aggregate((a,b) => a + Environment.NewLine + b)
/* Optional
* , ContentEncoding = ""
* , Name = "DoNotEmailList.csv"
*/
};
This is how I'm doing something similar. I'm treating it as a download:
var disposition = String.Format(
"attachment;filename=\"{0}.csv\"", this.Model.Name);
Response.AddHeader("content-disposition", disposition);
This should show up in the browser as a file download with the given filename.
I can't think of a reason why yours wouldn't work, though.
The answer you accepted is good enough, but it keeps the content of the output in memory as it outputs it. What if the file it generates is rather large? For example, when you dump a contents of the SQL table. Your application could run out of memory. What you do want in this case is to use FileStreamResult. One way to feed the data into the stream could be using pipe, as I described here

Resources