diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs
index 331cffc9cf..4152f2fa21 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs
@@ -88,7 +88,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{
if (!context.AllAttributes.ContainsKey(attribute) ||
context.AllAttributes[attribute] == null ||
- string.IsNullOrWhiteSpace(context.AllAttributes[attribute] as string))
+ (typeof(string).IsAssignableFrom(context.AllAttributes[attribute].GetType()) &&
+ string.IsNullOrWhiteSpace(context.AllAttributes[attribute] as string)))
{
// Missing attribute!
missingAttributes.Add(attribute);
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/FileVersionProvider.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/FileVersionProvider.cs
new file mode 100644
index 0000000000..d5ef0eda93
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/FileVersionProvider.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using Microsoft.AspNet.FileProviders;
+using Microsoft.AspNet.Http;
+using Microsoft.AspNet.WebUtilities;
+using Microsoft.Framework.Caching.Memory;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
+{
+ ///
+ /// Provides version hash for a specified file.
+ ///
+ public class FileVersionProvider
+ {
+ private const string VersionKey = "v";
+ private readonly IFileProvider _fileProvider;
+ private readonly IMemoryCache _cache;
+ private readonly PathString _requestPathBase;
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The file provider to get and watch files.
+ /// Name of the application.
+ /// where versioned urls of files are cached.
+ public FileVersionProvider(
+ [NotNull] IFileProvider fileProvider,
+ [NotNull] IMemoryCache cache,
+ [NotNull] PathString requestPathBase)
+ {
+ _fileProvider = fileProvider;
+ _cache = cache;
+ _requestPathBase = requestPathBase;
+ }
+
+ ///
+ /// Adds version query parameter to the specified file path.
+ ///
+ /// The path of the file to which version should be added.
+ /// Path containing the version query string.
+ ///
+ /// The version query string is appended as with the key "v".
+ ///
+ public string AddFileVersionToPath([NotNull] string path)
+ {
+ var resolvedPath = path;
+ var fileInfo = _fileProvider.GetFileInfo(resolvedPath);
+ if (!fileInfo.Exists)
+ {
+ if (_requestPathBase.HasValue &&
+ resolvedPath.StartsWith(_requestPathBase.Value, StringComparison.OrdinalIgnoreCase))
+ {
+ resolvedPath = resolvedPath.Substring(_requestPathBase.Value.Length);
+ fileInfo = _fileProvider.GetFileInfo(resolvedPath);
+ }
+
+ if (!fileInfo.Exists)
+ {
+ // if the file is not in the current server.
+ return path;
+ }
+ }
+
+ return _cache.GetOrSet(path, cacheGetOrSetContext =>
+ {
+ var trigger = _fileProvider.Watch(resolvedPath);
+ cacheGetOrSetContext.AddExpirationTrigger(trigger);
+ return QueryHelpers.AddQueryString(path, VersionKey, GetHashForFile(fileInfo));
+ });
+ }
+
+ private string GetHashForFile(IFileInfo fileInfo)
+ {
+ using (var sha256 = SHA256.Create())
+ {
+ var hash = sha256.ComputeHash(fileInfo.CreateReadStream());
+ return WebEncoders.Base64UrlEncode(hash);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs
index 3db20ddc7e..1bf94f9e9d 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs
@@ -5,12 +5,12 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-using System.Text;
using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.Framework.Caching.Memory;
using Microsoft.Framework.Logging;
+using Microsoft.Framework.Runtime;
using Microsoft.Framework.WebEncoders;
namespace Microsoft.AspNet.Mvc.TagHelpers
@@ -29,6 +29,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[TargetElement("link", Attributes = FallbackTestClassAttributeName)]
[TargetElement("link", Attributes = FallbackTestPropertyAttributeName)]
[TargetElement("link", Attributes = FallbackTestValueAttributeName)]
+ [TargetElement("link", Attributes = FileVersionAttributeName)]
public class LinkTagHelper : TagHelper
{
private const string HrefIncludeAttributeName = "asp-href-include";
@@ -40,8 +41,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
private const string FallbackTestPropertyAttributeName = "asp-fallback-test-property";
private const string FallbackTestValueAttributeName = "asp-fallback-test-value";
private const string FallbackJavaScriptResourceName = "compiler/resources/LinkTagHelper_FallbackJavaScript.js";
+ private const string FileVersionAttributeName = "asp-file-version";
+ private const string HrefAttributeName = "href";
+
+ private FileVersionProvider _fileVersionProvider;
private static readonly ModeAttributes[] ModeDetails = new[] {
+ // Regular src with file version alone
+ ModeAttributes.Create(Mode.FileVersion, new[] { FileVersionAttributeName }),
// Globbed Href (include only) no static href
ModeAttributes.Create(Mode.GlobbedHref, new [] { HrefIncludeAttributeName }),
// Globbed Href (include & exclude), no static href
@@ -76,15 +83,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
private enum Mode
{
+ ///
+ /// Just adding a file version for the generated urls.
+ ///
+ FileVersion = 0,
///
/// Just performing file globbing search for the href, rendering a separate <link> for each match.
///
- GlobbedHref = 0,
+ GlobbedHref = 1,
///
/// Rendering a fallback block if primary stylesheet fails to load. Will also do globbing for both the
/// primary and fallback hrefs if the appropriate properties are set.
///
- Fallback = 1,
+ Fallback = 2,
}
///
@@ -108,6 +119,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[HtmlAttributeName(FallbackHrefAttributeName)]
public string FallbackHref { get; set; }
+ ///
+ /// Value indicating if file version should be appended to the href urls.
+ ///
+ ///
+ /// If true then a query string "v" with the encoded content of the file is added.
+ ///
+ [HtmlAttributeName(FileVersionAttributeName)]
+ public bool? FileVersion { get; set; }
+
///
/// A comma separated list of globbed file patterns of CSS stylesheets to fallback to in the case the primary
/// one fails.
@@ -149,7 +169,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[HtmlAttributeName(FallbackTestValueAttributeName)]
public string FallbackTestValue { get; set; }
- // Properties are protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
+ // Properties are protected to ensure subclasses are correctly activated.
+ // Internal for ease of use when testing.
[Activate]
protected internal ILoggerFactory LoggerFactory { get; set; }
@@ -194,9 +215,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var builder = new DefaultTagHelperContent();
- if (mode == Mode.Fallback && string.IsNullOrEmpty(HrefInclude))
+ if (mode == Mode.Fallback && string.IsNullOrEmpty(HrefInclude) || mode == Mode.FileVersion)
{
- // No globbing to do, just build a tag to match the original one in the source file
+ // No globbing to do, just build a tag to match the original one in the source file.
+ // Or just add file version to the link tag.
BuildLinkTag(attributes, builder);
}
else
@@ -218,14 +240,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
// Build a tag for each matched href as well as the original one in the source file
string staticHref;
- attributes.TryGetValue("href", out staticHref);
+ attributes.TryGetValue(HrefAttributeName, out staticHref);
EnsureGlobbingUrlBuilder();
var urls = GlobbingUrlBuilder.BuildUrlList(staticHref, HrefInclude, HrefExclude);
foreach (var url in urls)
{
- attributes["href"] = HtmlEncoder.HtmlEncode(url);
+ attributes[HrefAttributeName] = url;
BuildLinkTag(attributes, builder);
}
}
@@ -233,10 +255,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
private void BuildFallbackBlock(TagHelperContent builder)
{
EnsureGlobbingUrlBuilder();
- var fallbackHrefs = GlobbingUrlBuilder.BuildUrlList(FallbackHref, FallbackHrefInclude, FallbackHrefExclude);
+ var fallbackHrefs =
+ GlobbingUrlBuilder.BuildUrlList(FallbackHref, FallbackHrefInclude, FallbackHrefExclude).ToArray();
- if (fallbackHrefs.Any())
+ if (fallbackHrefs.Length > 0)
{
+ if (ShouldAddFileVersion())
+ {
+ for (var i=0; i < fallbackHrefs.Length; i++)
+ {
+ fallbackHrefs[i] = _fileVersionProvider.AddFileVersionToPath(fallbackHrefs[i]);
+ }
+ }
+
builder.Append(Environment.NewLine);
// Build the tag that's used to test for the presence of the stylesheet
@@ -269,17 +300,46 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
}
- private static void BuildLinkTag(IDictionary attributes, TagHelperContent builder)
+ private void EnsureFileVersionProvider()
{
+ if (_fileVersionProvider == null)
+ {
+ _fileVersionProvider = new FileVersionProvider(
+ HostingEnvironment.WebRootFileProvider,
+ Cache,
+ ViewContext.HttpContext.Request.PathBase);
+ }
+ }
+
+ private void BuildLinkTag(IDictionary attributes, TagHelperContent builder)
+ {
+ EnsureFileVersionProvider();
builder.Append("");
}
+
+ private bool ShouldAddFileVersion()
+ {
+ return FileVersion ?? false;
+ }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs
index 226949f725..e3a2e4d8b4 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Hosting;
@@ -11,6 +10,7 @@ using Microsoft.AspNet.Mvc.TagHelpers.Internal;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.Framework.Caching.Memory;
using Microsoft.Framework.Logging;
+using Microsoft.Framework.Runtime;
using Microsoft.Framework.WebEncoders;
namespace Microsoft.AspNet.Mvc.TagHelpers
@@ -27,6 +27,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[TargetElement("script", Attributes = FallbackSrcIncludeAttributeName)]
[TargetElement("script", Attributes = FallbackSrcExcludeAttributeName)]
[TargetElement("script", Attributes = FallbackTestExpressionAttributeName)]
+ [TargetElement("script", Attributes = FileVersionAttributeName)]
public class ScriptTagHelper : TagHelper
{
private const string SrcIncludeAttributeName = "asp-src-include";
@@ -36,8 +37,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
private const string FallbackSrcExcludeAttributeName = "asp-fallback-src-exclude";
private const string FallbackTestExpressionAttributeName = "asp-fallback-test";
private const string SrcAttributeName = "src";
+ private const string FileVersionAttributeName = "asp-file-version";
+
+ private FileVersionProvider _fileVersionProvider;
private static readonly ModeAttributes[] ModeDetails = new[] {
+ // Regular src with file version alone
+ ModeAttributes.Create(Mode.FileVersion, new[] { FileVersionAttributeName }),
// Globbed src (include only)
ModeAttributes.Create(Mode.GlobbedSrc, new [] { SrcIncludeAttributeName }),
// Globbed src (include & exclude)
@@ -66,15 +72,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
private enum Mode
{
+ ///
+ /// Just adding a file version for the generated urls.
+ ///
+ FileVersion = 0,
///
/// Just performing file globbing search for the src, rendering a separate <script> for each match.
///
- GlobbedSrc = 0,
+ GlobbedSrc = 1,
///
/// Rendering a fallback block if primary javascript fails to load. Will also do globbing for both the
/// primary and fallback srcs if the appropriate properties are set.
///
- Fallback = 1
+ Fallback = 2
}
///
@@ -98,6 +108,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[HtmlAttributeName(FallbackSrcAttributeName)]
public string FallbackSrc { get; set; }
+ ///
+ /// Value indicating if file version should be appended to src urls.
+ ///
+ ///
+ /// A query string "v" with the encoded content of the file is added.
+ ///
+ [HtmlAttributeName(FileVersionAttributeName)]
+ public bool? FileVersion { get; set; }
+
///
/// A comma separated list of globbed file patterns of JavaScript scripts to fallback to in the case the
/// primary one fails.
@@ -137,12 +156,12 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[Activate]
protected internal IMemoryCache Cache { get; set; }
- // Internal for ease of use when testing.
- protected internal GlobbingUrlBuilder GlobbingUrlBuilder { get; set; }
-
[Activate]
protected internal IHtmlEncoder HtmlEncoder { get; set; }
+ // Internal for ease of use when testing.
+ protected internal GlobbingUrlBuilder GlobbingUrlBuilder { get; set; }
+
///
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
@@ -167,9 +186,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var builder = new DefaultTagHelperContent();
var originalContent = await context.GetChildContentAsync();
- if (mode == Mode.Fallback && string.IsNullOrEmpty(SrcInclude))
+ if (mode == Mode.Fallback && string.IsNullOrEmpty(SrcInclude) || mode == Mode.FileVersion)
{
// No globbing to do, just build a tag to match the original one in the source file
+ // Or just add file version to the script tag.
BuildScriptTag(originalContent, attributes, builder);
}
else
@@ -194,14 +214,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
// Build a ");
}
}
@@ -268,17 +294,37 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
}
- private static void BuildScriptTag(
+ private void EnsureFileVersionProvider()
+ {
+ if (_fileVersionProvider == null)
+ {
+ _fileVersionProvider = new FileVersionProvider(
+ HostingEnvironment.WebRootFileProvider,
+ Cache,
+ ViewContext.HttpContext.Request.PathBase);
+ }
+ }
+
+ private void BuildScriptTag(
TagHelperContent content,
IDictionary attributes,
TagHelperContent builder)
{
+ EnsureFileVersionProvider();
builder.Append("");
}
- private void AppendSrc(TagHelperContent content, string srcKey, string srcValue)
+ private bool ShouldAddFileVersion()
{
- // Append src attribute in the original place and replace the content the fallback content
- // No need to encode the key because we know it is "src".
- content.Append(" ")
- .Append(srcKey)
- .Append("=\\\"")
- .Append(HtmlEncoder.HtmlEncode(srcValue))
- .Append("\\\"");
+ return FileVersion ?? false;
}
+
+ private void AppendAttribute(TagHelperContent content, string key, string value, bool escapteQuotes)
+ {
+ content
+ .Append(" ")
+ .Append(key)
+ .Append(escapteQuotes ? "=\\\"" : "=\"")
+ .Append(value)
+ .Append(escapteQuotes ? "\\\"" : "\"");
+ }
+
}
}
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html
index 9dd1127c76..be221d21d0 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html
@@ -110,6 +110,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Script.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Script.html
index 70102a9ac7..1aa41653c6 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Script.html
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Script.html
@@ -92,5 +92,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+