Including a MVC View or string in a javascript bundle using Bundling framework - asp.net-mvc

I'm trying to bundle several javascript files together, but I also need to include a variable from app.config in the js.
My thought was to use a Controller to return a string to set the variable, so going to
~/Javascript/Index would return var foo = "bar"; This works fine.
But when I try to build a bundle, the static files are being included, but the string (or view) isn't showing up. In looking around I found that the Optimizing framework was limited to static files up until 1.1 when support for VirtualPathProviders was implemented.
I upgraded to the latest package, but I can't find any information on how to get a mix of static files and ones generated by a Controller/View to bundle. I guess I just want to use the MVC path provider?
My other thought was to try to use the bundling engine to build a string of the bundled static files and then just append my string and return it all to the browser. But, I can't find a method that allows me to use the bundling engine to return a result of the bundling process.

Integration of dynamic content into the bundling process requires the following steps:
Writing the logic that requests / builds the required content. Generating content from Controller directly requires a bit of work:
public static class ControllerActionHelper
{
public static string RenderControllerActionToString(string virtualPath)
{
HttpContext httpContext = CreateHttpContext(virtualPath);
HttpContextWrapper httpContextWrapper = new HttpContextWrapper(httpContext);
RequestContext httpResponse = new RequestContext()
{
HttpContext = httpContextWrapper,
RouteData = RouteTable.Routes.GetRouteData(httpContextWrapper)
};
// Set HttpContext.Current if RenderActionToString is called outside of a request
if (HttpContext.Current == null)
{
HttpContext.Current = httpContext;
}
IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory();
IController controller = controllerFactory.CreateController(httpResponse,
httpResponse.RouteData.GetRequiredString("controller"));
controller.Execute(httpResponse);
return httpResponse.HttpContext.Response.Output.ToString();
}
private static HttpContext CreateHttpContext(string virtualPath)
{
HttpRequest httpRequest = new HttpRequest(string.Empty, ToDummyAbsoluteUrl(virtualPath), string.Empty);
HttpResponse httpResponse = new HttpResponse(new StringWriter());
return new HttpContext(httpRequest, httpResponse);
}
private static string ToDummyAbsoluteUrl(string virtualPath)
{
return string.Format("http://dummy.net{0}", VirtualPathUtility.ToAbsolute(virtualPath));
}
}
Implement a virtual path provider that wraps the existing one and intercept all virtual paths that should deliver the dynamic content.
public class ControllerActionVirtualPathProvider : VirtualPathProvider
{
public ControllerActionVirtualPathProvider(VirtualPathProvider virtualPathProvider)
{
// Wrap an existing virtual path provider
VirtualPathProvider = virtualPathProvider;
}
protected VirtualPathProvider VirtualPathProvider { get; set; }
public override string CombineVirtualPaths(string basePath, string relativePath)
{
return VirtualPathProvider.CombineVirtualPaths(basePath, relativePath);
}
public override bool DirectoryExists(string virtualDir)
{
return VirtualPathProvider.DirectoryExists(virtualDir);
}
public override bool FileExists(string virtualPath)
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPath))
{
return true;
}
return VirtualPathProvider.FileExists(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies,
DateTime utcStart)
{
AggregateCacheDependency aggregateCacheDependency = new AggregateCacheDependency();
List<string> virtualPathDependenciesCopy = virtualPathDependencies.Cast<string>().ToList();
// Create CacheDependencies for our virtual Controller Action paths
foreach (string virtualPathDependency in virtualPathDependenciesCopy.ToList())
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPathDependency))
{
aggregateCacheDependency.Add(new ControllerActionCacheDependency(virtualPathDependency));
virtualPathDependenciesCopy.Remove(virtualPathDependency);
}
}
// Aggregate them with the base cache dependency for virtual file paths
aggregateCacheDependency.Add(VirtualPathProvider.GetCacheDependency(virtualPath, virtualPathDependenciesCopy,
utcStart));
return aggregateCacheDependency;
}
public override string GetCacheKey(string virtualPath)
{
return VirtualPathProvider.GetCacheKey(virtualPath);
}
public override VirtualDirectory GetDirectory(string virtualDir)
{
return VirtualPathProvider.GetDirectory(virtualDir);
}
public override VirtualFile GetFile(string virtualPath)
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPath))
{
return new ControllerActionVirtualFile(virtualPath,
new MemoryStream(Encoding.Default.GetBytes(ControllerActionHelper.RenderControllerActionToString(virtualPath))));
}
return VirtualPathProvider.GetFile(virtualPath);
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
return VirtualPathProvider.GetFileHash(virtualPath, virtualPathDependencies);
}
public override object InitializeLifetimeService()
{
return VirtualPathProvider.InitializeLifetimeService();
}
}
public class ControllerActionVirtualFile : VirtualFile
{
public CustomVirtualFile (string virtualPath, Stream stream)
: base(virtualPath)
{
Stream = stream;
}
public Stream Stream { get; private set; }
public override Stream Open()
{
return Stream;
}
}
You also have to implement CacheDependency if you need it:
public class ControllerActionCacheDependency : CacheDependency
{
public ControllerActionCacheDependency(string virtualPath, int actualizationTime = 10000)
{
VirtualPath = virtualPath;
LastContent = GetContentFromControllerAction();
Timer = new Timer(CheckDependencyCallback, this, actualizationTime, actualizationTime);
}
private string LastContent { get; set; }
private Timer Timer { get; set; }
private string VirtualPath { get; set; }
protected override void DependencyDispose()
{
if (Timer != null)
{
Timer.Dispose();
}
base.DependencyDispose();
}
private void CheckDependencyCallback(object sender)
{
if (Monitor.TryEnter(Timer))
{
try
{
string contentFromAction = GetContentFromControllerAction();
if (contentFromAction != LastContent)
{
LastContent = contentFromAction;
NotifyDependencyChanged(sender, EventArgs.Empty);
}
}
finally
{
Monitor.Exit(Timer);
}
}
}
private string GetContentFromControllerAction()
{
return ControllerActionHelper.RenderControllerActionToString(VirtualPath);
}
}
Register your virtual path provider:
public static void RegisterBundles(BundleCollection bundles)
{
// Set the virtual path provider
BundleTable.VirtualPathProvider = new ControllerActionVirtualPathProvider(BundleTable.VirtualPathProvider);
bundles.Add(new Bundle("~/bundle")
.Include("~/Content/static.js")
.Include("~/JavaScript/Route1")
.Include("~/JavaScript/Route2"));
}
Optional: Add Intellisense support to your views. Use <script> tags within your View and let them be removed by a custom ViewResult:
public class DynamicContentViewResult : ViewResult
{
public DynamicContentViewResult()
{
StripTags = false;
}
public string ContentType { get; set; }
public bool StripTags { get; set; }
public string TagName { get; set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (string.IsNullOrEmpty(ViewName))
{
ViewName = context.RouteData.GetRequiredString("action");
}
ViewEngineResult result = null;
if (View == null)
{
result = FindView(context);
View = result.View;
}
string viewResult;
using (StringWriter viewContentWriter = new StringWriter())
{
ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, viewContentWriter);
View.Render(viewContext, viewContentWriter);
if (result != null)
{
result.ViewEngine.ReleaseView(context, View);
}
viewResult = viewContentWriter.ToString();
// Strip Tags
if (StripTags)
{
string regex = string.Format("<{0}[^>]*>(.*?)</{0}>", TagName);
Match res = Regex.Match(viewResult, regex,
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Singleline);
if (res.Success && res.Groups.Count > 1)
{
viewResult = res.Groups[1].Value;
}
else
{
throw new InvalidProgramException(
string.Format("Dynamic content produced by View '{0}' expected to be wrapped in '{1}' tag.", ViewName, TagName));
}
}
}
context.HttpContext.Response.ContentType = ContentType;
context.HttpContext.Response.Output.Write(viewResult);
}
}
Use an extension method or add an helper function to your controller:
public static DynamicContentViewResult JavaScriptView(this Controller controller, string viewName, string masterName, object model)
{
if (model != null)
{
controller.ViewData.Model = model;
}
return new DynamicContentViewResult
{
ViewName = viewName,
MasterName = masterName,
ViewData = controller.ViewData,
TempData = controller.TempData,
ViewEngineCollection = controller.ViewEngineCollection,
ContentType = "text/javascript",
TagName = "script",
StripTags = true
};
}
The steps are similiar for other type of dynamic contents. See Bundling and Minification and Embedded Resources for example.
I added a proof of concept repository to GitHub if you want to try it out.

