This project is read-only.

Possible to coexist with custom ControllerFactory? Or...how to redo that

Feb 4, 2014 at 10:10 PM
Edited Feb 4, 2014 at 10:11 PM
Hello,

I'm trying to augment an existing system so that it can use MvcCodeRouting.

A problem I'm facing is that this system has code like this:

I don't see anywhere in the MvcCodeRouting code where you actually create an IControllerFactory, so I'm wondering is there some way I could delegate to MvcCodeRouting if-and-when this other factory cannot find a "suitable controller"?

Thanks!
public class IceNineControllerFactory : IControllerFactory
{
    public static readonly IControllerFactory Instance = new IceNineControllerFactory();
    private static readonly ControllerTypeCache _staticControllerTypeCache = new ControllerTypeCache();

    private IceNineControllerFactory() {}

    IController IControllerFactory.CreateController(RequestContext requestContext, string controllerName)
    {
        if (requestContext == null)
            throw new ArgumentNullException("requestContext");
        if (string.IsNullOrEmpty(controllerName))
            throw new ArgumentException("Value not specified", "controllerName");
        Type controllerType = GetControllerType(requestContext, controllerName);
        if (controllerType == null)
            throw new HttpException((int)HttpStatusCode.NotFound, string.Format("No suitable controller class found: {0}", requestContext.HttpContext.Request.Path));

        new RequestParameterBuilder(Locator.Resolve<IEntitySpaceManager>().GetRequestEntitySpace())
            .LoadParameters(requestContext.HttpContext.Request.Form, requestContext.HttpContext.Request.QueryString);

        return GetControllerInstance(controllerType);
    }
Feb 4, 2014 at 10:26 PM
Ok, so I was able to do that like this:
        if (controllerType == null)
        {
            var controller = VersionOne.Web.IceNine.Shim.IceNineCore.OriginalControllerFactory.CreateController(requestContext, controllerName);
            if (controller == null)
                throw new HttpException((int)HttpStatusCode.NotFound, string.Format("No suitable controller class found: {0}", requestContext.HttpContext.Request.Path));
            return controller;
        }
Since your code is simple and unobtrusive, all it does is populate the routes table and allow Mvc to do it's normal controller creation job. So, I saved a reference to the "OriginalControllerFactory", and just delegated to it!
Feb 4, 2014 at 11:41 PM
Yep, MVC supports finding controllers by namespace, so I just let it do its thing. Although, it wouldn't work in the super rare case two assemblies have controllers with the same namespace.

On Web API however I do register a custom controller selector.
Feb 5, 2014 at 4:03 AM
maxtoroq wrote:
Yep, MVC supports finding controllers by namespace, so I just let it do its thing. Although, it wouldn't work in the super rare case two assemblies have controllers with the same namespace.

On Web API however I do register a custom controller selector.
Thanks Max, I think I am getting close, but still having some head-scratching issues.

First of all, I'm using F# with a MEF-based module loader I rewrote from C#. That part seems to be working fine. But I'm not sure I'm getting things correct.

Here's my routes.axd output:
// MvcCodeRouting v1.2.0
// Format: C# - Visual Basic

#region Arena

routes.MapRoute(null, "Arena/Get", 
    new { controller = "Arena", action = "Get" }, 
    new[] { "VersionOne.Arena" });

routes.MapHttpRoute(null, "Arena/Api", 
    new { controller = "Api" });

routes.MapRoute(null, "Arena/Web/Get", 
    new { controller = "Web", action = "Get" }, 
    new[] { "VersionOne.Arena.Web" });

#endregion
I was not expecting that. I was expecting Arena/Api, Arena/Api/ByName, Arena/Api/Version, Arena/Api/NewEndpoint

And, Arena/Web/Get

Because, here are the controllers that I used:
namespace VersionOne.Arena

open System
//open System.Web.Http
open System.Net.Http
open System.Net
open System.Web.Mvc
open VersionOne.Arena.Intrastructure.Web

type ArenaController() =
    inherit Controller()
    member this.Get() =
        this.Content("Nothing to see here")

type public ArenaModule() =
    inherit DefaultModule<ArenaController>()
    override this.GetBaseControllerRoute() = 
        "Arena"

namespace VersionOne.Arena.Api

open System
open System.Web.Http
open System.Net.Http
open System.Net
open System.Web.Mvc
open VersionOne.Arena.Intrastructure.Web

type ApiController() =
    inherit System.Web.Http.ApiController()
    member this.Get () =
        this.Request.CreateResponse(HttpStatusCode.OK, "We got it! Here's your messages!")

    [<HttpGet>]
    member this.ByName (name : string) =
        this.Request.CreateErrorResponse(HttpStatusCode.OK, "Thanks! Here's your name: " + name)

    [<HttpGet>]
    member this.Version () =
        this.Request.CreateErrorResponse(HttpStatusCode.OK, "Version B")

    [<HttpGet>]
    member this.NewEndpoint () =
        this.Request.CreateErrorResponse(HttpStatusCode.OK, "Hey I did not exist in Version A!")

namespace VersionOne.Arena.Web

open System
open System.Web.Http
open System.Net.Http
open System.Net
open System.Web.Mvc
open VersionOne.Arena.Intrastructure.Web

type WebController() =
    inherit Controller()
    
