OData Routing with Optional Parameter - wcf

I have an OData (v3) Web API 2 project that is a wrapper to another wcf web service. The intended client for this odata connection is SharePoint 2013. I am creating CRUD operations within this wrapper and noticed that when sharepoint is asked to delete something it send a request in this format: /Entity(Identity=XX) instead of it's normal /Entity(XX) that i have working normally. I need to be able to handle that request without breaking the other one. Here is my code:
public IHttpActionResult GetSchool([FromODataUri] int key, ODataQueryOptions<School> queryOptions)
{
// validate the query.
try
{
queryOptions.Validate(_validationSettings);
}
catch (ODataException ex)
{
return BadRequest(ex.Message);
}
SchoolDataSource data = new SchoolDataSource();
var result = data.GetByID(key);
return Ok<School>(result);
//return StatusCode(HttpStatusCode.NotImplemented);
}
This works fine for a request for /Schools(1), but not for /Schools(ID=1). i have tried adding:
[Route("Schools(ID={key}")]
And this makes the /Schools(ID=1) route work, but breaks pretty much everything else (406 Errors). i tried adding the above attribute and
[Route("Schools({key})")]to see if i can get them both working, but it doesn't function correctly either. I am very new to this, and was hoping to at least get some direction. Here is my WebApiConfig:
config.MapHttpAttributeRoutes();
config.EnableQuerySupport();
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
// Web API configuration and services
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<School>("Schools");
builder.DataServiceVersion = new Version("2.0");
config.Routes.MapODataRoute("odata", null, builder.GetEdmModel());
// Web API routes
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Errors i get:
406 if i have the route attribute set. 500 if i dont have the route attribute set. it seems as though my service has no idea how to handle the parameter unless i specify it, but if i do, all calls get 406 errors.

may not be the best approach, but made it work with this class:
public class SharePointRoutingConvention : EntitySetRoutingConvention
{
public override string SelectAction(ODataPath odataPath, HttpControllerContext context,
ILookup<string, HttpActionDescriptor> actionMap)
{
//Gets the entity type
IEdmEntityType entityType = odataPath.EdmType as IEdmEntityType;
//makes sure the format is correct
if (odataPath.PathTemplate == "~/entityset/key")
{
//parses out the path segment (Identity=X)
KeyValuePathSegment segment = odataPath.Segments[1] as KeyValuePathSegment;
//Gets the verb from the request header
string actionName = context.Request.Method.ToString();
// Add keys to route data, so they will bind to action parameters.
KeyValuePathSegment keyValueSegment = odataPath.Segments[1] as KeyValuePathSegment;
//Checks to see if the "Identity=" part is in the url
if (keyValueSegment.Value.Contains("Identity="))
{
//removes the extra text
context.RouteData.Values[ODataRouteConstants.Key] = keyValueSegment.Value.Replace("Identity=", "");
}
else
{
//parses it normally
context.RouteData.Values[ODataRouteConstants.Key] = keyValueSegment.Value;
}
//returns the verb
return actionName;
}
// Not a match.
return null;
}
}
and make the change to the webapiconfig:
var conventions = ODataRoutingConventions.CreateDefault();
//adding the custom odata routing convention
conventions.Insert(0, new SharePointRoutingConvention());
config.Routes.MapODataRoute(
routeName: "odata",
routePrefix: null,//this is so that you can type the base url and get metadata back (http://localhost/)
model: builder.GetEdmModel(),
pathHandler: new DefaultODataPathHandler(),
routingConventions: conventions //this assigns the conventions to the route
);

Related

Migrate IRouter usage to ASP.NET Core 3.1

