Navigation Identity for VS 2013 - Identity Version 2.0

Mar 26 at 6: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 10: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 6:24 PM
Edited Apr 10 at 6:24 PM
Hi,

Just wondered how you're getting on and whether you need any help?
Apr 10 at 7: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 8: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 12:11 PM
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 6:01 PM
Edited Apr 25 at 6: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 11: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 11:36 AM
Thanks Graham - sounds good.
Coordinator
Jul 13 at 4: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 5: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 5: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 6: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 5: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 7: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 9: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.
Jul 19 at 11: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
Jul 19 at 12:07 PM
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
Jul 19 at 12:56 PM

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
Jul 19 at 1: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
Jul 19 at 1: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
Jul 19 at 1: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
Jul 19 at 1: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
Jul 19 at 1: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?
Jul 19 at 2:00 PM

Sure no problem.

Neil

Coordinator
Jul 19 at 5: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.
Jul 19 at 5: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
Jul 19 at 5: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
Jul 19 at 5: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
Jul 19 at 5:34 PM
Edited Jul 19 at 5: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?
Jul 19 at 5: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
Jul 19 at 6: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
Jul 19 at 6: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
Jul 19 at 6: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
Jul 19 at 7: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
Jul 20 at 8:24 AM
Edited Jul 20 at 8: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
Jul 20 at 8: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.
Jul 20 at 9: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
Jul 20 at 9: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?
Jul 20 at 10: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.
Jul 20 at 10: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
Jul 20 at 10: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.
Jul 20 at 11: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
Jul 20 at 11: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?
Jul 20 at 11: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
Jul 20 at 11: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?
Jul 20 at 11:46 AM
Edited Jul 20 at 11: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
Jul 20 at 11: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.
Jul 20 at 12:33 PM
Edited Jul 20 at 12:50 PM
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
Jul 20 at 1: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?
Jul 20 at 1: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
Jul 20 at 1: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
Jul 20 at 1:45 PM
Have a look at the details of your pull request and you'll see the Changes tab shows 0
Jul 20 at 2: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
Jul 20 at 2: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
Jul 20 at 3: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?
Jul 20 at 3: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 :)
Jul 20 at 3:23 PM
Edited Jul 20 at 4: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
Jul 20 at 9: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)
Jul 21 at 5: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
Jul 21 at 8: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.
Jul 21 at 1: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
Jul 21 at 8:46 PM
The patch release is out. Much thanks.

Speak later about Navigation Identity and the msbuild script issue you encountered.
Jul 22 at 9: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
Jul 22 at 5: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.
Jul 22 at 6:03 PM
Edited Jul 22 at 6: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
Jul 22 at 9: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?
Jul 23 at 3: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.
Jul 23 at 3: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
Jul 23 at 6: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.
Jul 23 at 6: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
Jul 23 at 6: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?
Jul 23 at 6: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
Jul 23 at 7: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");
Jul 25 at 1:33 PM
You were on the money, adding two static MapPageRoutes (one for each error page) and removing the routes from StateInfo has fixed the problem.
public static class RouteConfig
{
   public static void RegisterRoutes(RouteCollection routes)
   {
      routes.MapPageRoute(
         "PageNotFound", 
         "PageNotFound", 
         "~/Views/Error/PageNotFound.aspx");

      routes.MapPageRoute(
         "ApplicationError",
         "ApplicationError",
         "~/Views/Error/ApplicationError.aspx");
   }
}
Coordinator
Jul 25 at 5:16 PM
Thanks, that's great. You only had to move the PageNotFound one out but there's no harm moving them both.
Jul 26 at 10:07 AM
Yes, I dithered over whether to leave the ApplicationError route in StateInfo or move it to the ASP.net route. In the end I decided that it was cleaner to keep the error routing together - happy to be wrong though :)
Jul 26 at 10:10 AM
I have created my first GitHub repository (least, I think I have!). See https://github.com/Neilski/NavigationIdentity2

If you have the time, I would appreciate your comments - I am feeling unsettling nervous about my efforts (possibly the fear of looking publicly stupid)...
Coordinator
Jul 26 at 10:36 AM
Neil, it looks awesome. I wanted to send you a quick note of congratulations to settle your nerves. I know how scary and exciting sharing code is, it can make you feel pretty exposed. I'm so pleased you did and honoured that my project was involved.

I'll get back to you once I've looked through the code in more detail.
Coordinator
Jul 27 at 3:46 PM
I've been through the code in more detail and I think you've done a great job. It's something to be proud of and deserves to be seen by more people.