    [<HttpGet>]
    member this.Get() = 
        this.View()
So, those are the controllers. Here's the IModule definition, and the ModuleLoader:
namespace VersionOne.Arena.Infrastructure.Composition

open System
open System.ComponentModel.Composition

type MapCodeRoutesDelegate = delegate of String * Type -> unit

[<InheritedExport(typedefof<IModule>)>]
type IModule =
    abstract member Initialize : callback : MapCodeRoutesDelegate -> unit

namespace VersionOne.Arena.Intrastructure.Web

open System.ComponentModel.Composition
open VersionOne.Arena.Infrastructure.Composition

[<AbstractClass>]
type DefaultModule<'T>() =
    abstract member GetBaseControllerRoute : unit -> string
    interface IModule with
        member this.Initialize mapCodeRoutesDelegate =
            let baseControllerRoute = this.GetBaseControllerRoute()
            mapCodeRoutesDelegate.Invoke(baseControllerRoute, typedefof<'T>)
The ModuleLoader:

This is where I'm having the most difficulty. At first I messed up and didn't use MvcCodeRouting.Web.Http.WebHost. I just used MvcCodeRouting.Web.Http.

I have used a very similar approach before, though written in C#. The original code is buried in this prior commit on this branch: https://github.com/JogoShugh/ModularAspNetMvc/blob/2102ac8f96d7bda87b5d25886587e2facb350a12/FieldReporting.Infrastructure/Ui/Mvc/Modularity/ModuleLoader.cs
namespace VersionOne.Arena.Infrastructure.Composition

open System
open System.Web.Routing
open System.Web
open System.Web.Http
open System.ComponentModel.Composition
open MvcCodeRouting

module ModuleFuncs =
    let mapCodeRoutes (baseRoute : String) (rootControllerType: Type) =
        let routes = RouteTable.Routes
        let settings = CodeRoutingSettings()        
        settings.UseImplicitIdToken <- true
        settings.EnableEmbeddedViews <- true
        //settings.Configuration <- GlobalConfiguration.Configuration
        //GlobalConfiguration.Configuration.MapCodeRoutes(baseRoute, rootControllerType, settings) |> ignore
        routes.MapCodeRoutes(baseRoute, rootControllerType, settings) |> ignore

type ModuleLoader() =
    member this.LoadAllModules () =
        let callback = MapCodeRoutesDelegate(ModuleFuncs.mapCodeRoutes)
        let parts = PartsList<IModule>(fun m -> m.Initialize(callback) ) |> ignore
        ()
I ran into issues with using System.Web.Mvc; and using System.Web.Http, each have an HttpGet attribute!!!!

But, I think I solved it now :-D

I'll revise this post int he morning. I'm tired now.

NOTE TO SELF:

I'm at the point with these routes now:
#region Arena

routes.MapRoute(null, "Arena/Get", 
    new { controller = "Arena", action = "Get" }, 
    new[] { "VersionOne.Arena" });

routes.MapHttpRoute(null, "Arena/Api/{action}", 
    new { controller = "Api", action = RouteParameter.Optional }, 
    new { action = new SetRouteConstraint("ByName", "Version", "NewEndpoint") });

routes.MapRoute(null, "Arena/Web/Get", 
    new { controller = "Web", action = "Get" }, 
    new[] { "VersionOne.Arena.Web" });

#endregion
The API ones work great. The others, not so much.
#region Arena

routes.MapHttpRoute(null, "Arena/{action}", 
    new { controller = "Arena", action = RouteParameter.Optional }, 
    new { action = new SetRouteConstraint("ByName", "Version", "NewEndpoint") });

#endregion
I still have the old code, because I pasted it to a coworker. Here it is:
type ArenaController() =
    inherit ApiController()
    member this.Get() =
        this.Request.CreateResponse(HttpStatusCode.OK, "We got it! Here's your messages!")

    [<HttpGet>]
    member this.ByName name =
        this.Request.CreateErrorResponse(HttpStatusCode.OK, "Thanks! Here's your name: " + name)

    [<HttpGet>]
    member this.Version() =
        this.Request.CreateErrorResponse(HttpStatusCode.OK, "Version B")

    [<HttpGet>]
    member this.NewEndpoint() =
        this.Request.CreateErrorResponse(HttpStatusCode.OK, "Hey I did not exist in Version A!")


//[<Export(typedefof<IModule>)>]
type public ArenaModule() =
    inherit DefaultModule<ArenaController>()
    override this.GetBaseControllerRoute () = 
        "Arena"
When I decompile to C# with Telerik, I get this:

OLD DLL:

using Microsoft.FSharp.Core;
using System;
using VersionOne.Arena.Intrastructure.Web;

namespace VersionOne.Arena
{
    [CompilationMapping(SourceConstructFlags.ObjectType)]
    [Serializable]
    public class ArenaModule : DefaultModule<ArenaController>
    {
        public ArenaModule()
        {
        }

        public override string GetBaseControllerRoute()
        {
            return "Arena";
        }
    }
}


using Microsoft.FSharp.Core;
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace VersionOne.Arena
{
    [CompilationMapping(SourceConstructFlags.ObjectType)]
    [Serializable]
    public class ArenaController : ApiController
    {
        public ArenaController()
        {
        }

        [HttpGet]
        public HttpResponseMessage ByName(string name)
        {
            return this.Request.CreateErrorResponse(HttpStatusCode.OK, string.Concat("Thanks! Here's your name: ", name));
        }

        public HttpResponseMessage Get()
        {
            return this.Request.CreateResponse<string>(HttpStatusCode.OK, "We got it! Here's your messages!");
        }

        [HttpGet]
        public HttpResponseMessage NewEndpoint()
        {
            return this.Request.CreateErrorResponse(HttpStatusCode.OK, "Hey I did not exist in Version A!");
        }

        [HttpGet]
        public HttpResponseMessage Version()
        {
            return this.Request.CreateErrorResponse(HttpStatusCode.OK, "Version B");
        }
    }
}
NEW BUILD:
using Microsoft.FSharp.Core;
using System;
using VersionOne.Arena.Intrastructure.Web;

namespace VersionOne.Arena
{
    [CompilationMapping(SourceConstructFlags.ObjectType)]
    [Serializable]
    public class ArenaModule : DefaultModule<ArenaController>
    {
        public ArenaModule()
        {
        }

        public override string GetBaseControllerRoute()
        {
            return "Arena";
        }
    }
}

using Microsoft.FSharp.Core;
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Mvc; // <--- THE OFFENDER

namespace VersionOne.Arena
{
    [CompilationMapping(SourceConstructFlags.ObjectType)]
    [Serializable]
    public class ArenaController : ApiController
    {
        public ArenaController()
        {
        }

        [HttpGet]
        public HttpResponseMessage ByName(string name)
        {
            return this.Request.CreateErrorResponse(HttpStatusCode.OK, string.Concat("Thanks! Here's your name: ", name));
        }

        public HttpResponseMessage Get()
        {
            return this.Request.CreateResponse<string>(HttpStatusCode.OK, "We got it! Here's your messages!");
        }

        [HttpGet]
        public HttpResponseMessage NewEndpoint()
        {
            return this.Request.CreateErrorResponse(HttpStatusCode.OK, "Hey I did not exist in Version A!");
        }

        [HttpGet]
        public HttpResponseMessage Version()
        {
            return this.Request.CreateErrorResponse(HttpStatusCode.OK, "Version B");
        }
    }
}
Feb 5, 2014 at 4:41 AM
That's a lot to take in :)

Some tips

In MVC:

Controllers:
using System.Web.Mvc;
using MvcCodeRouting.Web.Mvc;
Registration:
RouteTable.Routes.MapCodeRoutes()
In Web API:

Controllers:
using System.Web.Http;
using MvcCodeRouting.Web.Http;
Registration (WebHost):
RouteTable.Routes.MapCodeRoutes()
Registration (SelfHost):
configuration.MapCodeRoutes()
Feb 5, 2014 at 1:30 PM
Max, thanks for your fast responses. I really appreciate it! This is our "hack week" where we get to build whatever we want and demo on Friday to 120 people. What I am trying to build is a plugin system for the application at www.VersionOne.com. It would be awesome for us because we can start breaking the app into more discrete bundles and deliver updates like that. I plan to combine the mvccoderouting part with a recipe I found in Apress Pro NuGet book that shows how create plugins with MEF as NuGet packages for your own app, utilizing NuGet.Core to download packages and perform shadow copying and app restart and all that stuff. Our Interaction Designer can't wait to see this in action because she wants to be able to tell users to "try out" early access or "beta" features without them needing a whole new build. The idea is that we'll have a private NuGet feed which lists the available modules and their versions and the administrator can pop one in or swap it out or turn off others.

Anyway.....
So, the last step I think is to make sure Embedded Views can work right for MVC UI.

Do I have to make two distinct calls if I have a single module with both API and MVC controllers?

In my module I have the ArenaApiController : ApiController and also WebController : Controller.

Earlier I had been making separate calls but then I read in the docs that you don't have to, you can make a single call and that the controllers could be either. When I debugged and stepped through I saw the provider checks and the criteria being the typeof checks, ! IsAbstract, IsPublic, etc....

And, when I did make two separate calls the routes were all kinds of mangled, now it is at least recognizing both API and Web, but only Api is working.

I will go edit last nights post. It was very late and I was experimenting with it for about 8 hours at that point :)

Thanks again for all your help, and for this project in general.

It was almost 8 years ago when I asked about modular asp.net apps: http://forums.asp.net/t/1005483.aspx

At that point the best example I knew of was DotNetNuke, and maybe it still is. But, your project is making it possible in a better way, especially if I can get the embedded resources to work. In F# I had to add a manual compiler flag for --resource: because it doesn't even give you a project tab for it haha.



maxtoroq wrote:
That's a lot to take in :)

Some tips

In MVC:

Controllers:
using System.Web.Mvc;
using MvcCodeRouting.Web.Mvc;
Registration:
RouteTable.Routes.MapCodeRoutes()
In Web API:

Controllers:
using System.Web.Http;
using MvcCodeRouting.Web.Http;
Registration (WebHost):
RouteTable.Routes.MapCodeRoutes()
Registration (SelfHost):
configuration.MapCodeRoutes()
Feb 5, 2014 at 2:09 PM
That's right, you don't need to make different calls for MVC and API controllers.

What you are building sounds cool, any chance you make it open source? :-)
Feb 5, 2014 at 2:10 PM
Edited Feb 5, 2014 at 2:18 PM
Note: I am using MVC 5.1, not 5.0.

