Navigation Identity for VS 2013 - Identity Version 2.0

Mar 26 at 5:08 PM
Hello Graham,

Just to let you know I have updated the sample project from ASP.NET Identity 1.0 to 2.0 without issue - it was simply a case of running in the NuGet package and forcing the underlying database to rebuild (I think this may be a problem with the ASP.NET Identity migration as others have commented on this).

The next step, I think, is to extend the demo to replicate some of the later features and functionality demonstrated by the Microsoft.AspNet.Identity.Samples -Version 2.0.0-beta2 MVC project.
Coordinator
Mar 26 at 9:06 PM
That's a great start, glad to hear it's backward compatible.
Let me know if you find any of the sample puzzling or you just want to chat about some code.

Cheers,
Graham
Coordinator
Apr 10 at 5:24 PM
Edited Apr 10 at 5:24 PM
Hi,

Just wondered how you're getting on and whether you need any help?
Apr 10 at 6:57 PM
Hi Graham,

I'm afraid, despite my best intentions, I have not made very much progress. Work has, unexpectedly, gone crazy (being self-employed it is often a case of feast or famine) and that has limited my time - sorry. Hopefully things will settle down shortly. Anyway, enough excuses.

I have spent some time looking at general Web Forms data binding (this was new to me). I also note that Microsoft have added Identity 2.0 Visual Studio 2013 Update 2 RC, so I will review this too.

Thank you for your continued support.
Coordinator
Apr 10 at 7:50 PM
No worries, the day job puts food on the table.

Sounds like you're looking into the right things. Hope you like the new Web Forms data binding as much as I do.
Apr 25 at 11:11 AM
Hi Graham,

Unexpectedly, I have found myself with a couple of days of free (well, you know what I mean!) time while my customers test some features and am having a crack a creating a clean ASP.net Identity 2.0 example web site.

