Action is called twice - asp.net-mvc-4

All, I encountered a problem which MVC action is called twice. Please help to review it.
The view code is simple.
<div class="divContainer">
<ul>
#foreach (var blobName in ViewBag.BlobList)
{
<li>#Html.ActionLink("Delete", "Delete", "LogBlob", new { blobUrl = blobName }, null)</li>
}
</ul>
</div>
public class LogBlobController : Controller
{
public ActionResult Delete(string blobUrl)
{
//...
//The action is call twice.
}
}
Since the LogBlobController belong to an MVC Area named Log. So the route config in the AreaRegistration looks like below.
public class LogAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Log";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Log_default",
"Log/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
And the default implement of RouteConfig of MVC is below.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Login", action = "Login", id = UrlParameter.Optional }
);
}
And the Html in the page looks like below.
Delete
I doubted the reason of twice is they(LogAreaRegistration and RouteConfig) both worked at the same time. thanks.

Related

Overriding RouteValueDictionary in a Constraint in ASP.NET Core

I've just started using ASP.NET Core MVC and I want one route (global slug) to go to multiple controllers and actions depending on what type of page I'm serving to the user. For example, I want to use {*slug} for category and product pages.
I'm trying to override the default controller and action in a constraint.
In past versions of MVC, you could change either values["controller"] or values["action"] in a constraint and it would re-route to the appropriate controller and action.
public class Startup
{
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMvc(routes =>
{
routes.MapRoute(
name: "Category",
template: "{*slug}",
defaults: new { controller = "Page", action = "Home" },
constraints: new { slug = new PageConstraint() }
);
});
}
}
public partial class PageConstraint : IRouteConstraint
{
public virtual bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.UrlGeneration)
{
return true;
}
var slug = values["slug"] != null ? values["slug"].ToString() : null;
if (string.IsNullOrWhiteSpace(slug))
{
// Homepage
return true;
}
// Get category service
var categoryService = httpContext.RequestServices.GetRequiredService<ICategoryService>();
var category = categoryService.GetBySlug(slug);
if (category == null)
{
return false;
}
values["controller"] = "Category";
values["action"] = "Listing";
values["category"] = category;
return true;
}
}
In PageConstraint, I look to see if the Category exists, and if it does, it changes the Controller to Category and the Action to Listing in the RouteValueDictionary. However, when I debug the code through, it still goes to the Controller Page and the Action Home.
Anyone know why this is happening, or whether there is a better way of doing this? I know I could have one action in a controller doing all the work that the constraint is, but I would prefer to house the code in separate controllers and actions.
For your requirement, you could try to implement custom IRouter
public class RouterFromAppSettings : IRouter
{
private readonly IRouter _defaulRouter;
private readonly IConfiguration _config;
public RouterFromAppSettings(IRouter defaulRouter
, IConfiguration config)
{
_defaulRouter = defaulRouter;
_config = config;
}
public async Task RouteAsync(RouteContext context)
{
var controller = _config.GetSection("Router").GetValue<string>("Controller");
var action = _config.GetSection("Router").GetValue<string>("Action");
context.RouteData.Values["controller"] = controller;
context.RouteData.Values["action"] = action;
await _defaulRouter.RouteAsync(context);
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return _defaulRouter.GetVirtualPath(context);
}
}
And then register it in Startup.cs like
app.UseMvc(routes =>
{
routes.Routes.Insert(0, new RouterFromAppSettings(routes.DefaultHandler,Configuration));
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

HTTP Context is null inside a asp.net core controller

I'm using ASP.Net Core 2.1.1. I have an issue while calling HttpContext in my controller. When i want to use HttpContext the program returns NullReferenceException and says HttpContext.get returns null.
I'm very confused because it's inside a controller. can you help me with potential reasons for that?
CartController .cs
public class CartController : Controller
{
private readonly IProductServices _productServices;
private readonly ICartServices _cartServices;
public CartController(IProductServices productServices, ICartServices cartServices)
{
_productServices = productServices;
_cartServices = cartServices;
cartServices.Cart = GetCart();
}
public RedirectToActionResult AddToCart(int productID, string returnUrl)
{
ProductViewModel product = _productServices.GetByID(productID);
if (product != null)
{
_cartServices.AddItem(product, 1);
SaveCart(_cartServices.Cart);
}
return RedirectToAction("Index", new { returnUrl });
}
public RedirectToActionResult RemoveFromCart(int productID, string returnUrl)
{
ProductViewModel product = _productServices.GetByID(productID);
if (product != null)
{
_cartServices.RemoveLine(product);
SaveCart(_cartServices.Cart);
}
return RedirectToAction("Index", new { returnUrl });
}
public IActionResult Index(string returnUrl)
{
return View(new CartIndexViewModel()
{
Cart = GetCart(),
ReturnUrl = returnUrl
});
}
private CartViewModel GetCart()
{
return HttpContext.Session.GetJson<CartViewModel>("Cart") ?? new CartViewModel();
}
private void SaveCart(CartViewModel cart)
{
HttpContext.Session.SetJson<CartViewModel>("Cart", cart);
}
}
When this line calls: Cart = GetCart(), it returns null.
Startup.cs
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSession();
services.AddMemoryCache();
services.AddMvc();
services.RegisterStartupServices(Configuration);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseStatusCodePages();
app.UseStaticFiles();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: null,
template: "{category}/Page{page:int}",
defaults: new { controller = "Product", action = "List" }
);
routes.MapRoute(
name: null,
template: "Page{page:int}",
defaults: new { controller = "Product", action = "List", page = 1 }
);
routes.MapRoute(
name: null,
template: "{category}",
defaults: new { controller = "Product", action = "List", page = 1 }
);
routes.MapRoute(
name: null,
template: "",
defaults: new { controller = "Product", action = "List", page = 1 }
);
routes.MapRoute(
name: "default",
template: "{controller=Product}/{action=List}/{id?}"
);
});
}
}
I wrote application dependency injection codes in another assembly and call it from Sturtup.cs
StartupExtensions.cs
public static class StartupExtensions
{
public static void RegisterStartupServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<SportStoreDbContext>(x => x.UseSqlServer(configuration.GetConnectionString("SportStoreDatabase")));
MapperConfiguration mappingConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new MappingProfile());
});
IMapper mapper = mappingConfig.CreateMapper();
services.AddSingleton(mapper);
services.AddTransient<IProductServices, ProductServices>();
services.AddTransient<ICategoryServices, CategoryServices>();
services.AddTransient<ICartServices, CartServices>();
}
}
Thanks
You call your method GetCart inside your constructor :
public CartController(IProductServices productServices, ICartServices cartServices)
{
_productServices = productServices;
_cartServices = cartServices;
cartServices.Cart = GetCart();
}`
...
private CartViewModel GetCart()
{
return HttpContext.Session.GetJson<CartViewModel>("Cart") ?? new CartViewModel();
}
but the HttpContext property is not yet initialized. You can have a Http context only while processing a request.

Can't change destination controller/action via IRouter

I have simple MyRouter:
public class MyRouter : IRouter
{
private readonly IRouteBuilder _routeBuilder;
public MyRouter(IRouteBuilder routeBuilder)
{
_routeBuilder = routeBuilder;
}
public async Task RouteAsync(RouteContext context)
{
if (ShouldReroute(...))
{
SetNeededPath(context, reroute);
}
await GetDefaultRouter().RouteAsync(context);
}
private bool ShouldReroute(...)
{
return true;
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return GetDefaultRouter().GetVirtualPath(context);
}
private IRouter GetDefaultRouter()
{
return _routeBuilder.DefaultHandler;
}
private void SetNeededPath(RouteContext context, Reroute reroute)
{
context.RouteData.Values.Clear();
context.RouteData.Values["action"] = "StoreContacts";
context.RouteData.Values["controller"] = "Information";
}
}
As you can see it should change the destination of the request to:
[Route("")]
public class InformationController : Controller
{
[Route("StoreContacts")]
public IActionResult StoreContacts()
{
return View();
}
}
The routers description in Startup.cs is:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Home}/{action=Index}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.Routes.Add(new MyRouter(routes));
});
So in my brain, it should redirect all unmapped requests like mysite.com/unexistingRoute should go to InformationController.StoreContacts, but I get only 404.
Also the mysite.com/StoreContacts is available via the direct URL.
Attribute routing will take over conventional routing , so you can remove the attribute routing :
public class InformationController : Controller
{
public IActionResult StoreContacts()
{
return View();
}
}
And move your logic into custom route via IRouter . mysite.com/unexistingRoute won't map to existed route template config in Startup.cs . So remove attribute should work in your scenario . To map other url like mysite.com/OtherAction , you can write custom logic like :
if (context.HttpContext.Request.Path.Value.StartsWith("/StoreContacts"))
{
context.RouteData.Values["controller"] = "Information";
context.RouteData.Values["action"] = "StoreContacts";
}

ASP.Net MVC 4 WebAPI POST returns 404

I've looked at the many similar issues posted but couldn't find a solution that worked for me. So the call to Get is working fine but call to POST returns 404. I created a simple WebAPI project (MVC 4).
public class CasesController : ApiController
{
[Inject]
public ICaseManager CaseManager { get; set; }
// GET api/cases
public IEnumerable<Case> Get()
{
return CaseManager.ListCases();
}
// POST api/cases
[HttpPost]
public void Post([FromBody]Case objCase)
{
}
}
So when I navigate to http://localhost:34645/api/cases I get the following:
[{"CaseID":1,"CaseCode":"one","CaseDescription":"case one"},{"CaseID":2,"CaseCode":"two","CaseDescription":"case two"}]
I created another project (ASP.Net) and have an html file within it with the following code:
<script src="Scripts/jquery-2.0.3.js"></script>
<script src="Scripts/jquery-2.0.3.intellisense.js"></script>
<script type="text/javascript">
function postData() {
$.post('http://localhost:34645/api/cases', { "CaseID": 3, "CaseCode": "three", "CaseDescription": "case three" }).done(function (data) { alert("Success " + data); }).fail(function (xhr, textStatus, errorThrown) { alert("Error " + xhr.status); });
}
</script>
Every time I click the button that invokes postData, I get an alert "Error 404".
Here are my routes:
Global.asax:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
WebAPIConfig.Register:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
//RA: to get JSON
var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml");
config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
}
}
RouteConfig:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Please advise.
Be careful about the order of the WebApi registration line. I found when I specifically had the Global.asax.cs code in this order it worked:
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
Otherwise, it failed with 404 error.
If these are two separate solutions, check they're both running - it's possible that they're trying to share a server instance, so the WebAPI you're trying to hit isn't running when the other app is. If they're projects within the same solution, check that they're both set to run on startup, or again, the WebAPI won't be running when the ASP.NET project tries to access it.
Try below. It works for me. I have removed some properties for brevity.
public class CasesController : ApiController {
// GET api/cases
public IEnumerable<Case> Get() {
var caseManager = new CaseManager();
return caseManager.ListCases();
}
// POST api/cases
[HttpPost]
public string Post([FromBody]Case objCase) {
return objCase.CaseName;
}
}
public interface ICaseManager {
IEnumerable<Case> ListCases();
}
public class CaseManager {
public IEnumerable<Case> ListCases()
{
return new List<Case>() { new Case() { CaseID = 1, CaseName = "one" } };
}
}
public class Case {
public int CaseID { get; set; }
public string CaseName { get; set; }
}
View
<script type="text/javascript">
//function postData() {
// $.post('http://localhost:58820/api/cases', { "CaseID": 3, "CaseCode": "three", "CaseDescription": "case three" })
// .done(function (data) { alert("Success " + data); }).fail(function (xhr, textStatus, errorThrown)
// { alert("Error " + xhr.status); });
//}
$(document).ready(function () {
$('#save-source').click(function (e) {
e.preventDefault();
var source = {
'ID': 0,
'CaseID': 3,
'CaseName': "three",
};
$.ajax({
type: "POST",
dataType: "json",
url: "/api/cases",
data: source,
success: function (data) {
alert(data);
},
error: function (error) {
jsonValue = jQuery.parseJSON(error.responseText);
}
});
});
});
</script>
#using (Html.BeginForm(null, null, FormMethod.Post, new { id = "myForm"}))
{
<input type="submit" id="save-source" name="save-source" value="Add" />
}
After different attempts, this article helped me the most:
WebAPI and CORS enabled REST services
I also installed the Ninject WebApi DependencyResolver package through NuGet.
You write that you post to $.post('http://localhost:34645/api/cases'...
Either you change the url to include the action method name explicitly, like: $.post('http://localhost:34645/api/cases/post'..
or you add in your config.Routes.MapHttpRoute a default action which will be used when none action specified in the url
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { action="Post", id = RouteParameter.Optional }
);
OR you can change your route to
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
);
(without {action} and then web api will reach the Post method when you use a post http verb (it knows to do it automatically, but if you set a default action it'll override it)

A route is already in the route collection for Area Route

Here's my main routing table for my mvc project:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
AreaRegistration.RegisterAllAreas();
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute("Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}
}
And I have an area called Docs and here is its registration:
public class DocsAreaRegistration : AreaRegistration
{
public override string AreaName
{
get{ return "Docs";}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute("Docs_default", "Docs/{controller}/{action}", new { controller = "Wiki", action = "Index" });
context.MapRoute("RESTApi", "wiki/restAPI/v1", new { controller = "Wiki", action = "RestAPI" });
context.MapRoute("RESTApi", "wiki/test", new { controller = "Wiki", action = "action1" });
context.MapRoute("RESTApi_test0", "wiki/test0", new { controller = "Wiki", action = "action2" });
context.MapRoute("RESTApi_test1", "wiki/test2", new { controller = "Wiki", action = "productions" });
context.MapRoute("RESTApi_test2", "wiki/test3", new { controller = "Wiki", action = "action3" });
context.MapRoute("RESTApi_test3", "wiki/test4", new { controller = "Wiki", action = "action4" });
}
}
I'm getting the runtime error: A route named 'Docs_default' is already in the route collection. Route names must be unique.
I don't see where there is a conflict here. Or am I missing something?
(FYI, Ignore the names of the actions, they're just for testing purposes...so they won't make sense.)
You are probably calling RegisterRoutes twice in Application_Start.
The problem is a duplicate `AreaRegistration.RegisterAllAreas(); on route and on global.asax
so need only this:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
OR do the below steps:-
Check your bin folder. Maybe there is another .dll that adds the same route to the RouteCollection.
This happened to me when I was renaming a project. I had 2 .dlls in my bin folder:
MyProject.Web.dll
MyProjectNewName.Web.dll