To summarize where I'm at to this point, my routes now look like this:
#region Arena

routes.MapRoute(null, "Arena/Get", 
    new { controller = "Arena", action = "Get" }, 
    new[] { "VersionOne.Arena" });

routes.MapHttpRoute(null, "Arena/Api/{action}", 
    new { controller = "Api", action = RouteParameter.Optional }, 
    new { action = new SetRouteConstraint("ByName", "Version", "NewEndpoint") });

routes.MapRoute(null, "Arena/Web/Get", 
    new { controller = "Web", action = "Get" }, 
    new[] { "VersionOne.Arena.Web" });

#endregion
Of those, the Arena/Api ones work perfectly. The others are not working at all, and I cannot figure out why. I just get a 404, resource not found if I attempt Arena/Get or Arena/ or Arena/Web/Get or Arena/Web.

The controllers are defined like this:
namespace VersionOne.Arena

open System
open System.Web.Http
open System.Net.Http
open System.Net
open VersionOne.Arena.Intrastructure.Web
// --resource:"Views\Arena\Get.spark","VersionOne.Arena.Module.Views.Arena.Get.spark"

type ArenaController() =
    inherit System.Web.Mvc.Controller()
    member this.Get() =
        this.Content("Nothing to see here")

type public ArenaModule() = 
    inherit DefaultModule<ArenaController>()
    override this.GetBaseControllerRoute() = 
        "Arena"