So far so good, I have installed the Navigation framework and all seems to be working correctly (very neat by the way), but I do have one immediate question (I'm sure there will be more!).

Currently, as per you original identity example, my site home page is defined by the StateInfo dialog definition:
<dialog key="Home" initial="Page" path="~/Views/Home/Index.aspx">
   <state key="Page" 
      page="~/Views/Home/Index.aspx"
      route="Authentication/Home" 
      defaultTypes="Logoff=bool"
      trackCrumbTrail="false">
   </state>
</dialog>
and I can navigate to the page as expected, e.g http://localhost/Authentication/Home My question is, how do I define a default dialog/route so that when I open the site (http://localhost/) I can route to my chosen starting dialog? I have a nagging feeling that I am missing an important concept here and that I might be confusing Navigation with routing and how they interact - perhaps you could point me in the right direction? Thanks.
Coordinator
Apr 25 at 5:01 PM
Edited Apr 25 at 5:02 PM
Good question. I'm sure at one point .NET had problems with mapping a Web Form to a blank route but I've just tried it in VS 2013 and it works fine.

I currently don't allow it because I consider a blank route attribute the same as not supplying one, but looks like I need to rethink this. I've raised Issue #4 to cover this and hopefully I'll come up with something for the next release.

You can work around it by changing the path attribute of the dialog to "~/Default.aspx" and then requesting http ://localhost will redirect to that dialog. However, the url in the browser will change to the route of the state.
Coordinator
Apr 25 at 10:28 PM
I've fixed Issue #4, in the next release you'll be able to configure the blank route using ~, e.g.,
<state key="Listing" page="~/Listing.aspx" route="~">
</state>
Apr 27 at 10:36 AM
Thanks Graham - sounds good.
Coordinator
Jul 13 at 3:42 PM
I've released version 1.9 of Navigation for ASP.NET which means you can specify a default route (by setting the route attribute to ~).

As part of this release I've simplified the documentation. This means I've removed the link to the Navigation Identity sample. But I'm still hopeful that you'll update it to use the latest Identity version. Have you had any luck or did you run out of steam?
Jul 13 at 4:23 PM
I am ashamed to admit that I ran out of steam. I could bore you with a list of excuses, but I will, at least, spare you that! Things should be calming down in a week or two (I'm sure I said that before too!) and I have promised myself that I would pick this up again then as I need to develop a core identity/membership template for some upcoming projects so it would make sense to kill two birds with the one stone.
Jul 14 at 4:35 PM
Hi Graham, I've made some progress with this today - at least there is a new web project where you can register, login and logout. The new default navigation route is excellent - works beautifully.

Before I go too much farther, I wanted to check something with you... I would normally create two library classes, one for the identity models and one for the data EF context implementation. Since both these libraries will need to reference the Identity.Core and Identity.EntityFramework libraries, this does not achieve the degree of abstraction that I would normally try and achieve, but old habits die hard!

For this project however, would you prefer it if everything was self-contained in the one web project?
Coordinator
Jul 14 at 5:25 PM
Hi, part of the thanks for the default route goes to you. Have a look at the bottom of the release notes, http://navigation.codeplex.com/releases/view/125376, where I mention you by name.

I agree with you that the sample Identity code isn't great. I wouldn't have the EF in with the ViewModels and all the code stuffed into the Controller. But my aim for rewriting the Identity code was to show how much better Web Forms could be by using Navigation for ASP.NET. I decided to structure the code exactly the same as the MVC Identity code to show it could have all the goodness typically associated with MVC.

This made my life simple because I could just copy a lot of the existing code without having to think too much about it. The only thinking I did was when it came to adding in the Navigation concepts to the UI.
Jul 15 at 4:38 PM
I have made some good progress today and now only have a couple more views to implement (I am essentially replicating the features from the ASP.net Identity 2 WebForms sample - and trying to tidy things up a bit as I go without refactoring the whole project - as you suggested). Unfortunately, I'm going to have to leave it for a day or two now, but hopefully will get back to it at the end of the week.

My main reason for writing however was to say how impressed I am with your Navigation library. I hadn't really 'got it' until the penny dropped this afternoon and now I'm in love with it!

I'll give you an update in a day or two.
Coordinator
Jul 15 at 6:57 PM
You're too kind. I'm so glad you like it.

Did you notice that I wrote two Navigation Identity samples? The second one is a Single Page Application that I think you'll find interesting. The code's very similar to the first sample but with a slight tweak that means Navigation for ASP.NET turns the hyperlinks into Ajax requests. I'd be interested to hear what you think about it.
Jul 15 at 8:27 PM
I did - looks good. I was going to have a crack at the SPA project once I get the standard pages nailed down.
Sat at 10:03 AM
Hi Graham,

I'm pretty much there with 'version 0.1' - just need to tidy up a couple of routes and comment the code a little better and I think I'm done. I was going to load the project onto a publicly accessible web server so you could have a look, but I have run into a problem during deployment. When I load the project onto an IIS 8.0 Windows 2012 web server, I get the following 'yellow screen of death':

[MissingMethodException: Method not found: 'System.Web.Http.Controllers.ServicesContainer System.Web.Http.HttpConfiguration.get_Services()'.]
Navigation.WebApiRouteConfig..cctor() +0

[TypeInitializationException: The type initializer for 'Navigation.WebApiRouteConfig' threw an exception.]
Navigation.WebApiRouteConfig.AddHttpRoute(State state) +0
Navigation.StateInfoConfig.AddStateRoutes() +805

[InvalidOperationException: The pre-application start initialization method AddStateRoutes on type Navigation.StateInfoConfig threw an exception with the following error message: The type initializer for 'Navigation.WebApiRouteConfig' threw an exception..]
System.Web.Compilation.BuildManager.InvokePreStartInitMethodsCore(ICollection1 methods, Func1 setHostingEnvironmentCultures) +12969195
System.Web.Compilation.BuildManager.InvokePreStartInitMethods(ICollection`1 methods) +12968904
System.Web.Compilation.BuildManager.CallPreStartInitMethods(String preStartInitListPath, Boolean& isRefAssemblyLoaded) +280
System.Web.Compilation.BuildManager.ExecutePreAppStart() +172
System.Web.Hosting.HostingEnvironment.Initialize(ApplicationManager appManager, IApplicationHost appHost, IConfigMapPathFactory configMapPathFactory, HostingEnvironmentParameters hostingParameters, PolicyLevel policyLevel, Exception appDomainCreationException) +1151

[HttpException (0x80004005): The pre-application start initialization method AddStateRoutes on type Navigation.StateInfoConfig threw an exception with the following error message: The type initializer for 'Navigation.WebApiRouteConfig' threw an exception..]
System.Web.HttpRuntime.FirstRequestInit(HttpContext context) +12968244
System.Web.HttpRuntime.EnsureFirstRequestInit(HttpContext context) +159
System.Web.HttpRuntime.ProcessRequestNotificationPrivate(IIS7WorkerRequest wr, HttpContext context) +12807949

A quick Google suggested that this was down to IIS using an older version of System.Web.Http, but I don't think this is the case.

The IIS box is fully patched and is happily running other ASP.net 4.5 Web Forms using Owin and ASP.net Identity. I suspect the problem is a local IIS config issue, but I thought I would ask if you had seen it before?

Neil.
Coordinator
Sat at 11:07 AM
Great work. I'm looking forward to seeing it.

Thanks for letting me now about the exception. In Navigation for ASP.NET 1.9 I added support for WebApi and it's this code that's throwing the exception. The code is trying to register a ValueProvider factory so that NavigationData can fit into the WebApi data binding framework. I should probably only register this provider if I detect a WebApi State in the StateInfo.config. But at the moment I'm always registering it.

I think what your Google suggested might be the cause. If this is the problem then it won't effect your other ASP.NET 4.5 Web Forms apps because they won't be using WebApi. One way to test this out is if you try to deploy a ASP.NET WebApi app to the same server. The one generated by the VS templates should be sufficient for the test with the following line added to the WebApiConfig file (just to make sure)
var services = config.Services;
Thanks again,
Graham
Sat at 11:56 AM

Hi Graham,

Created a new WebApi project as suggested and added the services reference to the WebApiConfig.cs file. Deployed the project to the same IIS web site (i.e. overwrote the Identity project) and it works fine – see http://navid.abldev.com.

Maybe I should just add the WebApi package to the Identity project?

Neil.

Coordinator
Sat at 12:05 PM
That's interesting. Adding the WebApi package is a good idea.

Once we get to the bottom of the problem I'll create an issue (and probably do a patch release).

Cheers,
Graham
Sat at 12:13 PM

Hi Graham,

Yes, that fixed it – great.

Note, I’ve taken the site off-line now until I tidy up a bit!

Thanks for your help.

Neil.

Coordinator
Sat at 12:22 PM
Phew. Thanks for taking time to think through the problem with me.

Sounds like I need to patch the release because I've broken all existing Navigation Web Form deployments. Do you agree?

Cheers,
Graham
Sat at 12:35 PM

It’s possibly/likely, though I suspect many projects have WebApi references in them these days, but you can’t rely on it. Interestingly it was fine under local IIS Express development, it was only when it was deployed to a full-fat IIS server that it failed (this server is particularly ‘clean’ as it was only rebuilt 2/3 weeks ago).

Even so, on balance, it would be safest to patch the code to avoid any production aggravation.

Sorry – didn’t mean to make work for you.

Neil.

Coordinator
Sat at 12:55 PM
Don't apologise, you're doing my work for me. I appreciate the help.

I've created Issue #5 and pasted in some info from this thread. I'm going to fix the issue by moving the registration of the WebApi ValueProvider factory so it's only done if a WebApi route is found (a small change to WebApiRouteConfig.cs)

Would you try it out for me before I release it to NuGet, please?
Sat at 1:00 PM

Sure no problem.

Neil

Coordinator
Sat at 4:02 PM
Just thought that perhaps you'd like to fix the bug? It'd be a shame for me to take the glory when you've done the hard work.
Sat at 4:09 PM

Ha Ha, I’m happy for you to take the glory and I suspect that this is beyond my skill-set.

I did download the project source code, but couldn’t open the solution (something about unrecognised project type ‘DSL’) and at that point I realised my limitations!

Neil.

Coordinator
Sat at 4:16 PM
If the project's too hard to work on then that's my fault and not yours. It's an area I'm trying to improve on, so don't give up on it yet.

The DSL is only needed if you're working on the Navigation Designer, a Visual Studio plug in for graphically building the StateInfo configuration.

You won't need to touch the DSL project to fix the issue. Does the solution not open at all or is it that the solution opens but the DSL project isn't loaded?

Graham
Sat at 4:27 PM

It does load, with errors, but it won’t build…

· Error 1 The imported project "C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v12.0\DSLTools\Microsoft.DslTools.settings.targets" was not found. Confirm that the path in the <Import> declaration is correct, and that the file exists on disk. D:\Users\nmartin.ABLDEV\Desktop\navigation_892823517e6f\Dsl\Dsl.csproj 4 3 Navigation.Designer.Test

· Warning 2 The referenced component 'Dsl' could not be found. Navigation.Designer.Test

· Warning 3 The referenced component 'Glimpse.Core' could not be found. Navigation.Glimpse

I tried downloading and installing the VS Visualization and Modelling SDK, but it refused to install because it requires the VS SDK (which it couldn’t find – looking for the disk now…).

Neil.

Coordinator
Sat at 4:34 PM
Edited Sat at 4:36 PM
What about if you Unload the three Dsl-specific projects called Dsl, DslPackage and Navigation.Designer.Test? Does the solution build then?
Or what about if you exclude them from the build?
Sat at 4:56 PM

OK, I have installed the various SDKs and can now build the solution without errors – yeah!

Looking at the Navigation.WebApiRouteConfig, I can see two static methods – one the class’s constructor. I am guessing (no I really am guessing J) that it is this constructor that needs to be modified? What I am not sure about is how you would check for an existing WebApi route. Three things (at least) bother me…

1) How can you be certain that the WebApi route is registered before the Navigation invocation?

2) How can you check if a route has been registered without access to the route table or, at least, an HttpContextBase?

3) What if the developer has overridden the default route?

routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = System.Web.Http.RouteParameter.Optional }
);

Sorry, I bet you wish you had done this yourself now – but I did warn you J

Neil.

Coordinator
Sat at 5:13 PM
I definitely don't wish I'd done it myself, I'm enjoying talking things over with you v much.

We only need to register our ValueProvider factory if there are any WebApi States in the StateInfo config file. A WebApi State is configured by adding apiController and action attributes instead of the page attribute you're used to for a WebForms State. So we don't check the route table we just look for these two State attributes.

You can see this check is already in place in the AddHttpRoute method in the same WebApiRouteConfig class. If we move the code from the static constructor inside this check and make sure it only runs once then that should do the trick.

Graham
Coordinator
Sat at 5:26 PM
I've just done a check in that separates the project into two solutions. One for the main Navigation code, Navigation.sln, and another for the Dsl, Navigation.Dsl.sln.

So the problem of having to install the SDK's should be a thing of the past because those pesky Dsl projects aren't part of the main solution.

Graham
Sat at 5:46 PM
You say that now.... :)

Ok, I'll have a play around - I assume I can test it by simply copying the .dll into the main project bin. I did notice that you have some unit tests configured but a) I'm really only familiar with the concept (more shame) and b) it's more an environment issue rather than a code/logic problem (possibly irrelevant, but it makes me feel better).

I'm going to have to sign off for the day, but will get back to it tomorrow.

Thanks for your patience - I am learning a lot.

Neil
Abilitation Limited

Coordinator
Sat at 6:38 PM
I'm grateful you're helping me out.

In theory copying the dll is all you need to do. You might have a problem because the Navigation project is built in .NET 4.5.1 and your project might use an earlier version. Your best bet is to run my automated build process which will generate a Navigation.dll for all the main framework types and then you can pick the one that's right for your project.

Here's how to run the automated build:
  1. Open a Visual Studio Command Prompt
  2. cd to the folder the source is in (the folder that contains the build.xml file)
  3. Run the command msbuild build.xml /t:Navigate
  4. When the command completes (it takes about a minute) a Build folder will be created alongside the source folder
  5. Find the Navigation.dll for the framework of .NET you're using inside the bin folder of this Build folder
Then you can drop this in your project's bin folder.

You won't need to add any unit tests for your code change but it's still worth getting familiar with them (the automated build process will check all these unit tests pass). I'll help you with this when you're ready.

Graham
Sun at 7:24 AM
Edited Sun at 7:50 AM
Hello Graham,

This is what I have come up with...
using System.Linq;
#if NET40Plus
using System.Web.Http;
using System.Web.Http.ValueProviders;
using System.Web.Routing;

namespace Navigation
{
    internal static class WebApiRouteConfig
    {
        private static volatile bool _isInitialised = false;

        static WebApiRouteConfig()
        {
            // GlobalConfiguration.Configuration.Services
            //  .Insert(typeof(ValueProviderFactory), 0, new NavigationDataValueWebApiProviderFactory());
        }

        internal static void AddHttpRoute(State state)
        {
            string apiController = state.Attributes["apiController"] != null ? state.Attributes["apiController"].Trim() : string.Empty;
            string action = state.Attributes["action"] != null ? state.Attributes["action"].Trim() : string.Empty;
            if (apiController.Length != 0 && action.Length != 0)
            {
                if (!_isInitialised)
                {
                    GlobalConfiguration.Configuration.Services
                        .Insert(typeof(ValueProviderFactory), 0, new NavigationDataValueWebApiProviderFactory());
                    _isInitialised = true;
                }
                state.StateHandler = new WebApiStateHandler();
                RouteValueDictionary defaults = StateInfoConfig.GetRouteDefaults(state, state.Route);
                defaults["controller"] = apiController;
                defaults["action"] = action;
                GlobalConfiguration.Configuration.Routes.MapHttpRoute("WebApi" + state.Id, state.Route, defaults);
                Route route = (Route) RouteTable.Routes["WebApi" + state.Id];
                route.DataTokens = new RouteValueDictionary() { { NavigationSettings.Config.StateIdKey, state.Id } };
                route.RouteHandler = new WebApiStateRouteHandler(state);
            }
        }
    }
}
#endif
I was hoping for something more elegant by checking the GlobalConfiguration.Configuration.Services collection (rather than setting a flag), but I could not find a good way of doing this - at least not particularly efficiently. I also had/have some concerns about thread safety.

The above does seem to work with the limited testing I have done so far. Comments please - I won't be offended!
Coordinator
Sun at 7:59 AM
I'm impressed. It looks v good. Great that it seems to work, too.

I had the same idea about checking the Services collection but agree a flag is better in this case.

We're ok with Thread safety because it's running inside the write lock on the route collection (see the AddStateRoutes method of the StateInfoConfig.cs).

Here are my comments, apologies if you were planning to do these anyway:
  1. Delete the static constructor. I don't like leaving in commented code.
  2. Rename _isInitialised to _Initialised. I don't think the 'is' is necessary and my naming standard is to start with capital after the underscore.
  3. Remove the volatile keyword. The code's only ever executed by a single Thread when the app starts.
Can you make the same changes to the RouteConfig.cs, please? This is the equivalent class but for Mvc States.
Sun at 8:29 AM
Here we go...

MVC - RouteConfig
#if NET40Plus
using System.Web.Mvc;
using System.Web.Routing;

namespace Navigation
{
    internal static class RouteConfig
    {

        private static bool _Initialised = false;

        internal static void AddRoute(State state)
        {
            string controller = state.Attributes["controller"] != null ? state.Attributes["controller"].Trim() : string.Empty;
            string action = state.Attributes["action"] != null ? state.Attributes["action"].Trim() : string.Empty;
            if (controller.Length != 0 && action.Length != 0)
            {
                if (!_Initialised)
                {
                    ValueProviderFactories.Factories.Insert(3, new NavigationDataMvcValueProviderFactory());
                    GlobalFilters.Filters.Add(new RefreshAjaxAttribute());
                    _Initialised = true;
                }
                string area = state.Attributes["area"] != null ? state.Attributes["area"].Trim() : string.Empty;
                state.StateHandler = new MvcStateHandler();
                Route route = RouteTable.Routes.MapRoute("Mvc" + state.Id, state.Route);
                route.Defaults = StateInfoConfig.GetRouteDefaults(state, state.Route);
                route.Defaults["controller"] = controller;
                route.Defaults["action"] = action;
                route.DataTokens = new RouteValueDictionary() { { NavigationSettings.Config.StateIdKey, state.Id } };
                if (area.Length != 0)
                    route.DataTokens["area"] = area;
                route.RouteHandler = new MvcStateRouteHandler(state);
            }
        }
    }
}
#endif
WebApi - WebApiRouteConfig
#if NET40Plus
using System.Web.Http;
using System.Web.Http.ValueProviders;
using System.Web.Routing;

namespace Navigation
{
    internal static class WebApiRouteConfig
    {

        private static bool _Initialised = false;

        internal static void AddHttpRoute(State state)
        {
            string apiController = state.Attributes["apiController"] != null ? state.Attributes["apiController"].Trim() : string.Empty;
            string action = state.Attributes["action"] != null ? state.Attributes["action"].Trim() : string.Empty;
            if (apiController.Length != 0 && action.Length != 0)
            {
                if (!_Initialised)
                {
                    GlobalConfiguration.Configuration.Services
                        .Insert(typeof(ValueProviderFactory), 0, new NavigationDataValueWebApiProviderFactory());
                    _Initialised = true;
                }
                state.StateHandler = new WebApiStateHandler();
                RouteValueDictionary defaults = StateInfoConfig.GetRouteDefaults(state, state.Route);
                defaults["controller"] = apiController;
                defaults["action"] = action;
                GlobalConfiguration.Configuration.Routes.MapHttpRoute("WebApi" + state.Id, state.Route, defaults);
                Route route = (Route)RouteTable.Routes["WebApi" + state.Id];
                route.DataTokens = new RouteValueDictionary() { { NavigationSettings.Config.StateIdKey, state.Id } };
                route.RouteHandler = new WebApiStateRouteHandler(state);
            }
        }
    }
}
#endif
By the way, I am having a problem with the build command (up until now I have simply pulled the Navigation project into my solution so I could trace/debug the code easily). When I run msbuild command I get 12 invalid references, for example:
  • error CS0234: The type or namespace name 'ValueProviders' does not exist in the namespace 'System.Web.Http'
  • error CS0246: The type or namespace name 'IValueProvider' could not be found
  • error CS0234: The type or namespace name 'Controllers' does not exist in the namespace 'System.Web.Http
  • error CS0246: The type or namespace name 'ValueProviderResult' could not be found
Sorry.
Coordinator
Sun at 8:48 AM
Thanks, that looks great.

Have you completed testing?
Are you sure it fixes the issue?

If you are, then the next stage is to submit the change as a pull request so I can merge it into the code base. That's exciting - you'll be my first ever contributor!

Here's some guidance on creating a Fork. I'm not great with source control but know enough to get by so let me know if you're not sure what to do.

Regarding the msbuild errors. I'll run the msbuild once I merge your changes in so it's not essential that we get it working at this stage. But it would be good if we could look at it after that. How does that sound?
Sun at 9:04 AM
I am pretty certain that it fixes my problem, not 100% sure it doesn't introduce another problem!

I think I am going to to build a completely new, clean project and test against that to make sure i have not got any superfluous references 'hiding' anywhere and then double check against that.
Sun at 9:39 AM
I spoke too soon. I have just created a new project with minimal references (i.e. a clean implementation) and added the modified Navigate.dll.

When I deploy the project to the IIS server, the error has returned. I can only assume that my earlier test project still had a reference to the WebApi library.
[MissingMethodException: Method not found: 'System.Web.Http.Controllers.ServicesContainer System.Web.Http.HttpConfiguration.get_Services()'.]
   Navigation.WebApiRouteConfig.AddHttpRoute(State state) +0
   Navigation.StateInfoConfig.AddStateRoutes() +915

[InvalidOperationException: The pre-application start initialization method AddStateRoutes on type Navigation.StateInfoConfig threw an exception with the following error message: Method not found: 'System.Web.Http.Controllers.ServicesContainer System.Web.Http.HttpConfiguration.get_Services()'..]
   System.Web.Compilation.BuildManager.InvokePreStartInitMethodsCore(ICollection`1 methods, Func`1 setHostingEnvironmentCultures) +12969195
   System.Web.Compilation.BuildManager.InvokePreStartInitMethods(ICollection`1 methods) +12968904
   System.Web.Compilation.BuildManager.CallPreStartInitMethods(String preStartInitListPath, Boolean& isRefAssemblyLoaded) +280
   System.Web.Compilation.BuildManager.ExecutePreAppStart() +172
   System.Web.Hosting.HostingEnvironment.Initialize(ApplicationManager appManager, IApplicationHost appHost, IConfigMapPathFactory configMapPathFactory, HostingEnvironmentParameters hostingParameters, PolicyLevel policyLevel, Exception appDomainCreationException) +1151

[HttpException (0x80004005): The pre-application start initialization method AddStateRoutes on type Navigation.StateInfoConfig threw an exception with the following error message: Method not found: 'System.Web.Http.Controllers.ServicesContainer System.Web.Http.HttpConfiguration.get_Services()'..]
   System.Web.HttpRuntime.FirstRequestInit(HttpContext context) +12968244
   System.Web.HttpRuntime.EnsureFirstRequestInit(HttpContext context) +159
   System.Web.HttpRuntime.ProcessRequestNotificationPrivate(IIS7WorkerRequest wr, HttpContext context) +12807949
Ideas?
Coordinator
Sun at 9:42 AM
Check your StateInfo.config file. Make sure you haven't got any WebApi States configured by accident. So check there aren't any apiController and action attributes in the deployed StateInfo.config.
Sun at 10:09 AM
That was my first thought, but no, it doesn't. What is strange is that when I debug the project locally, the code in the if (!Initialised) {} section is never called - as you would expect, so I am not clear why it appears to be called on the IIS server. I have tried re-booting the server to make sure there is nothing odd going on caching wise, but still the same problem.
Coordinator
Sun at 10:12 AM
My guess is that it's making sure the whole AddHttpRoute method is syntactically correct regardless of whether it actually goes inside the if check.

We can test this theory out in a couple of ways
  1. We could move the registering of the value provider into a separate method in the WebApiRouteConfig class and call this method from the AddHttpRoute method.
  2. We could move the registering of the value provider into a separate class and call this class from the AddHttpRoute method.
I think the first test will fail but the second will pass?
Sun at 10:24 AM
Good call, I moved the code into a separate method, and the problem has gone away.
#if NET40Plus
using System.Web.Http;
using System.Web.Http.ValueProviders;
using System.Web.Routing;

namespace Navigation
{
    internal static class WebApiRouteConfig
    {

        private static bool _Initialised = false;

        internal static void AddHttpRoute(State state)
        {
            string apiController = state.Attributes["apiController"] != null ? state.Attributes["apiController"].Trim() : string.Empty;
            string action = state.Attributes["action"] != null ? state.Attributes["action"].Trim() : string.Empty;
            if (apiController.Length != 0 && action.Length != 0)
            {
                if (!_Initialised)
                {
                    RegisterNavigationDataValueWebApiProvider();
                    _Initialised = true;
                }
                state.StateHandler = new WebApiStateHandler();
                RouteValueDictionary defaults = StateInfoConfig.GetRouteDefaults(state, state.Route);
                defaults["controller"] = apiController;
                defaults["action"] = action;
                GlobalConfiguration.Configuration.Routes.MapHttpRoute("WebApi" + state.Id, state.Route, defaults);
                Route route = (Route)RouteTable.Routes["WebApi" + state.Id];
                route.DataTokens = new RouteValueDictionary() { { NavigationSettings.Config.StateIdKey, state.Id } };
                route.RouteHandler = new WebApiStateRouteHandler(state);
            }
        }

        internal static void RegisterNavigationDataValueWebApiProvider()
        {
            GlobalConfiguration.Configuration.Services
                .Insert(typeof(ValueProviderFactory), 0, new NavigationDataValueWebApiProviderFactory());
        }

    }
}
#endif
I have done the same thing to the MVC RouteConfig.
Coordinator
Sun at 10:31 AM
Very interesting and suprising! I think it's to do with the C# JIT compilation model

Can you rename RegisterNavigationDataValueWebApiProvider to Initialise and make it private, please?
Sun at 10:46 AM
Edited Sun at 10:53 AM
Done.

I really need to do some more testing to make sure it hasn't broken the WebApi support - unfortunately I don't have time to do that just now.

I have also tried to create a new fork and pull request (prematurely - but I was interested to see what was involved). It seems to have created a request, but not sure what happens now.

This is quite a learning curve!
Coordinator
Sun at 10:56 AM
Thanks.

There's no rush. The most important thing is the testing side so just make sure you've done enough to satisfy yourself. I'll do my own on top of that so no worries.

I saw your fork come in but it's not showing any code changes. You need to push your changes to your fork. This guide helped me get started.

As always, just let me know if you get stuck.
Sun at 11:33 AM
Edited Sun at 11:50 AM
I've had a crack at pushing the changes back to the repository. I got pretty lost along the way with TortoiseHg, HgWorkbench etc. but I think I have committed the two modified files...
Coordinator
Sun at 12:22 PM
Congrats, your changes are back in your repository but they're not showing up in your pull request. I guess that's because you submitted the pull request before you pushed your changes back.

Can you update that earlier pull request or do you have to submit a new one?
Sun at 12:29 PM
I have created a new pull request - not sure if that's actually done anything...

On the testing front, I have run some more tests on both WebForms and WebApi and all seems well. The sample code on your project also compiles and runs as expected so, as far as I can tell, all looks OK.
Coordinator
Sun at 12:32 PM
Didn't see any change. I think you should update your current pull request because that would be the normal way the code review would take place:
You'd submit a pull request, I'd add review comments, you'd update the pull request etc.

The testing you've done sounds great.
Coordinator
Sun at 12:45 PM
Have a look at the details of your pull request and you'll see the Changes tab shows 0
Sun at 1:41 PM
Hhmm, I think I will delete the fork and try again from scratch now I have all the local tools installed.

Watch this space!
Coordinator
Sun at 1:48 PM
Ok, it would be great if you add a mention to the Issue #5 that the pull request is fixing.

The good news is that I've already merged the changes into the main code base, so I'll accept the pull request once it comes in.
I was impressed that you didn't rush anything, especially when it would've been tempting to say the issue was fixed but you could see that it wasn't.

Thank you for being my first contributor. It's a bit of a milestone for me.

I tweet about Navigation for ASP.NET from @grahammendick so I'll be mentioning your great contribution. Do you have a twitter address so I can mention you by name?
Coordinator
Sun at 2:14 PM
I'll be out for the rest of the day, but I'm looking forward to tweeting about your fix.
I'll start work on the patched release tomorrow.
Could you update Issue #5 with details of your fix, please?
Sun at 2:20 PM
I'm still grappling with the 'pull request'. When I run the Hg Workbench against the local repository and try and 'push the outgoing changes', I get the following log:
% hg push https://hg.codeplex.com/navigation
pushing to https://hg.codeplex.com/navigation
searching for changes
http authorization required for https://hg.codeplex.com/navigation
realm: CodePlex
abort: authorization failed
[command returned code 255 Sun Jul 20 15:11:39 2014]
Navigation% 
I have checked and rechecked that I am using the same username/password on Codeplex. Googling(?) there are some posts suggesting that this is a 'permissions issue' on the project. Is this something that makes sense to you?

I quite understand if this is becoming a bit too labour intensive - I think I have long since crossed the line between helpful and hindrance :)
Sun at 2:23 PM
Edited Sun at 3:03 PM
No problem.

I am a Social Network hermit, I did create a Twitter account a couple of years ago, but apart from a couple of test postings that's been about it. @abilitation
Coordinator
Sun at 8:52 PM
You've successfully pushed the changes to your fork because I've merged these into the core. If you create a pull request from that fork you should be ok.

From the looks of that error message I think you might be trying to push the changes to the core instead of to your fork? Check what the HgWorkbench thinks the Remote Repository is because it should be https://hg.codeplex.com/forks/neilski/webapidependency

Don't beat yourself up, everybody needs a helping hand. You've really helped me a lot this weekend so you're still a long way in the black ;o)
Mon at 4:48 AM
Thank you Graham, I've certainly had a very interesting couple of days - if I have been of some use then that's even better :-)

