Inject Controller Libraries Dynamically at Runtime in ASP.NET Web API using Custom Assembly Resolvers

Posted by: Suprotim Agarwal , on 5/20/2013, in Category ASP.NET
Views: 83996
Abstract: An introduction to Web API’s Custom Assembly Resolver feature that enables you to add Controller libraries dynamically at run time. To add some pizazz, we generate the entire Controller on the fly and see how we can access it from a Web API Client.

Recently I was working with a CMS application from a reputed commercial vendor and it looked like they were doing dynamic code gen as the CMS features and components were added. It was intriguing and I kept that at the back of my head.

Recently while exploring Web API features, I came across an extension point I had missed earlier – CustomAssemblyResolvers. This extension point is very interesting as in, it is invoked whenever WebAPI has to resolve a Url to a corresponding controller and it can’t find it in its ‘cache’. This gives us a nice opportunity to ‘inject’ Web API controller libraries into a system dynamically.

 

This article has been co-authored by Sumit Maitra and me

Consider the scenario where you need a System that can dynamically spin up CRUD operation based on an Entity defined at Run time. For example, users creating an Invoice template via an ‘Invoice Designer’ UI where they can Add/Remove fields to the Invoice as desired. Once the Form has been designed, they will save it in the system and upon execution, a UI will be generated containing the fields defined earlier. These fields need to be submitted to a service when the Invoice is being saved. This sounds like a good case of doing a Code Generation of the Service whenever the Invoice design is changed in the ‘Invoice Designer’. If there are no changes, the previous designer continues to work.

Today we will see how we can generate Code that are essentially Web API controllers and then compile them into a dll, which is then invoked at runtime using the Custom Assembly Resolver extension point. Our Code Gen will however be limited to generating a very simple “Hello World” Web API today, but you’ll get the gist.

Creating a Web API Controller Dynamically and packaging it into a Dll

To package our ‘on the fly’ Web API Controller, we’ll need a Code Gen module, so we’ll start with a simple Console Application called CustomAssemblyResolverDemo.

Step 1: Start Visual Studio (Desktop Editor for Express users) and create a new Console Application called CustomAssemblyResolverDemo.

We will add a Web API Self Host server to this later, so we’ll keep this Dll empty for now and add another Class Library project to the solution where we’ll do the Code Gen. We’ll call this library WebApiCodeGenLib

add-reference

Step 2: We add a class library to the solution called WebApiCodeGenLib.

Step 3: Next we rename Class1.cs into WebApiGenerator.cs and add a field and a Constructor.

public class WebApiGenerator
{
string _controllerName;

public WebApiGenerator(string controllerName)
{
  _controllerName = controllerName.Replace(" ", string.Empty);
}
---
}

The constructor simply takes a name that we’ll use later in the Controller. Let’s go through the dicrf

Step 4: Next we have the crux of the dll, the code to generate dll at runtime. The CreateDll method is in the WebApiGenerator.dll itself.

- We initialize the Compiler Version to v.4

- Next we initialize the C# CodDomProvider.

- We use the name passed to use, to use it as the name of the dll

- We create a CompilerParameters instance and set them up to

  • Generate a dll (as opposed to an Exe)
  • Provide the name of the output dll
  • Add Assembly references. These need to be present in the folder where the dynamic dll is being compiled. So be sure to add reference to the Dlls below. You can use the Package Manager Console as follows

PM> install-package Microsoft.Net.Http

PM> install-package Microsoft.AspNet.WebApi.Core

- Finally we add the source code to codeProvider. Note we have used the _controllerName passed into the WebApiGenerator as the class name and also reflected it in the Message.

- The CompileAssemblyFromSource call tries to compile the code into a Dll and if it fails all the Build errors are listed out.

- On success, a Build Succeeded message is shown.

- This completes our Code generator.