namespace VersionOne.Arena.Api

open System
open System.Web.Http
open System.Net.Http
open System.Net
open VersionOne.Arena.Intrastructure.Web

type ApiController() =
    inherit System.Web.Http.ApiController()
    member this.Get () = "We got it! Here's your messages!"

    [<HttpGet>]
    member this.ByName name = "Thanks! Here's your name: " + name

    [<HttpGet>]
    member this.Version () = "Version B"

    [<HttpGet>]
    member this.NewEndpoint () = "Hey I did not exist in Version A!"

namespace VersionOne.Arena.Web

open System
open System.Web.Http
open System.Net.Http
open System.Net
open VersionOne.Arena.Intrastructure.Web

type WebController() =
    inherit System.Web.Mvc.Controller()
    
    [<System.Web.Mvc.HttpGet>] // Just playing around with this...
    member this.Get() = 
        this.View() 
And, my configuration for routing is below. I didn't need to open MvcCodeRourting.Web.Http or MvcCodeRouting.Web.Mvc. When I did examine those. MvcCodeRouting.CodeRoutingExtensions has extends RouteCollection, so isn't this the right way?
namespace VersionOne.Arena.Infrastructure.Composition

open System
open System.Web.Routing
open MvcCodeRouting

module ModuleFuncs =
    let mapCodeRoutes (baseRoute : String) (rootControllerType: Type) =
        let routes = System.Web.Routing.RouteTable.Routes
        let settings = CodeRoutingSettings()        
        settings.UseImplicitIdToken <- true
        settings.EnableEmbeddedViews <- true
        //settings.Configuration <- GlobalConfiguration.Configuration
        //GlobalConfiguration.Configuration.MapCodeRoutes(baseRoute, rootControllerType, settings) |> ignore
        routes.MapCodeRoutes(baseRoute, rootControllerType, settings) |> ignore

type ModuleLoader() =
    member this.LoadAllModules () =
        let callback = MapCodeRoutesDelegate(ModuleFuncs.mapCodeRoutes)
        let parts = PartsList<IModule>(fun m -> m.Initialize(callback) ) |> ignore
        ()
Feb 5, 2014 at 2:12 PM
Definitely! I'd love to. I'll have to break it out along-side our Core app and into our main set of open source offering, but I don't see any reason why not.

We have a lot of open source in our github accounts at https://github.com/versionone
Feb 5, 2014 at 2:20 PM
Perhaps your custom controller factory is messing something up?

About Arena/Get, if you intend to ommit Get from the URL you should name it Index, or use the [DefaultAction] attribute. This applies to MVC only.
Feb 5, 2014 at 2:28 PM
Edited Feb 5, 2014 at 2:39 PM
Ok I'll try that. It may be possible about the custom factory, although no breakpoints are hitting where that thing normally runs.
. . .

When I tried that, I get routes like this:
#region Arena

routes.MapRoute(null, "Arena/{action}", 
    new { controller = "Arena", action = "Get" }, 
    new { action = new SetRouteConstraint("Get") }, 
    new[] { "VersionOne.Arena" });

routes.MapHttpRoute(null, "Arena/Api/{action}", 
    new { controller = "Api", action = RouteParameter.Optional }, 
    new { action = new SetRouteConstraint("ByName", "Version", "NewEndpoint") });

routes.MapRoute(null, "Arena/Web/{action}", 
    new { controller = "Web", action = "Get" }, 
    new { action = new SetRouteConstraint("Get") }, 
    new[] { "VersionOne.Arena.Web" });

#endregion
That looked promising to me, but the same results, 404 for Arena/ Arena/Get Arena/Web and Arena/Web/Get

Code:
namespace VersionOne.Arena

open System
open System.Web.Http
open System.Net.Http
open System.Net
open VersionOne.Arena.Intrastructure.Web
// --resource:"Views\Arena\Get.spark","VersionOne.Arena.Module.Views.Arena.Get.spark"

type ArenaController() =
    inherit System.Web.Mvc.Controller()
    [<MvcCodeRouting.Web.Mvc.DefaultAction>]    
    member this.Get() =
        this.Content("Nothing to see here")

type public ArenaModule() = 
    inherit DefaultModule<ArenaController>()
    override this.GetBaseControllerRoute() = 
        "Arena"

namespace VersionOne.Arena.Api

open System
open System.Web.Http
open System.Net.Http
open System.Net
open VersionOne.Arena.Intrastructure.Web

type ApiController() =
    inherit System.Web.Http.ApiController()
    member this.Get () = "We got it! Here's your messages!"

    [<HttpGet>]
    member this.ByName name = "Thanks! Here's your name: " + name

    [<HttpGet>]
    member this.Version () = "Version B"

    [<HttpGet>]
    member this.NewEndpoint () = "Hey I did not exist in Version A!"