I'm reading through Adam Freeman's Pro ASP.NET Core MVC 2 and one of the chapters about advanced routing features includes a mechanism whereby you can implement two-way legacy URL handling via IRouter. The gist of it is this:
Suppose you have a "legacy" URL like "/article/Windows_3.1_Overview.html"
Using a custom IRouter implementation, Core 2.0 lets you:
Direct that legacy URL to a specific action (e.g. Legacy/GetLegacyUrl) while passing in the URL as a parameter as so:
public async Task RouteAsync(RouteContext context)
{
string requestedUrl = context.HttpContext.Request.Path.Value.TrimEnd('/');
if (urls.Contains(requestedUrl, StringComparer.OrdinalIgnoreCase))
{
context.RouteData.Values["controller"] = "Legacy";
context.RouteData.Values["action"] = "GetLegacyUrl";
context.RouteData.Values["legacyUrl"] = requestedUrl;
await mvcRoute.RouteAsync(context); // mvcRoute is an instance of MvcRouteHandler
}
}
Generate that same URL using a tag helper: (<a asp-route-legacyurl="/article/Windows_3.1_Overview.html">Old Link</a>) using the following:
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
if (context.Values.ContainsKey("legacyUrl"))
{
string url = context.Values["legacyUrl"] as string;
if (urls.Contains(url))
{
return new VirtualPathData(this, url);
}
}
return null;
}
My question is: how do I do that in Core 3.0? I've tried this approach but there is no MvcRouteHandler anymore. I've tried implementing DynamicRouteValueTransformer like so:
public async override ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext,
RouteValueDictionary values)
{
string requestedUrl = httpContext.Request.Path.Value.TrimEnd('/');
return await Task.FromResult(new RouteValueDictionary()
{
["controller"] = "Legacy",
["action"] = "GetLegacyUrl",
["legacyUrl"] = requestedUrl
});
}
... but as far as I've read, this only works one way. It's also the only thing mentioned in Microsoft's 2.2 -> 3.0 migration guide. I've tried to just literally map the URL using
routes.MapRoute(
name: "",
template: route,
defaults: new { controller = "Legacy", action = "GetLegacyUrl", legacyUrl = route });
But this also doesn't generate the legacy URL, instead opting for Legacy/GetLegacyUrl/?legacyUrl=%2Farticle%2FWindows_3.1_Overview.html
I'm not really sure how else I can achieve this and I've been racking my brain and the documentation for several hours now. "Routing in ASP.NET Core" didn't help, neither did "Migrate from ASP.NET Core 2.2 to 3.0".
I'm probably missing something obvious, but I just can't seem to find an answer.
You could get the default mvc route handler using routes.DefaultHandler
In LegacyRoute.cs file, change your constructor signature from
public LegacyRoute(IServiceProvider services, params string[] targetUrls)
To
public LegacyRoute(IRouter routeHandler, params string[] targetUrls)
In Startup.cs file, add the route like this given below
routes.Routes.Add(new LegacyRoute(routes.DefaultHandler, "/articles/Windows_3.1_Overview.html", "/old/.NET_1.0_Class_Library"));

Netcore 2.2 Localized Routing - Route values for default culture always ignored