Here are some points to chew over.
  • You added a BaseAccountPage. I guess you weren't keen on adding a CallingDataMethods listener to each page? Is there any reason you're reusing the AccountController in this Base Page instead of creating a new AccountController each time?
  • I think the code should work without JavaScript. The only thing stopping the sample from working if JavaScript is disabled is the use of LinkButtons, e.g., Remove Phone. Changing these to Buttons would solve this.
  • You're sending down jquery, jquery ui and bootstrap. I think the only JavaScript is the slideup on the Manage.aspx. If we removed this then do we need to send any JavaScript?
  • Some of the pages are vulnerable to XSS attacks because the content isn't encoded. Below is the vulnerable code with its non-vulnerable equivalent underneath
<asp:Literal runat="server" ID="PhoneNumber" Text="<%# Item.PhoneNumber %>">
<asp:Literal runat="server" ID="PhoneNumber" Text="<%# Item.PhoneNumber %>" Mode="Encode">
<%# Item.PhoneNumber %>
<%: Item.PhoneNumber %>
  • Some of the Controller methods use async. This works in MVC but not in WebForms. Have a look at the WebForms Identity 2 samples and see what method they call, it'll be the non-async equivalent.
  • The Verify pages use a HiddenField where it would be better to use the DataKeyNames on the FormView instead.
  • You can use the dynamic Bag on the StateContext to avoid casting
DemoCode = StateContext.Data["DemoCode"] as string
DemoCode = StateContext.Bag.DemoCode
Jul 27 at 5:07 PM
Hello Graham,

Thank you for your continued support and encouragement and particularly for taking the time to to review the code and provide such constructive suggestions. I will work through your suggestions over the next few days and update the code accordingly (I really like the StateContext.Bag.DemoCode construct - really clever).

To answer your questions...
  • It seemed easier to create the AccountController as part of the BaseAccountPage. My reasoning was that the AccountController would then be available for other methods if required (e.g. if required by a User Control referenced on the page). It also seemed that creating a single instance of the AccountController was slightly tidier compared to creating an instance for each page method. I would be interested to hear your thoughts on this as I suspect you have an alternative view.
  • I included the JavaScript libraries in an effort to be 'helpful' for people wishing to extend the base project, but I completely agree that this project would be better without depending on JavaScript at all so will remove this.
  • I hadn't fully considered the XSS implications - very slack - thank you for pointing this out.
  • I hadn't fully appreciated the use of the DataKeyNamesproperty - again I will research this further
  • I had tried to avoid async methods, but found a couple of methods with no apparent non-async equivalents. I will research this further.
Thanks again, and watch this space!
Coordinator
Jul 27 at 7:37 PM
I'm just not keen on base Page/UserControl classes, reminds me of too much of bad WebForms. I prefer to add event listeners directly in the code behind.
(On a separate note, GitHub for Windows is worth checking out).
Jul 28 at 12:17 PM
Thanks Graham,

I have removed the base Page and UserControl classes to follow your suggestion. I have also updated the code in most the areas suggested. If you have the time, I have some questions/comments - sorry :(

Is StateContext.Data["DemoCode"] always completely interchangeable with StateContext.Bag.DemoCode ?

I have replaced all but one instance of the HiddenField instances on the pages. I have one remaining construct where I have a FormView nested within a ListView and use the HiddenField as a [Control] attribute on the public AuthenticationDescription GetOpenAuthProvider([Control] string provider) method (I notice you did something similar on the Identity 1.0 samples). My question is, is there a better way?
   <asp:ListView runat="server" 
      ItemType="Microsoft.Owin.Security.AuthenticationDescription"
      SelectMethod="GetOpenAuthProviders"
      DataKeyNames="AuthenticationType"
      OnCallingDataMethods="GetAccountController">
      <EmptyDataTemplate>
         <p>
            There are no external authentication services configured.
            See <a href="http://go.microsoft.com/fwlink/?LinkId=313242">this article</a>
            for details on setting up this ASP.NET application to support 
            logging in via external services.
         </p>
      </EmptyDataTemplate>
      <LayoutTemplate>
         <ul class="list-unstyled">
         <li runat="server" ID="itemPlaceholder" />
         </ul>
      </LayoutTemplate>
      <ItemTemplate>
         <li class="margin-1">
            <asp:HiddenField ID="Provider" runat="server" Value="<%# Item.AuthenticationType %>" />
            <asp:FormView runat="server" RenderOuterTable="false"
               ItemType="Microsoft.Owin.Security.AuthenticationDescription"
               DefaultMode="Edit"
               DataKeyNames="AuthenticationType"
               SelectMethod="GetOpenAuthProvider"
               UpdateMethod="AssignOAuthProvider" 
               OnCallingDataMethods="GetAccountController">
               <EditItemTemplate>
                  <asp:Button runat="server" CommandName="Update" Text="<%# Item.AuthenticationType %>" 
                     CssClass="btn btn-default full-width" ToolTip='<%#: String.Format("Log in using your {0} account", Item.Caption) %>' />
               </EditItemTemplate>
            </asp:FormView>
         </li>
      </ItemTemplate>
   </asp:ListView>
I have looked at the async methods in the AccountController and I am fairly sure that those that remain do not have a non-async alternative and are 'as used' in the Microsoft ASP.net 2.0 samples. These methods generally appear to be associated with the 2FA extensions and, where required, are wrapped in a Task/Result construct.

I had found GitHub for Windows by accident - for a GitHub novice like me it makes life much easier :)
Coordinator
Jul 28 at 5:54 PM
Great, I hoped you'd come back with questions and comments.
  • StateContext.Data["DemoCode"] is identical to StateContext.Bag.DemoCode. But an example where you can't do a simple replace is
