Change component view location in Asp.Net 5 - asp.net-core

On ASP.NET 5 a Component view must be in one of two places:
Views/NameOfControllerUsingComponent/Components/ComponentName/Default.cshtml
Views/Shared/Components/ComponentName/Default.cshtml
Is there a way to change this to:
Views/NameOfControllerUsingComponent/Components/ComponentName.cshtml
Views/Shared/Components/ComponentName.cshtml
So basically, remove the folder ComponentName and change the view name from Default.cshtml to ComponentName.cshtml.
For me it makes more sense ... Is it possible?

That convention is only applied if you create a view component that derives from the base ViewComponent provided by the framework.
That class defines the View helpers, which return a ViewViewComponentResult:
public ViewViewComponentResult View<TModel>(string viewName, TModel model)
{
var viewData = new ViewDataDictionary<TModel>(ViewData, model);
return new ViewViewComponentResult
{
ViewEngine = ViewEngine,
ViewName = viewName,
ViewData = viewData
};
}
The ViewViewComponentResult is where the conventions are defined:
private const string ViewPathFormat = "Components/{0}/{1}";
private const string DefaultViewName = "Default";
public async Task ExecuteAsync(ViewComponentContext context)
{
...
string qualifiedViewName;
if (!isNullOrEmptyViewName &&
(ViewName[0] == '~' || ViewName[0] == '/'))
{
// View name that was passed in is already a rooted path, the view engine will handle this.
qualifiedViewName = ViewName;
}
else
{
// This will produce a string like:
//
// Components/Cart/Default
//
// The view engine will combine this with other path info to search paths like:
//
// Views/Shared/Components/Cart/Default.cshtml
// Views/Home/Components/Cart/Default.cshtml
// Areas/Blog/Views/Shared/Components/Cart/Default.cshtml
//
// This supports a controller or area providing an override for component views.
var viewName = isNullOrEmptyViewName ? DefaultViewName : ViewName;
qualifiedViewName = string.Format(
CultureInfo.InvariantCulture,
ViewPathFormat,
context.ViewComponentDescriptor.ShortName,
viewName);
}
...
}
Notice that if you return from your view component the full path to a view as the view name, then the view component will use the specified view.
Something like:
return View("~/Views/Shared/Components/ComponentName.cshtml")
Since there is no way to modify the conventions in ViewViewComponentResult and your approach would only work for view components with a single view, you could build something using the root view paths approach:
Create your own ViewComponent class extending the existing one.
Add new helper methods or hide the existing View methods to return a view using a full path:
public ViewViewComponentResult MyView<TModel>(TModel model)
{
var viewName = string.Format(
"~/Views/Shared/Components/{0}.cshtml",
this.ViewComponentContext.ViewComponentDescriptor.ShortName)
return View(viewName, model);
}
If you add new methods you might be able to add them as extension methods of ViewComponent instead of having to create your own class.
Another alternative would be creating a class SingleViewViewComponent copying the code for ViewComponent but replacing the implementation of ViewViewComponentResult View<TModel>(string viewName, TModel model). Then when creating your view components, you would inherit from SingleViewViewComponent instead of ViewComponent.

