Setup Synthetic Route for asp.net MVC static assets - c#

I can't figure out how to enable a custom route for static files.
I'd like to setup my asp.net mvc application to use synthetic URLs for versioned assets. This allows me to set expires to max for my static assets, and to have the version be incremented whenever I do a build.
I'd like to put that version into the path of my static assets when composing my cshtml templates. Lastly, I need to setup the routing to ignore the version component of the URL and just serve the static asset.
For example: /js/1.2.3.4/libs/require.js -> /js/libs/require.js
I've got the first two pieces working, but I can't figure out how to have part of a URL ignored for static files.
Layout.cshtml
...
<script data-main="/js/#(((myApplication)(HttpContext.Current.ApplicationInstance)).Version)/config"
src="/js/#(((myApplication)(HttpContext.Current.ApplicationInstance)).Version)/libs/require.js"></script>
...
myApplication.cs
public class myApplication : HttpApplication {
...
public string Version {
get {
var baseVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
#if DEBUG
var span = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0).ToLocalTime());
baseVersion += "_" + span.TotalMilliseconds;
#endif
return baseVersion;
}
}
}
Edit:
I managed to make a workable solution (courtesy of sheikhomar) using an OnBeginRequest handler at the application to rewrite URL when it matched a regex, but that seems excessive to do that on every request:
private readonly Regex jsPattern = new Regex("^/js/[0-9._]*/(.*)$", RegexOptions.IgnoreCase);
protected void OnBeginRequest(Object sender, EventArgs e) {
var match = jsPattern.Match(Request.Url.AbsolutePath);
if (match.Success) {
var fileName = match.Groups[1].Value;
Context.RewritePath(string.Format("/js/{0}", fileName));
}
}

I would suggest just letting MVC handle the route. Doing URL rewriting outside of the normal MVC route handling is something I would advise against unless you have a really good reason for it. The point of ASP.NET MVC is to eliminate the need for doing these kinds of low-level actions.
Anyway, in MVC, map your route similar to this:
routes.MapRoute("staticJs", "js/{version}/{*fileName}", new { controller = "Static", action = "Js" });
and then in StaticController
public ActionResult Js(string version, string fileName) {
var basePath = #"c:\path\to\js";
var realPath = Path.Combine(basePath, fileName);
return File(realPath, "application/javascript");
}
which will just stream the file back to the client (assuming that c:\path\to\js is the path to the javascript file on disk).
You can consult this answer for more info on FileResults.

Related

Returning file from ASP.NET Controller [duplicate]