var userId = StateContext.Data[UserIdKey] as string;
That's because it won't be able to infer the type of userId. To get around this you can change var to string
string userId = StateContext.Data.UserIdKey;
  • You'll need to keep the HiddenField for the FormView nested inside a ListView.
  • Happy with the async, thanks for checking back to the original Web Forms sample.
GitHub for windows is great. Command line source control is way too hard.
Jul 29 at 5:47 AM
That great Graham. I'll review my use of the StateContext and standardise where appropriate to tidy things up.

Thanks again for your help - I have learned more in the past couple of weeks than I'd normally pick up in months.
Coordinator
Jul 29 at 6:15 PM
That's cool, I've had a blast working with you.

Let me know once you're happy with your code changes.
Jul 30 at 8:39 AM
I think I'm as happy as I'm going to be - for now at least. I'm sure there are things that could be improved, but I think I've hit the phase of diminishing returns (I'm at that point where I keep looking at the same line of code and changing it backwards and forwards for not good reason!).
Coordinator
Jul 30 at 6:23 PM
The code looks great.

There's no need to Encode text that ends up as attributes because .NET knows to do that. Only text that ends up as the tag content needs encoding. So encoding attributes will mean they get encoded twice, once by you and again by .NET. So, if the value was A & B this would display as A &amp; B. You can try it and see what I mean.
For example, this doesn't need encoding because it ends up as an attribute
<asp:Button runat="server" ToolTip='<%# Item.AuthenticationType %>' />
But this does need encoding because it ends up inside a tag
<asp:Literal runat="server" Text='<%#: Item.AuthenticationType %>' />
Jul 30 at 6:50 PM
Thanks Graham,

I have fixed the asp.net control attribute encoding - the only information I could find was that HTML attributes should be encoded, I hadn't considered that ASP.net might do it for me. I actually did a test with a Label 'Text' property and that was not encoded, so I assumed nothing else would be. Assumption, it seems, will be my downfall :(
Coordinator
Jul 30 at 7:12 PM
.NET does the encoding for you if it's a Control property rendered as an html attribute.
If it's an html attribute rendered outside of a Control then you'll need to encode it.
Does that make sense?
Jul 31 at 7:51 AM
It does indeed. I have modified the sample code accordingly. Thanks for clarifying. This has, not before time, prompted me to investigate XSS in more detail, it's something I have not been rigorous enough with in the past - so thanks for the prompt.
Coordinator
Jul 31 at 6:07 PM
Glad I could help with your XSS understanding. I think the code looks grand.

I guess the next step is promoting it. This is the hard part. Do you blog (I don't)?
Jul 31 at 6:23 PM
No, I don't blog, tweet or facetime - I think it is an age thing - I have an inherent distrust of anything with the word 'social' related to it :)

I will keep an eye on Stackoverflow, and if anyone is grappling with this, I'll point them in the right direction.
Coordinator
Jul 31 at 6:43 PM
We could always try what I did and post a discussion on the aspnetidentity codeplex project. After all, if I hadn't done that we would never have collaborated. If you're not comfortable let me know and I'll put something together. What do you think?
Jul 31 at 9:11 PM
Edited Jul 31 at 9:12 PM
I think I am already feeling less comfortable than I would like :)

I've been 'playing with code' for many years (I am a mechanical engineer by trade), and have made a living out of it for the past ten (land of the blind etc.); but I am self-taught and one thing this experiencehas taught me is that there is still a lot of even the basic stuff (XSS, Unit Testing etc. etc.) that I have not been rigorous enough in keeping up to date with. I work alone and therefore I tend to know what I know and am gloriously oblivious to what I don't know. In a more public arena this truth comes home to roost. I think you are the first person that has ever even looked at my code in all that time!

I am sure it is 'whimpy', but in terms of any promotion of the project, I simply don't feel I have the credentials to blow the horn so, if I may, I will leave it to you to do whatever you feel might be appropriate - if indeed anything.

Regards

Horace (Wimp)
Coordinator
Jul 31 at 9:37 PM
I'm not going to let you get off that easy ;o). In the short time we've been working together I've been impressed by your code and communication skills. Your confidence, or lack thereof, is the only thing holding you back. You should know that everybody who shares their code online feels like an imbecile and I certainly think people are laughing at my code.

Anyway, I'm not going to pressure you but wanted to give you pause for thought. If you still want me to write it up then I'll happily do so.
Aug 1 at 8:52 AM
Edited Aug 1 at 8:53 AM
Ok, I'll man-up! Leave it with me for a day or so and I'll put something together. Now, Olive, where's me spinach?...

On a different, but related topic, I have found an issue with ASP.net Identity 2.0...

I have three commercial web sites under development (for the same client) that use Identity 2.0. For some time now the customer has complained that occasionally when they login, their login 'does not get recognised'. In other words, the login process completes, but as far as the web site is concerned they are still anonymous. Only a couple of times have I seen the problem for myself and I have not been able to replicate/debug the problem. I posted a question on Stackoverflow some months ago, but until last night, I seemed to be the only one with the problem.

Last night I received a comment and a link to another developer with the same problem who has had more success understanding the root cause. It appears that there is a Session related bug in the Microsoft.Owin.Host.SystemWeb implementation and an issue has been raised on the Katana Codeplex project.

It appears that whilst the Katana developers acknowledge the problem, they do not intend to fix it on the System.Web platform, rather they intend to wait for a fix in the brave new world of vNext. It is not clear whether it is impossible to fix the problem with System.Web, or rather resources would better be spent driving towards vNext!

Either way, it raises the issue, in my mind at least, as to whether Identity 2.0 is actually viable in a current ASP.net production application. I will do some testing based on the findings from other users to see if a suggested workaround provides a possible, if kludgy, solution.
Aug 1 at 2:27 PM
Here's a link to a simple web site that demonstrates the problem http://identitybugdemo.abldev.com/
Coordinator
Aug 1 at 7:13 PM
That's not an Identity bug, that's a Katana bug. Why are you using Katana? Stop using it and the bug will go away.
Aug 1 at 7:39 PM
Not quite sure I'm following this. If I create a new ASP.net Web Forms project, the current templates build a default web site that implements ASP.net Identity 2.0 (as we know from the Navigation Identity project). Without adding anything else, this project exhibits the problem (as can be demonstrated here. I think that the default implementation of Identity 2.0 implements OWIN which, for Microsoft servers, is built on Katana (but I could be wrong). So I guess my question is, how could I (easily) avoid using Katana - or have I lost the plot?
Coordinator
Aug 1 at 7:47 PM
I thought OWIN/Katana was a different hosting option, didn't realise it was an Identity dependency.
Can't you just remove the OWIN package? Will it all fall to pieces?
Coordinator
Aug 1 at 8:10 PM
Just had a quick skim of http://www.asp.net/aspnet/overview/owin-and-katana and I can see it's built on OWIN. As you can tell, I don't know much about it. Sorree.
Aug 1 at 9:21 PM
No problem. It's funny how these things link together. It was because of this issue that I found your ASP.net 1.0 example in the first place. I had found the problem and had assumed that I had made an error with my implementation and was looking for examples for inspiration. As it turns out, its is a problem with the core Microsoft/Katana implementation.

The bad news is that Microsoft feel that it isn't practical/necessary/possible (delete as applicable) to fix with the current ASP.net System.Web architecture and have decided to leave it broken with a fix scheduled for their new focus: vNext. All well and good, but that is not much help for people that have adopted Identity 2.0 (as encouraged to do so by Microsoft) and have the problem now!

The good(?) news is that if you don't use Sessions then you are probably fine. If you do, you can workaround the issue (though not fix it) by creating a dummy Session variable before login (as part of the session start event for example).

Either way, I can't help feeling that as a 'poster child' development for OWIN and ASP.net security in general, this is a pretty poor effort and they should do more to fix the problem, or at the very least make it clear to adopters that there are problems.

I have probably wasted several weeks on this problem in total and for some developments it looks very much like I might need to re-engineer them back to the 'old and outdated' ASP.net Membership service to ensure reliability.

Ah well, we live and learn I guess.
Coordinator
Aug 1 at 10:02 PM
Microsoft have a habit of jumping ship and leaving people high and dry (mixing my metaphors).
Have you tried rolling back to an earlier version of Identity? The bug might not be there in version 1.
Aug 2 at 8:08 AM
Identity version 1.0 is not really suitable for production - it should have only ever been a preview release in my humble opinion - see this article by Brock Allen. I would also suspect that it would have the same issues as it too uses OWIN. I know they are working on a 2.1 'bug fix' release, but I don't believe this will fix the problem which, as you say, is in Katana.

I think the problem is that Microsoft's new development structure (small teams and individuals) are, arguably correctly, focused on the new and shiny vNext release. This must be attractive from their perspective - shed best part of 20 years of legacy code and use modern code and practices to start afresh - what developer wouldn't put their hand up for that?

The problem is of course, that out here in the real world of customers with limited budget and where you work on fixed price contracts, the need to be able to adopt new technologies quickly and reliably is paramount. Whilst technically I'm looking forward to vNext, commercially it is irrelevant to me at this stage.

The situation with Identity 2.0 is a little different. Microsoft made a big effort to promote its virtues over Membership and I suspect there are a lot of developers out there that bought into that. As I say, at the very least they should be transparent enough to highlight the know issues with their code - even if it does mean losing face.

Sorry, I'm whining on. I'll stop now as I am just winding myself up for no good reason :)
Coordinator
Aug 2 at 2:00 PM
What about trying Identity 2 but with an earlier version of OWIN?
Aug 2 at 4:27 PM
Good call, but it seems the whole Identity, OWIN, Katana stack is pretty much new for version 2.0.