I'm tied up for the next couple of days, but when I get back I will tidy up the identity project and send you a link and we can take it from there.
Coordinator
Mon at 7:29 AM
Could you try sending me a pull request from your fork?
It would help my documentation if I could accept your pull request.
Mon at 12:52 PM
Hi Graham, I think I might have cracked CodePlex (a 2 hour train journey and an iPad do have their uses!). I am hoping that you can now see a pull request from my fork (sorry, I had to modify the code formatting again to enable me to commit from the local repository). I think something had gone awry with my TortoiseHg installation as after a local reboot, the explorer extension is now showing the correct file states - it wasn't doing that yesterday. Anyway, I am slightly more optimistic than before...
Coordinator
Mon at 7:46 PM
The patch release is out. Much thanks.

Speak later about Navigation Identity and the msbuild script issue you encountered.
Tue at 8:13 AM
Quick question if I may...

I am currently tidying up the Identity demo code and have a question regarding C# code formatting/preferences...

With long statements, I generally like to keep the code length to less than 80 or at most 100 columns (I think this is a hangover from my Epson Mx80 dot-matrix printer days!). Is this OK with you, or do you prefer the one statement, one line approach?

Here's an example:

Wrapped
manager.RegisterTwoFactorProvider(
   "PhoneCode",
   new PhoneNumberTokenProvider<ApplicationUser>
   {
      MessageFormat = "Your security code is: {0}"
   }
);
Single Line
manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser> { MessageFormat = "Your security code is: {0}" });
I'm easy either way, but thought consistency would be worth aiming for.
Coordinator
Tue at 4:30 PM
The only style preference I have is to keep the code consistent within a project. The Identity demo is your baby so you get to call the shots. I think if I were starting the Navigation project over I would probably go with the 80 cols or less too.
Tue at 5:03 PM
Edited Tue at 5:05 PM
I have posted the development site up to http://navigation-identity.abldev.com I think everything is working, but testing has been a bit light so far! Here are some of the things I know about:
  • Fix typos and spelling mistakes
  • Investigate problem where date field always shows time element
  • Check all the OAuth providers are correctly/securely configured at the service end
  • Check I have managed the Navigation routing correctly/optimally
  • Handle application and 404 errors.
