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:
David Paquette 2015-05-07 13:25:38 -05:00
parent a591e53dc9
commit ab4d2eec31
7 changed files with 422 additions and 0 deletions

View File

@ -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 &lt;img&gt; 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);
}
}
}
}

View File

@ -47,6 +47,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
[InlineData("Link", null)]
// Testing the ScriptTagHelper
[InlineData("Script", null)]
// Testing the ImageTagHelper
[InlineData("Image", null)]
// Testing InputTagHelper with File
[InlineData("Input", null)]
public async Task MvcTagHelpers_GeneratesExpectedResults(string action, string antiForgeryPath)

View File

@ -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="&lt;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>

View File

@ -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;
}
}
}

View File

@ -162,6 +162,11 @@ namespace MvcTagHelpersWebSite.Controllers
return View();
}
public IActionResult Image()
{
return View();
}
public IActionResult Script()
{
return View();

View File

@ -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="&lt;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