Took me a weekend to finally find a way around this that didn't involve writing a custom ViewComponentResult.
in MVC .Net Core, you can add your own IViewLocationExpander to the RazorViewEngineOptions in your startup.cs's ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
...
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new CustomLocationExpander());
});
}
This allows you to add custom Paths that are used in conjuction with the ViewLocationExpanderContext ViewName ({0}), ControllerName ({1}).
The main issue is that you can't alter the context's values, which makes it seemingly impossible to change the default View Component's ViewName of Component/ComponentName/Default
Seemingly impossible
Here's the trick, the ExpandViewLocations is called with each View(), each time it doesn't have a fully qualified view path. Which means you can add custom logic. What I did was add a catch to detect ViewComponents in the PopulateValues method, then added to the context.Values dictionary, and then if that dictionary has those custom values, it will prepend to the Paths the list of paths that use my generated view name instead of the context.
It's fully reverse compatible, and shouldn't impact performance one bit.
public class CustomLocationExpander : IViewLocationExpander
{
private const string _CustomViewPath = "CustomViewPath";
private const string _CustomController = "CustomController";
public void PopulateValues(ViewLocationExpanderContext context)
{
Regex DefaultComponentDetector = new Regex(#"^((?:[Cc]omponents))+\/+([\w\.]+)\/+(.*)");
/*
* If successful,
* Group 0 = FullMatch (ex "Components/MyComponent/Default")
* Group 1 = Components (ex "Component")
* Group 2 = Component Name (ex "MyComponent")
* Group 3 = View Name (ex "Default")
* */
var DefaultComponentMatch = DefaultComponentDetector.Match(context.ViewName);
if (DefaultComponentMatch.Success)
{
// Will render Components/ComponentName as the new view name
context.Values.Add(_CustomViewPath, string.Format("{0}/{1}", DefaultComponentMatch.Groups[1].Value, DefaultComponentMatch.Groups[2].Value));
context.Values.Add(_CustomController, context.ControllerName);
}
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
/* Parameters:
* {2} - Area Name
* {1} - Controller Name
* {0} - View Name
* */
List<string> Paths = new List<string> {
// Default View Locations to support imported / legacy paths
"/Views/{1}/{0}.cshtml",
"/Views/Shared/{0}.cshtml",
// Adds Feature Folder Rendering
"/Features/{1}/{0}.cshtml",
"/Features/Shared/{0}.cshtml",
// Handles My Custom rendered views
"/{0}.cshtml"
};
// Add "Hard Coded" custom view paths to checks, along with the normal default view paths for backward compatibility
if (context.Values.ContainsKey(_CustomViewPath))
{
// Generate full View Paths with my custom View Name and Controller Name
var CombinedPaths = new List<string>(Paths.Select(x => string.Format(x, context.Values[_CustomViewPath], context.Values[_CustomController], "")));
// Add in original paths for backward compatibility
CombinedPaths.AddRange(Paths);
return CombinedPaths;
}
// Returns the normal view paths
return Paths;
}
}

Related

Remove route from RouteCollection in Asp.Net Core and add new with same route name (nopCommerce-4.00)

I want to remove existing route from RouteCollection and want to add new route with same route name in nopCommerce 4.00 via plugin
Existing route name:
//home page
routeBuilder.MapLocalizedRoute("HomePage", "",
new { controller = "Home", action = "Index" });
I Want to replace it with
routeBuilder.MapLocalizedRoute("HomePage", "",
new { controller = "CustomPage", action = "Homepage" });
I tried several ways but not get any luck.
In my case, I have to replace the robots.txt generation.
I created a new public controller in my plugin, and I copy the original action here:
public class MiscCommonController : BasePublicController
{
#region Fields
private readonly ICommonModelFactory _commonModelFactory;
#endregion Fields
#region Ctor
public MiscCommonController(
ICommonModelFactory commonModelFactory
)
{
this._commonModelFactory = commonModelFactory;
}
#endregion Ctor
#region Methods
//robots.txt file
//available even when a store is closed
[CheckAccessClosedStore(true)]
//available even when navigation is not allowed
[CheckAccessPublicStore(true)]
public virtual IActionResult RobotsTextFile()
{
var robotsFileContent = _commonModelFactory.PrepareRobotsTextFile();
return Content(robotsFileContent, MimeTypes.TextPlain);
}
#endregion Methods
}
After this I create a RouteProvider for my plugin, and I replaced the original route to my own one.
public partial class RouteProvider : IRouteProvider
{
/// <summary>
/// Gets a priority of route provider
/// </summary>
public int Priority => -1;
/// <summary>
/// Register routes
/// </summary>
/// <param name="routeBuilder">Route builder</param>
public void RegisterRoutes(IRouteBuilder routeBuilder)
{
Route route = null;
foreach (Route item in routeBuilder.Routes)
{
if (item.Name == "robots.txt")
{
route = item;
break;
}
}
if (route != null) routeBuilder.Routes.Remove(route);
routeBuilder.MapRoute(
"robots.txt",
"robots.txt",
new { controller = "MiscCommon", action = "RobotsTextFile" }
);
}
}
That's all.
After this implementation, the routing works fine, and the get request landed in my own controller, which is act like the original.
Now, I can replace the generation logic with my own.
I hope it helps.
in the RouteProvider.cs of your plugin write these codes (based on your names):
var lastExistingRoute= routeBuilder.Routes.FirstOrDefault(x => ((Route)x).Name == "HomePage");
routeBuilder.Routes.Remove(lastExistingRoute);
routeBuilder.MapRoute("HomePage", "",
new { controller = "CustomPage", action = "Homepage", });
and the below codes worked for myself version 4.20:
var lastDownloadRoute=routeBuilder.Routes.FirstOrDefault(x => ((Route)x).Name == "GetDownload");
routeBuilder.Routes.Remove(lastDownloadRoute);
routeBuilder.MapRoute("GetDownload", "download/getdownload/{guid}/{agree?}",
new { controller = "AzTechProduct", action = "GetPayed", });
There are two potential ways to deal with this in nopCommerce 4.3 that I see with a quick examination of the code.
First, you could create an IRouteProvider, add your route that has the same signature as the one you wish to 'delete' and make sure the Priority on the provider is greater than 1.
Doing this will basically override the default route built into Nop. This is my preferred method.
public partial class RouteProvider: IRouteProvider
{
public void RegisterRoutes(IEndpointRouteBuilder endpointRouteBuilder)
{
var pattern = string.Empty;
if (DataSettingsManager.DatabaseIsInstalled)
{
var localizationSettings = endpointRouteBuilder.ServiceProvider.GetRequiredService<LocalizationSettings>();
if (localizationSettings.SeoFriendlyUrlsForLanguagesEnabled)
{
var langservice = endpointRouteBuilder.ServiceProvider.GetRequiredService<ILanguageService>();
var languages = langservice.GetAllLanguages().ToList();
pattern = "{language:lang=" + languages.FirstOrDefault().UniqueSeoCode + "}/";
}
}
// Handle the standard request
endpointRouteBuilder.MapControllerRoute("Wishlist", pattern + "wishlist/{customerGuid?}",
new { controller = "MyShoppingCart", action = "Wishlist" });
return;
}
public int Priority => 100;
}
The key to the code above is the Priority value. This route will get added to the list first and will therefore take precedence over the default route. Using this technique eliminates the need to delete the default route.
The second possible method turns out to not work because the endpointRouteBuilder.DataSources[n].Endpoints collection is read only. So, as far as I know, you can't remove mappings from that list after they have been added.

OData service with multiple routes while using unbound functions

Does anyone know how to get OData v4 hosted in a .NET service to work with multiple routes?
I have the following:
config.MapODataServiceRoute("test1", "test1", GetEdmModelTest1());
config.MapODataServiceRoute("test2", "test2", GetEdmModelTest2());
Each of the GetEdmModel methods have mapped objects.
I can get to the service as following (this is working fine):
http://testing.com/test1/objects1()
http://testing.com/test2/objects2()
But if I try to call a function like the following (will not work):
[HttpGet]
[ODataRoute("test1/TestFunction1()")]
public int TestFunction1()
{ return 1; }
It will throw the following error:
The path template 'test1/TestFunction1()' on the action 'TestFunction1' in controller 'Testing' is not a valid OData path template. Resource not found for the segment 'test1'.
Yet if I remove the "MapODataServiceRoute" for "test2" so there is only one route, it all works.
How do I get this to work with multiple routes?
** I have posted a full example of the issue at the following **
https://github.com/OData/WebApi/issues/1223
** I have tried the OData version sample listed below with the following issues **
https://github.com/OData/ODataSamples/tree/master/WebApi/v4/ODataVersioningSample
I have tried the "OData Version" example before and it did not work.
It seems that unbound (unbound is the goal) does not follow the same routing rules are normal service calls.
Ex. If you download the "OData Version" example and do the following.
In V1 -> WebApiConfig.cs add
builder.Function(nameof(Controller.ProductsV1Controller.Test)).Returns<string>();
In V2 -> WebApiConfig.cs add
builder.Function(nameof(Controller.ProductsV2Controller.Test)).Returns<string>();
In V1 -> ProductsV1Controller.cs add
[HttpGet]
[ODataRoute("Test()")]
public string Test()
{ return "V1_Test"; }
In V2 -> ProductsV2Controller.cs add
[HttpGet]
[ODataRoute("Test()")]
public string Test()
{ return "V2_Test"; }
Now call it by this. " /versionbyroute/v1/Test() " and you will get "V2_Test"
The problem is that "GetControllerName" does not know how to get the controller when it is using unbound functions / actions.
This is why most sample code I have found fails when trying to "infer" the controller.
Have a look at OData Versioning Sample for a primer.
The key point of trouble is usually that the DefaultHttpControllerSelector maps controllers by local name, not fullname/namespace.
If your entity types and therefore controller names are unique across both EdmModels you will not have to do anything special, it should just work out of the box. The above sample takes advantage of this concept by forcing you to inject a string value into the physical names of the controller classes to make them unique and then in the ODataVersionControllerSelector GetControllerName is overridden to maps the incoming route to the customised controller names
If unique names for the controllers seems to hard, and you would prefer to use the full namespace (meaning your controller names logic remains standard) then you can of course implement your own logic to select the specific controller class instance when overriding DefaultHttpControllerSelector. simply override SelectController instead. This method will need to return an instance of HttpControllerDescriptor which is a bit more involved than the sample.
To show you what I mean, I will post the solution to a requirement from an older project, that was a little bit different to yours. I have a single WebAPI project that manages access to multiple databases, these databases have similar schema, many Entity names are the same which means that those controller classes will have the same names. The controllers are structured by folders/namespaces such that there is a root folder called DB, then there is a folder for each database, then the controllers are in there.
You can see that this project has many different schemas, they effectively map to versions of an evolving solution, the non-DB namespaces in this image are a mix of OData v4, v3 and standard REST apis. It is possible to get all these beasts to co-exist ;)
This override of the HttpControllerSelector inspects the runtime once to cache a list of all the controller classes, then maps the incoming route requests by matching the route prefix to the correct controller class.
/// <summary>
/// Customised controller for intercepting traffic for the DB Odata feeds.
/// Any route that is not prefixed with ~/DB/ will not be intercepted or processed via this controller
/// <remarks>Will instead be directed to the base class</remarks>
/// </summary>
public class DBODataHttpControllerSelector : DefaultHttpControllerSelector
{
private readonly HttpConfiguration _configuration;
public DBODataHttpControllerSelector(HttpConfiguration config)
: base(config)
{
_configuration = config;
}
// From: http://www.codeproject.com/Articles/741326/Introduction-to-Web-API-Versioning
private Dictionary<string, HttpControllerDescriptor> _controllerMap = null;
private List<string> _duplicates = new List<string>();
/// <summary>
/// Because we are interested in supporting nested namespaces similar to MVC "Area"s we need to
/// Index our available controller classes by the potential url segments that might be passed in
/// </summary>
/// <returns></returns>
private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
{
if(_controllerMap != null)
return _controllerMap;
_controllerMap = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
// Create a lookup table where key is "namespace.controller". The value of "namespace" is the last
// segment of the full namespace. For example:
// MyApplication.Controllers.V1.ProductsController => "V1.Products"
IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
foreach (Type t in controllerTypes)
{
var segments = t.Namespace.Split(Type.Delimiter);
// For the dictionary key, strip "Controller" from the end of the type name.
// This matches the behavior of DefaultHttpControllerSelector.
var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);
var key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", segments[segments.Length - 2], segments[segments.Length - 1], controllerName);
// Check for duplicate keys.
if (_controllerMap.Keys.Contains(key))
{
_duplicates.Add(key);
}
else
{
_controllerMap[key] = new HttpControllerDescriptor(_configuration, t.Name, t);
}
}
// Remove any duplicates from the dictionary, because these create ambiguous matches.
// For example, "Foo.V1.ProductsController" and "Bar.V1.ProductsController" both map to "v1.products".
// CS: Ahem... thats why I've opted to go 3 levels of depth to key name, but this still applies if the duplicates are there again
foreach (string s in _duplicates)
{
_controllerMap.Remove(s);
}
return _controllerMap;
}
/// <summary>
/// Because we are interested in supporting nested namespaces we want the full route
/// to match to the full namespace (or at least the right part of it)
/// </summary>
/// <returns></returns>
private Dictionary<string, HttpControllerDescriptor> _fullControllerMap = null;
private Dictionary<string, HttpControllerDescriptor> InitializeFullControllerDictionary()
{
if(_fullControllerMap != null)
return _fullControllerMap;
_fullControllerMap = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
// Create a lookup table where key is "namespace.controller". The value of "namespace" is the last
// segment of the full namespace. For example:
// MyApplication.Controllers.V1.ProductsController => "V1.Products"
IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
foreach (Type t in controllerTypes)
{
var segments = t.Namespace.Split(Type.Delimiter);
// For the dictionary key, strip "Controller" from the end of the type name.
// This matches the behavior of DefaultHttpControllerSelector.
var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);
var key = t.FullName;// t.Namespace + "." + controllerName;
_fullControllerMap[key] = new HttpControllerDescriptor(_configuration, t.Name, t);
}
return _fullControllerMap;
}
/// <summary>
/// Select the controllers with a simulated MVC area sort of functionality, but only for the ~/DB/ route
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public override System.Web.Http.Controllers.HttpControllerDescriptor SelectController(System.Net.Http.HttpRequestMessage request)
{
string rootPath = "db";
IHttpRouteData routeData = request.GetRouteData();
string[] uriSegments = request.RequestUri.LocalPath.Split('/');
if (uriSegments.First().ToLower() == rootPath || uriSegments[1].ToLower() == rootPath)
{
#region DB Route Selector
// If we can find a known api and a controller, then redirect to the correct controller
// Otherwise allow the standard select to work
string[] knownApis = new string[] { "tms", "srg", "cumulus" };
// Get variables from the route data.
/* support version like this:
* config.Routes.MapODataRoute(
routeName: "ODataDefault",
routePrefix: "{version}/{area}/{controller}",
model: model);
object versionName = null;
routeData.Values.TryGetValue("version", out versionName);
object apiName = null;
routeData.Values.TryGetValue("api", out apiName);
object controllerName = null;
routeData.Values.TryGetValue("controller", out controllerName);
* */
// CS: we'll just use the local path AFTER the root path
// db/tms/contact
// db/srg/contact
// Implicity parse this as
// db/{api}/{controller}
// so [0] = ""
// so [1] = "api"
// so [2] = "version" (optional)
// so [2 or 3] = "controller"
if (uriSegments.Length > 3)
{
string apiName = uriSegments[2];
if (knownApis.Contains(string.Format("{0}", apiName).ToLower()))
{
string version = "";
string controllerName = uriSegments[3];
if (controllerName.ToLower().StartsWith("v")
// and the rest of the name is numeric
&& !controllerName.Skip(1).Any(c => !Char.IsNumber(c))
)
{
version = controllerName;
controllerName = uriSegments[4];
}
// if the route has an OData item selector (#) then this needs to be trimmed from the end.
if (controllerName.Contains('('))
controllerName = controllerName.Substring(0, controllerName.IndexOf('('));
string fullName = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", apiName, version, controllerName).Replace("..", ".");
// Search for the controller.
// _controllerTypes is a list of HttpControllerDescriptors
var descriptors = InitializeControllerDictionary().Where(t => t.Key.EndsWith(fullName, StringComparison.OrdinalIgnoreCase)).ToList();
if (descriptors.Any())
{
var descriptor = descriptors.First().Value;
if (descriptors.Count > 1)
{
descriptor = null;
// Assume that the version was missing, and we have implemented versioning for that controller
// If there is a row with no versioning, so no v1, v2... then use that
// if all rows are versioned, use the highest version
if (descriptors.Count(d => d.Key.Split('.').Length == 2) == 1)
descriptor = descriptors.First(d => d.Key.Split('.').Length == 2).Value;
else if (descriptors.Count(d => d.Key.Split('.').Length > 2) == descriptors.Count())
descriptor = descriptors
.Where(d => d.Key.Split('.').Length > 2)
.OrderByDescending(d => d.Key.Split('.')[1])
.First().Value;
if (descriptor == null)
throw new HttpResponseException(
request.CreateErrorResponse(HttpStatusCode.InternalServerError,
"Multiple controllers were found that match this un-versioned request."));
}
if (descriptor != null)
return descriptor;
}
if (_duplicates.Any(d => d.ToLower() == fullName.ToLower()))
throw new HttpResponseException(
request.CreateErrorResponse(HttpStatusCode.InternalServerError,
"Multiple controllers were found that match this request."));
}
}
#endregion DB Route Selector
}
else
{
// match on class names that match the route.
// So if the route is odata.tms.testController
// Then the class name must also match
// Add in an option to doing a string mapping, so that
// route otms can mapp to odata.tms
// TODO: add any other custom logic for selecting the controller that you want, alternatively try this style syntax in your route config:
//routes.MapRoute(
// name: "Default",
// url: "{controller}/{action}/{id}",
// defaults: new { controller = "Home", action = "RegisterNow", id = UrlParameter.Optional },
// namespaces: new[] { "YourCompany.Controllers" }
//);
// Because controller path mapping might be controller/navigationproperty/action
// We need to check for the following matches:
// controller.navigationproperty.actionController
// controller.navigationpropertyController
// controllerController
string searchPath = string.Join(".", uriSegments).ToLower().Split('(')[0] + "controller";
var descriptors = InitializeFullControllerDictionary().Where(t => t.Key.ToLower().Contains(searchPath)).ToList();
if (descriptors.Any())
{
var descriptor = descriptors.First().Value;
if (descriptors.Count > 1)
{
descriptor = null;
// In this mode, I think we should only ever have a single match, ready to prove me wrong?
if (descriptor == null)
throw new HttpResponseException(
request.CreateErrorResponse(HttpStatusCode.InternalServerError,
"Multiple controllers were found that match this namespace request."));
}
if (descriptor != null)
return descriptor;
}
}
return base.SelectController(request);
}
}
You can use a Custsom MapODataServiceRoute.
The below is an example from WebApiConfig.cs
The controllers are registered with the CustomMapODataServiceRoute and its a bit cumbersome having to include typeof(NameOfController) for every controller. One of my endpoints has 22 separate controllers, but thus far it's worked.
Registering Controllers - Showing two separate OData endpoints in the same project, both containing custom functions
// Continuing Education
ODataConventionModelBuilder continuingEdBuilder = new ODataConventionModelBuilder();
continuingEdBuilder.Namespace = "db_api.Models";
var continuingEdGetCourse = continuingEdBuilder.Function("GetCourse");
continuingEdGetCourse.Parameter<string>("term_code");
continuingEdGetCourse.Parameter<string>("ssts_code");
continuingEdGetCourse.Parameter<string>("ptrm_code");
continuingEdGetCourse.Parameter<string>("subj_code_prefix");
continuingEdGetCourse.Parameter<string>("crn");
continuingEdGetCourse.ReturnsCollectionFromEntitySet<ContinuingEducationCoursesDTO>("ContinuingEducationCourseDTO");
config.CustomMapODataServiceRoute(
routeName: "odata - Continuing Education",
routePrefix: "contEd",
model: continuingEdBuilder.GetEdmModel(),
controllers: new[] { typeof(ContinuingEducationController) }
);
// Active Directory OData Endpoint
ODataConventionModelBuilder adBuilder = new ODataConventionModelBuilder();
adBuilder.Namespace = "db_api.Models";
// CMS Groups
var cmsGroupFunc = adBuilder.Function("GetCMSGroups");
cmsGroupFunc.Parameter<string>("user");
cmsGroupFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
// Departments
var deptUsersFunc = adBuilder.Function("GetADDepartmentUsers");
deptUsersFunc.Parameter<string>("department");
deptUsersFunc.ReturnsCollectionFromEntitySet<ADUser>("ADUser");
var adUsersFunc = adBuilder.Function("GetADUser");
adUsersFunc.Parameter<string>("name");
adUsersFunc.ReturnsCollectionFromEntitySet<ADUser>("ADUser");
var deptFunc = adBuilder.Function("GetADDepartments");
deptFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
var instDeptFunc = adBuilder.Function("GetADInstructorDepartments");
instDeptFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
var adTitleFunc = adBuilder.Function("GetADTitles");
adTitleFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
var adOfficeFunc = adBuilder.Function("GetADOffices");
adOfficeFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
var adDistListFunc = adBuilder.Function("GetADDistributionLists");
adDistListFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
config.CustomMapODataServiceRoute(
routeName: "odata - Active Directory",
routePrefix: "ad",
model: adBuilder.GetEdmModel(),
controllers: new[] { typeof(DepartmentsController), typeof(CMSGroupsController)
});
Creating Custom Map OData Service Route
public static class HttpConfigExt
{
public static System.Web.OData.Routing.ODataRoute CustomMapODataServiceRoute(this HttpConfiguration configuration, string routeName,
string routePrefix, Microsoft.OData.Edm.IEdmModel model, IEnumerable<Type> controllers)
{
var routingConventions = ODataRoutingConventions.CreateDefault();
// Multiple Controllers with Multiple Custom Functions
routingConventions.Insert(0, new CustomAttributeRoutingConvention(routeName, configuration, controllers));
// Custom Composite Key Convention
//routingConventions.Insert(1, new CompositeKeyRoutingConvention());
return configuration.MapODataServiceRoute(routeName,
routePrefix,
model,
new System.Web.OData.Routing.DefaultODataPathHandler(),
routingConventions,
defaultHandler: System.Net.Http.HttpClientFactory.CreatePipeline( innerHandler: new System.Web.Http.Dispatcher.HttpControllerDispatcher(configuration),
handlers: new[] { new System.Web.OData.ODataNullValueMessageHandler() }));
}
}
public class CustomAttributeRoutingConvention : AttributeRoutingConvention
{
private readonly List<Type> _controllers = new List<Type> { typeof(System.Web.OData.MetadataController) };
public CustomAttributeRoutingConvention(string routeName, HttpConfiguration configuration, IEnumerable<Type> controllers)
: base(routeName, configuration)
{
_controllers.AddRange(controllers);
}
public override bool ShouldMapController(System.Web.Http.Controllers.HttpControllerDescriptor controller)
{
return _controllers.Contains(controller.ControllerType);
}
}