On the final point, what is the best way of structuring this within the project when using Navigation for ASP.NET. I tried creating a couple of routes:
<dialog key="PageNotFound" initial="Page">
   <state key="Page" page="~/Views/Error/PageNotFound.aspx" route="PageNotFound" trackCrumbTrail="false"></state>
</dialog>
<dialog key="ApplicationError" initial="Page">
   <state key="Page" page="~/Views/Error/ApplicationError.aspx" route="ApplicationError" trackCrumbTrail="false"></state>
</dialog>
and then mapping to these as usual in the Web.config
<customErrors mode="RemoteOnly" defaultRedirect="/ApplicationError">
   <error statusCode="404" redirect="/PageNotFound" />
</customErrors>
But this did not seem to work - attempting to access a non-existent page throws the error "The string parameter 'key' cannot be null or empty.
Parameter name: key" - I'm guessing I've done something (else!) stupid :)
Coordinator
Tue at 8:15 PM
You finished that quick once you got into it. Great you didn't have any problems using the Navigation.

It would be great if you could make the code visible. Why not create a github project (I prefer github to codeplex)?

I noticed a couple of things that, from memory, isn't how Identity 1 behaved:
  • When I signed in with Google it took me to the register page but allowed me to change my email address.
  • After signing in with Google I clicked to remove it from External Auth Providers list. Then I logged back in with same Google email but it wouldn't let me register because it said the email was already in use.
