This question already has an answer here:
Multiple levels in MVC custom routing
(1 answer)
Closed 7 years ago.
I have an Asp.Net MVC site with a MS SQL database. This site has an administration panel where, apart from other things, the administrator can change the menu of the site.
What we want to do is allow the owner of the site to change dynamically not only the menu names but also the page routes, so they can decide the url of any page in the site.
Imagine that we have different pages(views) like videos, news, photos...the default routes (url) for those view can be:
www.site.com/videos
www.site.com/news
www.site.com/photos
The admin has to be able to change dynamically those routes son when a user hit the news page it shows the URL they want, for example:
www.site.com/my-videos
www.site.com/latest-news
www.site.com/photo-gallery
The idea is loading the site menu from DB, getting the name of the menu, the controller, the action and the route of the page. And from there we have to call a controller and action to load a view but we need to show in the URL the route the admin has set for that view.
Also it is possible that we have multiple actions(views) in the same controller. For example news and videos are in the same controller.
If we pass a parameter "customRoute" to the Route.Config it gives us an error because the name of that parameter is the same for those actions in the same controller.
How can we do this with the ASP.NET routing?
Thanks in advance.
The code below shows how to add routes from a database to your route config (this only gets executed when the application pool starts)
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("favicon.ico");
var redirects = LegacyRedirectRepo.GetRedirects();
foreach (var legacyRedirect in redirects)
{
if (!legacyRedirect.Source.Contains("?"))
{
routes.Add(new LegacyRoute(legacyRedirect.Source, legacyRedirect.Destination));
}
}
routes.IgnoreRoute("{folder}/{*pathInfo}", new { folder = "upload" });
routes.IgnoreRoute("{folder}/{*pathInfo}", new { folder = "content" });
routes.IgnoreRoute(
"{*staticfile}",
new { staticfile = #".*\.(jpg|gif|jpeg|png|js|css|htm|html)$" }
);
//static routing rules
}
Or you could override the BeginProcessRequest with something like this
public class LegacyHandler : MvcHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="LegacyHandler"/> class.
/// </summary>
/// <param name="requestContext">The request context.</param>
public LegacyHandler(RequestContext requestContext)
: base(requestContext)
{
}
/// <summary>
/// Called by ASP.NET to begin asynchronous request processing.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <param name="callback">The asynchronous callback method.</param>
/// <param name="state">The state of the asynchronous object.</param>
/// <returns>The status of the asynchronous call.</returns>
protected override System.IAsyncResult BeginProcessRequest(HttpContext httpContext, System.AsyncCallback callback, object state)
{
var legacyRoute = RequestContext.RouteData.Route as LegacyRoute;
httpContext.Response.Status = "301 Moved Permanently";
var urlBase = RequestContext.HttpContext.Request.Url.GetLeftPart(System.UriPartial.Authority);
var url = string.Format("{0}/{1}", urlBase, legacyRoute.Target);
if (!string.IsNullOrWhiteSpace(RequestContext.HttpContext.Request.Url.Query))
{
var pathAndQuery = RequestContext.HttpContext.Request.Url.PathAndQuery;
pathAndQuery = pathAndQuery.Substring(1, pathAndQuery.Length - 1);
var redirect = LegacyRedirectRepo.GetRedirect(pathAndQuery);
url = string.Format(#"{0}/{1}", urlBase, redirect.Destination);
}
httpContext.Response.RedirectPermanent(url);
httpContext.Response.End();
return null;
}
}
Related
We have a .Net Framework Web API, with Token based OAuth authentication, and are trying to make a call to it via an Exchange HTML Add-In. I wish to allow access to several domains, as we may be using several different apps to access it, but we do not wish to allow general (*) access, as it is a proprietary web API, so there is no need for it to be accessed beyond known domains.
I have tried the following in order to satisfy the pre-flight:
Add the Access-Control-Allow-Origin headers with multiple domains via <system.webServer> - this returns a "header contains multiple values" CORS error when including multiple domains
Adding the Access-Control-Allow-Origin headers with multiple domains via a PreflightRequestsHandler : Delegating Handler - same result
If I set these up with one domain, and used the config.EnableCors with an EnableCorsAttribute with the domains, it would add those on to the headers and give an error with redundant domains.
How can I set up my Web API with OAuth and CORS settings for multiple domains?
You can add the header "Access-Control-Allow-Origin" in the response
of authorized sites in Global.asax file
using System.Linq;
private readonly string[] authorizedSites = new string[]
{
"https://site1.com",
"https://site2.com"
};
private void SetAccessControlAllowOrigin()
{
string origin = HttpContext.Current.Request.Headers.Get("Origin");
if (authorizedSites.Contains(origin))
HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", origin);
}
protected void Application_BeginRequest()
{
SetAccessControlAllowOrigin();
}
Found the following from Oscar Garcia (#ozkary) at https://www.ozkary.com/2016/04/web-api-owin-cors-handling-no-access.html, implemented it and it worked perfectly! Added to AppOAuthProvider which Microsoft had set up on project creation:
/// <summary>
/// match endpoint is called before Validate Client Authentication. we need
/// to allow the clients based on domain to enable requests
/// the header
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override Task MatchEndpoint(OAuthMatchEndpointContext context)
{
SetCORSPolicy(context.OwinContext);
if (context.Request.Method == "OPTIONS")
{
context.RequestCompleted();
return Task.FromResult(0);
}
return base.MatchEndpoint(context);
}
/// <summary>
/// add the allow-origin header only if the origin domain is found on the
/// allowedOrigin list
/// </summary>
/// <param name="context"></param>
private void SetCORSPolicy(IOwinContext context)
{
string allowedUrls = ConfigurationManager.AppSettings["allowedOrigins"];
if (!String.IsNullOrWhiteSpace(allowedUrls))
{
var list = allowedUrls.Split(',');
if (list.Length > 0)
{
string origin = context.Request.Headers.Get("Origin");
var found = list.Where(item => item == origin).Any();
if (found){
context.Response.Headers.Add("Access-Control-Allow-Origin",
new string[] { origin });
}
}
}
context.Response.Headers.Add("Access-Control-Allow-Headers",
new string[] {"Authorization", "Content-Type" });
context.Response.Headers.Add("Access-Control-Allow-Methods",
new string[] {"OPTIONS", "POST" });
}
I've got have the following controller:
[Route("xapi/statements")] << -- NOTICE THE ROUTE
[Produces("application/json")]
public class StatementsController : ApiControllerBase
With he following actions
/// <summary>
/// Stores a single Statement with the given id.
/// </summary>
/// <param name="statementId"></param>
/// <param name="statement"></param>
/// <returns></returns>
[AcceptVerbs("PUT", "POST", Order = 1)]
public async Task<IActionResult> PutStatement([FromQuery]Guid statementId, [ModelBinder(typeof(StatementPutModelBinder))]Statement statement)
{
await _mediator.Send(PutStatementCommand.Create(statementId, statement));
return NoContent();
}
/// <summary>
/// Create statement(s) with attachment(s)
/// </summary>
/// <param name="model"></param>
/// <returns>Array of Statement id(s) (UUID) in the same order as the corresponding stored Statements.</returns>
[HttpPost(Order = 2)]
[Produces("application/json")]
public async Task<ActionResult<ICollection<Guid>>> PostStatements(StatementsPostModelBinder model)
{
ICollection<Guid> guids = await _mediator.Send(CreateStatementsCommand.Create(model.Statements));
return Ok(guids);
}
The actions are executed in the following order:
1. PutStatement
2. PostStatements
But PutStatement should only be triggered if the statementId parameter is provided. This is not the case.
I'm using 2 model binders to parse the content of the streams as either application/json or multipart/form-data if the statements have any attachments.
1. StatementPutModelBinder
2. StatementsPostModelBinder
How do i prevent the action from being excuted if the statementId parameter is not provided?
Eg. /xapi/statements/ => Hits PutStatement
I did not find a answer for my own question, but i made a mistake and was under the impression that the xAPI statements resource should allow statementId as a parameter for POST requests. Therefore i do not have the issue any more, which started my question.
I've got a Silverlight 5 app that is calling an OData service (the OOTB one incuded with SharePoint 2010) to pull data back form a list. The site is secured using Windows Authentication. When I run my test I get prompted to login but the results always say there are zero results returned in the result set.
Now here's what's strange. I know there's data in the list (and when I manually plug in the OData request URL, I see results come back in the browser). When I watch Fiddler while running the test, I see a few requests for clientaccesspolicy.xml (all result in a 401 response)... then I login & it successfully obtains the clientaccesspolicy.xml file. However, even though the app says it ran the query and got zero results back, I don't see the actual OData service request in Fiddler (nothing after the successful call to clientaccesspolicy.xml.
Here's what the code looks like:
private DataServiceCollection<InstructorsItem> _dataCollection = new DataServiceCollection<InstructorsItem>();
private Action<IEnumerable<Instructor>> _callbackWithData;
/// <summary>
/// Retrieves a list of instructors from the data service.
/// </summary>
public void GetInstructors(Action<IEnumerable<Instructor>> callback) {
// save callbacks
ResetCallbacks();
_callbackWithData = callback;
// get the instructors
var query = from instructor in IntranetContext.Instructors
select instructor;
// execute query
RunQuery(query);
}
/// <summary>
/// Retrieves instructors from the data source based on the specified query.
/// </summary>
/// <param name="query">Query to execute</param>
private void RunQuery(IQueryable<InstructorsItem> query) {
// clear the collection & register the load completed method
_dataCollection.Clear();
_dataCollection.LoadCompleted += OnLoadDataCompleted;
// fire the load
_dataCollection.LoadAsync(query.Take(5));
}
/// <summary>
/// Handler when the data has been loaded from the service.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void OnLoadDataCompleted(object sender, LoadCompletedEventArgs e) {
// remove the event handler preventing double loads
_dataCollection.LoadCompleted -= OnLoadDataCompleted;
// convert the data to a generic list of objects
var results = _dataCollection.ToList<InstructorsItem>();
// TODO: convert results to local objects
List<Instructor> convertedResults = new List<Instructor>();
foreach (var item in results) {
convertedResults.Add(new Instructor() {
SharePointId = item.Id,
Name = item.Title
});
}
// run the callback
_callbackWithData(convertedResults);
}
And here's what the test runner looks like that's triggering it:
[TestMethod]
[Asynchronous]
[Description("Test loading instructors from the OData Intranet service.")]
public void TestGetInstructors() {
bool asyncCallCompleted = false;
List<Instructor> result = null;
// call data service
_dataService.GetInstructors(asyncResult => {
asyncCallCompleted = true;
result = new List<Instructor>(asyncResult);
});
// run test when call completed
EnqueueConditional(() => asyncCallCompleted);
EnqueueCallback(
() => Assert.IsTrue(result.Count > 0, "Didn't retrieve any instructors."));
EnqueueTestComplete();
}
Can't for the life of me figure out (1) why i'm not seeing the query showing up in Fiddler when it is saying there are no errors, in fact it says there are zero errors when running the test.
If you are running the server and client on the same machine, there is no external HTTP traffic so there is nothing for Fiddler to pick up.
Looked for a method on the MvcContrib.TestHelper.RouteTestingExtensions class named ShouldNotMap. There is ShouldBeIgnored, but I don't want to test an IgnoreRoute invocation. I want to test that a specific incoming route should not be mapped to any resource.
Is there a way to do this using MvcContrib TestHelper?
Update
Just tried this, and it seems to work. Is this the correct way?
"~/do/not/map/this".Route().ShouldBeNull();
I think you are looking for the following:
"~/do/not/map/this".ShouldBeIgnored();
Behind the scenes this asserts that the route is processed by StopRoutingHandler.
I was looking for the same thing. I ended up adding the following extension methods to implement ShouldBeNull and the even shorter ShouldNotMap:
In RouteTestingExtensions.cs:
/// <summary>
/// Verifies that no corresponding route is defined.
/// </summary>
/// <param name="relativeUrl"></param>
public static void ShouldNotMap(this string relativeUrl)
{
RouteData routeData = relativeUrl.Route();
routeData.ShouldBeNull(string.Format("URL '{0}' shouldn't map.", relativeUrl));
}
/// <summary>
/// Verifies that the <see cref="RouteData">routeData</see> is null.
/// </summary>
public static void ShouldNotMap(this RouteData routeData)
{
routeData.ShouldBeNull("URL should not map.");
}
In GeneralTestExtensions.cs :
///<summary>
/// Asserts that the object should be null.
///</summary>
///<param name="actual"></param>
///<param name="message"></param>
///<exception cref="AssertFailedException"></exception>
public static void ShouldBeNull(this object actual, string message)
{
if (actual != null)
{
throw new AssertFailedException(message);
}
}
I've tried to create custom route for *.ogg file which is played in website with HTML 5 audio tag.
The fact is that FireFox doesn't play ogg if we don't provide application/ogg as ContentType.
Unlike Chrome plays ogg without extra work.
By the way, I think custom route and register it in Global.asax is a good solution and reusable code.
For every path with .ogg extension, server sends ogg file with application/ogg ContentType.
The problem is that I don't know why path with *.ogg is not processed with custom route.
This is my code for OggHandler.cs
public class OggHandler : IRouteHandler, IHttpHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return this;
}
/// <summary>
/// process ogg file
/// </summary>
/// <param name="context"></param>
public void ProcessRequest(HttpContext context)
{
var request = context.Request;
var response = context.Response;
try
{
//response ogg file with Content Type Header
response.ContentType = "application/ogg";
response.WriteFile(request.PhysicalPath);
}
catch (Exception ex)
{
response.Write("<html>\r\n");
response.Write("<head><title>Ogg HTTP Handler</title></head>\r\n");
response.Write("<body>\r\n");
response.Write("<h1>" + ex.Message + "</h1>\r\n");
response.Write("</body>\r\n");
response.Write("</html>");
}
}
public bool IsReusable
{
get { return true; }
}
}
and the registration in Global.asax
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new Route
(
"{resource}.ogg/{*pathInfo}"
, new OggHandler()
));
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
Hope to get good suggestion from you all. :)
Lately, I also attached simple project file as the following link.
http://codesanook.com/shared/MvcOgg.zip
First thing first: have you checked it doesn't work if you specify the additional application/ogg mimetype mapped to .ogg files either in IIS or web.config web server section?
Then, I think the problem is that your route registration will only match single segment URL (/filename.ogg) . You should specify your route pattern as {*musicpath}/.ogg
Third thing, more stylistic than other, you should make a oggroutehandler class that crates the ogghttphandler. This way you keep separation of the two competences.
Sorry I couldn't check it better but I'm on a smartphone :)
HTH
Simone