Asp.net core Custom routing

I am trying to implement custom routing on an asp.net core application.
The desired result is the following:
http://Site_URL/MyController/Action/{Entity_SEO_Name}/
Entity_SEO_Name parameter will be a unique value saved into the database that it is going to help me identify the id of the entity that I am trying to display.
In order to achieve that I have implemented a custom route:
routes.MapMyCustomRoute(
name: "DoctorDetails",
template: " {controller=MyController}/{action=TestRoute}/{name?}");
public class MyTemplateRoute : TemplateRoute
{
public override async Task RouteAsync(RouteContext context)
{
//context.RouteData.Values are always empty. Here is the problem.
var seo_name = context.RouteData.Values["Entity_SEO_Name"];
int entityId = 0;
if (seo_name != null)
{
entityId = GetEntityIdFromDB(seo_name);
}
//Here i need to have the id and pass it to controller
context.RouteData.Values["id"] = entityId;
await base.RouteAsync(context);
}
}
My controller actionresult:
public ActionResult TestRoute(int id)
{
var entity = GetEntityById(id);
return Content("");
}
The problem with this approach is that the context.RouteData.Values are always empty.
Any ideas on how to move forward with this one ?
Your solution too complicated. You can have route template like
template: "{controller=Home}/{action=Index}/{seo?}"
and controller action just like
public ActionResult TestRoute(string seo)
{
var entity = GetEntityBySeo(seo);
return Content("");
}
It is enough, asp.net mvc is smart enough to bind seo variable to the parameter from url path.