namespace VersionOne.Arena.Web

open System
open System.Web.Http
open System.Net.Http
open System.Net
open VersionOne.Arena.Intrastructure.Web

type WebController() =
    inherit System.Web.Mvc.Controller()    
    [<MvcCodeRouting.Web.Mvc.DefaultAction>]    
    member this.Get() = 
        this.View() 
Feb 5, 2014 at 2:44 PM
Edited Feb 5, 2014 at 3:14 PM
Actually, the custom controller factory was not even running because I commented it out.

However, when I turn it back on, I actually get the same result:
Server Error in '/' Application.

The resource cannot be found.

Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable.  Please review the following URL and make sure that it is spelled correctly. 

Requested URL: /Arena

Version Information: Microsoft .NET Framework Version:4.0.30319; ASP.NET Version:4.0.30319.18408
It's just that I get it as a caught exception because I'm manually calling this:
                var controller = VersionOne.Web.IceNine.Shim.IceNineCore.OriginalControllerFactory.CreateController(requestContext, controllerName);
That is delegating to the default Mvc Controller factory that I stored in that variable before replacing it with the custom controller.

So, it looks like Mvc itself is having trouble finding the controller.

Could it be something to do with it's name and namespace?

I am trying to track it down in this code: https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/DefaultControllerFactory.cs#L194-L242
Feb 5, 2014 at 3:24 PM
If you send me the solution to maxtoroq@gmail.com I can take a look.
Feb 5, 2014 at 3:27 PM
I'd like to, but it's in the code our of product and the solution is pretty huge. I'm debugging with the MS Symbols right now so I can step into MVC.

If I can't solve it that way, maybe we can set up a screen share and look at it that way? I have a GoToMeeting account that we use all the time to work remotely with people.

Thanks Max!
Feb 5, 2014 at 3:32 PM
Maybe you can try to create a new MVC app, copy the controllers and try to run it, it should work.
Feb 5, 2014 at 4:04 PM
Edited Feb 5, 2014 at 4:08 PM
Incredibly, it seems that the MVC code does not account for the idea of dynamically loaded assemblies, such as those loaded by MEF.

Here's what I've figured out:

This bit of code, TypeCacheUtil.GetFilteredTypesFromAssemblies looks only at assemblies "referenced by the application":

For now, I'm going to try setting an actual hard-reference to the assembly and see what that does. If that solves it, then I'll see about modifying MVC and submitting a pull request, or resort to something like these guys:

