I am a newbie in asp mvc, and would like to define links in views to image/content folder in a way so I don't have to change each link if a image folder changes.
Is is possible using ActionLink and routing, bundling or there is a better way to achieve this.
I could not find a good example anywhere so I did not try anything any coding far.
I am thinking of storing a fixed path somewhere, but is that really a mvc type solution?
There are a number of ways you could do this. Here's one approach to extend the Url.Content() method.
1. Create an extension method
We'll called it Virtual().
namespace TestApp.Extensions
{
public static class UrlHelperExtensions
{
private const string _settingPattern = "path:{0}";
private const string _regexPattern = #"\{\w+\}";
public static string Virtual(this UrlHelper helper, string url)
{
Regex r = new Regex(_regexPattern);
var matches = r.Matches(url);
if (matches.Count == 0) return url;
var sb = new StringBuilder(url);
var keys = WebConfigurationManager.AppSettings.AllKeys;
foreach (var match in matches)
{
string key = match.ToString().TrimStart('{').TrimEnd('}');
string pattern = string.Format(_settingPattern, key);
foreach (var k in keys)
{
if (k == pattern)
{
sb.Replace(match.ToString(), WebConfigurationManager.AppSettings.Get(k));
}
}
}
return helper.Content(sb.ToString());
}
}
}
2. Add settings to the main Web.config
Freely add any paths you want.
<add key="path:images" value="~/Content/images" />
<add key="path:scripts" value="~/scripts" />
3. Add the namespace to the Web.config of your views directory
<namespaces>
<add namespace="TestApp.Extensions"/>
</namespaces>
4. Use the new method
#Url.Virtual("{images}/mypic.png")
Output:
/Content/images/mypic.png
You can now use Virtual() where you would Content().
This solution is arguably excessive, but it is comprehensive.
Related
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
I have just started working with Web Api OData and I find myself wanting to do something that I was able to do a standard ApiController, that is get an object from a field other than the Id field. I know it is standard to do this for example when getting an object based on it's Id :
[Queryable]
public SingleResult<Group> GetGroup([FromODataUri] int key)
{
return SingleResult.Create(db.Groups.Where(group => group.GroupId == key));
}
But if I want to get group by groupName field for example, what would I do? I have tried this as something similar worked with an ApiController :
Controller :
public Group GetGroup([FromODataUri] string groupName)
{
var group = _repository.GetGroupByGroupName(groupName);
if (group == null)
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
}
return group;
}
Repository :
public Group GetGroupByGroupName(string groupName)
{
Group group = (from u in _db.Groups
where u.GroupName == groupName
select u).FirstOrDefault();
return group;
}
My WebApiConfig looks like this :
public static void Register(HttpConfiguration config)
{
config.EnableCors();
var json = config.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling =
Newtonsoft.Json.PreserveReferencesHandling.Objects;
config.Formatters.Remove(config.Formatters.XmlFormatter);
// OData
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<City>("Cities");
modelBuilder.EntitySet<Stage>("Stages");
modelBuilder.EntitySet<Team>("Teams");
modelBuilder.EntitySet<Fixture>("Fixtures");
modelBuilder.EntitySet<Roster>("Rosters");
modelBuilder.EntitySet<Standing>("Standings");
modelBuilder.EntitySet<Group>("Groups");
var model = modelBuilder.GetEdmModel();
config.Routes.MapODataRoute("ODataRoute", "odata", model);
}
I want to be able to get a group object based on a groupname using odata/Groups/GroupName, but what I currently have does not work. How would I do this, or am I approaching this from totally the wrong direction?
You are already using QueryableAttribute so if you define something like:
[Queryable]
public IQueryable<Group> Get()
{
// Returns all groups as a queryable (do not execute query here!)
}
You can then access this with an ajax request and any odata query, like:
http://server/Api/Group?$filter=GroupName eq 'test'
The query is built on the client, not restricted by server parameters.
Let's say you STILL need to call a custom function. OData V4 allows that. The question is a bit old but I believe it is still valid today.
First, you need to specify a Namespace to the ODataConventionModelBuilder since OData V4 needs this for actions and functions:
var builder = new ODataConventionModelBuilder();
builder.Namespace = "Default";
Be aware that this might result in IIS giving 404 because the dot is interpreted by IIS. To prevent this, add the following to your Web.config:
<system.webServer>
<handlers>
<clear/>
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="/*"
verb="*" type="System.Web.Handlers.TransferRequestHandler"
preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
</system.webServer>
then, you need to declare the function too:
builder.EntitySet<Group>("Groups").EntityType
.Collection
.Function("GetGroupByName")
.ReturnsFromEntitySet<Group>("Groups")
.Parameter<string>("name");
Notice the function is declared on the COLLECTION, so it's bound to it. Then the ODataController method:
[ODataRoute("Groups/Default.GetGroupByName(name={name})")]
[HttpGet]
public IHttpActionResult GetGroupByName([FromODataUri]string name)
{
return Ok(new Group() or whatever);
}
Here the ODataRoute annotation is necessary only if you use a parameter. It defines in order: the entityset, the namespace, the method and the parameters.
You can then use:
http://localhost:xxxx/api/Groups/Default.GetByGroupName(name='sausage')
Also, see this SO answer to pass multiple parameters
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
What is the correct way to find the absolute path to the App_Data folder from a Controller in an ASP.NET MVC project? I'd like to be able to temporarily work with an .xml file and I don't want to hardcode the path.
This does not work:
[HandleError]
public class HomeController : Controller
{
public ActionResult Index()
{
string path = VirtualPathUtility.ToAbsolute("~/App_Data/somedata.xml");
//.... do whatever
return View();
}
}
I think outside of the web context VirtualPathUtility.ToAbsolute() doesn't work.
string path comes back as "C:\App_Data\somedata.xml"
Where should I determine the path of the .xml file in an MVC app?
global.asax and stick it an application-level variable?
ASP.NET MVC1 -> MVC3
string path = HttpContext.Current.Server.MapPath("~/App_Data/somedata.xml");
ASP.NET MVC4
string path = Server.MapPath("~/App_Data/somedata.xml");
MSDN Reference:
HttpServerUtility.MapPath Method
string path = AppDomain.CurrentDomain.GetData("DataDirectory").ToString();
This is probably a more "correct" way of getting it.
I try to get in the habit of using HostingEnvironment instead of Server as it works within the context of WCF services too.
HostingEnvironment.MapPath(#"~/App_Data/PriceModels.xml");
The most correct way is to use HttpContext.Current.Server.MapPath("~/App_Data");. This means you can only retrieve the path from a method where the HttpContext is available. It makes sense: the App_Data directory is a web project folder structure [1].
If you need the path to ~/App_Data from a class where you don't have access to the HttpContext you can always inject a provider interface using your IoC container:
public interface IAppDataPathProvider
{
string GetAppDataPath();
}
Implement it using your HttpApplication:
public class AppDataPathProvider : IAppDataPathProvider
{
public string GetAppDataPath()
{
return MyHttpApplication.GetAppDataPath();
}
}
Where MyHttpApplication.GetAppDataPath looks like:
public class MyHttpApplication : HttpApplication
{
// of course you can fetch&store the value at Application_Start
public static string GetAppDataPath()
{
return HttpContext.Current.Server.MapPath("~/App_Data");
}
}
[1] http://msdn.microsoft.com/en-us/library/ex526337%28v=vs.100%29.aspx
Phil Haak has an example that I think is a bit more stable when dealing with paths with crazy "\" style directory separators. It also safely handles path concatenation. It comes for free in System.IO
var fileName = Path.GetFileName(file.FileName);
var path = Path.Combine(Server.MapPath("~/App_Data/uploads"), fileName);
However, you could also try "AppDomain.CurrentDomain.BaseDirector" instead of "Server.MapPath".
string filePath = HttpContext.Current.Server.MapPath("~/folderName/filename.extension");
OR
string filePath = HttpContext.Server.MapPath("~/folderName/filename.extension");
This way i got the hosting path.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
namespace IHostingEnvironmentExample.Controllers
{
public class HomeController : Controller
{
private IHostingEnvironment _env;
public HomeController(IHostingEnvironment env)
{
_env = env;
}
public IActionResult Index()
{
var webRoot = _env.WebRootPath;
var file = System.IO.Path.Combine(webRoot, "test.txt");
System.IO.File.WriteAllText(file, "Hello World!");
return View();
}
}
}
https://forums.asp.net/t/1696005.aspx?How+to+get+Local+Server+path+in+mvc
string Index = i;
string FileName = "Mutton" + Index + ".xml";
XmlDocument xmlDoc = new XmlDocument();
var path = Path.Combine(Server.MapPath("~/Content/FilesXML"), FileName);
xmlDoc.Load(path); // Can use xmlDoc.LoadXml(YourString);
this is the best Solution to get the path what is exactly need for now
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.