Naming conventions for view pages and setting controller action for view

I am unsure on how I should be naming my View pages, they are all CamelCase.cshtml, that when viewed in the browser look like "http://www.website.com/Home/CamelCase".
When I am building outside of .NET my pages are named like "this-is-not-camel-case.html". How would I go about doing this in my MVC4 project?
If I did go with this then how would I tell the view to look at the relevant controller?
Views/Home/camel-case.cshtml
Fake edit: Sorry if this has been asked before, I can't find anything via search or Google. Thanks.
There are a few ways you can do this:
Name all of your views in the style you would like them to show up in the url
This is pretty simple, you just add the ActionName attribute to all of your actions and specify them in the style you would like your url to look like, then rename your CamelCase.cshtml files to camel-case.cshtml files.
Use attribute routing
Along the same lines as above, there is a plugin on nuget to enable attribute routing which lets you specify the full url for each action as an attribute on the action. It has convention attributes to help you out with controller names and such as well. I generally prefer this approach because I like to be very explicit with the routes in my application.
A more framework-y approach
It's probably possible to do something convention based by extending the MVC framework, but it would be a decent amount of work. In order to select the correct action on a controller, you'd need to map the action name on its way in to MVC to its CamelCase equivalent before the framework uses it to locate the action on the controller. The easiest place to do this is in the Route, which is the last thing to happen before the MVC framework takes over the request. You'll also need to convert the other way on the way out so the urls generated look like you want them to.
Since you don't really want to alter the existing method to register routes, it's probably best write a function in application init that loops over all routes after they have been registered and wraps them with your new functionality.
Here is an example route and modifications to application start that achieve what you are trying to do. I'd still go with the route attribute approach however.
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
WrapRoutesWithNamingConvention(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
AuthConfig.RegisterAuth();
}
private void WrapRoutesWithNamingConvention(RouteCollection routes)
{
var wrappedRoutes = routes.Select(m => new ConventionRoute(m)).ToList();
routes.Clear();
wrappedRoutes.ForEach(routes.Add);
}
private class ConventionRoute : Route
{
private readonly RouteBase baseRoute;
public ConventionRoute(RouteBase baseRoute)
: base(null, null)
{
this.baseRoute = baseRoute;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var baseRouteData = baseRoute.GetRouteData(httpContext);
if (baseRouteData == null) return null;
var actionName = baseRouteData.Values["action"] as string;
var convertedActionName = ConvertHyphensToPascalCase(actionName);
baseRouteData.Values["action"] = convertedActionName;
return baseRouteData;
}
private string ConvertHyphensToPascalCase(string hyphens)
{
var capitalParts = hyphens.Split('-').Select(m => m.Substring(0, 1).ToUpper() + m.Substring(1));
var pascalCase = String.Join("", capitalParts);
return pascalCase;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var valuesClone = new RouteValueDictionary(values);
var pascalAction = valuesClone["action"] as string;
var hyphens = ConvertPascalCaseToHyphens(pascalAction);
valuesClone["action"] = hyphens;
var baseRouteVirtualPath = baseRoute.GetVirtualPath(requestContext, valuesClone);
return baseRouteVirtualPath;
}
private string ConvertPascalCaseToHyphens(string pascal)
{
var pascalParts = new List<string>();
var currentPart = new StringBuilder();
foreach (var character in pascal)
{
if (char.IsUpper(character) && currentPart.Length > 0)
{
pascalParts.Add(currentPart.ToString());
currentPart.Clear();
}
currentPart.Append(character);
}
if (currentPart.Length > 0)
{
pascalParts.Add(currentPart.ToString());
}
var lowers = pascalParts.Select(m => m.ToLower());
var hyphens = String.Join("-", lowers);
return hyphens;
}
}
}

