I build an MVC Core application with single-page-clients.
I have configured some routes for /api/... which works well. Additionally I want to serve static files for some routes. e.g.:
For all sub-routes of /Home/ I want to receive /Home/index.html
For all sub-routes of /App/ I want to receive /App/index.html
I added app.UseStaticFiles() to Configure() so I can access /Home/index.html but it does not work for any other sub-route.
What is missing?
I changed my routing-system to attribute routing. Among others I added a HomeController:
[Route("")]
public class HomeController : Controller
{
[Route("")]
public IActionResult Index()
{
return View(); // The Home-page
}
[Route("Error")]
public IActionResult Error()
{
// show an error page
return Content(Activity.Current?.Id?.ToString() ?? HttpContext.TraceIdentifier.ToString());
}
[Route("{client}/{*tail}")]
[Produces("text/html")]
public IActionResult ClientApp(string client, string tail)
{
// show a client app
try
{
return new ContentResult()
{
Content = System.IO.File.ReadAllText($"./wwwroot/{client}/index.html"),
ContentType = "text/html"
};
}
catch
{
return RedirectToAction("/Error");
}
}
}
My client apps have had an index.html file inside its own folder (client routing-part) inside of wwwroot. When a request tries to access /something/... the route of ClientApp matches with something as the client-app folder name and the index.html is sent to the client. There is no redirect and the url stays the same.
It causes no problem with static files if you add UseStaticFiles before AddMvc in Startup:
app.UseStaticFiles();
app.UseMvc();
Tested in ASP.NET MVC Core 2.0.
Related
I'm currently implementing a Web API using Web API 2's attribute routing (http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2). I am also using the Help Pages module in order to automatically generate documentation from XML comments (http://www.asp.net/web-api/overview/creating-web-apis/creating-api-help-pages).
For this API I am providing support for optional return format extensions, so that every API method has a pair of routes defined on it like so:
[HttpGet]
[Route("Path/Foo")]
[Route("Path/Foo.{ext}")]
public HttpResponseMessage DoFoo()
{
// Some API function.
}
This allows a user to hit any of these and get a result:
www.example.com/api/Controller/Path/Foo
www.example.com/api/Controller/Path/Foo.json
www.example.com/api/Controller/Path/Foo.xml
My issue is that when Help Pages uses MapHttpAttributeRoutes() to generate documentation, it is picking up both routes for each method. So right now I see help for:
api/Controller/Foo
api/Controller/Foo.{ext}
But I want to only see:
api/Controller/Foo.{ext}
I would prefer to hide the non-extension route on each method, so that every method only shows a single Help Page entry.
Has anyone else tried something similar? Is there a work around that I am missing?
My question would be is that, would consumers of your api figure out easily that the {ext} is optional?...personally, I would prefer the default behavior...but anyways following are some workarounds that I can think of:
A quick and dirty workaround. Split the DoFoo into 2 actions like DoFoo() and DoFooWithExt maybe. Notice that I am using an attribute called ApiExplorerSettings, which is for HelpPage purposes. Example below:
[HttpGet]
[Route("Path/Foo")]
[ApiExplorerSettings(IgnoreApi=true)]
public HttpResponseMessage DoFoo()
{
return DoFooHelper();
}
[HttpGet]
[Route("Path/Foo.{ext}")]
public HttpResponseMessage DoFooWithExt()
{
return DoFooHelper();
}
private HttpResponseMessage DoFooHelper()
{
//do something
}
Create a custom ApiExplorer (which HelpPage feature uses internally) and check for specific routes like the following and can decide whether to show the action or not for that particular route.
// update the config with this custom implementation
config.Services.Replace(typeof(IApiExplorer), new CustomApiExplorer(config));
public class CustomApiExplorer : ApiExplorer
{
public CustomApiExplorer(HttpConfiguration config) : base(config)
{
}
public override bool ShouldExploreAction(string actionVariableValue, HttpActionDescriptor actionDescriptor, IHttpRoute route)
{
if (route.RouteTemplate.EndsWith("Path/Foo", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return base.ShouldExploreAction(actionVariableValue, actionDescriptor, route);
}
}
Get list of all ApiDescription from the default ApiExplorer and then filter out the descriptions which you do not like. Example:
Configuration.Services.GetApiExplorer().ApiDescriptions.Where((apiDesc) => !apiDesc.RelativePath.EndsWith("Path/Foo", StringComparison.OrdinalIgnoreCase))
I´m struggling with URLs for ajax-reader/JSON. Each time I think I understand it, it seems that I haven´t.
Please, can anybody explain the logic behind this???
I got this Controller:
public class ServiceController : DnnApiController
{
[AllowAnonymous]
[HttpGet]
public HttpResponseMessage GetAllItems(int moduleId)
{
MyProjectController controller = new MyProjectController();
IEnumerable<ItemInfo> items = controller.GetAllItems(moduleId);
return Request.CreateResponse(HttpStatusCode.OK, items);
}
}
I got this Routemapper:
public class RouteMapper : IServiceRouteMapper
{
public void RegisterRoutes(IMapRoute mapRouteManager)
{
mapRouteManager.MapHttpRoute("MyProject",
"default",
"{controller}/{action}",
new[] { "MyCompany.MyProject.Services" });
}
}
At what URL can I read the data with $.ajax() and what is the URL showing me the data in a browser?
Thanx in Advance!
Asle :)
This is how I do it (Note: this will only work with DNN6.2 and above);
In the View.ascx.cs add
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
ServicesFramework.Instance.RequestAjaxScriptSupport();
ServicesFramework.Instance.RequestAjaxAntiForgerySupport();
jQuery.RequestDnnPluginsRegistration();
}
This ensures that jquery and the required DNN ajax plugins are added.
Initiate the services framework jquery plugin in the View.ascx like this inside javascript script tags (S.O. wouldn't allow me to include them)
var modId = <%=ModuleId %>;
var sf = $.ServicesFramework(modId);
Now in a separate javascript file or in the view.ascx control add the ajax function
function getAllItems(){
$.ajax({
type:"GET",
url:sf.getServiceRoot("MyProject")+"Service/GetAllItems",
beforeSend:sf.setModuleHeaders,
data:{moduleId:modId},
cache:false
}).done(function(data){
alert("Success!");
}).fail(function(){
alert("Crashed!");
}).always(function(){
//something you want done whether passed or failed
//like hide progress bar, ajax spinner etc.
});
}
The DNN jquery plugin will build the url which will look similar to this (Note: 142 is just for illustration purpose and will be replace with actual module id)
/DesktopModules/MyProject/API/Service/GetAllItems?moduleId=142
The URL will be something like
/desktopmodules/SlidePresentation/API/SlidePresetnation.ashx/ListOfSlides
I have examples at
https://slidepresentation.codeplex.com/SourceControl/latest
but they were for DNN6, they might require a few updates due to the API changes for DNN 7
you can see a DNN7 module that has a service layer at https://dnnsimplearticle.codeplex.com/SourceControl/latest#cs/services/
We have an MVC project that I am attempting to update to include WebApi. In order to get the required routes we are using AttributeRouting. All the calls seem to be routing correctly except for [PUT] which returns a 405. I have simplified the controller and actions and still receive the error with the [PUT] unless I include [HttpPut] also. Not sure what I am missing.
[RoutePrefix("api/Sites")]
public class SitesController : BaseApiController
{
[POST("")]
public bool CreateSite(SiteSignupArgs args)
{
...
}
[GET("Statuses")]
public IList<SiteAuditViewModel> GetStatuses()
{
...
}
[PUT("Statuses/{siteId}")]
[HttpPut] // This is required or 405 is returned
public HttpResponseMessage UpdateStatus(string siteId, UpdateStatusArgs args)
{
...
}
[DELETE("Statuses/{siteId}")]
public HttpResponseMessage Delete(string siteId)
{
return Request.CreateResponse(HttpStatusCode.OK);
}
}
Version 3.5.6 of AttributeRouting.Core, AttributeRouting.Core.Http, AttributeRouting.Core.Web, AttributeRouting.WebApi
MVC4
WebDAV is not installed.
What you are seeing is an expected behavior. Action Selector in Web API by default assumes the action to be of verb POST if the action name does not have a prefix with verbs like "Get", "Post", "Put", "Delete" etc.
Now it isn't working even if you have specified [PUT("Statuses/{siteId}")] attribute explicitly because, Action selector looks for attributes from System.Web.Http namespace like HttpGetAttribute, HttpPostAttribute, HttpPutAttribute etc.
Since AttributeRouting's PUTAttribute isn't of the above types, Action selector doesn't consider it and still thinks it to be the default one, which is POST. So your workaround of having HttpPut attribute is correct.
I'm trying to use signalr in ApiController that handles upload from a page.
Essentially I am invoking my Hub from my uploadController and then I want to talk to my client.
shortened down the upload controller looks like this:
public class UploadController : ApiController
{
public Task<HttpResponseMessage> PostFile()
{
var hubMan = new HubManager();
hubMan.showUpload("test");
}
}
I then have my HubManager that should take care of sending to my client:
public class HubManager : Hub
{
public HubManager()
{
}
public void showUpload(string src)
{
Caller.showUpload(src);
}
}
I also tried this in my showUpload:
public void showUpload(string str)
{
var context = GlobalHost.ConnectionManager.GetHubContext<HubManager>();
context.Clients[this.Context.ConnectionId].showUpload(str);
}
My client side code looks like this:
$(document).ready(function () {
var progress = $.connection('/signalr/hubs/hubManager');
progress.showUpload = function (src) {
alert(src);
};
// Start the connection
$.connection.hub.start();
});
Now the problem is that in my Hub class everything is Null.
My Caller,Clients and Context is null. So it seems something is not initialising properly.
Any suggestions?
You cannot create an instance of a SignalR hub yourself. Hubs need to be initialized by SignalR so the properties like Clients etc. are available.
If you want to broadcast to clients from outside the hub (e.g. your controller), you need to use the context object of the hub as described in the wiki:
public ActionResult ControllerAction()
{
var context = GlobalHost.ConnectionManager.GetHubContext<HubManager>();
context.Clients[ /* connectionId or group name */ ].showUpload();
// ...
}
Please note that you can't access context.ConnectionId or context.Caller there, because you are calling your ApiController and not SignalR, so the framework knows nothing about the ConnectionId in this case. You need to pass it to the controller in some other way, e.g. cookies or as a parameter of the controller action.
This must be simple and already answered, but I've wasted many hours on it. I can't figure how to get an error page on mistyped address. Also I'd prefer not to redirect, but to keep the URL. I've tried many combinations of CustomErrors, HttpErrors and Application_Error, but nothing works for non-existent controller - depending on HttpErrors I always get IIS 404.0 page or just an empty 404 response. Running on IIS 7.5, MVC 3.
I don't remember where I got the solution. But here is the code to handle the error:
First, you create a ErrorController:
public class ErrorController : Controller
{
//
// GET: /Error/
public ActionResult Index()
{
return RedirectToAction("Index", "Home");
}
public ActionResult Generic()
{
Exception ex = null;
try
{
ex = (Exception)HttpContext.Application[Request.UserHostAddress.ToString()];
}
catch { }
return View();
}
public ActionResult Error404()
{
return View();
}
}
Second, open Global file and add the following code:
protected void Application_Error(object sender, EventArgs e)
{
Exception ex = Server.GetLastError();
Application[HttpContext.Current.Request.UserHostAddress.ToString()] = ex;
}
Third, change customerror in your webconfig:
<customErrors mode="Off" defaultRedirect="/Error/Generic">
<error statusCode="404" redirect="/Error/Error404"/>
</customErrors>
More: I created one more error layout. It makes things even more clear. :)
Hope this helps you.
I use the following route to ensure all requests not matching any other route fall there, then you can handle that case very easily:
// this route is intended to catch 404 Not Found errors instead of bubbling them all the way up to IIS.
routes.MapRoute(
"PageNotFound",
"{*catchall}",
new { controller = "Error", action = "NotFound" }
);
Map that last (include that statement after any other .MapRoute statements).