Related

Load ASP.NET MVC views and controllers code from database

I have a system in which the end-user is a developer who can create ASP.NET MVC views/controllers and run them on the fly.
Currently, I have two database tables, one to store the view name and code and other to store controller code in C#. I can compile the build an assembly and save a dll file on the server folder.
Step 1: I added a custom controller factory to load my controller from the database, having an area in the project named (QZone).
public class QS_DynamicControllerFactory : DefaultControllerFactory//, IController
{
QS_DBConnection _db = new QS_DBConnection();
public QS_DynamicControllerFactory() { }
public override IController CreateController(RequestContext requestContext, string controllerName)
{
return (requestContext.RouteData.DataTokens["area"] != null &&
requestContext.RouteData.DataTokens["area"].ToString().ToLower() == "qzone") ?
QGetControllerInstance(controllerName) : base.CreateController(requestContext, controllerName);
}
internal IController QGetControllerInstance(string controllerName)
{
//load controller from the database and compile it then return an instance
}
public override void ReleaseController(IController controller)
{
base.ReleaseController(controller);
}
}
Step 2: I created a VirtualPathProvider, VirtualFile
QS_VirtualPathProvider class:
public class QS_VirtualPathProvider : VirtualPathProvider
{
public QDynamicView GetVirtualData(string viewPath)
{
QS_DBConnection _db = new QS_DBConnection();
QDynamicView view = (from v in _db.QDynamicViews
where v.Name.ToLower() == "TestView.cshtml".ToLower()//viewPath.ToLower()
select v).SingleOrDefault();
return view;
}
private bool IsPathVirtual(string virtualPath)
{
var path = (VirtualPathUtility.GetDirectory(virtualPath) != "~/") ? VirtualPathUtility.RemoveTrailingSlash(VirtualPathUtility.GetDirectory(virtualPath)) : VirtualPathUtility.GetDirectory(virtualPath);
if (path.ToLower().Contains("/qzone/"))
return true;
else
return false;
}
public override bool FileExists(string virtualPath)
{
if (IsPathVirtual(virtualPath))
{
QS_VirtualFile file = (QS_VirtualFile)GetFile(virtualPath);
bool isExists = file.Exists;
return isExists;
}
else
return Previous.FileExists(virtualPath);
}
public override VirtualFile GetFile(string virtualPath)
{
if (IsPathVirtual(virtualPath))
{
QDynamicView vw = GetVirtualData(virtualPath);
var bytes = Encoding.ASCII.GetBytes(vw.ViewCode);
return new QS_VirtualFile(virtualPath, bytes);
}
else
return Previous.GetFile(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
{
if (IsPathVirtual(virtualPath))
{
return null;
}
else
return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
if (IsPathVirtual(virtualPath))
return Guid.NewGuid().ToString();
return base.GetFileHash(virtualPath, virtualPathDependencies);
}
}
QS_VirtualFile class:
public class QS_VirtualFile : VirtualFile
{
private string content;
private QS_VirtualPathProvider spp;
public bool Exists
{
get { return (content != null); }
}
public QS_VirtualFile(string virtualPath, QS_VirtualPathProvider provider) : base(virtualPath)
{
this.spp = provider;
GetData(virtualPath);
}
public QS_VirtualFile(QDynamicView vw, string virtualPath) : base(virtualPath)
{
content = vw.ViewCode;
}
private byte[] _BinaryContent;
public QS_VirtualFile(string virtualPath, byte[] contents) : base(virtualPath)
{
this._BinaryContent = contents;
}
protected void GetData(string virtualPath)
{
QDynamicView QSView = spp.GetVirtualData(virtualPath);
if (QSView != null)
{
content = QSView.ViewCode;
}
}
public override Stream Open()
{
return new MemoryStream(_BinaryContent);
}
}
Step 3: register the controller factory and the virtual path provider in the in Global.asax** file:
HostingEnvironment.RegisterVirtualPathProvider(new QS_VirtualPathProvider());
ControllerBuilder.Current.SetControllerFactory(new QS_DynamicControllerFactory());
testing the code
in order to test the code above i added a controller named (test) and a view named (testView.cshtml) in the database and requested the url below:
http://localhost:1001/qzone/test/TestView
and I got this error
I guess this mean that the controller factory worked fine but the view was not loaded
Any ideas?
That's because it's looking for your view on the hard drive. The View Engine uses VirtualPathProvidersto resolve your views, so you need to write your own VirtualPathProvider and register it.
You can find the documentation here:
https://learn.microsoft.com/en-us/dotnet/api/system.web.hosting.virtualpathprovider?view=netframework-4.8
Unfortunately, it is way too much code for me to copy here, but you can find a full example there.
Mind you, the example is for .NET 4.8, so if you're using Core, this may not be applicable.

ASP.NET MVC 5 SignalR, SqlDependency and EntityFramework 6 - the sqlparameter is already contained by another sqlparametercollection

I am beginner with SignalR and SQLDepedency. I am trying to implement SignalR using EF Code First Approach. I am getting the error The sqlparameter is already contained by another sqlparametercollection if I am using where condition in LINQ class.
public class MessageHub : Hub
{
internal NotifierEntity NotifierEntity { get; private set; }
private MyDbContext db = new MyDbContext();
public void DispatchToClient()
{
Clients.All.broadcastMessage("Refresh");
}
public void Initialize(String userName)
{
if (!string.IsNullOrEmpty(userName))
{
NotifierEntity = db.GetNotifierEntity<Messages>(db.Messages.Where(x=>x.ApplicationUser.UserName== userName && !x.Status));
if (NotifierEntity == null)
return;
Action<String> dispatcher = (t) => { DispatchToClient(); };
PushSqlDependency.Instance(NotifierEntity, dispatcher);
}
}
}
The NotifierEntity Class
public class NotifierEntity
{
ICollection<SqlParameter> sqlParameters = new List<SqlParameter>();
public String SqlQuery { get; set; }
public String SqlConnectionString { get; set; }
public ICollection<SqlParameter> SqlParameters
{
get
{
return sqlParameters;
}
set
{
sqlParameters = value;
}
}
public static NotifierEntity FromJson(String value)
{
if (String.IsNullOrEmpty(value))
throw new ArgumentNullException("NotifierEntity Value can not be null!");
return new JavaScriptSerializer().Deserialize<NotifierEntity>(value);
}
}
public static class NotifierEntityExtentions
{
public static String ToJson(this NotifierEntity entity)
{
if (entity == null)
throw new ArgumentNullException("NotifierEntity can not be null!");
return new JavaScriptSerializer().Serialize(entity);
}
}
public class PushSqlDependency
{
static PushSqlDependency instance = null;
readonly SqlDependencyRegister sqlDependencyNotifier;
readonly Action<String> dispatcher;
public static PushSqlDependency Instance(NotifierEntity notifierEntity, Action<String> dispatcher)
{
if (instance == null)
instance = new PushSqlDependency(notifierEntity, dispatcher);
return instance;
}
private PushSqlDependency(NotifierEntity notifierEntity, Action<String> dispatcher)
{
this.dispatcher = dispatcher;
sqlDependencyNotifier = new SqlDependencyRegister(notifierEntity);
sqlDependencyNotifier.SqlNotification += OnSqlNotification;
}
internal void OnSqlNotification(object sender, SqlNotificationEventArgs e)
{
dispatcher("Refresh123");
}
}
public class SqlDependencyRegister
{
public event SqlNotificationEventHandler SqlNotification;
readonly NotifierEntity notificationEntity;
internal SqlDependencyRegister(NotifierEntity notificationEntity)
{
this.notificationEntity = notificationEntity;
RegisterForNotifications();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security",
"CA2100:Review SQL queries for security vulnerabilities")]
void RegisterForNotifications()
{
using (var sqlConnection = new SqlConnection(notificationEntity.SqlConnectionString))
using (var sqlCommand = new SqlCommand(notificationEntity.SqlQuery, sqlConnection))
{
foreach (var sqlParameter in notificationEntity.SqlParameters)
{
sqlCommand.Parameters.Add(sqlParameter);
}
sqlCommand.Notification = null;
var sqlDependency = new SqlDependency(sqlCommand);
sqlDependency.OnChange += OnSqlDependencyChange;
if (sqlConnection.State == ConnectionState.Closed)
sqlConnection.Open();
sqlCommand.ExecuteNonQuery();
}
}
void OnSqlDependencyChange(object sender, SqlNotificationEventArgs e)
{
if (SqlNotification != null)
SqlNotification(sender, e);
RegisterForNotifications();
}
}
public delegate void SqlNotificationEventHandler(object sender, SqlNotificationEventArgs e);
If I am using the same query without any parameters, the code is working perfectly. I can see the database changes instantly in frontend. The issue is coming after added a parameter in Where clause.
I got this idea from below link
https://www.codeproject.com/Tips/1075852/ASP-NET-MVC-SignalR-SqlDependency-and-EntityFramew
Sourcecode link
we.tl/njwwLl8g36

ASP NET MVC VirtualPathProvider avoid some paths

I wrote a VirtualPathProvider to dynamically load my MVC views, which are saved in a database. The problem is that it is taking too long to load the views because the class ViewPathProvider is getting all paths and searching for them in the database:
"/PrecompiledApp.config",
"~/_appstart.cshtml",
"~/_appstart.vbhtml"
"/Views/Home/Index.aspx",
"/Views/Home/Index.ascx",
"/Views/Shared/Index.aspx"
(...) and many others. Is there a way to define a list of paths to reduce time and the number of queries made against the db?
My Global.asax.cs
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
HostingEnvironment.RegisterVirtualPathProvider(new ViewPathProvider());
}
My VirtualPathProvider.cs
public class ViewPathProvider : VirtualPathProvider
{
public override bool FileExists(string virtualPath)
{
var page = FindPage(virtualPath);
if (page == null)
{
return base.FileExists(virtualPath);
}
else
{
return true;
}
}
public override VirtualFile GetFile(string virtualPath)
{
var page = FindPage(virtualPath);
if (page == null)
{
return base.GetFile(virtualPath);
}
else
{
return new MyVirtualFile(virtualPath, page);
}
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
return null;
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
return Guid.NewGuid().ToString();
}
private string FindPage(string virtualPath)
{
string filetype = virtualPath.Substring(virtualPath.Length - 5);
if(filetype != ".aspx" && filetype != ".ascx" && filetype != "bhtml")
{
Uri host = System.Web.HttpContext.Current.Request.Url;
string hostName = host.Host.ToString();
Company company = new Company();
CMSComponent companiesComponent = new CMSComponent();
company = companiesComponent.GetCompanyDetailsByCmsUrl(hostName, virtualPath);
Debug.Print(virtualPath);
if (company.CompanyId!=0)
{
return company.cmsHtml;
}
else
{
return null;
}
}
else
{
return null;
}
}
}
Thanks!