Successfully using the project laid out at Localized routing using ASP.NET Core MVC 2. The project is developed to display the Default language using just the controller/Action and Localized Routes for the alternate languages.
My Controller
[LocalizationRoute("en", "Portal/Dashboard")]
[LocalizationRoute("fr", "Portail/tableau-de-bord")]
[LocalizationRoute("es", "Portal/Tablero")]
public class DashboardController : PortalBaseController
{
private const string Title = "Dashboard";
[LocalizationRoute("en", "Dashboardv1")]
[LocalizationRoute("fr", "tableau-de-bordv1")]
[LocalizationRoute("es", "Tablerov1")]
public IActionResult Dashboardver1()
In my SignIn controller I wish to create a localised RedirectUrl...the below code provides a fully localized route for the the french and spanish but the default route is always just controller/action. If I manually type in the english, fully localized route, the page loads but the code below always returns just a regular path.
var culture = CultureInfo.CurrentCulture.Name;
var redirectUrl = LocalizationRouteDataHandler.GetUrl("Dashboard", "Dashboardver1", culture).Url;
//should be "en/portal/Dashboard/Dashboardv1"
//currently resolves to "Dashboard/Dashboardver1"
Suggestions as ?
I find that In LocalizationRouteDataHandler.AddControllerRouteData ,code ControllerRoutes[controllerKey].Names.TryAdd(culture, route); adds culture="en" and route="Dashboard" first to the Dictionary of Controller Name and then when it tries to add the condition culture="en" and route="en/Portal/Dashboard",it fails since the "en" key has added.
A workaround is that you could remove the unnecessary key and add the new key.
LocalizationRouteDataHandler.cs
public static void AddControllerRouteData(string controller, string culture, string route)
{
if (controller == "Dashboard")
{
Console.WriteLine("test");
}
string controllerKey = controller.ToLower();
// If the controller doesn't exist, create it!
if (!ControllerRoutes.ContainsKey(controllerKey))
{
ControllerRoutes.TryAdd(controllerKey, new CultureControllerRouteData());
}
// key removed
if (culture=="en" && ControllerRoutes[controllerKey].Names.Remove("en"))
{
ControllerRoutes[controllerKey].Names.TryAdd(culture, route);
}
// dictionary doesn't contain the key
else
{
ControllerRoutes[controllerKey].Names.TryAdd(culture, route);
}
}
When I test with
var redirectUrl = LocalizationRouteDataHandler.GetUrl("Dashboard", "Dashboardver1", "en").Url;
It returns /en/Portal/Dashboard/Dashboardver1

How to dynamically resolve controller with endpoint routing?

Upgrading to asp.net core 2.2 in my hobby project there is a new routing system I want to migrate to. Previously I implemented a custom IRouter to be able to set the controller for the request dynamically. The incoming request path can be anything. I match the request against a database table containing slugs and it looks up the a matching data container class type for the resolved slug. After that I resolve a controller type that can handle the request and set the RouteData values to the current HttpContext and passing it along to the default implementation for IRouter and everything works ok.
Custom implementaion of IRouter:
public async Task RouteAsync(RouteContext context)
{
var requestPath = context.HttpContext.Request.Path.Value;
var page = _pIndex.GetPage(requestPath);
if (page != null)
{
var controllerType = _controllerResolver.GetController(page.PageType);
if (controllerType != null)
{
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
newRouteData.Values["pageType"] = page.PageType;
newRouteData.Values["controller"] = controllerType.Name.Replace("Controller", "");
newRouteData.Values["action"] = "Index";
context.RouteData = newRouteData;
await _defaultRouter.RouteAsync(context);
}
}
}
A controller to handle a specific page type.
public class SomePageController : PageController<PageData>
{
public ActionResult Index(PageData currentPage)
{
return View("Index", currentPage);
}
}
However I got stuck when I'm trying to figure out how I can solve it using the new system. I'm not sure where I'm suppose to extend it for this behavior. I don't want to turn off the endpoint routing feature because I see an opportunity to learn something. I would aso appreciate a code sample if possible.
In ASP.NET 3.0 there is an new dynamic controller routing system. You can implement DynamicRouteValueTransformer.
Documentation is on the way, look at the github issue

ASP. NET Web Api 2 issue - The requested resource does not support HTTP method 'GET'

I am not sure why I am getting a "404 Not Found" on the following GET call to my api (using PostMan)
http://localhost:53840/api/v1/MessageLog/SomeStuff/3
The method in the Controller is as follows
[System.Web.Http.HttpGet]
public string SomeStuff(int s)
{
return "Received input !";
}
The Register method in the WebApiConfig class has the only route as follows :
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/v1/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
But when I change the code to
[System.Web.Http.HttpGet]
public string SomeStuff()
{
return "Received input !";
}
The call http://localhost:53840/api/v1/LogMessage/SomeStuff works and POSTMAN displays the "Recieved input !" string in the response body.
Is there a specific calling convention for passing in int/string etc. (I tried using a [FromUri] without much success) ? I have another POST method in the controlled which takes a JObject and that seems to be working perfectly fine.
It should be something like:
[System.Web.Http.HttpGet]
public string SomeStuff(int id)
{
return "Received input !";
}
Web API matches the parameter by name. In your route template, it is defined as {id} so the action parameter name must match that.
The reason the second one works is because the id is optional and the action matches the template.

asp.net mvc, generate url by custom route

i have created application, where url generates depends on database values.
i parse these urls without any problem and get controller and action from database in my route handler.
but when i try to generate url, i get troubles.
in my case, it seems like:
view
#Html.ActionLink("more", MVC.Blog.Post(item.Alias)) // i use T4MVC
MyRouteConstraint
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.UrlGeneration)
{
var data = GetDataFromDbByControllerActionAndParameters(values);
if (data == null)
return false;
var valuesToRemove = new List<string>();
var path = GenerateUrlByData(data, valuesToRemove);
values.Remove("controller");
values.Remove("action");
valuesToRemove.ForEach(v => values.Remove(v)); // remove values that is already used in path
values.Add("all", path) // path = "blog/post/postalias"
return true;
}
// skipped code
}
route rule
routes.MapRoute("Locations", "{*all}",
constraints: new { all = new LocationConstraints() },
defaults: new { },
namespaces: new []{typeof(BaseController).Namespace}).RouteHandler = new LocationRouteHandler();
and as result i got url like this
localhost:8553/?Controller=Blog&Action=Post&alias=postalias
but expect like this
localhost:8553/blog/post/postalias
how can I generate url? where it should be? i think not in the constrant, but why it is invoked in this case?
In my MVC application the closest route that matches the one you have is the one below:
routes.MapRoute(
name: "MyRouteName",
url: "{SomeFolder}/{SomePageName}",
defaults: new { controller = "MyController", action = "Index" },
constraints: new { routeConstraint = new MyRouteConstraint() }
);
SomeFolder can be fixed or changed from the database while SomePageName will be changed to a different value from database. The url should be whatever URL you want to match with this route and the one that will be replaced by the value from the database. The defaults address the Controller and Action that will render the page in the end of the MVC cycle. The constraints will lead to your Match method described.
With this configuration I have URLs like www.project.com/SomeFolder/SomePageNameFromDatabase.