I'm encountering a problem sending files stored in a database back to the user in ASP.NET MVC. What I want is a view listing two links, one to view the file and let the mimetype sent to the browser determine how it should be handled, and the other to force a download.
If I choose to view a file called SomeRandomFile.bak and the browser doesn't have an associated program to open files of this type, then I have no problem with it defaulting to the download behavior. However, if I choose to view a file called SomeRandomFile.pdf or SomeRandomFile.jpg I want the file to simply open. But I also want to keep a download link off to the side so that I can force a download prompt regardless of the file type. Does this make sense?
I have tried FileStreamResult and it works for most files, its constructor doesn't accept a filename by default, so unknown files are assigned a file name based on the URL (which does not know the extension to give based on content type). If I force the file name by specifying it, I lose the ability for the browser to open the file directly and I get a download prompt. Has anyone else encountered this?
These are the examples of what I've tried so far.
//Gives me a download prompt.
return File(document.Data, document.ContentType, document.Name);
//Opens if it is a known extension type, downloads otherwise (download has bogus name and missing extension)
return new FileStreamResult(new MemoryStream(document.Data), document.ContentType);
//Gives me a download prompt (lose the ability to open by default if known type)
return new FileStreamResult(new MemoryStream(document.Data), document.ContentType) {FileDownloadName = document.Name};
Any suggestions?
UPDATE:
This questions seems to strike a chord with a lot of people, so I thought I'd post an update. The warning on the accepted answer below that was added by Oskar regarding international characters is completely valid, and I've hit it a few times due to using the ContentDisposition class. I've since updated my implementation to fix this. While the code below is from my most recent incarnation of this problem in an ASP.NET Core (Full Framework) app, it should work with minimal changes in an older MVC application as well since I'm using the System.Net.Http.Headers.ContentDispositionHeaderValue class.
using System.Net.Http.Headers;
public IActionResult Download()
{
Document document = ... //Obtain document from database context
//"attachment" means always prompt the user to download
//"inline" means let the browser try and handle it
var cd = new ContentDispositionHeaderValue("attachment")
{
FileNameStar = document.FileName
};
Response.Headers.Add(HeaderNames.ContentDisposition, cd.ToString());
return File(document.Data, document.ContentType);
}
// an entity class for the document in my database
public class Document
{
public string FileName { get; set; }
public string ContentType { get; set; }
public byte[] Data { get; set; }
//Other properties left out for brevity
}
public ActionResult Download()
{
var document = ...
var cd = new System.Net.Mime.ContentDisposition
{
// for example foo.bak
FileName = document.FileName,
// always prompt the user for downloading, set to true if you want
// the browser to try to show the file inline
Inline = false,
};
Response.AppendHeader("Content-Disposition", cd.ToString());
return File(document.Data, document.ContentType);
}
NOTE: This example code above fails to properly account for international characters in the filename. See RFC6266 for the relevant standardization. I believe recent versions of ASP.Net MVC's File() method and the ContentDispositionHeaderValue class properly accounts for this. - Oskar 2016-02-25
I had trouble with the accepted answer due to no type hinting on the "document" variable: var document = ... So I'm posting what worked for me as an alternative in case anybody else is having trouble.
public ActionResult DownloadFile()
{
string filename = "File.pdf";
string filepath = AppDomain.CurrentDomain.BaseDirectory + "/Path/To/File/" + filename;
byte[] filedata = System.IO.File.ReadAllBytes(filepath);
string contentType = MimeMapping.GetMimeMapping(filepath);
var cd = new System.Net.Mime.ContentDisposition
{
FileName = filename,
Inline = true,
};
Response.AppendHeader("Content-Disposition", cd.ToString());
return File(filedata, contentType);
}
To view file (txt for example):
return File("~/TextFileInRootDir.txt", MediaTypeNames.Text.Plain);
To download file (txt for example):
return File("~/TextFileInRootDir.txt", MediaTypeNames.Text.Plain, "TextFile.txt");
note: to download file we should pass fileDownloadName argument
Darin Dimitrov's answer is correct. Just an addition:
Response.AppendHeader("Content-Disposition", cd.ToString()); may cause the browser to fail rendering the file if your response already contains a "Content-Disposition" header. In that case, you may want to use:
Response.Headers.Add("Content-Disposition", cd.ToString());
I believe this answer is cleaner, (based on
https://stackoverflow.com/a/3007668/550975)
public ActionResult GetAttachment(long id)
{
FileAttachment attachment;
using (var db = new TheContext())
{
attachment = db.FileAttachments.FirstOrDefault(x => x.Id == id);
}
return File(attachment.FileData, "application/force-download", Path.GetFileName(attachment.FileName));
}
Below code worked for me for getting a pdf file from an API service and response it out to the browser - hope it helps;
public async Task<FileResult> PrintPdfStatements(string fileName)
{
var fileContent = await GetFileStreamAsync(fileName);
var fileContentBytes = ((MemoryStream)fileContent).ToArray();
return File(fileContentBytes, System.Net.Mime.MediaTypeNames.Application.Pdf);
}
FileVirtualPath --> Research\Global Office Review.pdf
public virtual ActionResult GetFile()
{
return File(FileVirtualPath, "application/force-download", Path.GetFileName(FileVirtualPath));
}
Action method needs to return FileResult with either a stream, byte[], or virtual path of the file. You will also need to know the content-type of the file being downloaded. Here is a sample (quick/dirty) utility method. Sample video link
How to download files using asp.net core
[Route("api/[controller]")]
public class DownloadController : Controller
{
[HttpGet]
public async Task<IActionResult> Download()
{
var path = #"C:\Vetrivel\winforms.png";
var memory = new MemoryStream();
using (var stream = new FileStream(path, FileMode.Open))
{
await stream.CopyToAsync(memory);
}
memory.Position = 0;
var ext = Path.GetExtension(path).ToLowerInvariant();
return File(memory, GetMimeTypes()[ext], Path.GetFileName(path));
}
private Dictionary<string, string> GetMimeTypes()
{
return new Dictionary<string, string>
{
{".txt", "text/plain"},
{".pdf", "application/pdf"},
{".doc", "application/vnd.ms-word"},
{".docx", "application/vnd.ms-word"},
{".png", "image/png"},
{".jpg", "image/jpeg"},
...
};
}
}
If, like me, you've come to this topic via Razor components as you're learning Blazor, then you'll find you need to think a little more outside of the box to solve this problem. It's a bit of a minefield if (also like me) Blazor is your first forray into the MVC-type world, as the documentation isn't as helpful for such 'menial' tasks.
So, at the time of writing, you cannot achieve this using vanilla Blazor/Razor without embedding an MVC controller to handle the file download part an example of which is as below:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
[Route("api/[controller]")]
[ApiController]
public class FileHandlingController : ControllerBase
{
[HttpGet]
public FileContentResult Download(int attachmentId)
{
TaskAttachment taskFile = null;
if (attachmentId > 0)
{
// taskFile = <your code to get the file>
// which assumes it's an object with relevant properties as required below
if (taskFile != null)
{
var cd = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
{
FileNameStar = taskFile.Filename
};
Response.Headers.Add(HeaderNames.ContentDisposition, cd.ToString());
}
}
return new FileContentResult(taskFile?.FileData, taskFile?.FileContentType);
}
}
Next, make sure your application startup (Startup.cs) is configured to correctly use MVC and has the following line present (add it if not):
services.AddMvc();
.. and then finally modify your component to link to the controller, for example (iterative based example using a custom class):
<tbody>
#foreach (var attachment in yourAttachments)
{
<tr>
<td>#attachment.Filename </td>
<td>#attachment.CreatedUser</td>
<td>#attachment.Created?.ToString("dd MMM yyyy")</td>
<td><ul><li class="oi oi-circle-x delete-attachment"></li></ul></td>
</tr>
}
</tbody>
Hopefully this helps anyone who struggled (like me!) to get an appropriate answer to this seemingly simple question in the realms of Blazor…!

MVC How to bundle html templates of type "text/html"?

I have a series of template files (*.html) that are used by some JS components. Instead of having those JS components write the templates to the DOM when loaded, I wanted to bundle them together like scripts and have them downloaded separately by the client. This way should be faster (clientside), allow for caching (less round trips), and be more readable (template doesn't have to be stored in a JS string which breaks highlighting/intellisense).
How can this be accomplished?
I.
Use BundleTransformer [http://bundletransformer.codeplex.com/] and Mustache templates [https://mustache.github.io/] or Handlebars [http://handlebarsjs.com/]
II.
[Angular example but you can inspire a lot]
I'm not saying this is the best approach for your case, but i cannot left it like a comment.
Here is an example where the OP stores his bundle in $templateCache. Angular has a templateCache object, which stores all the templates it has loaded so far. It also lets you pre-load templates into the template cache.
Create a BundleTransform class, as he did:
public class PartialsTransform : IBundleTransform
{
private readonly string _moduleName;
public PartialsTransform(string moduleName)
{
_moduleName = moduleName;
}
public void Process(BundleContext context, BundleResponse response)
{
var strBundleResponse = new StringBuilder();
// Javascript module for Angular that uses templateCache
strBundleResponse.AppendFormat(
#"angular.module('{0}').run(['$templateCache',function(t){{",
_moduleName);
foreach (var file in response.Files)
{
// Get the partial page, remove line feeds and escape quotes
var content = File.ReadAllText(file.FullName)
.Replace("\r\n", "").Replace("'", "\\'");
// Create insert statement with template
strBundleResponse.AppendFormat(
#"t.put('partials/{0}','{1}');", file.Name, content);
}
strBundleResponse.Append(#"}]);");
response.Files = new FileInfo[] {};
response.Content = strBundleResponse.ToString();
response.ContentType = "text/javascript";
}
}
But you can store the templates where you want [i don't know where you want to store them].
Then create a Bundle.
public class PartialsBundle : Bundle
{
public PartialsBundle(string moduleName, string virtualPath)
: base(virtualPath, new[] { new PartialsTransform(moduleName) })
{
}
}
And you can use it like a ScriptBundle or StyleBundle.
bundles.Add(new PartialsBundle("testSPA", "~/bundles/partials").Include(
"~/Partials/nav-bar.html",
"~/Partials/home-page.html",
"~/Partials/investment-filter.html",
"~/Partials/investments-component.html",
"~/Partials/sector-component.html",
"~/Partials/transactions-component.html"));
And render like this: #Scripts.Render("~/bundles/partials")
In production transforming in this:
<script src="/bundles/partials?v=dq0i_tF8ogDVZ0X69xyBCdV2O2Qr3nCu0iVsatAzhq41"></script>
This guy is using the $templateCache object forcing Angular not to dynamically download template when are needed.
Further reading here: http://blog.scottlogic.com/2014/08/18/asp-angular-optimisation.html

How to get the url prefixes asp.net MVC

I have a ASP.NET MVC application which will work on a IIS7.
I don't know the final URL yet, and that's the problem.
I want to get the URL-Parts between the Top-level-domain and my controller.
For examople: http://www.mydomain.com/myApplication/MyController/ControllerMethodshould return /myApplication/MyController/
This should also be possible, if the application is called via the standard method, for example http://www.mydomain.com/myApplication.
The Problem is that with my method it works perfectly if the full controller- and methodname is in the url, but as soon as there is only the controller name and the route takes the default index-method or there is no controller/method and the route takes the default controller/method, it will fail because my code puts wrong output.
I thought about hardcoding the controller-name and make a if-then-else orgy, but this doesn't seem very professional...
Maybe anyone of you has got an Idea.
Here's my function:
String segments = Request.Url.Segments;
System.Text.StringBuilder builder = new System.Text.StringBuilder();
String lastSegment = "";
int i= 0;
do
{
builder.Append(segments[i]);
lastSegment = segments[i++];
} while(!lastSegment.Equals("Home") && !lastSegment.Equals("Home/") && i < segments.Length);
return builder.toString();
Use the Url class to build the Urls for you http://msdn.microsoft.com/en-us/library/system.web.mvc.urlhelper(v=vs.118).aspx

Auto-generated code needs to make static class

I decided to help my friend with a project he's working on. I'm trying to write a test webpage for him to verify some new functionality, but in my auto-generated code I get
CS1106: Extension method must be defined in a non-generic static class
Implementing the code in index.cshtml isn't the best way to do this, but we are just trying to do a proof of concept and will do a proper implementation later.
In all the places I looked they pretty much said that all the functions I define must be in a static class (as the error states). That wouldn't be so bad except for the class that holds all my functions is auto-generated and not static. I'm not really sure what settings I can change to fix this.
Here is a copy of the relevant (I believe) parts of code. The implementation of some or all of the functions may be incorrect. I haven't tested them yet
#{
HttpRequest req = System.Web.HttpContext.Current.Request;
HttpResponse resp = System.Web.HttpContext.Current.Response;
var url = req.QueryString["url"];
//1 Download web data from URL
//2 Write the final edited version of the document to the response object using resp.write(String x);
//3 Add Script tag for dom-outline-1.0 to html agility pack document
//4 Search for relative URLs and correct them to become absolute URL's that point back to the hostname
}
#functions
{
public static void PrintNodes(this HtmlAgilityPack.HtmlNode tag)
{
HttpResponse resp = System.Web.HttpContext.Current.Response;
resp.Write(tag.Name + tag.InnerHtml);
if (!tag.HasChildNodes)
{
return;
}
PrintNodes(tag.FirstChild);
}
public static void AddScriptNode(this HtmlAgilityPack.HtmlNode headNode, HtmlAgilityPack.HtmlDocument htmlDoc, string filePath)
{
string content = "";
using (StreamReader rdr = File.OpenText(filePath))
{
content = rdr.ReadToEnd();
}
if (headNode != null)
{
HtmlAgilityPack.HtmlNode scripts = htmlDoc.CreateElement("script");
scripts.Attributes.Add("type", "text/javascript");
scripts.AppendChild(htmlDoc.CreateComment("\n" + content + "\n"));
headNode.AppendChild(scripts);
}
}
}
<HTML CODE HERE>
If you were really smart you would encapsulate the design to take Delegates, reason being if you use a delegate you don't have to worry about referencing something static.
public delegate void MyUrlThing(string url, object optional = null);
Possibly some state...
public enum UrlState
{
None,
Good,
Bad
}
Then void would become UrlState...
Also if you wanted you could also setup a text box and blindly give it CIL....
Then you would compile the delegates using something like this
http://www.codeproject.com/Articles/578116/Complete-Managed-Media-Aggregation-Part-III-Quantu
This way you can use also then optionally just use the IL to augment whatever you wanted.
You could also give it CSharp code I suppose...
If you want to keep you design you can also then optionally use interfaces... and then put the compiled dll in a directory and then load it etc... as traditionally

razor extension method for url.content to prevent caching

I would like to override Url.Content to append a query string parameter to the resulting string on Url.Content.
The reason being, I have a web application that I develop, and with each release, users must clear their cache to get the new css and js. A solution for this is to append a version number to the querystring to force loading of the new version.
A working solution is as follows:
#{ var version = "?v=" + ViewBag.VersionNumber; }
<head>
<link href="#Url.Content("~/ux/css/base.css")#version" rel="stylesheet" type="text/css" />
</head>
Version is set in a config file so with each release, the version is updated. I would like this to be more automatic though, as currently any time a new css reference is added, we must remember to add #version to the string. An extension method the returns the path with the version number already appended would be perfect.
Also, if anyone knows who I could make changing the version number automatic with TFS check-ins or compiles that would be really useful too.
You could do something like this:
public static string VersionedContent(this UrlHelper urlHelper, string contentPath)
{
string result = urlHelper.Content(contentPath);
var versionService = Engine.IocService.GetInstance<IVersionService>();
string tag = versionService.GetVersionTag();
if (result.Contains('?'))
{
result += "&v="+tag;
}
else
{
result += "?v="+tag;
}
return result;
}
Version Service could look something like this:
public class VersionService : IVersionService
{
string _versionTag;
public VersionService()
{
_versionTag = Assembly.GetExecutingAssembly().GetName().Version.ToString();
_versionTag = _versionTag.Replace('.', '-');
}
#region IVersionedContentService Members
public string GetVersionTag()
{
return _versionTag;
}
#endregion
}
You might want to take a look at cassette
* EDIT *
For autom. build numbers with TFS, check out:
automatic-assembly-file-version-numbering-in-tfs-2010

Categories