Trying to implement IVirtualImageProvider in ImageResizer - asp.net-mvc

I'm trying to implement an IVirtualImageProvider plugin for ImageResizer as explained here. I didn't find the instructions hard to follow, however it doesn't seem like any of my images are passing through the plugin. My images are stored in a Windows folder located outside of the ASP .NET root.
Any image path that starts with "assets" or "images" should be handled by the plugin. Here is my implementation of the IVirtualImageProvider and IVirtualFile interfaces:
namespace ImageResizer.Plugins.Basic
{
public class ResizerVirtFolder : IPlugin, IVirtualImageProvider
{
public IPlugin Install(Configuration.Config c)
{
c.Plugins.add_plugin(this);
return this;
}
public bool Uninstall(Configuration.Config c)
{
c.Plugins.remove_plugin(this);
return true;
}
public bool FileExists(string virtualPath, System.Collections.Specialized.NameValueCollection queryString)
{
return (virtualPath.StartsWith("assets", StringComparison.OrdinalIgnoreCase) || virtualPath.StartsWith("images", StringComparison.OrdinalIgnoreCase));
}
public IVirtualFile GetFile(string virtualPath, System.Collections.Specialized.NameValueCollection queryString)
{
return new ResizerVirtualFile(virtualPath);
}
}
public class ResizerVirtualFile : IVirtualFile
{
public ResizerVirtualFile(string virtualPath)
{
this._virtualPath = virtualPath;
}
protected string _virtualPath;
public string VirtualPath
{
get { return _virtualPath; }
}
public System.IO.Stream Open()
{
string sitePath = System.Configuration.ConfigurationManager.AppSettings["PageFilesLocation"];
_virtualPath = _virtualPath.Contains("assets/") ? _virtualPath.Substring(_virtualPath.IndexOf("assets/") + 7) : _virtualPath;
string assetPath = Path.Combine(sitePath, _virtualPath.TrimStart('/').Replace("/", #"\"));
System.IO.FileStream oStream = new FileStream(assetPath, FileMode.Open);
return oStream;
}
}
}
Here's a brief snippet of the Web.config modification I made for the plugin:
<resizer>
<plugins>
<add name="MvcRoutingShim" />
<add name="ResizerVirtFolder" />
</plugins>
</resizer>
ImageResizer.Plugins.Basic.ResizerVirtFolder shows up under registered plugins when I go to resizer.debug.ashx, so I believe that means the plugin is loaded. However, when I put a breakpoint on the FileExists or GetFile functions, it isn't triggered.
I thought to use the VirtualFolder plugin, but it doesn't look like it's included in the download any more. I'm using v 3.4.3.
Edit: Added link to the debug output Gist here.
Longer Edit: I should add that the images that are not showing up do not have query strings in their requests and are not being resized in any way. Does that mean that ImageResizer will not look at them at all, and as a result, the Virtual Image Provider's functions will not be executing in this case?
Another Edit: Looking at this page, it seems like the simplest way to get the images to work in ImageResizer might be to add a different prefix rather than /assets or /images, like perhaps /resize. In this case, should I add an ignore route for /resize or not? There is a route handler provided by my CMS which will eventually try to deal with this route if I do not ignore it.

Well, looks like I found my own solution. Here's the problem:
return (virtualPath.StartsWith("assets", StringComparison.OrdinalIgnoreCase)
StartsWith will always return false because the virtualPath will start with my hostname. Switching to a Contains() statement has this working perfectly.
Great plugin, by the way!

Related

How to make ASP.NET Core MVC routes generation relative?

Context - the application
I'm developing an ASP.NET Core application using netcoreapp2.2 (dotnet core 2.2). This application is distributed as a Docker image and it's working well. It's an Add-On for HASS.IO, an automated environment for Home Assistant based on docker. Everything works well.
The missing feature in my app: HASS.IO's ingress
But... I want to make use of a HASS.IO feature called Ingress: https://developers.home-assistant.io/docs/en/next/hassio_addon_presentation.html#ingress
The goal of this feature is to allow Home Assistant to route the http traffic to the add-on without having to manage the authentication part and without requiring the system owner to setup a port mapping on its firewall for the communication. So it's a very nice feature.
MVC routing paths are absolute
To use HASS.IO ingress, the application needs to provide relative paths for navigation. By example, when the user is loading the url https://my.hass.io/a0a0a0a0_myaddon/, the add-on container will receive a / http request. It means all navigation in the app must be relative.
By example, while on the root page (https://my.hass.io/a0a0a0a0_myaddon/ translated to a HTTP GET / for the container), we add the following razor code:
<a asp-action="myAction" asp-route-id="123">this is a link</a>
We'll get a resulting html like this, which is wrong in this case:
this is a link <!-- THIS IS A WRONG LINK! -->
It's wrong because it's getting translated to https://my.hass.io/Home/myAction/123 by the browser while the correct address would be https://my.hass.io/a0a0a0a0_myaddon/Home/myAction/123.
To fix this, I need the resulting html to be like that:
<!-- THIS WOULD BE THE RIGHT LINK [option A] -->
this is a link
<!-- THIS WOULD BE GOOD TOO [option B] -->
this is a link
The problem to solve
[option A]
Is there a way to setup the MVC's routing engine to output relative paths instead of absolute ones? That would solve my problem.
It also means when you're on https://my.hass.io/a0a0a0a0_myaddon/Home/myAction/123 and you want to go home, the result should be
Return home
---OR---
[option B]
Another approach would be to find a way to discover the actual absolute path and find a way to prepend it in the MVC's routing mechanism.
I found the solution to my own question. I don't know if it's the best way to do it, but it worked!
1. Create a wrapper for the existing IUrlHelper
This one converts absolute paths to relative ones...
private class RelativeUrlHelper : IUrlHelper
{
private readonly IUrlHelper _inner;
private readonly HttpContext _contextHttpContext;
public RelativeUrlHelper(IUrlHelper inner, HttpContext contextHttpContext)
{
_inner = inner;
_contextHttpContext = contextHttpContext;
}
private string MakeUrlRelative(string url)
{
if (url.Length == 0 || url[0] != '/')
{
return url; // that's an url going elsewhere: no need to be relative
}
if (url.Length > 2 && url[1] == '/')
{
return url; // That's a "//" url, means it's like an absolute one using the same scheme
}
// This is not a well-optimized algorithm, but it works!
// You're welcome to improve it.
var deepness = _contextHttpContext.Request.Path.Value.Split('/').Length - 2;
if (deepness == 0)
{
return url.Substring(1);
}
else
{
for (var i = 0; i < deepness; i++)
{
url = i == 0 ? ".." + url : "../" + url;
}
}
return url;
}
public string Action(UrlActionContext actionContext)
{
return MakeUrlRelative(_inner.Action(actionContext));
}
public string Content(string contentPath)
{
return MakeUrlRelative(_inner.Content(contentPath));
}
public bool IsLocalUrl(string url)
{
if (url?.StartsWith("../") ?? false)
{
return true;
}
return _inner.IsLocalUrl(url);
}
public string RouteUrl(UrlRouteContext routeContext) => _inner.RouteUrl(routeContext);
public string Link(string routeName, object values) => _inner.Link(routeName, values);
public ActionContext ActionContext => _inner.ActionContext;
}
2. Create a wrapper for IUrlHelperFactory
public class RelativeUrlHelperFactory : IUrlHelperFactory
{
private readonly IUrlHelperFactory _previous;
public RelativeUrlHelperFactory(IUrlHelperFactory previous)
{
_previous = previous;
}
public IUrlHelper GetUrlHelper(ActionContext context)
{
var inner = _previous.GetUrlHelper(context);
return new RelativeUrlHelper(inner, context.HttpContext);
}
}
3. Wrap the IUrlHelper in DI/IoC
Put this in the ConfigureServices() of the Startup.cs file:
services.Decorate<IUrlHelperFactory>((previous, _) => new RelativeUrlHelperFactory(previous));
IMPORTANT: You need to install the nuget package Scrutor for that https://www.nuget.org/packages/Scrutor/.
Finally...
I posted my solution as a PR there: https://github.com/yllibed/Zigbee2MqttAssistant/pull/2

CacheDependency and the VirtualPathProvider for paths wirh dependencies

We are using the BundleTransformer library in an ASP.NET MVC 4 setup. Our web application is a rather thin layer, with all server logic handled in a backend service.
After an installation all resource will be installed along side the web application on the file system, but for update reasons we need to be able to serve resources like JavaScript and CSS (LESS) from the service - they will then override the local (file system) versions.
In essence, if available from the service, we serve a requested resource from there. If not, we fall-back to the file system and serve the file from there.
It was all working like a charm, then we introduced LESS and #import statements, now things are not working so fine anymore.
We still want to cache the result of the LESS transformation in the Http Cache, and we would like to invalidate that result whenever a dependency changes. The current implementation in the VirtualPathProvider does that, but if I update a single file (a JavaScript file for instance), it will not be updated.
My VirtualPathProvider looks like this:
public class ViewsAndScriptsPathProvider : VirtualPathProvider {
private static IApplicationServiceResourcesManager ResourceManager {
get { return InstanceProvider.Get<IApplicationServiceResourcesManager>(); }
}
public override bool FileExists(string virtualPath) {
var exists = ResourceManager.Exists(virtualPath);
return exists || base.FileExists(virtualPath);
}
public override VirtualFile GetFile(string virtualPath) {
VirtualFile file;
if (ResourceManager.TryGet(virtualPath, out file)) {
return file;
}
return base.GetFile(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) {
bool isRelevant = ResourceManager.IsRelevant(virtualPath);
if (isRelevant) {
var cachekeys = virtualPathDependencies.Cast<string>().Where(dependency => virtualPath != dependency).ToArray();
return new CacheDependency(null, cachekeys);
}
if (IsBundle(virtualPath)) {
return new CacheDependency(null, new[] { ResourceManager.ComponentCacheKey }, utcStart);
}
return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
private bool IsBundle(string virtualPath) {
return virtualPath.StartsWith("~/bundles/", StringComparison.InvariantCultureIgnoreCase)
|| virtualPath.StartsWith("~/css/", StringComparison.InvariantCultureIgnoreCase);
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies) {
byte[] bhash;
string filehash;
if (ResourceManager.TryGetFileHash(virtualPath, out bhash)) {
filehash = BitConverter.ToString(bhash);
} else {
filehash = Previous.GetFileHash(virtualPath, virtualPathDependencies);
}
return filehash;
}
}
Think of the ResourceManager as a proxy / cache towards the service.
My big issue is that I don't understand exactly the CacheDependency work. If I add a cachekey (the second parameter) that includes the virtualPath itself, then I get an infinite loop in the server.
If I simply return null it won't work for LESS #imports.
If anybody could explain or point to how the VirtualPathProvider is supposed to implement the GetCacheDependency and the GetFileHash functions, I might be able to solve this one.
I actually worked out a solution some time ago, and it has to do with how the HttpCache works with the CacheDependency object. It is rather complex however.
Basically I have three scenarios:
The resource is hosted only on the file system.
The resource is hosted only at the service.
The resource is hosted at both the service and the local file system.
For 1. I use the CacheDependency object for the file location. This is standard and how the VirtualPathProvider works by default.
For 2. I use a custom (derived) ResourceCacheDependency which implement logic to invalidate itself when the proxy has a new version.
For 3. I use the AggregateCacheDependency object that has both a CacheDependency to the physical file and a ResourceCacheDependency object.
For all virtual path dependencies (the list of dependencies for the resource) I repeat the above assumptions, and build it into the AggreateCacheDependency (it will potentially have a lot of dependencies).
In my custom implementation of the VirtualPathProvider I override the GetCacheDependency method to return the relevant CacheDependency object based on the above analysis.

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

How can I dynamically alter the files in the /Content directory in ASP.NET MVC?

When I serve a javascript file to a user from the /Content Directory I want to replace a string token in that file with a value, so that when the user requests a given file, it has all the customizations they expect.
I think that means I need to somehow proxy requests to the /Content directory, perform the dynamic insertion, and give the file to the user.
I'm interested in performing this insertion as a stream or as a in -memory file. I'd prefer to use a stream just because it's probably more efficient memory wise.
How do I get ASP.NET to proxy this directory?
I've attempted
Using routes to point to a controller
WCF to proxy a URL
But they all seem "ugly" to me and I'd like to make this insertion/replacement as transparent as possible int he project.
Is there a cleaner way?
The easiest way is to create an action on a controller.
public class JavascriptController : Controller
{
public ActionResult Load(string file)
{
var content = System.IO.File.ReadAllText(Server.MapPath(string.Format("~/Content/{0}", file)));
//make replacements io content here
return this.Content(content, "application/javascript");
}
}
You can then access the javascript like this (assuming you have the default routing):
http://localhost:53287/Javascript/Load?file=file.js
where file.js is the name of the file you are requesting.
Don't worry about the url, you can customise this by creating another route if necessary
Here is alternative answer to the answer I posted above, taking into account your comment regarding dynamic javascript.
Firstly, I don't know of a way to do this specifically using either mvc or wcf.. the only way I know how to do this is with a lower-level HttpModule
Take a look at the following code:
public class JavascriptReplacementModule : IHttpModule
{
public class ResponseFilter : MemoryStream
{
private Stream outputStream = null;
public ResponseFilter(Stream output)
{
outputStream = output;
}
public override void Flush()
{
base.Flush();
this.Seek(0, SeekOrigin.Begin);
var sr = new StreamReader(this);
string contentInBuffer = sr.ReadToEnd();
//Do replacements here
outputStream.Write(UTF8Encoding.UTF8.GetBytes(contentInBuffer), 0, UTF8Encoding.UTF8.GetByteCount(contentInBuffer));
outputStream.Flush();
}
protected override void Dispose(bool disposing)
{
outputStream.Dispose();
base.Dispose(disposing);
}
}
public void Dispose() { }
public void Init(HttpApplication context)
{
context.PostRequestHandlerExecute += new EventHandler(context_PostRequestHandlerExecute);
}
void context_PostRequestHandlerExecute(object sender, EventArgs e)
{
var context = (HttpApplication)sender;
if (context.Request.Url.AbsolutePath.StartsWith("/Content") && context.Request.Url.AbsolutePath.EndsWith(".js"))
{
HttpContext.Current.Response.Filter = new ResponseFilter(HttpContext.Current.Response.Filter);
}
}
}
And register the module like this (make sure you put the full type in the type attribute):
<system.webServer>
<modules>
<add name="JavascriptReplacementModule" type="JavascriptReplacementModule"/>
</modules>
</system.webServer>
This allows you to modify the output stream before it gets to the client

Best practice for ASP.NET MVC resource files

What are the best usage of the following resource files.
Properties → Resources (Phil used this resource for localization in DataAnnotation)
App_GlobalResources folder
App_LocalResources folder
I also would like to know what is the difference between (1) and (2) in asp.net mvc application.
You should avoid App_GlobalResources and App_LocalResources.
Like Craig mentioned, there are problems with App_GlobalResources/App_LocalResources because you can't access them outside of the ASP.NET runtime. A good example of how this would be problematic is when you're unit testing your app.
K. Scott Allen blogged about this a while ago. He does a good job of explaining the problem with App_GlobalResources in ASP.NET MVC here.
If you go with the recommended solution (1) (i.e. as in K. Scott Allen's blog):
For those of you trying to use explicit localization expressions (aka declarative resource binding expressions), e.g. <%$ Resources, MyResource:SomeString %>
public class AppResourceProvider : IResourceProvider
{
private readonly string _ResourceClassName;
ResourceManager _ResourceManager = null;
public AppResourceProvider(string className)
{
_ResourceClassName = className;
}
public object GetObject(string resourceKey, System.Globalization.CultureInfo culture)
{
EnsureResourceManager();
if (culture == null)
{
culture = CultureInfo.CurrentUICulture;
}
return _ResourceManager.GetObject(resourceKey, culture);
}
public System.Resources.IResourceReader ResourceReader
{
get
{
// Not needed for global resources
throw new NotSupportedException();
}
}
private void EnsureResourceManager()
{
var assembly = typeof(Resources.ResourceInAppToGetAssembly).Assembly;
String resourceFullName = String.Format("{0}.Resources.{1}", assembly.GetName().Name, _ResourceClassName);
_ResourceManager = new global::System.Resources.ResourceManager(resourceFullName, assembly);
_ResourceManager.IgnoreCase = true;
}
}
public class AppResourceProviderFactory : ResourceProviderFactory
{
// Thank you, .NET, for providing no way to override global resource providing w/o also overriding local resource providing
private static Type ResXProviderType = typeof(ResourceProviderFactory).Assembly.GetType("System.Web.Compilation.ResXResourceProviderFactory");
ResourceProviderFactory _DefaultFactory;
public AppResourceProviderFactory()
{
_DefaultFactory = (ResourceProviderFactory)Activator.CreateInstance(ResXProviderType);
}
public override IResourceProvider CreateGlobalResourceProvider(string classKey)
{
return new AppResourceProvider(classKey);
}
public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
{
return _DefaultFactory.CreateLocalResourceProvider(virtualPath);
}
}
Then, add this to your web.config:
<globalization requestEncoding="utf-8" responseEncoding="utf-8" fileEncoding="utf-8" culture="en-US" uiCulture="en"
resourceProviderFactoryType="Vendalism.ResourceProvider.AppResourceProviderFactory" />
Properties → Resources can be seen outside of your views and strong types are generated when you compile your application.
App_* is compiled by ASP.NET, when your views are compiled. They're only available in the view. See this page for global vs. local.

Resources