I spotted a bug. If you sign in with Google and click cancel when you get to the screen where Google asks if it can share its details with the app, then it goes to the error page.

The routes you set up for handling errors look good to me. They seem to work because going to http://navigation-identity.abldev.com/a.aspx will show the PageNotFound page. It might not be working for a non-existent route?
Today at 2:14 PM
Thank you for taking the time to look at the web site. I have fixed the bug when cancelling an external authentication request. Your comment about the workflow being different to Identity 1.0 prompted me to investigate.

The code that I had implemented followed the MS Identity 2.0 samples. These samples also allow you to override the externally presented email. I think this is because MS are trying to ensure a concrete/valid local user (I could be wrong ion this point). Apart from the problem you have identified, if you create the local account via an external provider, the sample code does not force verification of this email address. In my humble opinion you then up up with a local account that is potentially invalid. I considered three options:
  1. Leave the workflow as it is and allow 'loose' account creation (as per the samples)
  2. Modify the login method to check for a verified email and fail the login if unverified
  3. Allow the user to login but restrict access and prompt them to verify their email address on the management page.
In the end, I implemented items 2 and 3 in the code, and opted from option 3 in the application. For now these features can be set by commenting/un-commenting two lines in the SignIn() helper method, but I think I will extend the ApplicationUserManager class to allow this to be configured as part of the bootstrap process.