http://blogs.microsoft.co.il/bnaya/2013/11/11/asp-net-web-api-minimal-touch-mef/
http://www.hanselman.com/blog/ExtendingNerdDinnerAddingMEFAndPluginsToASPNETMVC.aspx
    internal static class TypeCacheUtil
    {
        private static IEnumerable<Type> FilterTypesInAssemblies(IBuildManager buildManager, Predicate<Type> predicate)
        {
            // Go through all assemblies referenced by the application and search for types matching a predicate
            IEnumerable<Type> typesSoFar = Type.EmptyTypes;

            ICollection assemblies = buildManager.GetReferencedAssemblies();
            foreach (Assembly assembly in assemblies)
            {
                Type[] typesInAsm;
                try
                {
                    typesInAsm = assembly.GetTypes();
                }
                catch (ReflectionTypeLoadException ex)
                {
                    typesInAsm = ex.Types;
                }
                typesSoFar = typesSoFar.Concat(typesInAsm);
            }
            return typesSoFar.Where(type => TypeIsPublicClass(type) && predicate(type));
        }

        public static List<Type> GetFilteredTypesFromAssemblies(string cacheName, Predicate<Type> predicate, IBuildManager buildManager)
        {
            TypeCacheSerializer serializer = new TypeCacheSerializer();

            // first, try reading from the cache on disk
            List<Type> matchingTypes = ReadTypesFromCache(cacheName, predicate, buildManager, serializer);
            if (matchingTypes != null)
            {
                return matchingTypes;
            }

            // if reading from the cache failed, enumerate over every assembly looking for a matching type
            matchingTypes = FilterTypesInAssemblies(buildManager, predicate).ToList();

            // finally, save the cache back to disk
            SaveTypesToCache(cacheName, matchingTypes, buildManager, serializer);

            return matchingTypes;
        }
Here's what it gets called:

``
    public void EnsureInitialized(IBuildManager buildManager)
    {
        if (_cache == null)
        {
            lock (_lockObj)
            {
                if (_cache == null)
                {
                    List<Type> controllerTypes = TypeCacheUtil.GetFilteredTypesFromAssemblies(TypeCacheName, IsControllerType, buildManager);
                    var groupedByName = controllerTypes.GroupBy(
                        t => t.Name.Substring(0, t.Name.Length - "Controller".Length),
                        StringComparer.OrdinalIgnoreCase);
                    _cache = groupedByName.ToDictionary(
                        g => g.Key,
                        g => g.ToLookup(t => t.Namespace ?? String.Empty, StringComparer.OrdinalIgnoreCase),
                        StringComparer.OrdinalIgnoreCase);
                }
            }
        }
    }

    public ICollection<Type> GetControllerTypes(string controllerName, HashSet<string> namespaces)
    {
        HashSet<Type> matchingTypes = new HashSet<Type>();

        ILookup<string, Type> namespaceLookup;
        if (_cache.TryGetValue(controllerName, out namespaceLookup))
        {
            // this friendly name was located in the cache, now cycle through namespaces
            if (namespaces != null)
            {
                foreach (string requestedNamespace in namespaces)
                {
                    foreach (var targetNamespaceGrouping in namespaceLookup)
                    {
                        if (IsNamespaceMatch(requestedNamespace, targetNamespaceGrouping.Key))
                        {
                            matchingTypes.UnionWith(targetNamespaceGrouping);
                        }
                    }
                }
            }
            else
            {
                // if the namespaces parameter is null, search *every* namespace
                foreach (var namespaceGroup in namespaceLookup)
                {
                    matchingTypes.UnionWith(namespaceGroup);
                }
            }
        }

        return matchingTypes;
    }
```
Feb 5, 2014 at 4:18 PM
An alternative to hard-reference, or dynamically loaded assemblies, is to simply save the assembly to Bin (assuming it was downloaded at runtime). That will trigger an app restart and MVC will find it.

Long term, if dynamically loaded assemblies are important to you, you should provide your own controller factory that does the magic. I don't think the ASP.NET team will be thrilled to implement your corner case.
Feb 5, 2014 at 4:23 PM
Edited Feb 5, 2014 at 4:34 PM
Yes!! That was it!!!

After I hard-set the reference to the module in the main Web project, the DeafultControllerFactory finds it, and I get this:

The controller:
type ArenaController() =
    inherit System.Web.Mvc.Controller()
    [<MvcCodeRouting.Web.Mvc.DefaultAction>]    
    member this.Get() =
        this.Content("Nothing to see here")

type public ArenaModule() = 
    inherit DefaultModule<ArenaController>()
    override this.GetBaseControllerRoute() = 
        "Arena"
The request:
http://localhost:12244/Arena/Get 

or

http://localhost:12244/Arena

or even

http://localhost:12244/Arena/
The result:
Nothing to see here
Now, the last sticking point seems to be the embedded views.

I got the F# to embed the view like this as a compiler flag:

--resource:"Views\Arena\Get.spark","VersionOne.Arena.Module.Views.Arena.Get.spark"

When I view the assembly with Reflector, I do see that it has a resource embedded:
@{
    ViewBag.Title = "Module Version";
}

You've found the Module Version view. It's not very thrilling yet.
I guess that's actual Razor syntax, not spark. But, is the issue with the name? I know you said in
https://mvccoderouting.codeplex.com/wikipage?title=Embedded%20Views that it should be "assemblyName" based, which is what I tried. The namespace is actually just VersionOne.Arena, while the DLL is VersionOne.Arena.Module.

I get this message:
The view 'Get' or its master was not found or no view engine supports the searched locations. The following locations were searched:
Arena/Web\Get.spark
Shared\Get.spark
Arena/Web\Get.shade
Shared\Get.shade
In any case, I'm much less concerned about this part for the purpose of our demo for Friday, because we can always just return a content body that containns full on HTML markup and embedded JavaScript or return a JS file as a result of another route call. No big deal there for now. But, I'll definitely want to figure out the embedding part for the long haul.

Josh
Feb 5, 2014 at 4:26 PM
maxtoroq wrote:
An alternative to hard-reference, or dynamically loaded assemblies, is to simply save the assembly to Bin (assuming it was downloaded at runtime). That will trigger an app restart and MVC will find it.

Long term, if dynamically loaded assemblies are important to you, you should provide your own controller factory that does the magic. I don't think the ASP.NET team will be thrilled to implement your corner case.
The problem with putting the files into the bin\ folder is that when composing a catalog from it, many of the other types go crazy.

But, I suppose I could filter the catalog to contain files only of a certain naming convention. I'm not sure if that's possible, but I'll check. I am not sure if that would actually solve the TypeCacheUtil thing or not. It sounded like it was actually reading references, not files in the bin folder, but maybe that's how it works internally.
Feb 5, 2014 at 5:10 PM
Actually, since it's working on Web API, I could do something similar on MVC. I'll look into it.
Feb 5, 2014 at 5:19 PM
JoshuaGough wrote:
I guess that's actual Razor syntax, not spark. But, is the issue with the name? I know you said in
https://mvccoderouting.codeplex.com/wikipage?title=Embedded%20Views that it should be "assemblyName" based, which is what I tried. The namespace is actually just VersionOne.Arena, while the DLL is VersionOne.Arena.Module.
I think the assembly name and the root namespace must be the same. Try making that change.
Feb 5, 2014 at 5:45 PM
maxtoroq wrote:
Actually, since it's working on Web API, I could do something similar on MVC. I'll look into it.
Oh, so you mean the fact that it loads from the dynamic modules? Yeah, that would be pretty awesome.
Feb 5, 2014 at 5:46 PM
maxtoroq wrote:
JoshuaGough wrote:
I guess that's actual Razor syntax, not spark. But, is the issue with the name? I know you said in
https://mvccoderouting.codeplex.com/wikipage?title=Embedded%20Views that it should be "assemblyName" based, which is what I tried. The namespace is actually just VersionOne.Arena, while the DLL is VersionOne.Arena.Module.
I think the assembly name and the root namespace must be the same. Try making that change.
I'm trying some stuff here. It's getting confusing for me at the moment. I may end up just embedded literal HTML and script code from a Controller method at least until the week is over.
Feb 5, 2014 at 6:00 PM
JoshuaGough wrote:
maxtoroq wrote:
JoshuaGough wrote:
I guess that's actual Razor syntax, not spark. But, is the issue with the name? I know you said in
https://mvccoderouting.codeplex.com/wikipage?title=Embedded%20Views that it should be "assemblyName" based, which is what I tried. The namespace is actually just VersionOne.Arena, while the DLL is VersionOne.Arena.Module.
I think the assembly name and the root namespace must be the same. Try making that change.
I'm trying some stuff here. It's getting confusing for me at the moment. I may end up just embedded literal HTML and script code from a Controller method at least until the week is over.
I'm going to leave this part alone for now.

Here's what I'm leaving off:

Code:

Here, the dll is named VersionOne.Arena.Module.dll, and the namespaces all have VersionOne.Arena.Module.

My API routes are fine:

http://localhost:12244/Arena/Api returns the message as expected. As dothe Version/ and ByName?name=blah endpoints

When I hit: http://localhost:12244/Arena I get the "nothing to see" message as expected.
namespace VersionOne.Arena.Module

open System
open System.Web.Http
open System.Net.Http
open System.Net
open VersionOne.Arena.Intrastructure.Web

type ArenaController() =
    inherit System.Web.Mvc.Controller()
    [<MvcCodeRouting.Web.Mvc.DefaultAction>]    
    member this.Get() =
        this.Content("Nothing to see here")

type public ArenaModule() = 
    inherit DefaultModule<ArenaController>()
    override this.GetBaseControllerRoute() = 
        "Arena"

namespace VersionOne.Arena.Module.Api

open System
open System.Web.Http
open System.Net.Http
open System.Net
open VersionOne.Arena.Intrastructure.Web

type ApiController() =
    inherit System.Web.Http.ApiController()
    member this.Get () = "We got it! Here's your messages!"

    [<HttpGet>]
    member this.ByName name = "Thanks! Here's your name: " + name

    [<HttpGet>]
    member this.Version () = "Version B"

    [<HttpGet>]
    member this.NewEndpoint () = "Hey I did not exist in Version A!"

namespace VersionOne.Arena.Module.Web

open System
open System.Web.Http
open System.Net.Http
open System.Net
open VersionOne.Arena.Intrastructure.Web

type WebController() =
    inherit System.Web.Mvc.Controller()    
    [<MvcCodeRouting.Web.Mvc.DefaultAction>]    
    member this.Get() = 
        this.View("Get") 
        // Also tried this:
       // this.View()
But, when I hit Arena/Web I get:
[InvalidOperationException: The view 'Get' or its master was not found or no view engine supports the searched locations. The following locations were searched:
Arena/Web\Get.spark
Shared\Get.spark
Arena/Web\Get.shade
Shared\Get.shade]
   System.Web.Mvc.ViewResult.FindView(ControllerContext context) +504
   System.Web.Mvc.ViewResultBase.ExecuteResult(ControllerContext context) +230
   System.Web.Mvc.ControllerActionInvoker.InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) +39
   System.Web.Mvc.ControllerActionInvoker.InvokeActionResultFilterRecursive(IList`1 filters, Int32 filterIndex, ResultExecutingContext preContext, ControllerContext controllerContext, ActionResult actionResult) +116
   System.Web.Mvc.ControllerActionInvoker.InvokeActionResultFilterRecursive(IList`1 filters, Int32 filterIndex, ResultExecutingContext preContext, ControllerContext controllerContext, ActionResult actionResult) +529
   System.Web.Mvc.ControllerActionInvoker.InvokeActionResultWithFilters(ControllerContext controllerContext, IList`1 filters, ActionResult actionResult) +106
   System.Web.Mvc.Async.<>c__DisplayClass2b.<BeginInvokeAction>b__1c() +321
   System.Web.Mvc.Async.<>c__DisplayClass21.<BeginInvokeAction>b__1e(IAsyncResult asyncResult) +185
   System.Web.Mvc.Async.WrappedAsyncResult`1.CallEndDelegate(IAsyncResult asyncResult) +42
   System.Web.Mvc.Async.WrappedAsyncResultBase`1.End() +133
   System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +56
   System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeAction(IAsyncResult asyncResult) +40
   System.Web.Mvc.Controller.<BeginExecuteCore>b__1d(IAsyncResult asyncResult, ExecuteCoreState innerState) +34
   System.Web.Mvc.Async.WrappedAsyncVoid`1.CallEndDelegate(IAsyncResult asyncResult) +70
   System.Web.Mvc.Async.WrappedAsyncResultBase`1.End() +139
   System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +59
   System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +40
   System.Web.Mvc.Controller.EndExecuteCore(IAsyncResult asyncResult) +44
   System.Web.Mvc.Controller.<BeginExecute>b__15(IAsyncResult asyncResult, Controller controller) +39
   System.Web.Mvc.Async.WrappedAsyncVoid`1.CallEndDelegate(IAsyncResult asyncResult) +62
   System.Web.Mvc.Async.WrappedAsyncResultBase`1.End() +139
   System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +59
   System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +40
   System.Web.Mvc.Controller.EndExecute(IAsyncResult asyncResult) +39
   System.Web.Mvc.Controller.System.Web.Mvc.Async.IAsyncController.EndExecute(IAsyncResult asyncResult) +39
   System.Web.Mvc.MvcHandler.<BeginProcessRequest>b__5(IAsyncResult asyncResult, ProcessRequestState innerState) +39
   System.Web.Mvc.Async.WrappedAsyncVoid`1.CallEndDelegate(IAsyncResult asyncResult) +70
   System.Web.Mvc.Async.WrappedAsyncResultBase`1.End() +139
   System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +59
   System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +40
   System.Web.Mvc.MvcHandler.EndProcessRequest(IAsyncResult asyncResult) +40
   System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.EndProcessRequest(IAsyncResult result) +38
   System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +9514812
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +155
In my compiler options, I have this:

