Dynamic API Extension in ASP.Net Core

 Introduction

This article describes about extending API in asp.net core dynamically at runtime. This means we do not have to recompile the code again if there are any new API is introduced. We can develop the extension project in a separate solution and deploy only the dll file to the server.

Some use cases of this project are,

1.       Create a dynamic API at runtime and add the API to existing server.

2.       When a third-party team want to extend the API for their own use.

3.        A scenario in where we do not have to deploy the extended services as a separate microservices.

Some advantages of using this architecture,

1.       Modularize the project, so that it is easy to maintain different parts of the project.

2.       No need to restart the server for deployment.

3.       Restrict the extension API instead of giving full control to the third-party team.

Architecture

This architecture uses the application part and assembly part from the asp.net core to extend the controllers. An application part is an abstraction over the resources of an app. Application Parts allow ASP.NET Core to discover controllers, view components, tag helpers, Razor Pages, razor compilation sources, and more. Assembly part is an application part. AssemblyPart encapsulates an assembly reference and exposes types and compilation references.

Image 1 Plugin architecture diagram

From the above architecture diagram the Plugin Framework in the interface between Base API and PluginOne project. This way the Base API identifies the plugin and registers within. The Plugin are developed and deployed independently regardless of the Base API. It then deployed to the Base API root folder. The Base API now discovers all the APIs that is within the added plugin and publish those API along with the Base API. Users and clients now can invoke the API from PluginOne

Plugin interface

The IRegisterService interface is the base framework where all the extended library should be implanted. The IRegisterService acts as a plugin interface and bridges between our main application and plugin.

namespace PluginFramework

{

    public interface IRegisterService

    {

        string ServiceName { get; set; }

    }

}

Listing 1 IRegisterService interface

The IRegisterService interface is used as type to search for plugin by main application. Listing 1 shows the interface where it has only ServiceName as member variable. The service name is the name of the plugin and used the main application to identify the plugin.

The below Listing 2 shows how the plugin got registered itself by using the IRegisterService interface. The “PluginOne” name is used by our main application to identify the plugin.

namespace PluginOne

{

    public class RegisterService : IRegisterService

    {

        public string ServiceName 

        { 

            get => "PluginOne"; 

        }

    }

}

Listing 2 Concrete implementation of IRegisterService interface by PluginOne

Registering plugin

The below piece of code extracted from ‘PluginStatusController.cs’ where we post the REST api to register the plugin with main API project. The API takes the PluginStatus and the model contains the plugin name. By default the plugin dll copied to the ‘Plugin’ folder within the root directory. The LoadFromAssemblyPath load the dll module and attach to the running process. Then the file is added to the application part which enables the process to identify the controller built within the plugin.

 [HttpPost]

 [SwaggerOperation(Summary = "Upload a plugin to activate", Description = "Upload a plugin to activate")]

 [SwaggerResponse(200, "All is OK")]

 public PluginStatus Post([FromBody] PluginStatus plugin)

 {

    string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugin");

    if (Directory.Exists(path))

    {

        string assemblyPath = Path.Combine(path, plugin.Name + ".dll");

        var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);

        if (assembly != null)

        {

            _partManager.ApplicationParts.Add(new AssemblyPart(assembly));

            // Notify change

            ActionDescriptorChangeProvider.Instance.HasChanged = true;

            ActionDescriptorChangeProvider.Instance.TokenSource.Cancel();

        }

    }

    

    return new PluginStatus

    {

        Name = "Plugin1",

        Version = "1"

    };

}

Listing 3 Registering the PluginOne with BaseAPI

Extension controller

The extension controller implements the simple API controller with the operations defined. In our example we had implemented OperationOne API. The sample code is shown below,

namespace PluginOne.Controller

{

    [ApiController]

    [Route("[controller]")]

    public class OperationOne : ControllerBase

    {

        private static readonly string[] Summaries = new[]

        {

            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"

        };

 

        [HttpGet]

        [SwaggerOperation(Summary = "OperationOne Get API", Description = "This method is from CustomerOne module")]

        [SwaggerResponse(200, "All is OK")]

        public IEnumerable<ModelOne> Get()

        {

            var rng = new Random();

            return Enumerable.Range(1, 5).Select(index => new ModelOne

            {

                Date = DateTime.Now.AddDays(index),

                TemperatureC = rng.Next(-20, 55),

                Summary = Summaries[rng.Next(Summaries.Length)]

            })

            .ToArray();

        }

 

        [SwaggerOperation(Summary = "OperationOne Post API", Description = "This method is from CustomerOne module")]

        [SwaggerResponse(200, "All is OK")]

        [HttpPost]

        public ModelOne Post(ModelOne modelOne)

        {

            return new ModelOne { };

        }

    }

}

Listing 4: Sample PluginOne extension controller

Running the extension service

To test the extension plugin, run the Base API service. The below screen shot shows the swagger documentation for Base API before the PluginOne integrated. As you can see the swagger doc contains only the API we defined in Base API.

 

Image 2: Swagger doc after PluginOne integrated

To see the extension API, copy the PluginOne.dll file from the PluginOne project to BaseAPI root folder\Plugin folder. The call the POST method of api/PluginStatus and pass the following json object in the body,

 {

    "name": "PluginOne.dll",

    "version": "v1",

    "activate": true

}

Listing 5 Json body object to be passed in api/pluginstagus

 

This call enable the PluginOne attached to the BaseAPI process. Refresh your page again and you will now see the OperationOne APi listed in the swagger doc. The below after registration screen shot shows the same.

Image 2: Swagger doc after PluginOne integrated

Sample Code

The sample code for the above discussed project can be downloaded from this github repository.

Conclusion

Although in microservices architecture, an extension API can be installed in separate micro service. The above article is used in a particular case where API need an extension but still need to continue to be deployed as single monolithic application. This architecture is also helpful when security is considered where request and response is controlled.





Comments

Popular posts from this blog

Debugging and Testing Helm Charts Using VS Code

Handle Multipart Contents in Asp.Net Core

Validate appsettings in ASP.net Core using FluentValidation