I'll have a look at GitHub.

Thanks again for all your help and support.
Today at 2:20 PM
On the subject of 404 errors, you are right...

If I navigate to http://navigation-identity.abldev.com/nonexistent/a.aspx a 404 'Page not found' error is raised.
If I try to navigate to a route (no .aspx extension) such as http://navigation-identity.abldev.com/nonexistent/a I get a "The string parameter 'key' cannot be null or empty" exception (I think from the Navigation framework.

I was wondering if it would be possible/desirable for the Navigation framework to raise an HTTP 404 error instead of throwing the exception?
Coordinator
Today at 5:11 PM
I'd leave the code to match the functionality from the original samples. If we veer away from just porting the original samples to use the Navigation then it might be confusing.

Can you attach the stack trace from the key cannot be null or empty exception so I can see where about in the Navigation code it's thrown.
Today at 5:18 PM
Here you go...
[ArgumentException: The string parameter 'key' cannot be null or empty.
Parameter name: key]
   System.Web.UI.StateBag.Add(String key, Object value) +9664958
   Navigation.NavigationData.Add(String key, Object value) +181
   Navigation.NavigationData.set_Item(String key, Object value) +40
   Navigation.StateController.SetStateContext(String stateId, HttpContextBase context) +1163

[UrlException: The Url is invalid]
   Navigation.StateController.SetStateContext(String stateId, HttpContextBase context) +1370
   Navigation.StateAdapter.Page_PreInit(Object sender, EventArgs e) +124
   System.Web.UI.Page.OnPreInit(EventArgs e) +9706026
   System.Web.UI.Page.PerformPreInit() +31
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +335
Coordinator
Today at 5:28 PM
It looks to me that it's matching one of your routes. Have you got a catch-all route like route="{*param}"?
Could you attach your StateInfo.config too, please?
Today at 5:31 PM
No catch all. here's the StateInfo file...
<StateInfo>

   <!--
   ****************************************************************************
   ** This file is where you configure your navigation. To find out more      *
   ** about it, head over to http://navigation.codeplex.com/documentation     *
   ****************************************************************************
   -->

   <!-- Standard Content Pages -->
   <dialog key="Home" initial="Page" path="~/Views/Home/Index.aspx">
      <state key="Page" page="~/Views/Home/Index.aspx" route="~" defaultTypes="Logout=bool" trackCrumbTrail="false"></state>
   </dialog>
   <dialog key="About" initial="Page">
      <state key="Page" page="~/Views/Home/About.aspx" route="About" trackCrumbTrail="false"></state>
   </dialog>
   <dialog key="Contact" initial="Page">
      <state key="Page" page="~/Views/Home/Contact.aspx" route="Contact" trackCrumbTrail="false"></state>
   </dialog>
   <dialog key="PageNotFound" initial="Page">
      <state key="Page" page="~/Views/Error/PageNotFound.aspx" route="PageNotFound" trackCrumbTrail="false"></state>
   </dialog>
   <dialog key="ApplicationError" initial="Page">
      <state key="Page" page="~/Views/Error/ApplicationError.aspx" route="ApplicationError" trackCrumbTrail="false"></state>
   </dialog>

   <!-- User Registration -->
   <dialog key="Register" initial="Page">
      <state key="Page" page="~/Views/Account/Register.aspx" route="Account/Register" trackCrumbTrail="false"></state>
   </dialog>
   <dialog key="ResendVerificationEmail" initial="Page">
      <state key="Page" page="~/Views/Account/ResendVerificationEmail.aspx" route="Account/ResendVerificationEmail" trackCrumbTrail="false"></state>
   </dialog>
   <dialog key="RegistrationConfirmation" initial="Page">
      <state key="Page" page="~/Views/Account/RegistrationConfirmation.aspx" route="Account/RegistrationConfirmation" trackCrumbTrail="false"></state>
   </dialog>
   <dialog key="RegistrationEmailVerification" initial="Page">
      <state key="Page" page="~/Views/Account/RegistrationEmailVerification.aspx" route="Account/RegistrationEmailVerification" trackCrumbTrail="false"></state>
   </dialog>

   <!-- Login/Authentication Pages -->
   <dialog key="Login" initial="Page">
      <state key="Page" page="~/Views/Account/Login.aspx" route="Account/Login" trackCrumbTrail="false"></state>
   </dialog>
   <dialog key="RequestPasswordReset" initial="Page">
      <state key="Page" page="~/Views/Account/RequestPasswordReset.aspx" route="Account/RequestPasswordReset" trackCrumbTrail="false">
         <transition key="RequestPasswordResetConfirmation" to="RequestPasswordResetConfirmation" />
      </state>
      <state key="RequestPasswordResetConfirmation" page="~/Views/Account/RequestPasswordResetConfirmation.aspx" route="Account/RequestResetPasswordConfirmation" trackCrumbTrail="false"></state>
   </dialog>
   <dialog key="ResetPassword" initial="Page">
      <state key="Page" page="~/Views/Account/ResetPassword.aspx" route="Account/ResetPassword" trackCrumbTrail="false">
         <transition key="ResetPasswordConfirmation" to="ResetPasswordConfirmation" />
      </state>
      <state key="ResetPasswordConfirmation" page="~/Views/Account/ResetPasswordConfirmation.aspx" route="Account/ResetPasswordConfirmation" trackCrumbTrail="false"></state>
   </dialog>
   <dialog key="AccountLocked" initial="Page">
      <state key="Page" page="~/Views/Account/AccountLocked.aspx" route="Account/AccountLocked" trackCrumbTrail="false"></state>
   </dialog>

   <!-- Two-Factor Authentication -->
   <dialog key="ExternalLoginHandler" initial="Page">
      <state key="Page" page="~/Views/Account/ExternalLoginHandler.aspx" route="Account/ExternalLoginHandler" trackCrumbTrail="false"></state>
   </dialog>
   <dialog key="SendTwoFactorCode" initial="Page">
      <state key="Page" page="~/Views/Account/SendTwoFactorCode.aspx" route="Account/SendTwoFactorCode" trackCrumbTrail="false">
         <transition key="VerifyTwoFactorCode" to="VerifyTwoFactorCode" />
      </state>
      <state key="VerifyTwoFactorCode" page="~/Views/Account/VerifyTwoFactorCode.aspx" route="Account/VerifyTwoFactorCode" trackCrumbTrail="false"></state>
   </dialog>
   <dialog key="UserNotVerified" initial="Page">
      <state key="Page" page="~/Views/Account/UserNotVerified.aspx" route="Account/UserNotVerified" trackCrumbTrail="false"></state>
   </dialog>

   <!-- Account Management Pages -->
   <dialog key="Manage" initial="Page">
      <state key="Page" page="~/Views/Account/Manage.aspx" route="Account/Manage" trackCrumbTrail="false">
         <transition key="ChangePassword" to="ChangePassword" />
         <transition key="UpdatePersonalDetails" to="UpdatePersonalDetails" />
         <transition key="AddPhoneNumber" to="AddPhoneNumber" />
      </state>
      <state key="ChangePassword" page="~/Views/Account/ChangePassword.aspx" route="Account/ChangePassword" trackCrumbTrail="false"></state>
      <state key="UpdatePersonalDetails" page="~/Views/Account/UpdatePersonalDetails.aspx" route="Account/UpdatePersonalDetails" trackCrumbTrail="false"></state>
      <state key="AddPhoneNumber" page="~/Views/Account/AddPhoneNumber.aspx" route="Account/AddPhoneNumber" trackCrumbTrail="false">
         <transition key="VerifyPhoneNumber" to="VerifyPhoneNumber" />
      </state>
      <state key="VerifyPhoneNumber" page="~/Views/Account/VerifyPhoneNumber.aspx" route="Account/VerifyPhoneNumber" trackCrumbTrail="false"></state>
   </dialog>
</StateInfo>
Just to let you know, I'll now be away from the office until Friday/Saturday - so you should get some peace :)
Coordinator
Today at 6:45 PM
It's a bit weird because it works if there's a query string, http://navigation-identity.abldev.com/nonexistent?a=b

I think it's a .NET bug. When .NET gets a 404 it does a Server.Transfer instead of a Response.Redirect to the PageNotFound. This is so that the Url stays as entered instead of changing to PageNotFound. However, Routes don't work well with Server.Transfer.

You can probably work around this by removing the PageNotFound State from the StateInfo.config and configuring it directly using MapPageRoute instead
routes.MapPageRoute("PageNotFound", "PageNotFound", "~/Views/Error/PageNotFound.aspx");