I want to be able to reorganize my ASP.NET MVC site structure to more closely match the way Rails does it (We do both rails and ASP.net at my company).
In Rails, there is a "public" folder that behaves as the root of the site. For example, I could drop in a "test.html" and access the file with the url http://domain.com/test.html which would serve up the static html file.
In asp.net MVC there is a "Content" folder that I want to behave as the root. So instead of accessing http://domain.com/content/myfile.html, i want to be able to do http://domain.com/myfile.html.
I know I can just drop the file in the root of the project, but i need to do this with many files including css, js, html, images, etc and want to share some standardized assets across rails and aspnetmvc.
Is there a way to do this?
There is another possible solution. Instead of using code, you can use a rewrite rule to handle this for you. If you are using IIS 7 or above, you can use Microsoft's URL Rewrite Module.
A rule like the following would probably do it:
<rule name="Rewrite static files to content" stopProcessing="true">
<match url="^([^/]+(?:\.css|\.js))$" />
<conditions>
<add input="{APPL_PHYSICAL_PATH}content{SCRIPT_NAME}" matchType="IsFile" />
</conditions>
<action type="Rewrite" url="/content/{R:1}" />
</rule>
The rule checks for a request to a css or js file off the root of the site. Then it checks to see if the file exists in the content folder. If it exists, then the rewrite will return the file in the content folder. I've only tested this a little bit, but it seems to work. It certainly needs more testing, and possible refinement.
The only solution I can think of, is to use a custom controller and route to do this for you. But it isn't a clean solution.
First you need a PublicController class with a GetFile action method. This assumes that all files are in the public/content folder directly. Handling folders makes things more complicated.
public class PublicController : Controller
{
private IDictionary<String, String> mimeTypes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{{"css", "text/css"}, {"jpg", "image/jpg"}};
public ActionResult GetFile(string file)
{
var path = Path.Combine(Server.MapPath("~/Content"), file);
if (!System.IO.File.Exists(path)) throw new HttpException(404, "File Not Found");
var extension = GetExtension(file); // psuedocode
var mimetype = mimeTypes.ContainsKey(extension) ? mimeTypes[extension] : "text/plain";
return File(path, mimetype);
}
}
Now, you just need a route near the bottom of your list of routes that looks like this:
routes.MapRoute("PublicContent", "{file}", new {controller = "Public", action = "GetFile"});
The problem is, now when you just put in a controller name like 'Home' instead of defaulting to the Index action method on the HomeController, it assumes you want to download a file called "Home" from the content directory. So, above the file route, you would need to add a route for each controller so it knows to get the Index action instead.
routes.MapRoute("HomeIndex", "Home", new { controller = "Home", action = "Index" });
So, one way around that is to change the route to this:
routes.MapRoute("PublicContent", "{file}.{extension}", new {controller = "Public", action = "GetFile"});
And the action method to this:
public ActionResult GetFile(string file, string extension)
{
var path = Path.Combine(Server.MapPath("~/Content"), file + "." + extension);
if (!System.IO.File.Exists(path)) throw new HttpException(404, "File Not Found");
var mimetype = mimeTypes.ContainsKey(extension) ? mimeTypes[extension] : "text/plain";
return File(path, mimetype);
}
Like I said, this assumes that all files are in the content directory, and not in subfolders. But if you wanted to do subfolders like Content/css/site.css you could add your routes like this:
routes.MapRoute("PublicContent_sub", "{subfolder}/{file}.{extension}", new { controller = "Public", action = "GetFileInFolder" });
routes.MapRoute("PublicContent", "{file}.{extension}", new { controller = "Public", action = "GetFile"});
Now the action method has to change too.
public ActionResult GetFile(string file, string extension)
{
return GetFileInFolder("", file, extension);
}
public ActionResult GetFileInFolder(string subfolder, string file, string extension)
{
var path = Path.Combine(Server.MapPath("~/Content"), subfolder, file + "." + extension);
if (!System.IO.File.Exists(path)) throw new HttpException(404, "File Not Found");
var mimetype = mimeTypes.ContainsKey(extension) ? mimeTypes[extension] : "text/plain";
return File(path, mimetype);
}
If you start getting multiple levels deep in the folder structure, this gets uglier and uglier. But maybe this will work for you. I'm sure you were hoping for a checkbox in the project properties, but if there is one, I don't know about it.
Related
One of my old project we do have a static content file folder named XYZ which we are keeping in the same location fo the root.
Currently we are directly passing the url like 'siteaddress/XYZ/test.pdf' or siteaddress/XYZ/2020/Test1.pdf to get the pdf files.
Now we have a requirement to store some of the confidential files also in the path. So we are planning to restrict the direct access to the path and serve via MVC pipeline
we have added a handlers to enable the requests from the folder, to go through mvc pipeline
<add
name="ManagedPdfExtension"
path="XYZ/*/*.pdf"
verb="GET"
type="System.Web.Handlers.TransferRequestHandler"
preCondition="integratedMode,runtimeVersionv4.0"
/>
<add
name="ManagedPdfInnerFolderExtension"
path="CommonFiles/*/*.pdf"
verb="GET"
type="System.Web.Handlers.TransferRequestHandler"
preCondition="integratedMode,runtimeVersionv4.0"
/>
Also created a method to return the file in controller
[HttpGet]
[Route("XYZ/{attachmentName}")]
public ActionResult CommonFiles(string attachmentName)
{
var path = System.Web.Hosting.HostingEnvironment.MapPath("~/XYZ/"+ attachmentName);
byte[] fileBytes = System.IO.File.ReadAllBytes(path);
string fileName = "Test.pdf";
var cd = new ContentDisposition
{
Inline = true,
FileName = fileName
};
Response.Clear();
Response.AddHeader(CoreConstants.ContentDisposition, cd.ToString());
Response.AddHeader(CoreConstants.WindowTarget, CoreConstants.WindowTargetBlank);
Response.BufferOutput = false;
return File(fileBytes, "application/pdf");
}
This code works ok with files which are directly under folder XYZ
That means if I try a url like
siteaddress/XYZ/test.pdf which is working.
But for the pdf that are inside another folder, I am not able to get with the existing approach.
Since we have only single param attachmentName defined in the method i couldn't get the files under subfolders.
Is there any way to do the same in MVC ??
Because of some reasons, I cannot move all these items to database , change the folder structure . Also i cannot create a mapping table like
url : key and use the key instead.
Th urls are coming from a common table which is used in many applications. So changing that is bit difficult.
If the folder and subfolders are limited then may be with multiple route i could handle this. But here the subfolder number can be a variable too.
In fact from the following urls
siteadress/XYZ/abc/bn/test.pdf
siteadress/XYZ/abc/cf/bn/test.pdf
siteadress/XYZ/abc/bn/test.pdf
is there any way to make it hit a single controller method with a string params like
abc/bn/test.pdf
abc/cf/bn/test.pdf
abc/bn/test.pdf
??
Added a route with * in the property part.
[HttpGet]
[Route("CommonFiles/{*any}")]
public ActionResult CommonFiles(string attachmentName)
{
var filePathWithName = this.RouteData.Values["any"];
var path = System.Web.Hosting.HostingEnvironment.MapPath("~/CommonFiles/"+ filePathWithName);
path = path.Replace("//", "/").Replace("/","//");
byte[] fileBytes = System.IO.File.ReadAllBytes(path);
string fileName = "Test.pdf";
var cd = new ContentDisposition
{
Inline = true,
FileName = fileName
};
Response.Clear();
Response.AddHeader(CoreConstants.ContentDisposition, cd.ToString());
Response.AddHeader(CoreConstants.WindowTarget, CoreConstants.WindowTargetBlank);
Response.BufferOutput = false;
return File(fileBytes, "application/pdf");
}
I'm trying to return an image stored inside wwwwroot/images folder, this is the structure:
Inside the View I have the following tag:
<img src="#Url.Action("GetAvatar", "User", new { username = User.Identity.Name })" />
as you can see for display the image it simply call GetAvatar method from User controller passing the username as parameter.
The method have the following configuration:
[HttpGet]
public FileResult GetAvatar(string username)
{
User user = _repo.GetUser(username);
if(user.UserDetail != null)
return File(user.UserDetail?.UserPhoto, "image/png");
//The user has no custom image, will displayed the default.
string root = Path.Combine(_env.WebRootPath, "images");
return File(Path.Combine(root, "avatar_default.png"), "image/png");
}
the firtst part of the method that retrieve the image from the database works, but the last part which try to get the image from the wwwroot folder doesn't work. Infact when I load the View I get the broked thumbnail which mean not found.
I'm also injected the IHostingEnvironment for access to wwwroot folder.
Any idea?
The File method you're using has the following signature:
public VirtualFileResult File (string virtualPath, string contentType);
As its name suggests, the first parameter here represents the virtual path of the file you want to serve; not the physical path. By default, this means you need to provide a path that is essentially just relative to the wwwroot folder. In your example, the path would be images/avatar_default.png. With this, there's no need for Path.Combine or IHostingEnvironment in your example. Here's the updated version:
[HttpGet]
public FileResult GetAvatar(string username)
{
User user = _repo.GetUser(username);
if(user.UserDetail != null)
return File(user.UserDetail?.UserPhoto, "image/png");
return File("images/avatar_default.png", "image/png");
}
I am working on a site that need to support host based Multi Tenancy, and I got this whole part figured out. The issue I have is that I have in the CSS folder a subfolder for every tenant (1,2,3).
CSS
|_ tenant_1
|_ tenant_2
|_ tenant_3
|_ tenant (virtual)
in the tenant_X folder there are custom css files used for stypling every specific tenant.
My idea was to somehow create a virtual location (tenant) that would be mapped to the tenant's folder and only one additional line of coude would be needed in the _Layout. I am not profound in MVC and so far I know, I think I can get this to work with a custom route.
One other reason for this approach is that the tenants user is not allowed to see that there are other tenants. I have to exclude the possibility to have the user loaded the wrong files.
Is this the right approach? can you suggest any better way?
A possible implementation to achieve this just by adding 1 line to the _Layout page, could be to get a css file from a controller as text/css.
So assuming that the current tenant ID is available on front-end you could call a method on controller with that id
For example something like this:
#Styles.Render(string.Format("/CustomizationController/GetCssForTenant?tenantId={0}", loggedTeanant == null ? (int?) null : loggedTenant.Id))
And now create a customization controller with the method as follows
public class CustomizationController : Controller
{
//this will cache cliente side the css file but if the duration expires
// or the tenantId changes it will be ask for the new file
[OutputCache(Duration = 43200, VaryByParam = "tenantId")]
public FileResult GetCssForTenant(int? tenantId)
{
var contentType = "text/css";
//if teanant id is null return empty css file
if(!tenantID.HasValue)
return new FileContentResult(new byte[0], contentType);
//load the real css file here <-
var result = ...
//---
//if having problems with the encoding use this ...
//System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
//var content = encoding.GetBytes(result);
//---
Response.ContentType = contentType;
return new FileContentResult(result, contentType);
//return new FileContentResult(content, contentType);
}
}
Hope that this help achieve what you need. Remember that this is a sketch of an possible implementation.
Edit
If you want to make a quick try of my suggested implementation use this
public class CustomizationController : Controller
{
//this will cache cliente side the css file but if the duration expires
// or the tenantId changes it will be ask for the new file
[OutputCache(Duration = 43200, VaryByParam = "tenantId")]
public FileResult GetCssForTenant(int? tenantId)
{
var contentType = "text/css";
//if teanant id is null return empty css file
if(!tenantID.HasValue)
return new FileContentResult(new byte[0], contentType);
//load the real css file here <-
var result = Environment.NewLine;
if(tenantID = 1)
result "body{ background-color: black !important;}"
else
result "body{ background-color: pink !important;}"
result += Environment.NewLine;
System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
var content = encoding.GetBytes(result);
Response.ContentType = contentType;
return new FileContentResult(result, contentType);
}
}
And change the _Layout
#Styles.Render(string.Format("/CustomizationController/GetCssForTenant?tenantId={0}", 1))
Now the background-color of the page should change to black if you send 1 and to pink if you send 2.
You also can see in the network that if you ask 2 time with the same id the status will be 304 this means that the file comes from cache.
If you change the id the status will be 200 that is a not cached response.
If you pass null the css file will come empty so it will fallback to your default css.
I want to write a little helper function that returns the site url.
Coming from PHP and Codeigniter, I'm very upset that I can't get it to work the way I want.
Here's what I'm trying:
#{
var urlHelper = new UrlHelper(Html.ViewContext.RequestContext);
var baseurl = urlHelper.Content("~");
}
<script>
function base_url(url) {
url = url || "";
return '#baseurl' + url;
}
</script>
I want to return the base url of my application, so I can make ajax calls without worrying about paths. Here's how I intend to use it:
// Development
base_url(); // http://localhost:50024
// Production
base_url("Custom/Path"); // http://site.com/Custom/Path
How can I do something like that?
EDIT
I want absolute paths because I have abstracted js objects that makes my ajax calls.
So suppose I have:
function MyController() {
// ... js code
return $resource('../MyController/:id');
}
// then
var my_ctrl = MyController();
my_ctrl.id = 1;
my_ctrl.get(); // GET: ../MyController/1
This works when my route is http://localhost:8080/MyController/Edit but will fail when is http://localhost:8080/MyController .
I managed to do it like this:
#{
var url = Request.Url;
var baseurl = url.GetLeftPart(UriPartial.Authority);
}
Thank you all!
Are you aware of #Url.Action("actionname") and #Url.RouteUrl("routename") ?
Both of these should do what you're describing.
Instead of manually creating your URL's, you can use #Url.Action() to construct your URLs.
<p>#Url.Action("Index", "Home")</p>
/Home/Index
<p>#Url.Action("Edit", "Person", new { id = 1 })</p>
/Person/Edit/1
<p>#Url.Action("Search", "Book", new { title = "Gone With The Wind" })</p>
/Book/Search?title="Gone+With+The+Wind"
Now the absolute best reason to go with this option is that #Url.Action automatically applies any vanity URL routes you have defined in your Global.asax file. DRY as the sub-saharan desert! :)
In your case, your can create a 'custom path' in two ways.
Option A)
<p>#Url.Action("Path", "Custom")</p>
/Custom/Path
Option B)
You can create a route using the Global.asax file. So your controller/action combo can be anything you want, and you can create a custom vanity route url - regardless of the controller/action combo.
In my Asp.Net Mvc project I'd like to have a good looking urls, e.g. mysite.com/Page2, and I want to redirect from my old style urls (such as mysite.com?page=2) with 301 state so that there won't be two urls with identical content. Is there a way to do it?
As far as I know Asp.Net binding framework doesn't make difference between query string and curly brace params
I am not sure, I got your question right. It seems, your current setup relies on those GET parameters (like mysite.com?page=2). If you dont want to change this, you will have to use those parameters further. There would be no problem in doing so, though. Your users do not have to use or see them. In order to publish 'new style URLs' only, you may setup a URL redirect in your web server. That would change new style URLs to old style URLs.
The problem is the 301. If the user requests an old style URL, it would be accepted by the webserver as well. Refusing the request with a 301 error seems hard to achieve for me.
In order to get around this, I guess you will have to change your parameter scheme. You site may still rely on GET parameters - but they get a new name. Lets say, your comments are delivered propery for the following (internal) URL in the old scheme:
/Article/1022/Ms-Sharepoint-Setup-Manual?newpage=2
Note the new parameter name. In your root page (or master page, if you are using those), you may handle the redirect permanent (301) manually. Therefore, incoming 'old style requests' are distinguishable by using old parameter names. This could be used to manually assemble the 301 in the response in ASP code.
Personally, I would sugesst, to give up the 301 idea and just use URL redirection.
Well, as far as I can see performing such redirection in ASP.NET MVC might be tricky. This is how I did it:
global.asax:
routes.Add(new QueryStringRoute());
routes.MapRoute(null, "Article/{id}/{name}",
new { controller = "Article", action = "View", page = 1 },
new { page = #"\d+" }
);
routes.MapRoute(null, "Article/{id}/{name}/Page{page}",
new { controller = "Article", action = "View" },
new { page = #"\d+" }
);
QueryStringRoute.cs:
public class QueryStringRoute : RouteBase
{
private static string[] queryStringUrls = new string[]
{
#"~/Article/\d{1,6}/.*?page=\d{1,3}"
};
public override RouteData GetRouteData(HttpContextBase httpContext)
{
string url = httpContext.Request.AppRelativeCurrentExecutionFilePath;
foreach (string queryStringUrl in queryStringUrls)
{
Regex regex = new Regex(queryStringUrl);
if (regex.IsMatch(url))
{
long id = 0; /* Parse the value from regex match */
int page = 0; /* Parse the value from regex match */
string name = ""; /* Parse the value from regex match */
RouteData rd = new RouteData(this, new MvcRouteHandler());
rd.Values.Add("controller", "QueryStringUrl");
rd.Values.Add("action", "Redirect");
rd.Values.Add("id", id);
rd.Values.Add("page", page);
rd.Values.Add("name", name);
rd.Values.Add("controllerToRedirect", "Article");
rd.Values.Add("actionToRedirect", "View");
return rd;
}
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
QueryStringUrlController.cs:
public class QueryStringUrlController : Controller
{
public RedirectToRouteResult Redirect(long id, int page, string name,
string controllerToRedirect, string actionToRedirect)
{
return RedirectToActionPermanent(actionToRedirect, controllerToRedirect, new { id = id, page = page, name = name });
}
}
Assuming you have such routing as in my global.asax file (listed above) you can create a custom Route class that will handle incoming requests and map them on a special redirection controller which will then redirect them to appropriate urls with 301 state. Then you must add this route to global.asax before your "Article" routes
If you're using IIS 7, the URL Rewrite Module should work for your scenario.