ImageTagHelper
An ImageTagHelper that supports cache busting by appending a file version hash to the image src attribute [Resolves #2249] Code cleanup
This commit is contained in:
parent
a591e53dc9
commit
ab4d2eec31
|
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNet.Hosting;
|
||||||
|
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
|
||||||
|
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||||
|
using Microsoft.Framework.Caching.Memory;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="ITagHelper"/> implementation targeting <img> elements that supports file versioning.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The tag helper won't process for cases with just the 'src' attribute.
|
||||||
|
/// </remarks>
|
||||||
|
[TargetElement("img", Attributes = FileVersionAttributeName + "," + SrcAttributeName)]
|
||||||
|
public class ImageTagHelper : TagHelper
|
||||||
|
{
|
||||||
|
private static readonly string Namespace = typeof(ImageTagHelper).Namespace;
|
||||||
|
|
||||||
|
private const string FileVersionAttributeName = "asp-file-version";
|
||||||
|
private const string SrcAttributeName = "src";
|
||||||
|
|
||||||
|
private FileVersionProvider _fileVersionProvider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Source of the image.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Passed through to the generated HTML in all cases.
|
||||||
|
/// </remarks>
|
||||||
|
[HtmlAttributeName(SrcAttributeName)]
|
||||||
|
public string Src { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Value indicating if file version should be appended to the src urls.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If <c>true</c> then a query string "v" with the encoded content of the file is added.
|
||||||
|
/// </remarks>
|
||||||
|
[HtmlAttributeName(FileVersionAttributeName)]
|
||||||
|
public bool FileVersion { get; set; }
|
||||||
|
|
||||||
|
[Activate]
|
||||||
|
[HtmlAttributeNotBound]
|
||||||
|
public IHostingEnvironment HostingEnvironment { get; set; }
|
||||||
|
|
||||||
|
[Activate]
|
||||||
|
[HtmlAttributeNotBound]
|
||||||
|
public ViewContext ViewContext { get; set; }
|
||||||
|
|
||||||
|
[Activate]
|
||||||
|
[HtmlAttributeNotBound]
|
||||||
|
public IMemoryCache Cache { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||||
|
{
|
||||||
|
if (FileVersion)
|
||||||
|
{
|
||||||
|
EnsureFileVersionProvider();
|
||||||
|
output.Attributes[SrcAttributeName] = _fileVersionProvider.AddFileVersionToPath(Src);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Pass through attribute that is also a well-known HTML attribute.
|
||||||
|
output.CopyHtmlAttribute(SrcAttributeName, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureFileVersionProvider()
|
||||||
|
{
|
||||||
|
if (_fileVersionProvider == null)
|
||||||
|
{
|
||||||
|
_fileVersionProvider = new FileVersionProvider(
|
||||||
|
HostingEnvironment.WebRootFileProvider,
|
||||||
|
Cache,
|
||||||
|
ViewContext.HttpContext.Request.PathBase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,6 +47,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
[InlineData("Link", null)]
|
[InlineData("Link", null)]
|
||||||
// Testing the ScriptTagHelper
|
// Testing the ScriptTagHelper
|
||||||
[InlineData("Script", null)]
|
[InlineData("Script", null)]
|
||||||
|
// Testing the ImageTagHelper
|
||||||
|
[InlineData("Image", null)]
|
||||||
// Testing InputTagHelper with File
|
// Testing InputTagHelper with File
|
||||||
[InlineData("Input", null)]
|
[InlineData("Input", null)]
|
||||||
public async Task MvcTagHelpers_GeneratesExpectedResults(string action, string antiForgeryPath)
|
public async Task MvcTagHelpers_GeneratesExpectedResults(string action, string antiForgeryPath)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Image</title>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h2>Image Tag Helper Test</h2>
|
||||||
|
<!-- Plain image tag -->
|
||||||
|
<img src="/images/red.png" alt="Red block" title="<the title>">
|
||||||
|
|
||||||
|
<!-- Plain image tag with file version -->
|
||||||
|
<img alt="Red versioned" title="Red versioned" src="/images/red.png?v=W2F5D366_nQ2fQqUk3URdgWy2ZekXjHzHJaY5yaiOOk" />
|
||||||
|
|
||||||
|
<!-- Plain image tag with file version set to false -->
|
||||||
|
<img alt="Red explicitly not versioned" title="Red versioned" src="/images/red.png" />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,288 @@
|
||||||
|
// Copyright (c) .NET Foundation. 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.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNet.FileProviders;
|
||||||
|
using Microsoft.AspNet.Hosting;
|
||||||
|
using Microsoft.AspNet.Http.Internal;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNet.Mvc.Rendering;
|
||||||
|
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.Framework.Caching;
|
||||||
|
using Microsoft.Framework.Caching.Memory;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
|
{
|
||||||
|
public class ImageTagHelperTest
|
||||||
|
{
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreservesOrderOfSourceAttributesWhenRun()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = MakeTagHelperContext(
|
||||||
|
attributes: new TagHelperAttributeList
|
||||||
|
{
|
||||||
|
{ "alt", new HtmlString("alt text") },
|
||||||
|
{ "data-extra", new HtmlString("something") },
|
||||||
|
{ "title", new HtmlString("Image title") },
|
||||||
|
{ "src", "testimage.png" },
|
||||||
|
{ "asp-file-version", "true" }
|
||||||
|
});
|
||||||
|
var output = MakeImageTagHelperOutput(
|
||||||
|
attributes: new TagHelperAttributeList
|
||||||
|
{
|
||||||
|
{ "alt", new HtmlString("alt text") },
|
||||||
|
{ "data-extra", new HtmlString("something") },
|
||||||
|
{ "title", new HtmlString("Image title") },
|
||||||
|
});
|
||||||
|
|
||||||
|
var expectedOutput = MakeImageTagHelperOutput(
|
||||||
|
attributes: new TagHelperAttributeList
|
||||||
|
{
|
||||||
|
{ "alt", new HtmlString("alt text") },
|
||||||
|
{ "data-extra", new HtmlString("something") },
|
||||||
|
{ "title", new HtmlString("Image title") },
|
||||||
|
{ "src", "testimage.png?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk" }
|
||||||
|
});
|
||||||
|
|
||||||
|
var hostingEnvironment = MakeHostingEnvironment();
|
||||||
|
var viewContext = MakeViewContext();
|
||||||
|
var helper = new ImageTagHelper
|
||||||
|
{
|
||||||
|
HostingEnvironment = hostingEnvironment,
|
||||||
|
ViewContext = viewContext,
|
||||||
|
Src = "testimage.png",
|
||||||
|
FileVersion = true,
|
||||||
|
Cache = MakeCache(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
helper.Process(context, output);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedOutput.TagName, output.TagName);
|
||||||
|
Assert.Equal(4, output.Attributes.Count);
|
||||||
|
|
||||||
|
for(int i=0; i < expectedOutput.Attributes.Count; i++)
|
||||||
|
{
|
||||||
|
var expectedAtribute = expectedOutput.Attributes[i];
|
||||||
|
var actualAttribute = output.Attributes[i];
|
||||||
|
Assert.Equal(expectedAtribute.Name, actualAttribute.Name);
|
||||||
|
Assert.Equal(expectedAtribute.Value.ToString(), actualAttribute.Value.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RendersImageTag_AddsFileVersion()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = MakeTagHelperContext(
|
||||||
|
attributes: new TagHelperAttributeList
|
||||||
|
{
|
||||||
|
{ "alt", new HtmlString("Alt image text") },
|
||||||
|
{ "src", "/images/test-image.png" },
|
||||||
|
{ "asp-file-version", "true" }
|
||||||
|
});
|
||||||
|
var output = MakeImageTagHelperOutput(attributes: new TagHelperAttributeList
|
||||||
|
{
|
||||||
|
{ "alt", new HtmlString("Alt image text") },
|
||||||
|
});
|
||||||
|
var hostingEnvironment = MakeHostingEnvironment();
|
||||||
|
var viewContext = MakeViewContext();
|
||||||
|
var helper = new ImageTagHelper
|
||||||
|
{
|
||||||
|
HostingEnvironment = hostingEnvironment,
|
||||||
|
ViewContext = viewContext,
|
||||||
|
Src = "/images/test-image.png",
|
||||||
|
FileVersion = true,
|
||||||
|
Cache = MakeCache(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
helper.Process(context, output);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(output.Content.IsEmpty);
|
||||||
|
Assert.Equal("img", output.TagName);
|
||||||
|
Assert.Equal(2, output.Attributes.Count);
|
||||||
|
var srcAttribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("src"));
|
||||||
|
Assert.Equal("/images/test-image.png?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", srcAttribute.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RendersImageTag_DoesNotAddFileVersion()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = MakeTagHelperContext(
|
||||||
|
attributes: new TagHelperAttributeList
|
||||||
|
{
|
||||||
|
{ "alt", new HtmlString("Alt image text") },
|
||||||
|
{ "src", "/images/test-image.png" },
|
||||||
|
{ "asp-file-version", "false" }
|
||||||
|
});
|
||||||
|
var output = MakeImageTagHelperOutput(attributes: new TagHelperAttributeList
|
||||||
|
{
|
||||||
|
{ "alt", new HtmlString("Alt image text") },
|
||||||
|
});
|
||||||
|
var hostingEnvironment = MakeHostingEnvironment();
|
||||||
|
var viewContext = MakeViewContext();
|
||||||
|
var helper = new ImageTagHelper
|
||||||
|
{
|
||||||
|
HostingEnvironment = hostingEnvironment,
|
||||||
|
ViewContext = viewContext,
|
||||||
|
Src = "/images/test-image.png",
|
||||||
|
FileVersion = false,
|
||||||
|
Cache = MakeCache(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
helper.Process(context, output);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(output.Content.IsEmpty);
|
||||||
|
Assert.Equal("img", output.TagName);
|
||||||
|
Assert.Equal(2, output.Attributes.Count);
|
||||||
|
var srcAttribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("src"));
|
||||||
|
Assert.Equal("/images/test-image.png", srcAttribute.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RendersImageTag_AddsFileVersion_WithRequestPathBase()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = MakeTagHelperContext(
|
||||||
|
attributes: new TagHelperAttributeList
|
||||||
|
{
|
||||||
|
{ "alt", new HtmlString("alt text") },
|
||||||
|
{ "src", "/bar/images/image.jpg" },
|
||||||
|
{ "asp-file-version", "true" },
|
||||||
|
});
|
||||||
|
var output = MakeImageTagHelperOutput(attributes: new TagHelperAttributeList
|
||||||
|
{
|
||||||
|
{ "alt", new HtmlString("alt text") },
|
||||||
|
});
|
||||||
|
var hostingEnvironment = MakeHostingEnvironment();
|
||||||
|
var viewContext = MakeViewContext("/bar");
|
||||||
|
var helper = new ImageTagHelper
|
||||||
|
{
|
||||||
|
HostingEnvironment = hostingEnvironment,
|
||||||
|
ViewContext = viewContext,
|
||||||
|
Src = "/bar/images/image.jpg",
|
||||||
|
FileVersion = true,
|
||||||
|
Cache = MakeCache(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
helper.Process(context, output);
|
||||||
|
// Assert
|
||||||
|
Assert.True(output.Content.IsEmpty);
|
||||||
|
Assert.Equal("img", output.TagName);
|
||||||
|
Assert.Equal(2, output.Attributes.Count);
|
||||||
|
var srcAttribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("src"));
|
||||||
|
Assert.Equal("/bar/images/image.jpg?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", srcAttribute.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ViewContext MakeViewContext(string requestPathBase = null)
|
||||||
|
{
|
||||||
|
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
|
||||||
|
if (requestPathBase != null)
|
||||||
|
{
|
||||||
|
actionContext.HttpContext.Request.PathBase = new Http.PathString(requestPathBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadataProvider = new EmptyModelMetadataProvider();
|
||||||
|
var viewData = new ViewDataDictionary(metadataProvider);
|
||||||
|
var viewContext = new ViewContext(
|
||||||
|
actionContext,
|
||||||
|
Mock.Of<IView>(),
|
||||||
|
viewData,
|
||||||
|
Mock.Of<ITempDataDictionary>(),
|
||||||
|
TextWriter.Null);
|
||||||
|
|
||||||
|
return viewContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TagHelperContext MakeTagHelperContext(
|
||||||
|
TagHelperAttributeList attributes)
|
||||||
|
{
|
||||||
|
return new TagHelperContext(
|
||||||
|
attributes,
|
||||||
|
items: new Dictionary<object, object>(),
|
||||||
|
uniqueId: Guid.NewGuid().ToString("N"),
|
||||||
|
getChildContentAsync: () =>
|
||||||
|
{
|
||||||
|
var tagHelperContent = new DefaultTagHelperContent();
|
||||||
|
tagHelperContent.SetContent(default(string));
|
||||||
|
return Task.FromResult<TagHelperContent>(tagHelperContent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TagHelperOutput MakeImageTagHelperOutput(TagHelperAttributeList attributes)
|
||||||
|
{
|
||||||
|
attributes = attributes ?? new TagHelperAttributeList();
|
||||||
|
|
||||||
|
return new TagHelperOutput("img", attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IHostingEnvironment MakeHostingEnvironment()
|
||||||
|
{
|
||||||
|
var emptyDirectoryContents = new Mock<IDirectoryContents>();
|
||||||
|
emptyDirectoryContents.Setup(dc => dc.GetEnumerator())
|
||||||
|
.Returns(Enumerable.Empty<IFileInfo>().GetEnumerator());
|
||||||
|
var mockFile = new Mock<IFileInfo>();
|
||||||
|
mockFile.SetupGet(f => f.Exists).Returns(true);
|
||||||
|
mockFile
|
||||||
|
.Setup(m => m.CreateReadStream())
|
||||||
|
.Returns(() => new MemoryStream(Encoding.UTF8.GetBytes("Hello World!")));
|
||||||
|
var mockFileProvider = new Mock<IFileProvider>();
|
||||||
|
mockFileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny<string>()))
|
||||||
|
.Returns(emptyDirectoryContents.Object);
|
||||||
|
mockFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny<string>()))
|
||||||
|
.Returns(mockFile.Object);
|
||||||
|
var hostingEnvironment = new Mock<IHostingEnvironment>();
|
||||||
|
hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(mockFileProvider.Object);
|
||||||
|
|
||||||
|
return hostingEnvironment.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IMemoryCache MakeCache()
|
||||||
|
{
|
||||||
|
object result = null;
|
||||||
|
var cache = new Mock<IMemoryCache>();
|
||||||
|
cache.CallBase = true;
|
||||||
|
cache.Setup(c => c.TryGetValue(It.IsAny<string>(), It.IsAny<IEntryLink>(), out result))
|
||||||
|
.Returns(result != null);
|
||||||
|
|
||||||
|
var cacheSetContext = new Mock<ICacheSetContext>();
|
||||||
|
cacheSetContext.Setup(c => c.AddExpirationTrigger(It.IsAny<IExpirationTrigger>()));
|
||||||
|
cache
|
||||||
|
.Setup(
|
||||||
|
c => c.Set(
|
||||||
|
/*key*/ It.IsAny<string>(),
|
||||||
|
/*link*/ It.IsAny<IEntryLink>(),
|
||||||
|
/*state*/ It.IsAny<object>(),
|
||||||
|
/*create*/ It.IsAny<Func<ICacheSetContext, object>>()))
|
||||||
|
.Returns((
|
||||||
|
string input,
|
||||||
|
IEntryLink entryLink,
|
||||||
|
object state,
|
||||||
|
Func<ICacheSetContext, object> create) =>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
cacheSetContext.Setup(c => c.State).Returns(state);
|
||||||
|
return create(cacheSetContext.Object);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return cache.Object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -162,6 +162,11 @@ namespace MvcTagHelpersWebSite.Controllers
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IActionResult Image()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
public IActionResult Script()
|
public IActionResult Script()
|
||||||
{
|
{
|
||||||
return View();
|
return View();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Image</title>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h2>Image Tag Helper Test</h2>
|
||||||
|
<!-- Plain image tag -->
|
||||||
|
<img src="~/images/red.png" alt="Red block" title="<the title>">
|
||||||
|
|
||||||
|
<!-- Plain image tag with file version -->
|
||||||
|
<img src="~/images/red.png" alt="Red versioned" title="Red versioned" asp-file-version="true" />
|
||||||
|
|
||||||
|
<!-- Plain image tag with file version set to false -->
|
||||||
|
<img src="~/images/red.png" alt="Red explicitly not versioned" title="Red versioned" asp-file-version="false" />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 360 B |
Loading…
Reference in New Issue