--resource:"Views\Arena\Web\Get.spark","VersionOne.Arena.Module.Views.Arena.Web.Get.spark"

And, viewing the assembly under JustDecompile verifies that the resource is embedded, as does stepping into MvcCodeRouting code and seeing that it is getting added to the ViewResources list.
Feb 5, 2014 at 6:40 PM
It's weird that the error message says
The following locations were searched:
Arena/Web\Get.spark
Shared\Get.spark
Arena/Web\Get.shade
Shared\Get.shade
instead of:
The following locations were searched:
~/Views/Arena/Web/Get.spark
~/Views/Shared/Get.spark
~/Views/Arena/Web/Get.shade
~/Views/Shared/Get.shade
Does it work with if you use Razor views?
Feb 5, 2014 at 7:40 PM
Hmm..yeah that is weird.

I haven't tried yet. We have some code that "clears" all view engines. Maybe I should comment that out.
Feb 5, 2014 at 8:25 PM
JoshuaGough wrote:
maxtoroq wrote:
Actually, since it's working on Web API, I could do something similar on MVC. I'll look into it.
Oh, so you mean the fact that it loads from the dynamic modules? Yeah, that would be pretty awesome.
It was very easy to implement, if you have a local clone checkout the controller-factory branch. You need to call ControllerBuilder.Current.EnableCodeRouting() which sets a custom controller factory. I cannot make this a default because lot's of people, like yourself, use a custom controller factory for other purposes, like dependency injection, although on that case they should be using a custom controller activator instead.
Feb 5, 2014 at 11:27 PM
So I have not gotten a chance to try that yet.

