WebAPI has a naming convention "FooController" for controller names. This is similar to ASP.NET MVC. Our codebase uses underscores to separate words in identifier named, e.g. "Foo_Bar_Object". In order for the controller names to follow this convention, we need a way to name our controllers "Foo_Controller".
Basically, we don't want URL routes to look like "oursite.com/api/foo_/", and we don't want to have to make exceptions everywhere for the underscore (e.g. route config, names of view folders, etc). To do this in MVC, we do:
Global.asax
protected void Application_Start() {
...
ControllerBuilder.Current.SetControllerFactory(new Custom_Controller_Factory());
}
Custom_Controller_Factory.cs
public class Custom_Controller_Factory : DefaultControllerFactory
{
protected override Type GetControllerType(RequestContext request_context, string controller_name)
{
return base.GetControllerType(request_context, controller_name + "_");
}
}
This seems to completely take care of the problem all in once place in MVC. Is there a way to do the same for WebAPI? I've heard rumor of a DefaultHttpControllerFactory but I can't find it anywhere.
I think DefaultHttpControllerSelector is what you are looking for.
class CustomHttpControllerSelector : DefaultHttpControllerSelector {
public CustomHttpControllerSelector(HttpConfiguration configuration)
: base(configuration) { }
public override string GetControllerName(HttpRequestMessage request) {
IHttpRouteData routeData = request.GetRouteData();
string controller = (string)routeData.Values["controller"]
return controller + "_";
}
Global.asax
GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new sh_Custom_Http_Controller_Selector(GlobalConfiguration.Configuration));
Related
In ASP.NET WebForms (well, HTML to be honest) we could reference pages within folders. EG, my structure could be (in regards to folders only)
root -> MyProductsFolder -> Shoes -> Ladies
And my website would show
www.mysite.com/MyProducts/Shoes/Ladies/Page.aspx
In MVC, we use a controller and it would appear that we can only ever have 1 level (folder) deep.
Is this right?
Without using URL rewriting, is it possible to have
www.mysite.com/MyProducts/Shoes/Ladies/Page
I assume the only way to do this is in the controller, but I can't create a controller named Shoes/Ladies
You can use MVC routing to created this URL. Your routing table is usually found in your AppStart > RouteConfig.cs class. You can use the route table to create URL maps to your actions in your controllers.
Assuming that MyProducts is your controller, and Shoes, Ladies are variables you want to accept you can do something like:
routes.MapRoute("MyProducts",
"MyProducts/{category}/{subcategory}/Page",
new { controller = "MyProducts", action = "Index" });
Note that your routes should be in order of most to least specific, so add this route above the default route.
When you navigate to /MyProducts/Shoes/Ladies/Page, it will map to your index action result in your MyProducts controller, passing variables for category and subcategory, so your controller will look something like
public class MyProducts : Controller
{
public ActionResult Index(string category, string subcategory)
{
//Do something with your variables here.
return View();
}
}
If my presumption is wrong, you want a view returned just for that URL, your route will look like:
routes.MapRoute("MyProducts", "MyProducts/Shoes/Ladies/Page", new { controller = "MyProducts", action = "LadiesShoes" });
And your Controller:
public class MyProducts : Controller
{
public ActionResult LadiesShoes()
{
//Do something with your variables here.
return View();
}
}
You can safely omit the final "/page" on the URL if you want to.
If I haven't covered your exact scenario with the above examples, let me know and I will extend my answer.
UPDATE
You can still put your views in a folder structure under the views folder if you want - and then reference the view file location in the controller - in the following example, place your view file called Index.cshtml in Views/Shoes/Ladies/ folder:
public class MyProducts : Controller
{
public ActionResult LadiesShoes()
{
//Do something with your variables here.
return View("~/Views/Shoes/Ladies/Index.cshtml");
}
public ActionResult MensShoes()
{
//Do something with your variables here.
return View("~/Views/Shoes/Mens/Index.cshtml");
}
}
You can use Attribute Routing to define the url of each action like below.
public class ShoeController : Controller
{
// eg: /nike/shoes/lady
[Route("{productName}/shoes/{xxx}")]
public ActionResult View(string productName, string xxx)
{
}
}
Routing Attribute offers flexibility and better code organization. You can check the route definition in the same spot.
I'd like to create a ViewHelper to localize my ASP.NET MVC application. Something like this:
public class Translator
{
private readonly ITranslationRepository _repo;
public Translator(ITranslationRepository repo)
{
_repo = repo;
}
public static string Translate(TranslationEnum translationEnum)
{
return _repo.GetTranslation(translationEnum, Session.LanguageId);
}
}
Usage in a (Razor) View looks like this:
<p>#Translator.Translate(TranslationEnum.WelcomeMessage)</p>
Now the problem is of course, I cannot make the Translate method static, because I need to access the instance variable _repo.
How can I inject the repository into a ViewHelper so I can use it in a View like above?
The responsibility of the view is just to transform the data that comes back from the controller to a HTML structure. Views are hard (to impossible) to test automatically, so best is to keep them as dumb as possible.
Instead of using the Translator in your view, inject it into your controller and let the controller call the Translator. This solves a range of problems:
It keeps the view simple.
It improves maintainability.
It improves testability.
It improves the verifiability of your object graphs (because you don't fall back on static method calls or the Service Locator anti-pattern).
Long story short, add a property to the controller's view model and return that to the view. Example:
public class HomeController : Controller {
private readonly ITranslator translator;
public HomeController(ITranslator translator) {
this.translator = translator
}
public ActionResult Index() {
this.View(new HomeViewModel {
WelcomeMessage = this.translator.Translate(TranslationEnum.WelcomeMessage)
});
}
}
And your view can look as follows:
#model HomeViewModel
<p>#Model.WelcomeMessage</p>
first of all, the intention of your design is wrong because it violates the single responsibility principal. Why is a translator dependent on repository?
secondly, why do you need a translator, you can use asp.net globalization?
click me We should not reinvent the wheel.
thirdly, all the html helpers are extension methods which have to be static.
so my suggestion is if you have to use translator, please refactor the Translator class, decouple the repository from it then create a extension methods from there.
or you can use globalization, it sounds horrible to start with but trust me it's not as hard as it looks.
public class Translator
{
private static ITranslationRepository _repo;
public static ITranslationRepository Repo
{
get { /*check null here before return*/ return _repo; } set { _repo = Repo; }
}
public Translator()
{
}
public static string Translate(TranslationEnum translationEnum)
{
return _repo.GetTranslation(translationEnum, Session.LanguageId);
}
}
I want to use Areas so I set up the following:
public class ContentAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Content";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Content_default",
"Content/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
What I would like is for a person who enters the following URL to be directed to a controller inside my Content area.
www.stackoverflow.com/Content/0B020D/test-data
I would like a person entering any URL with "/Content/" followed by six characters to be sent to:
- Page action in a controller named ItemController
- Six characters passed as the parameter id
- Optional text after that (test-data in this case) to be put into parameter title
How can I do this? I am not very familiar with setting up routes when using areas.
the six digits to be put into a variable called ID
So you're looking for something like
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Content_default",
"Content/{id}/{optional}",
new { controller = "ItemController", action = "TheActionYouWantThisToAllRouteTo" }
}
This would default everything to one controller and action method (which you have to specify in your instance). You can then get the data like so:
public ActionResult TheActionYouWantThisToAllRouteTo (string id, string optional)
{
// Do what you need to do
}
The way the routes are setup, you can name the pieces of information you want in a URL by wrapping it in a pair of { } curly braces. If you'd rather the name of optional to be isTestData then you would just change the route to read "Content/{id}/{isTestData}".
Note: Since you didn't specify the default action method you want this to route to, I substituted it with TheActionYouWantThisToAllRouteTo. Change that string to read the action method you want this to all go to. This also means you can't have a "regular" controller named ContentController, either.
Edit
Stephen Walther has a good blog post on custom route constraints. It can be found here. It should be a good start to get done what you need.
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.
I am trying to come up with an approach to create "dynamic" routing. What I mean, exactly, is that I want to be able to assign the controller and action of a route for each hit rather than having it mapped directly.
For example, a route may look like this "path/{object}" and when that path is hit, a lookup is performed providing the appropriate controller / action to call.
I've tried discovering the mechanisms for creating a custom route handler, but the documentation / discoverability is a bit shady at the moment (I know, its beta - I wouldn't expect any more). Although, I'm not sure if thats even the best approach and perhaps a controller factory or even a default controller/action that performs all of the mappings may be the best route (no pun intended) to go.
Any advice would be appreciated.
You can always use a catch all syntax ( I have no idea if the name is proper).
Route:
routeTable.MapRoute(
"Path",
"{*path}",
new { controller = "Pages", action = "Path" });
Controller action is defined as:
public ActionResult Path(string path)
In the action for controller you will have a path, so just have to spilt it and analyse.
To call another controller you can use a RedirectToAction ( I think this is more proper way). With redirection you can set up a permanent redirectionfor it.
Or use a something like that:
internal class MVCTransferResult : RedirectResult
{
public MVCTransferResult(string url) : base(url)
{
}
public MVCTransferResult(object routeValues)
: base(GetRouteURL(routeValues))
{
}
private static string GetRouteURL(object routeValues)
{
UrlHelper url = new UrlHelper(
new RequestContext(
new HttpContextWrapper(HttpContext.Current),
new RouteData()),
RouteTable.Routes);
return url.RouteUrl(routeValues);
}
public override void ExecuteResult(ControllerContext context)
{
var httpContext = HttpContext.Current;
// ASP.NET MVC 3.0
if (context.Controller.TempData != null &&
context.Controller.TempData.Count() > 0)
{
throw new ApplicationException(
"TempData won't work with Server.TransferRequest!");
}
// change to false to pass query string parameters
// if you have already processed them
httpContext.Server.TransferRequest(Url, true);
// ASP.NET MVC 2.0
//httpContext.RewritePath(Url, false);
//IHttpHandler httpHandler = new MvcHttpHandler();
//httpHandler.ProcessRequest(HttpContext.Current);
}
}
However this method require to run on IIS or a IIS Expres Casinni is not supporting a Server.Transfer method