public void CreateDll()
{
IDictionary<string, string> compParams =
     new Dictionary<string, string>() { { "CompilerVersion", "v4.0" } };
CodeDomProvider codeProvider = CodeDomProvider.CreateProvider("CSharp", compParams);
string outputDll = _controllerName + ".dll";

System.CodeDom.Compiler.CompilerParameters parameters = new CompilerParameters();
parameters.GenerateExecutable = false;
parameters.OutputAssembly = outputDll;
parameters.ReferencedAssemblies.Add(@"System.Net.Http.dll");
parameters.ReferencedAssemblies.Add(@"System.Net.Http.WebRequest.dll");
parameters.ReferencedAssemblies.Add(@"System.Net.Http.Formatting.dll");
parameters.ReferencedAssemblies.Add(@"System.Web.Http.dll");
string code = new StringBuilder()
    .AppendLine("using System.Web.Http;")
    .AppendLine("namespace ControllerLibrary")
    .AppendLine("{")
    .AppendLine(string.Format("public class {0} : ApiController", _controllerName))
    .AppendLine(" {")
    .AppendLine("  public string Get()")
    .AppendLine("  {")
    .AppendLine(string.Format("return \"Hi from a Dynamic controller library- {0} !\";", _controllerName))
    .AppendLine("  }")
    .AppendLine(" }")
    .AppendLine("}")
    .ToString();
CompilerResults results = codeProvider.CompileAssemblyFromSource(parameters, code);  
if (results.Errors.Count > 0)
{
  Console.WriteLine("Build Failed");
  foreach (CompilerError CompErr in results.Errors)
  {
   Console.WriteLine(
   "Line number " + CompErr.Line +
   ", Error Number: " + CompErr.ErrorNumber +
   ", '" + CompErr.ErrorText + ";" +
   Environment.NewLine + Environment.NewLine);
  }
}
else
{
  Console.WriteLine("Build Succeeded");
  return Assembly.LoadFrom(outputDll);
}
Console.ReadLine();
return null;
}

Step 5: To test the CodeGen, we go back to the CustomAssemblyResolverDemo.

- First we a add reference to the WebApiCodeGen project.

- Next we add the following code to the main method

static void Main(string[] args)
{
WebApiGenerator gen = new WebApiGenerator("DynamicWebApi");
gen.CreateDll();
}

Step 6: Run the Application and make sure you are getting a Build Succeeded message on the Console.

image

Once you get the above message it implies your new dll is working fine. Now to load the Dll and invoke it.

Implementing the AssemblyResolver

The Assembly Resolver used by Web API derives from DefaultAssembyResolver. In CustomAssemblyResolverDemo, we add a new Class called DynamicAssemblyResolver. The class is implemented as follows:

- We override the GetAssemblies method and retrieve the available list of assemblies from base.GetAssemblies.

- To this list, we add an Assembly created on the fly by WebApiGenerator class that we wrote above.

- If a dll is generated successfully, it is returned and added to the Assemblies collection.

public class DynamicAssemblyResolver : DefaultAssembliesResolver
{
public override ICollection<Assembly> GetAssemblies()
{
  ICollection<Assembly> baseAssemblies = base.GetAssemblies();
  List<Assembly> assemblies = new List<Assembly>(baseAssemblies);
  try
  {
   WebApiGenerator gen = new WebApiGenerator("DynamicWebApi");
   Assembly onTheFly = gen.CreateDll();
   if (onTheFly != null)
   {
    assemblies.Add(onTheFly);
   }
  }
  catch
  {
   // We ignore errors and just continue
  }
  return assemblies;
}
}

The Self-Host Web API Server and invoking Dynamic Dll from a Console Client

Now that we have the Dynamic Dll in place, let’s setup the server that will ‘serve’ up the controller. This could very well be an ASP.NET Web API server application but today we’ll take advantage of Web API self-host.

Setting up the Web API Client

