Unable to create a Role using RoleManager in Blazor Serverside - roles

I had some code using the RoleManager that I had copied from an MVC project to create roles in a Blazor project.
It compiles okay but when I step through the code it gets to that line and then appears to throw and exception, as it doesn't ever go to the next line. I am unable to catch an exception though.
Since my code didn't work I found code on the web and it behaved the same, so I changed how it was injected and I got a different error, so I found a third way of doing it and that too gets to the line that tests to see if there is a role already existing and blows up.
Here is the latest attempt
[Inject]
IServiceProvider services {get;set;}
public async Task Initialize()
{
using (var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>())
{
string proRole = "Pro";
if (!await roleManager.RoleExistsAsync(proRole))
{
var temp = await roleManager.CreateAsync(new IdentityRole { Name = proRole });
}
}
}
And it blows up on the awaited if statement.
Here is the previous code that should work that doesn't.
string proRole = "Pro";
string proClientRole = "ProClient";
if (!await _roleManager.RoleExistsAsync(proRole))
{
var temp = await _roleManager.CreateAsync(new IdentityRole { Name = proRole });
}
which is inside of a method.
I also had it as #{code in the Razor file and after an hour of trying different things there I moved it to a base class because they have been more stable in the past.
It is not the database connection because that is well verified and also because UserManager is called in the same class and it work
I also tried this code (Blazor Role Management Add Role trough UI (Crud)) both suggestions.
This is one bad thing about Blazor for me is it is buggy I never know if it is my bad, or just something wrong with the latest greatest. But I am assuming it is my bad in this case.
Anyway any help would be much appreciated, I am way too many hours on this,

I copy/pasted the same code to the "/" page and it works there.
For some reason it wouldn't work in a component loaded into a component but the standard Role code works as expected in the "/" page.
Most likely I should have restarted my computer as it now works in the original location too.

Related

Blazor concurrency problem using Entity Framework Core