Get custom attribute for parameter when model binding

I've seen a lot of similar posts on this, but haven't found the answer specific to controller parameters.
I've written a custom attribute called AliasAttribute that allows me to define aliases for parameters during model binding. So for example if I have: public JsonResult EmailCheck(string email) on the server and I want the email parameter to be bound to fields named PrimaryEmail or SomeCrazyEmail I can "map" this using the aliasattribute like this: public JsonResult EmailCheck([Alias(Suffix = "Email")]string email).
The problem: In my custom model binder I can't get a hold of the AliasAttribute class applied to the email parameter. It always returns null.
I've seen what the DefaultModelBinder class is doing to get the BindAttribute in reflector and its the same but doesn't work for me.
Question: How do I get this attribute during binding?
AliasModelBinder:
public class AliasModelBinder : DefaultModelBinder
{
public static ICustomTypeDescriptor GetTypeDescriptor(Type type)
{
return new AssociatedMetadataTypeTypeDescriptionProvider(type).GetTypeDescriptor(type);
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = base.BindModel(controllerContext, bindingContext);
var descriptor = GetTypeDescriptor(bindingContext.ModelType);
/*************************/
// this next statement returns null!
/*************************/
AliasAttribute attr = (AliasAttribute)descriptor.GetAttributes()[typeof(AliasAttribute)];
if (attr == null)
return null;
HttpRequestBase request = controllerContext.HttpContext.Request;
foreach (var key in request.Form.AllKeys)
{
if (string.IsNullOrEmpty(attr.Prefix) == false)
{
if (key.StartsWith(attr.Prefix, StringComparison.InvariantCultureIgnoreCase))
{
if (string.IsNullOrEmpty(attr.Suffix) == false)
{
if (key.EndsWith(attr.Suffix, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(key);
}
}
return request.Form.Get(key);
}
}
else if (string.IsNullOrEmpty(attr.Suffix) == false)
{
if (key.EndsWith(attr.Suffix, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(key);
}
}
if (attr.HasIncludes)
{
foreach (var include in attr.InlcludeSplit)
{
if (key.Equals(include, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(include);
}
}
}
}
return null;
}
}
AliasAttribute:
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class AliasAttribute : Attribute
{
private string _include;
private string[] _inlcludeSplit = new string[0];
public string Prefix { get; set; }
public string Suffix { get; set; }
public string Include
{
get
{
return _include;
}
set
{
_include = value;
_inlcludeSplit = SplitString(_include);
}
}
public string[] InlcludeSplit
{
get
{
return _inlcludeSplit;
}
}
public bool HasIncludes { get { return InlcludeSplit.Length > 0; } }
internal static string[] SplitString(string original)
{
if (string.IsNullOrEmpty(original))
{
return new string[0];
}
return (from piece in original.Split(new char[] { ',' })
let trimmed = piece.Trim()
where !string.IsNullOrEmpty(trimmed)
select trimmed).ToArray<string>();
}
}
Usage:
public JsonResult EmailCheck([ModelBinder(typeof(AliasModelBinder)), Alias(Suffix = "Email")]string email)
{
// email will be assigned to any field suffixed with "Email". e.g. PrimaryEmail, SecondaryEmail and so on
}
Gave up on this and then stumbled across the Action Parameter Alias code base that will probably allow me to do this. It's not as flexible as what I started out to write but probably can be modified to allow wild cards.
what I did was make my attribute subclass System.Web.Mvc.CustomModelBinderAttribute which then allows you to return a version of your custom model binder modified with the aliases.
example:
public class AliasAttribute : System.Web.Mvc.CustomModelBinderAttribute
{
public AliasAttribute()
{
}
public AliasAttribute( string alias )
{
Alias = alias;
}
public string Alias { get; set; }
public override IModelBinder GetBinder()
{
var binder = new AliasModelBinder();
if ( !string.IsNullOrEmpty( Alias ) )
binder.Alias = Alias;
return binder;
}
}
which then allows this usage:
public ActionResult Edit( [Alias( "somethingElse" )] string email )
{
// ...
}

ASP.NET bundling/minification: including dynamically generated Javascript

I have a site that dynamically generates Javascript. The generated code describes type-metadata and some server-side constants so that the clients can easily consume the server's services - so it's very cacheable.
The generated Javascript is served by an ASP.NET MVC controller; so it has a Uri; say ~/MyGeneratedJs.
I'd like to include this Javascript in a Javascript bundle with other static Javascript files (e.g. jQuery etc): so just like static files I want it to be referenced separately in debug mode and in minified form bundled with the other files in non-debug mode.
How can I include dynamically generated Javascript in a bundle?
With VirtualPathProviders this is now possible. Integration of dynamic content into the bundling process requires the following steps:
Writing the logic that requests / builds the required content. Generating content from Controller directly requires a bit of work:
public static class ControllerActionHelper
{
public static string RenderControllerActionToString(string virtualPath)
{
HttpContext httpContext = CreateHttpContext(virtualPath);
HttpContextWrapper httpContextWrapper = new HttpContextWrapper(httpContext);
RequestContext httpResponse = new RequestContext()
{
HttpContext = httpContextWrapper,
RouteData = RouteTable.Routes.GetRouteData(httpContextWrapper)
};
// Set HttpContext.Current if RenderActionToString is called outside of a request
if (HttpContext.Current == null)
{
HttpContext.Current = httpContext;
}
IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory();
IController controller = controllerFactory.CreateController(httpResponse,
httpResponse.RouteData.GetRequiredString("controller"));
controller.Execute(httpResponse);
return httpResponse.HttpContext.Response.Output.ToString();
}
private static HttpContext CreateHttpContext(string virtualPath)
{
HttpRequest httpRequest = new HttpRequest(string.Empty, ToDummyAbsoluteUrl(virtualPath), string.Empty);
HttpResponse httpResponse = new HttpResponse(new StringWriter());
return new HttpContext(httpRequest, httpResponse);
}
private static string ToDummyAbsoluteUrl(string virtualPath)
{
return string.Format("http://dummy.net{0}", VirtualPathUtility.ToAbsolute(virtualPath));
}
}
Implement a virtual path provider that wraps the existing one and intercept all virtual paths that should deliver the dynamic content.
public class ControllerActionVirtualPathProvider : VirtualPathProvider
{
public ControllerActionVirtualPathProvider(VirtualPathProvider virtualPathProvider)
{
// Wrap an existing virtual path provider
VirtualPathProvider = virtualPathProvider;
}
protected VirtualPathProvider VirtualPathProvider { get; set; }
public override string CombineVirtualPaths(string basePath, string relativePath)
{
return VirtualPathProvider.CombineVirtualPaths(basePath, relativePath);
}
public override bool DirectoryExists(string virtualDir)
{
return VirtualPathProvider.DirectoryExists(virtualDir);
}
public override bool FileExists(string virtualPath)
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPath))
{
return true;
}
return VirtualPathProvider.FileExists(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies,
DateTime utcStart)
{
AggregateCacheDependency aggregateCacheDependency = new AggregateCacheDependency();
List<string> virtualPathDependenciesCopy = virtualPathDependencies.Cast<string>().ToList();
// Create CacheDependencies for our virtual Controller Action paths
foreach (string virtualPathDependency in virtualPathDependenciesCopy.ToList())
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPathDependency))
{
aggregateCacheDependency.Add(new ControllerActionCacheDependency(virtualPathDependency));
virtualPathDependenciesCopy.Remove(virtualPathDependency);
}
}
// Aggregate them with the base cache dependency for virtual file paths
aggregateCacheDependency.Add(VirtualPathProvider.GetCacheDependency(virtualPath, virtualPathDependenciesCopy,
utcStart));
return aggregateCacheDependency;
}
public override string GetCacheKey(string virtualPath)
{
return VirtualPathProvider.GetCacheKey(virtualPath);
}
public override VirtualDirectory GetDirectory(string virtualDir)
{
return VirtualPathProvider.GetDirectory(virtualDir);
}
public override VirtualFile GetFile(string virtualPath)
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPath))
{
return new ControllerActionVirtualFile(virtualPath,
new MemoryStream(Encoding.Default.GetBytes(ControllerActionHelper.RenderControllerActionToString(virtualPath))));
}
return VirtualPathProvider.GetFile(virtualPath);
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
return VirtualPathProvider.GetFileHash(virtualPath, virtualPathDependencies);
}
public override object InitializeLifetimeService()
{
return VirtualPathProvider.InitializeLifetimeService();
}
}
public class ControllerActionVirtualFile : VirtualFile
{
public CustomVirtualFile (string virtualPath, Stream stream)
: base(virtualPath)
{
Stream = stream;
}
public Stream Stream { get; private set; }
public override Stream Open()
{
return Stream;
}
}
You also have to implement CacheDependency if you need it:
public class ControllerActionCacheDependency : CacheDependency
{
public ControllerActionCacheDependency(string virtualPath, int actualizationTime = 10000)
{
VirtualPath = virtualPath;
LastContent = GetContentFromControllerAction();
Timer = new Timer(CheckDependencyCallback, this, actualizationTime, actualizationTime);
}
private string LastContent { get; set; }
private Timer Timer { get; set; }
private string VirtualPath { get; set; }
protected override void DependencyDispose()
{
if (Timer != null)
{
Timer.Dispose();
}
base.DependencyDispose();
}
private void CheckDependencyCallback(object sender)
{
if (Monitor.TryEnter(Timer))
{
try
{
string contentFromAction = GetContentFromControllerAction();
if (contentFromAction != LastContent)
{
LastContent = contentFromAction;
NotifyDependencyChanged(sender, EventArgs.Empty);
}
}
finally
{
Monitor.Exit(Timer);
}
}
}
private string GetContentFromControllerAction()
{
return ControllerActionHelper.RenderControllerActionToString(VirtualPath);
}
}
Register your virtual path provider:
public static void RegisterBundles(BundleCollection bundles)
{
// Set the virtual path provider
BundleTable.VirtualPathProvider = new ControllerActionVirtualPathProvider(BundleTable.VirtualPathProvider);
bundles.Add(new Bundle("~/bundle")
.Include("~/Content/static.js")
.Include("~/JavaScript/Route1")
.Include("~/JavaScript/Route2"));
}
Optional: Add Intellisense support to your views. Use <script> tags within your View and let them be removed by a custom ViewResult:
public class DynamicContentViewResult : ViewResult
{
public DynamicContentViewResult()
{
StripTags = false;
}
public string ContentType { get; set; }
public bool StripTags { get; set; }
public string TagName { get; set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (string.IsNullOrEmpty(ViewName))
{
ViewName = context.RouteData.GetRequiredString("action");
}
ViewEngineResult result = null;
if (View == null)
{
result = FindView(context);
View = result.View;
}
string viewResult;
using (StringWriter viewContentWriter = new StringWriter())
{
ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, viewContentWriter);
View.Render(viewContext, viewContentWriter);
if (result != null)
{
result.ViewEngine.ReleaseView(context, View);
}
viewResult = viewContentWriter.ToString();
// Strip Tags
if (StripTags)
{
string regex = string.Format("<{0}[^>]*>(.*?)</{0}>", TagName);
Match res = Regex.Match(viewResult, regex,
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Singleline);
if (res.Success && res.Groups.Count > 1)
{
viewResult = res.Groups[1].Value;
}
else
{
throw new InvalidProgramException(
string.Format("Dynamic content produced by View '{0}' expected to be wrapped in '{1}' tag.", ViewName, TagName));
}
}
}
context.HttpContext.Response.ContentType = ContentType;
context.HttpContext.Response.Output.Write(viewResult);
}
}
Use an extension method or add an helper function to your controller:
public static DynamicContentViewResult JavaScriptView(this Controller controller, string viewName, string masterName, object model)
{
if (model != null)
{
controller.ViewData.Model = model;
}
return new DynamicContentViewResult
{
ViewName = viewName,
MasterName = masterName,
ViewData = controller.ViewData,
TempData = controller.TempData,
ViewEngineCollection = controller.ViewEngineCollection,
ContentType = "text/javascript",
TagName = "script",
StripTags = true
};
}
The steps are similiar for other type of dynamic contents. See Bundling and Minification and Embedded Resources for example.
I added a proof of concept repository to GitHub if you want to try it out.
Darin is right, currently bundling only works on static files. But if you can add a placeholder file with up to date content, bundling does setup file change notifications which will detect automatically when the placeholder file changes.
Also we are going to be moving to using VirtualPathProviders soon which might be a way to serve dynamically generated content.
Update: The 1.1-alpha1 release is out now which has support for VPP
This is not possible. Bundles work only with static files.

Resources