The Web API Client is nothing but an HttpClient instance that’s going to ping the Dynamically created Web API’s URL to check if it gets a response. If it does get a response, it prints it to the command line.

static async void RunDynamicClientAsync()
{
HttpClient client = new HttpClient();
Uri address = new Uri(_baseAddress, "/api/DynamicWebApi");
HttpResponseMessage response = await client.GetAsync(address);
response.EnsureSuccessStatusCode();
string content = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();
content = await response.Content.ReadAsStringAsync();
Console.WriteLine("On The Fly Controller says {0}", content);
}

Setting up the Self Host Server

With all dependencies set, we finally setup the Self host server. You can refer to our previous article on Web API Self hosting if you are not familiar with it. Before we get started, we make sure references to the following package are set

PM> install-package Microsoft.AspNet.WebApi.SelfHost

PM> install-package Microsoft.Net.Http

PM> install-package Microsoft.AspNet.WebApi.Core

We need an additional dependency of System.ServiceModel that we can add from the Add-Reference dialog

add-reference-service-model

Once we are set with the dependencies, we the Code is as follows –

1. Select a port at which the server will run and assign the url to the _baseAddress field.

2. Setup a Route in the configuration

3. Initialize an instance of the DynamicAssemblyResolver and replace the default assembly resolver in the routing config, with it.

4. Initialize HttpSelfHostServer with the above _baseAddress and routing Configuration.

5. Start the Server and wait for clients to connect

6. We run an infinite While loop as our dirty message pump. After initializing the server, we immediately invoke the client and get a response back from them. Thereafter we wait for the user to respond. If the User hits “Enter”, we ping the dynamic controller again via our client and print the response. This loop helps as verify that the ‘Dynamic Creation’ of dll only happens the first time when it is unavailable.

7. To shut down the server user simply needs to click Ctrl+C.

static readonly Uri _baseAddress = new Uri("http://localhost:60064");
static void Main(string[] args)
{
HttpSelfHostServer server = null;
try
{
  HttpSelfHostConfiguration config = new HttpSelfHostConfiguration(_baseAddress);
  config.HostNameComparisonMode = HostNameComparisonMode.Exact;
  config.Routes.MapHttpRoute(
   name: "DefaultApi",
   routeTemplate: "api/{controller}/{id}",
   defaults: new { id = RouteParameter.Optional }
  );

  DynamicAssemblyResolver assemblyResolver = new DynamicAssemblyResolver ();
  config.Services.Replace(typeof(IAssembliesResolver), assemblyResolver);

  server = new HttpSelfHostServer(config);
  server.OpenAsync().Wait();
  Console.WriteLine("Listening on " + _baseAddress);
  while (true)
  {
   RunClient();
   Console.WriteLine("Press Ctrl+C to exit...");
   Console.ReadLine();
  }
}
catch (Exception e)
{
  Console.WriteLine("Could not start server: {0}", e.GetBaseException().Message);
  Console.WriteLine("Hit ENTER to exit...");
  Console.ReadLine();
}
finally
{
  if (server != null)
  {
   server.CloseAsync().Wait();
  }
}
}

Web API Demo

Now that we have all the moving pieces set, let’s do a Demo. To verify our DLL is created only once, put a breakpoint in the WebApiGenerator.CreateDll method and run the application.

1. The system will break at the above breakpoint.

break-point-hit-first-time

2. Continue here and the Console will show the following messages

console-out-first-time

Note the ‘Build Succeeded’ message for the dynamic Dll creation the first time.

3. As the system waits for input, hit Enter to ping the Dynamic Web Api Controller again. Now you will note that the above breakpoint is not hit and neither is there a ‘Build Succeeded’ message in the next set of console output.

console-out-second-time

Conclusion

We saw how we could code a Web API controller at run time and access it as it is generated. This is not something you’ll require. It is feature geared more towards applications like CMS system and Enterprise Dynamic Forms over data apps. Though the Code Gen technique we saw is nothing new, it blended well to showcase Web API’s CustomAssemblyResolver extension point.