My goal
I want to create a new IdentityUser and show all the users already created through the same Blazor page. This page has:
a form through you will create an IdentityUser
a third-party's grid component (DevExpress Blazor DxDataGrid) that shows all users using UserManager.Users property. This component accepts an IQueryable as a data source.
Problem
When I create a new user through the form (1) I will get the following concurrency error:
InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread-safe.
I think the problem is related to the fact that CreateAsync(IdentityUser user) and UserManager.Users are referring the same DbContext
The problem isn't related to the third-party's component because I reproduce the same problem replacing it with a simple list.
Step to reproduce the problem
create a new Blazor server-side project with authentication
change Index.razor with the following code:
#page "/"
<h1>Hello, world!</h1>
number of users: #Users.Count()
<button #onclick="#(async () => await Add())">click me</button>
<ul>
#foreach(var user in Users)
{
<li>#user.UserName</li>
}
</ul>
#code {
[Inject] UserManager<IdentityUser> UserManager { get; set; }
IQueryable<IdentityUser> Users;
protected override void OnInitialized()
{
Users = UserManager.Users;
}
public async Task Add()
{
await UserManager.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" });
}
}
What I noticed
If I change Entity Framework provider from SqlServer to Sqlite then the error will never show.
System info
ASP.NET Core 3.1.0 Blazor Server-side
Entity Framework Core 3.1.0 based on SqlServer provider
What I have already seen
Blazor A second operation started on this context before a previous operation completed: the solution proposed doesn't work for me because even if I change my DbContext scope from Scoped to Transient I still using the same instance of UserManager and its contains the same instance of DbContext
other guys on StackOverflow suggests creating a new instance of DbContext per request. I don't like this solution because it is against Dependency Injection principles. Anyway, I can't apply this solution because DbContext is wrapped inside UserManager
Create a generator of DbContext: this solution is pretty like the previous one.
Using Entity Framework Core with Blazor
Why I want to use IQueryable
I want to pass an IQueryable as a data source for my third-party's component because its can apply pagination and filtering directly to the Query. Furthermore IQueryable is sensitive to CUD
operations.
UPDATE (08/19/2020)
Here you can find the documentation about how to use Blazor and EFCore together
UPDATE (07/22/2020)
EFCore team introduces DbContextFactory inside Entity Framework Core .NET 5 Preview 7
[...] This decoupling is very useful for Blazor applications, where using IDbContextFactory is recommended, but may also be useful in other scenarios.
If you are interested you can read more at Announcing Entity Framework Core EF Core 5.0 Preview 7
UPDATE (07/06/2020)
Microsoft released a new interesting video about Blazor (both models) and Entity Framework Core. Please take a look at 19:20, they are talking about how to manage concurrency problem with EFCore
General solution
I asked Daniel Roth BlazorDeskShow - 2:24:20 about this problem and it seems to be a Blazor Server-Side problem by design.
DbContext default lifetime is set to Scoped. So if you have at least two components in the same page which are trying to execute an async query then we will encounter the exception:
InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread-safe.
There are two workaround about this problem:
(A) set DbContext's lifetime to Transient
services.AddDbContext<ApplicationDbContext>(opt =>
opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Transient);
(B) as Carl Franklin suggested (after my question): create a singleton service with a static method which returns a new instance of DbContext.
anyway, each solution works because they create a new instance of DbContext.
About my problem
My problem wasn't strictly related to DbContext but with UserManager<TUser> which has a Scoped lifetime. Set DbContext's lifetime to Transient didn't solve my problem because ASP.NET Core creates a new instance of UserManager<TUser> when I open the session for the first time and it lives until I don't close it. This UserManager<TUser> is inside two components on the same page. Then we have the same problem described before:
two components that own the same UserManager<TUser> instance which contains a transient DbContext.
Currently, I solved this problem with another workaround:
I don't use UserManager<TUser> directly instead, I create a new instance of it through IServiceProvider and then it works. I am still looking for a method to change the UserManager's lifetime instead of using IServiceProvider.
tips: pay attention to services' lifetime
This is what I learned. I don't know if it is all correct or not.
I downloaded your sample and was able to reproduce your problem. The problem is caused because Blazor will re-render the component as soon as you await in code called from EventCallback (i.e. your Add method).
public async Task Add()
{
await UserManager.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" });
}
If you add a System.Diagnostics.WriteLine to the start of Add and to the end of Add, and then also add one at the top of your Razor page and one at the bottom, you will see the following output when you click your button.
//First render
Start: BuildRenderTree
End: BuildRenderTree
//Button clicked
Start: Add
(This is where the `await` occurs`)
Start: BuildRenderTree
Exception thrown
You can prevent this mid-method rerender like so....
protected override bool ShouldRender() => MayRender;
public async Task Add()
{
MayRender = false;
try
{
await UserManager.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" });
}
finally
{
MayRender = true;
}
}
This will prevent re-rendering whilst your method is running. Note that if you define Users as IdentityUser[] Users you will not see this problem because the array is not set until after the await has completed and is not lazy evaluated, so you don't get this reentrancy problem.
I believe you want to use IQueryable<T> because you need to pass it to 3rd party components. The problem is, different components can be rendered on different threads, so if you pass IQueryable<T> to other components then
They might render on different threads and cause the same problem.
They most likely will have an await in the code that consumes the IQueryable<T> and you'll have the same problem again.
Ideally, what you need is for the 3rd party component to have an event that asks you for data, giving you some kind of query definition (page number etc). I know Telerik Grid does this, as do others.
That way you can do the following
Acquire a lock
Run the query with the filter applied
Release the lock
Pass the results to the component
You cannot use lock() in async code, so you'd need to use something like SpinLock to lock a resource.
private SpinLock Lock = new SpinLock();
private async Task<WhatTelerikNeeds> ReadData(SomeFilterFromTelerik filter)
{
bool gotLock = false;
while (!gotLock) Lock.Enter(ref gotLock);
try
{
IUserIdentity result = await ApplyFilter(MyDbContext.Users, filter).ToArrayAsync().ConfigureAwait(false);
return new WhatTelerikNeeds(result);
}
finally
{
Lock.Exit();
}
}
Perhaps not the best approach but rewriting async method as non-async fixes the problem:
public void Add()
{
Task.Run(async () =>
await UserManager.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" }))
.Wait();
}
It ensures that UI is updated only after the new user is created.
The whole code for Index.razor
#page "/"
#inherits OwningComponentBase<UserManager<IdentityUser>>
<h1>Hello, world!</h1>
number of users: #Users.Count()
<button #onclick="#Add">click me. I work if you use Sqlite</button>
<ul>
#foreach(var user in Users.ToList())
{
<li>#user.UserName</li>
}
</ul>
#code {
IQueryable<IdentityUser> Users;
protected override void OnInitialized()
{
Users = Service.Users;
}
public void Add()
{
Task.Run(async () => await Service.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" })).Wait();
}
}
I found your question looking for answers about the same error message you had.
My concurrency issue appears to have been due to a change that triggered a re-rendering of the visual tree to occur at the same time as (or due to the fact that) I was trying to call DbContext.SaveChangesAsync().
I solved this by overriding my component's ShouldRender() method like this:
protected override bool ShouldRender()
{
if (_updatingDb)
{
return false;
}
else
{
return base.ShouldRender();
}
}
I then wrapped my SaveChangesAsync() call in code that set a private bool field _updatingDb appropriately:
try
{
_updatingDb = true;
await DbContext.SaveChangesAsync();
}
finally
{
_updatingDb = false;
StateHasChanged();
}
The call to StateHasChanged() may or may not be necessary, but I've included it just in case.
This fixed my issue, which was related to selectively rendering a bound input tag or just text depending on if the data field was being edited. Other readers may find that their concurrency issue is also related to something triggering a re-render. If so, this technique may be helpful.
Well, I have a quite similar scenario with this, and I 'solve' mine is to move everything from OnInitializedAsync() to
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(firstRender)
{
//Your code in OnInitializedAsync()
StateHasChanged();
}
{
It seems solved, but I had no idea to find out the proves. I guess just skip from the initialization to let the component success build, then we can go further.
/******************************Update********************************/
I'm still facing the problem, seems I'm giving a wrong solution to go. When I checked with this Blazor A second operation started on this context before a previous operation completed I got my problem clear. Cause I'm actually dealing with a lot of components initialization with dbContext operations. According to #dani_herrera mention that if you have more than 1 component execute Init at a time, probably the problem appears.
As I took his advise to change my dbContext Service to Transient, and I get away from the problem.
#Leonardo Lurci Had covered conceptually. If you guys are not yet wanting to move to .NET 5.0 preview, i would recommend looking at Nuget package 'EFCore.DbContextFactory', documentation is pretty neat. Essential it emulates AddDbContextFactory. Ofcourse, it creates a context per component.
So far, this is working fine for me so far without any problems...
I ensure single-threaded access by only interacting with my DbContext via a new DbContext.InvokeAsync method, which uses a SemaphoreSlim to ensure only a single operation is performed at a time.
I chose SemaphoreSlim because you can await it.
Instead of this
return Db.Users.FirstOrDefaultAsync(x => x.EmailAddress == emailAddress);
do this
return Db.InvokeAsync(() => ...the query above...);
// Add the following methods to your DbContext
private SemaphoreSlim Semaphore { get; } = new SemaphoreSlim(1);
public TResult Invoke<TResult>(Func<TResult> action)
{
Semaphore.Wait();
try
{
return action();
}
finally
{
Semaphore.Release();
}
}
public async Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> action)
{
await Semaphore.WaitAsync();
try
{
return await action();
}
finally
{
Semaphore.Release();
}
}
public Task InvokeAsync(Func<Task> action) =>
InvokeAsync<object>(async () =>
{
await action();
return null;
});
public void InvokeAsync(Action action) =>
InvokeAsync(() =>
{
action();
return Task.CompletedTask;
});
#Leonardo Lurci has a great answer with multiple solutions to the problem. I will give my opinion about every solution and which I think it is the best one.
Making DBContext transient - it is a solution but it is not optimized for this cases..
Carl Franklin suggestion - the singleton service will not be able to control the lifetime of the context and will depend on the service requester to dispose the context after use.
Microsoft documentation they talk about injecting DBContext Factory into a component with the IDisposable interface to Dispose the context when the component is destroied. This is not a very good solution, because a lot of problems happen with it, like: performing a context operation and leaving the component before it finishes that operation, will dispose the context and throw exception..
Finally. The best solution so far is to inject the DBContext Factory in the component yes, but whenever you need it, you create a new instance with using statement like bellow:
public async Task GetSomething()
{
using var context = DBFactory.CreateDBContext();
return await context.Something.ToListAsync();
}
Since DbFactory is optimazed when creating new context instances, there is no significante overhead, making it a better choice and better performing than Transient context, it also disposes the context at the end of the method because of "using" statement.
Hope it was useful.

Permission defaults ignored in Orchard

I try to set permissions for my new module. Otherwise they seem to work, but the defaults are ignored, nothing is checked for role-permission pairs I've set in the code. My code (Permissions.cs) seems OK:
using System.Collections.Generic;
using Orchard.Environment.Extensions.Models;
using Orchard.Security.Permissions;
using Orchard.Environment.Extensions;
using My.Module.Utils;
namespace My.Module
{
public class Permissions : IPermissionProvider {
public static readonly Permission AccessMyModule = new Permission {
Description = Constants.AccessAddon, Name = "AccessMyModule"
};
public virtual Feature Feature { get; set; }
public IEnumerable<Permission> GetPermissions() {
return new[] {
AccessMyModule
};
}
public IEnumerable<PermissionStereotype> GetDefaultStereotypes() {
return new[] {
new PermissionStereotype {
Name = Constants.MyModuleAdministratorRole,
Permissions = new[] { AccessMyModule }
}
};
}
}
}
I double checked that all the constants stored in Constants and the references for them are valid. The code snippet here is simplified, in fact I have more permissions and more roles in my project, but I confirmed commenting out everything but one permission and one role doesn't fix the problem. The defaults for the other modules in the same solution work fine, though there's no bug reported by IntelliSense and everything else in the module seems to work. So where else could be the root of the problem?
EDIT: I followed #mdameer's comment and confirmed that GetDefaultStereotypes() really runs only after reinstall. However, an error occurred while enabling the module after reinstall, so the defaults were not loaded. I know that the supposed way is to check the role - permission name in the dashboard, but I would like to find another workaround, because
I would like to solve the error that occurred and not to risk it happenning on the production server as well, and I can't delete and reinstall this rather complex module several times just to debug this. It was likely caused by the reinstall process, nowhere in the permission initialization, but I can't know without running the code.
there are tens of roles affected, so relying on someone to click the permissions by hand each time means that there will likely be errors due to human factor.
The GetDefaultStereotypes() method is being called from class DefaultRoleUpdater in Orchard.Roles. It is called automatically from somewhere deep in the Orchard core, so simply mimicking the call and running it on startup isn't that easy. I also tried to mimic the whole function and placed it into my permissions class (or into a custom service), but now I got lost on how to run it. It is not static, but it either is part of or refers to my Permissions class, which doesn't allow for ordinary referencing by default (it has no proper constructor) and I don't want to mess it even more by changing the class to something it is not and shouldn't be.
Just set the default permissions you need in a migration instead of using GetDefaultStereotypes(). Here is a short example:
public class MyMigration: Orchard.Data.Migration.DataMigrationImpl
{
// public
public MyMigration(Orchard.Roles.Services.IRoleService aRoleService)
{
mRoleService = aRoleService;
}
public int Create()
{
//mRoleService.CreateRole("MyRoleName");
//mRoleService.UpdateRole("MyRoleName", MyPermissions)
return 1;
}
// private
Orchard.Roles.Services.IRoleService aRoleService mRoleService;
}

The view or its master was not found or no view engine supports the searched locations

Error like:The view 'LoginRegister' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/MyAccount/LoginRegister.aspx
~/Views/MyAccount/LoginRegister.ascx
~/Views/Shared/LoginRegister.aspx
~/Views/Shared/LoginRegister.ascx
~/Views/MyAccount/LoginRegister.cshtml
~/Views/MyAccount/LoginRegister.vbhtml
~/Views/Shared/LoginRegister.cshtml
~/Views/Shared/LoginRegister.vbhtml
Actually my page view page is ~/Views/home/LoginRegister.cshtml so what i do
and my RouteConfig is
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 = "MyAccount", action = "LoginRegister", id = UrlParameter.Optional }
);
}
}
Be careful if your model type is String because the second parameter of View(string, string) is masterName, not model. You may need to call the overload with object(model) as the second parameter:
Not correct :
protected ActionResult ShowMessageResult(string msg)
{
return View("Message",msg);
}
Correct :
protected ActionResult ShowMessageResult(string msg)
{
return View("Message",(object)msg);
}
OR (provided by bradlis7):
protected ActionResult ShowMessageResult(string msg)
{
return View("Message",model:msg);
}
Problem:
Your View cannot be found in default locations.
Explanation:
Views should be in the same folder named as the Controller or in the Shared folder.
Solution:
Either move your View to the MyAccount folder or create a HomeController.
Alternatives:
If you don't want to move your View or create a new Controller you can check at this link.
In Microsoft ASP.net MVC, the routing engine, which is used to parse incoming and outgoing URL Combinations, is designed with the idea of Convention over Configuration. What this means is that if you follow the Convention (rules) that the routing engine uses, you don't have to change the Configuration.
The routing engine for ASP.net MVC does not serve web pages (.cshtml). It provides a way for a URL to be handled by a Class in your code, which can render text/html to the output stream, or parse and serve the .cshtml files in a consistent manner using Convention.
The Convention which is used for routing is to match a Controller to a Class with a name similar to ControllerNameController i.e. controller="MyAccount" means find class named MyAccountController. Next comes the action, which is mapped to a function within the Controller Class, which usually returns an ActionResult. i.e. action="LoginRegister" will look for a function public ActionResult LoginRegister(){} in the controller's class. This function may return a View() which would be by Convention named LoginRegister.cshtml and would be stored in the /Views/MyAccount/ folder.
To summarize, you would have the following code:
/Controllers/MyAccountController.cs:
public class MyAccountController : Controller
{
public ActionResult LoginRegister()
{
return View();
}
}
/Views/MyAccount/LoginRegister.cshtml: Your view file.
In your LoginRegister action when returning the view, do below, i know this can be done in mvc 5, im not sure if in mvc 4 also.
public ActionResult Index()
{
return View("~/Views/home/LoginRegister.cshtml");
}
Check the build action of your view (.cshtml file) It should be set to content. In some cases, I have seen that the build action was set to None (by mistake) and this particular view was not deploy on the target machine even though you see that view present in visual studio project file under valid folder
This could be a permissions issue.
I had the same issue recently. As a test, I created a simple hello.html page. When I tried loading it, I got an error message regarding permissions. Once I fixed the permissions issue in the root web folder, both the html page and the MVC rendering issues were resolved.
Check whether the View (.ASPX File) that you have created is having the same name as mentioned in the Controller. For e.g:
public ActionResult GetView()
{
return View("MyView");
}
In this case, the aspx file should be having the name MyView.aspx instead of GetView.aspx
I got this error because I renamed my View (and POST action).
Finally I found that I forgot to rename BOTH GET and POST actions to new name.
Solution : Rename both GET and POST actions to match the View name.
If the problem happens intermittently in production, it could be due to an action method getting interrupted. For example, during a POST operation involving a large file upload, the user closes the browser window before the upload completes. In this case, the action method may throw a null reference exception resulting from a null model or view object. A solution would be to wrap the method body in a try/catch and return null. Like this:
[HttpPost]
public ActionResult Post(...)
{
try
{
...
}
catch (NullReferenceException ex) // could happen if POST is interrupted
{
// perhaps log a warning here
return null;
}
return View(model);
}
I had this same issue.
I had copied a view "Movie" and renamed it "Customer" accordingly.
I also did the same with the models and the controllers.
The resolution was this...I rename the Customer View to Customer1 and
just created a new view and called it Customer....I then just copied
the Customer1 code into Customer.
This worked.
I would love to know the real cause of the problem.
UPDATE
Just for grins....I went back and replicated all the renaming scenario again...and did not get any errors.
I came across this error due to the improper closing of the statement,
#using (Html.BeginForm("DeleteSelected", "Employee", FormMethod.Post))
{
} //This curly bracket needed to be closed at the end.
In Index.cshtml view file.I didn't close the statement at the end of the program. instead, I ended up closing improperly and ran into this error.
I was sure there isn't a need of checking Controller ActionMethod code because I have returned the Controller method properly to the View. So It has to be the view that's not responding and met with similar Error.
If you've checked all the things from the above answers (which are common mistakes) and you're sure that your view is at the location in the exceptions, then you may need to restart Visual Studio.
:(
In my case, I needed to use RedirectToAction to solve the problem.
[HttpGet]
[ControleDeAcessoAuthorize("Report/ExportToPDF")]
public ActionResult ExportToPDF(int id, string month, string output)
{
try
{
// Validate
if (output != "PDF")
{
throw new Exception("Invalid output.");
}
else
{
...// code to generate report in PDF format
}
}
catch (Exception ex)
{
return RedirectToAction("Error");
}
}
[ControleDeAcessoAuthorize("Report/Error")]
public ActionResult Error()
{
return View();
}
I ran into this a while ago and it drove me crazy because it turned out to be simple. So within my View I was using a grid control that obtained data for the grid via an http request. Once the middle tier completed my request and returned the dataset, I received the same error. Turns out my return statement was 'return View(dataset);' instead of 'return Json(dataset);
I couldn't find any solution to this problem, until I found out the files didn't exist!
This took me a long time to figure out, because the Solution Explorer shows the files!
But when I click on Index.cshtml I get this error:
So that was the reason for this error to show. I hope this answer helps somebody.

Resolving constructor dependency on service used in NancyFX

I have the following bootstrap
public class NancyBootStrapper: DefaultNancyBootstrapper
{
protected override void ConfigureRequestContainer(TinyIoC.TinyIoCContainer container, NancyContext context)
{
base.ConfigureRequestContainer(container, context);
var ravenSession = container.Resolve< IRavenSessionProvider >().GetSession();
container.Register( ravenSession );
}
}
When my Nancy app tries to instantiate BlogService using the following constructor
public BlogService(IDocumentSession documentSession)
{
this.documentSession = documentSession;
}
the application blows up stating that it can't resolve document session, I have also tried the following within my test method (removing the constructor injection).
public void BuildCategories()
{
var container = TinyIoCContainer.Current;
documentSession = container.Resolve< IDocumentSession >();
documentSession.Store(new Category{Title = "test"});
documentSession.Store(new Category{Title = ".net"});
documentSession.SaveChanges();
}
This also blows up, pointing out that it can't resolve documentSession.
Now this is the first time I have used either NancyFX or TinyIoC so I could be doing something fundamentally wrong though I should mention that the documentSession does resolve within a Nancy module..
Can any one offer a fix or some suggestions?
When is the BlogService supposed to be instantiated? -My guess would be once for the application, in which case I believe you are registering the session in the wrong bootstrapper method, and should do it in ConfigureApplicationContainer.
I've been playing & digging into both NancyFx and the TinyIoC code bases and have figured out how to fix this issue... I don't like the fix... but hay it works :)
Basically, I am creating a RavenDB document session in the bootstrapper method configureRequestContainer as it is best practice to use the request as your unit of work scope.
Unfortunately anything that is auto wired by tinyIoC within configureApplicationContainer does not have any constructor injection performed using the child container being used by the Nancy request (this includes those that are marked as MultiInstance or as PerRequestSingleton.
To get around this, you need to re-register any components that depend on your per request components within the same child container.
As I said, I don't like the fix, but it is ultimately a fix :)

LockRecursionException calling Route.GetVirtualPath from another route's GetVirtualData in .Net 4

I have a route defined last in my ASP.Net MVC 2 app that will map old urls that are no longer used to the appropriate new urls to be redirected to. This route returns the action and controller that is responsible for actually performing the redirect and it also returns a url to the controller action which is the url to redirect to. Since the route is responsible for generating the new url to redirect to, it is calling the router to get the appropriate urls. This has worked just fine with .Net 3.5, but when I upgraded to .Net 4, the GetVirtualPath method throws a System.Threading.LockRecursionException: "Recursive read lock acquisitions not allowed in this mode.". The following code resolves the problem but is pretty ugly:
public static string GetActionUrl(HttpContextBase context, string routeName, object routeValues)
{
RequestContext requestContext = new RequestContext(context, new RouteData());
VirtualPathData vp = null;
try
{
vp = _Routes.GetVirtualPath(requestContext, routeName, new RouteValueDictionary(routeValues));
}
catch (System.Threading.LockRecursionException)
{
var tmpRoutes = new RouteCollection();
Router.RegisterRoutes(tmpRoutes);
vp = tmpRoutes.GetVirtualPath(requestContext, routeName, new RouteValueDictionary(routeValues));
}
if (vp == null)
throw new Exception(String.Format("Could not find named route {0}", routeName));
return vp.VirtualPath;
}
Does anybody know what changes in .Net 4 might have caused this error? Also, is calling a route from another route's GetRouteData method just bad practice and something I should not be doing at all?
As you figured out, it is not supported to call a route in the global route table from within another route in the global route table.
The global route table is a thread-safe collection to enable multiple readers or a single router. Unfortunately the code you have was never supported, even in .NET 3.5, though in some scenarios it may have coincidentally worked.
As a general note, routes should function independent of one another, so I'm not sure what your scenario is here.
v3.5 RouteCollection uses the following code:
private ReaderWriterLockSlim _rwLock;
public IDisposable GetReadLock()
{
this._rwLock.EnterReadLock();
return new ReadLockDisposable(this._rwLock);
}
v4.0 RouteCollection uses the following code:
private ReaderWriterLock _rwLock;
public IDisposable GetReadLock()
{
this._rwLock.AcquireReaderLock(-1);
return new ReadLockDisposable(this._rwLock);
}
GetRouteData(HttpContextBase httpContext) in both versions use the following code:
public RouteData GetRouteData(HttpContextBase httpContext)
{
...
using (this.GetReadLock())
{
GetVirtualPath uses the same logic.
The ReaderWriterLock used in v4.0 does not allow Recursive read locks by default which is why the error is occurring.
Copy routes to a new RouteCollection for second query, or change the ReaderWriterLock mode by reflecting in.