I'm currently doing some testing to try and establish just how big this problem is and whether it can be used reliably in anything other and a carefully constructed, narrow-footprint project. I think it probably can, providing you follow some basic rules. In reality, anything that touches the HttpContext.Response.Cookies[] collection is likely to upset OWIN - this is why initialising the Session object after login such a problem as ASP.net creates the SessionId cookie. If you can live with that then Identity seems fine, but I still need to test this with external authentication providers and also see what effect session time-outs have on the 'fix'.
Coordinator
Aug 27 at 7:31 PM
How's things? Have owin fixed that bug yet?
Have you had a chance to write up your Identity work yet?
Aug 28 at 5:58 AM
Hi Graham,

Well I have spent a fair amount of time researching this problem further and than lobbying relevant bulletin boards, blogs etc. of people at Microsoft. All to no avail. It seems that their focus is on vNext and that the issue is too difficult/time consuming to fix on the current ASP.net 4x framework so, I guess, that is that. I am surprised that there isn't more 'noise' about this problem in general: either people aren't using Identity; they are, but don't use Sessions; they are unconcerned about the problem. From my perspective I can accept that there is a problem, I just feel Microsoft should do more to make it a known issue.

I have done quite a bit of testing with creating a dummy Session variable on Session_Start and this does seem to alleviate most (but not all) operational issues. I also see that they have recently released Identity 2.1. This doesn't fix this problem, but does appear fix a number of other issues.

That's about as far as I have gone/can go with it - at least for now.
Coordinator
Mon at 6:40 PM
I'd like to plug the Navigation Identity work you did. Can I add a link to your project on reddit?
Tue at 9:27 AM
Hi Graham,

Yes, of course you can, no problem.

Things have been somewhat manic around here so apologies for the lack of continuity. With reference to the ASP.Net Identity 'bug', see Stackoverflow post - I've not had the chance to fully test this yet, but initial trials look promising.

Hope all is good with you.
Coordinator
Wed at 7:20 PM
I'm good thanks. I've just linked to your project from reddit, hope you get some favourable traffic.
Glad you've got some traction with that nasty Identity bug.