Download the entire source code of this article (Github)

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Suprotim Agarwal, MCSD, MCAD, MCDBA, MCSE, is the founder of DotNetCurry, DNC Magazine for Developers, SQLServerCurry and DevCurry. He has also authored a couple of books 51 Recipes using jQuery with ASP.NET Controls and The Absolutely Awesome jQuery CookBook.

Suprotim has received the prestigious Microsoft MVP award for Fifteen consecutive years. In a professional capacity, he is the CEO of A2Z Knowledge Visuals Pvt Ltd, a digital group that offers Digital Marketing and Branding services to businesses, both in a start-up and enterprise environment.

Get in touch with him on Twitter @suprotimagarwal or at LinkedIn



Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!
Comment posted by Pax on Friday, May 24, 2013 3:16 AM
I appreciate the post. but still could not really figure out the need of creating web api run time and loading through assembly resolver.
Comment posted by confused on Monday, May 27, 2013 10:39 PM
So why do we need a Web API , its just assembly resolver. cheating post.
Comment posted by Suprotim Agarwal on Thursday, July 25, 2013 1:20 AM
@Pax @confused Please read the first few lines of this article to understand the context
Comment posted by Visar on Wednesday, March 19, 2014 5:46 AM
Obviously @Pax and @confused either didn't read or didn't understand the article. I need to develop an "Enterprise Forms Solution" with "Extendable DataSets" and this is the right way to go.
Well written.
Cheers!
Comment posted by Robert Dytmire on Saturday, April 19, 2014 11:41 AM
Hi,

Excellent post.  I have a rather large enterprise project that can benefit from this.  

One question for the gang...is there an example of how I can unload this DLL during runtime?  For example, updating the dll without restarting anything.


Comment posted by stefanveliki on Tuesday, November 11, 2014 11:58 AM
Umm... so, when I added a WebAPI to your solution and tried to serve the dynamic dll's controllers, it didn't work.

1:
I moved the "create dll" code to the WebApiConfig section... no go.

The build code failed with errors specifying that
System.Net.Http.Formatting.dll and System.Net.Http.WebRequest.dll cannot be found.


2:
I took the dll that was created by the console app. Then I placed in the WebAPI test project's bin.

That also didn't work.

Any suggestions??
Comment posted by stefanveliki on Tuesday, November 11, 2014 12:33 PM
Umm... so, when I added a WebAPI to your solution and tried to serve the dynamic dll's controllers, it didn't work.

1:
I moved the "create dll" code to the WebApiConfig section... no go.

The build code failed with errors specifying that
System.Net.Http.Formatting.dll and System.Net.Http.WebRequest.dll cannot be found.


2:
I took the dll that was created by the console app. Then I placed in the WebAPI test project's bin.

That also didn't work.

Any suggestions??
Comment posted by Edson Loris on Tuesday, December 9, 2014 1:36 PM
Try this:

  var assemblies = AppDomain.CurrentDomain
                            .GetAssemblies()
                            .Where(a => !a.IsDynamic)
                            .Select(a => a.Location);

            parameters.ReferencedAssemblies.AddRange(assemblies.ToArray());
Comment posted by Edson Loris on Wednesday, December 10, 2014 5:11 AM
Try this:

  var assemblies = AppDomain.CurrentDomain
                            .GetAssemblies()
                            .Where(a => !a.IsDynamic)
                            .Select(a => a.Location);

            parameters.ReferencedAssemblies.AddRange(assemblies.ToArray());
Comment posted by Edson Loris on Wednesday, December 10, 2014 5:18 AM
Try this:

  var assemblies = AppDomain.CurrentDomain
                            .GetAssemblies()
                            .Where(a => !a.IsDynamic)
                            .Select(a => a.Location);

            parameters.ReferencedAssemblies.AddRange(assemblies.ToArray());