I got the basic approach of a plugin loader working including a lightweight Angular JS front end.

I did end up just copying the dlls into the bin folder from a parallel Plugins folder. Since asp.net automatically restarts when that happen it works out well. Using MEF is less important for this baseline scenario. Later on we want to be able to modularized on a per user basis, so having the ability to list components in catalog will be useful.

There is an overload for DirectoryCatalog which takes a file glob so that worked to prevent scanning of useless dlls.

I am going to try tomorrow to utilize NuGet.Core so that the modules themselves can be pulled in via a.private nuget feed per the Pro NuGet book's recipe.

So, I'd still like to get the embedded views working but it might not be until after this week that I get back into it.

Thanks for all your help so far.

If the rest of our team digs this, we may seek more help. By the way, do you ever do any side consulting?
Feb 6, 2014 at 1:12 AM
JoshuaGough wrote:
So, I'd still like to get the embedded views working but it might not be until after this week that I get back into it.
Maybe there's some kind of incompatibility with the Spark view engine, if you want to do a screen share tomorrow I can help you figure it out.

JoshuaGough wrote:
If the rest of our team digs this, we may seek more help. By the way, do you ever do any side consulting?
I do, if you are interested in my services email me at maxtoroq@gmail.com and we can discuss further :-)
Feb 6, 2014 at 1:37 AM
I removed the "ViewEngines.Clear()" statement, and now I get this:

The view 'Get' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/Arena/Web/Get.aspx
~/Views/Arena/Web/Get.ascx
~/Views/Shared/Get.aspx
~/Views/Shared/Get.ascx
~/Views/Arena/Web/Get.cshtml
~/Views/Arena/Web/Get.vbhtml
~/Views/Shared/Get.cshtml
~/Views/Shared/Get.vbhtml
Arena/Web\Get.spark
Shared\Get.spark
Arena/Web\Get.shade
Shared\Get.shade

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.InvalidOperationException: The view 'Get' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/Arena/Web/Get.aspx
~/Views/Arena/Web/Get.ascx
~/Views/Shared/Get.aspx
~/Views/Shared/Get.ascx
~/Views/Arena/Web/Get.cshtml
~/Views/Arena/Web/Get.vbhtml
~/Views/Shared/Get.cshtml
~/Views/Shared/Get.vbhtml
Arena/Web\Get.spark
Shared\Get.spark
Arena/Web\Get.shade
Shared\Get.shade

My view name is like this in the Assembly itself under JustDecompile:

VersionOne.Arena.Module.Views.Arena.Web.Get.cshtml

And it has this content:
@{
    ViewBag.Title = "Module Version";
}

You've found the Module Version view. It's not very thrilling yet.
I'm going to try removing .Module. from its name....

maxtoroq wrote:
JoshuaGough wrote:
So, I'd still like to get the embedded views working but it might not be until after this week that I get back into it.
Maybe there's some kind of incompatibility with the Spark view engine, if you want to do a screen share tomorrow I can help you figure it out.

JoshuaGough wrote:
If the rest of our team digs this, we may seek more help. By the way, do you ever do any side consulting?
I do, if you are interested in my services email me at maxtoroq@gmail.com and we can discuss further :-)
Feb 6, 2014 at 1:47 AM
Same result when I removed .Module. from the resource name:

The view 'Get' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/Arena/Web/Get.aspx
~/Views/Arena/Web/Get.ascx
~/Views/Shared/Get.aspx
~/Views/Shared/Get.ascx
~/Views/Arena/Web/Get.cshtml
~/Views/Arena/Web/Get.vbhtml
~/Views/Shared/Get.cshtml
~/Views/Shared/Get.vbhtml
Arena/Web\Get.spark
Shared\Get.spark
Arena/Web\Get.shade
Shared\Get.shade

Maybe I will try removing Spark entirely? :D
Feb 6, 2014 at 1:58 AM
Can you email me the DLL only?
Feb 6, 2014 at 2:27 AM
sure! Sending now