joomla mvc model results where ate they called?

I'm confused how to project models result in view in joomla 2.5
I have the controller which is initializing the model
class FrontpageMyComponentControllerItem extends JControllerLegacy
{
private $id;
public function display($cachable = false, $urlparams = array())
{
// Initialise variables.
$jinput = JFactory::getApplication()->input;
$this->id = $jinput->get('id');
$cachable = true;
$model = $this->getModel('item');
$result_in_view = $model->Item('23'); //$id what I get
// Set the default view name and format from the Request.
$viewName = $jinput->get('view', 'item');
$jinput->set('view', $viewName);
return parent::display($cachable, $safeurlparams);
}
}
now how do I have the result in my view?
In most components data (results) are being retrieved from Model by the View.
I'm not really sure why, but I guess it's to give more power to Views.
FrontpageMyComponentViewItem extends JViewLegacy
{
/** #var array Data are stored here */
public $items;
public function display($tpl = null)
{
/** Retrieve data from Model */
$items = $this->get('Items');
// Check for errors.
if (count($errors = $this->get('Errors')))
{
// Raise an error
JError::raiseWarning(500, implode("\n", $errors));
return false;
}
// Assign data, so layout can access these
$this->items =& $items;
parent::display($tpl);
}
}
This is different than usual MVC implementations where Controller is retrieving data from Model and passing it to View.
If you'd like to see example of this in Joomla, look at MediaController in Joomla 2.5.
Sometimes the View may not be necessary (output is in JSON) and then you may retrieve data inside controller and output it instantly using echo like in search suggestions
There are new MVC (vs legacy) classes in Joomla, but core components still don't use them.