This article introduces a package that helps developers to contain web controllers in class libraries for easy-deployment purpose. Tutorial of using the package and demo code are provided.
Introduction
The concept micro-service has been raised for a long time. While the idea of grouping relevant services together and deploying them separately from other irrelevant services really increases the flexibility and reduces complexity and coupling of code, it may raise maintainability issues as increasing number of micro-service usually leads to more complicated deployments. Apparently, deploying everything with one package into one website is much easier than dividing the package into several small parts and deploying them separately to different websites.
To mitigate the deployment problem, this article introduces a small package for ASP.NET Core developers to contain micro-services in class libraries, and simply deploy the micro-service binaries by copying them under a path and doing a little configuration at the web API host, so many such micro-services can be deployed under the same website as plug-ins.
Background
Traditional web APIs are usually built as a monolith, with a lot of controllers providing various features within one website. It would be easy to simply pile up all features whenever necessary to the same solution, but it is also common sense that this approach requires the whole code base to be re-published for any tiny update, increases the possibility of irrelevant code coupling, and without divide-and-conquer, the whole code base together may scare the code readers by its size.
The concept of micro-service that groups relevant APIs and deploys them separately solves the problems. Simply deploying each group of API into a separate website may cause another problem, that a lot of websites and a lot of ports need to be managed. It's not a serious problem, but can be a little annoying for management. Then an idea of solving this problem would be deploying all such micro-services under the same website. This is definitely possible because we can distribute web controllers in class libraries and make them work normally in a web API project.
The package Oasis.MicroService
is designed to encapsulate the details of distributing web controllers in separate class libraries with micro-service concept.
Using the Code
To demonstrate how to use the package, we need to build at least two simple microservices, and one website to host the microservices.
To Build a Micro-Service
-
Initialize a class library project as the micro-service main assembly, define all controllers in this assembly, and implement all relevant classes and interfaces.
-
Add reference of Oasis.MicroService
to the class library project, implement a class that inherits from Oasis.MicroService.MicroServiceContextBuilder
class. Note that:
-
This abstract
class as one abstract
method to be implemented, which is Initialize
method, in it all dependency injection for the microservice should be done to the IServiceCollection
parameter of the method, like the code piece below the section. One thing to highlight is that each micro-service has its own independent dependency injection container if configured this way, so micro-services are separated from each other.
-
Initialize
method has an second parameter of type IConfigurationRoot
, it is the content of configuration read with logic with protected virtual
method GetConfiguration
. The default implementation assumes the configuration file to be named appsettings.json and under the same directory with the micro-service assembly. Environment specific configuration works the same way as normal ASP.NET Core web API, which means appsettings.Development.json will be taken when environment variable ASPNETCORE_ENVIRONMENT =
Development
. Such configuration files are optional as normal ASP.NET Core web API behaves, if they are mandatory for the micro-service, some defensive code can be implemented in Initialize
method. The method GetConfiguration
is virtual for the possibility of customizing configuration file reading feature.
protected override void Initialize(IServiceCollection serviceCollection,
IConfigurationRoot configuration)
{
serviceCollection.AddSingleton<ISimpleDemoServiceDemoService>(
new SimpleDemoServiceDemoService());
var simpleDemoServiceConfiguration =configuration.Get<SimpleDemoServiceConfiguration>();
if (simpleDemoServiceConfiguration == null)
{
throw new FileLoadException(
$"Configuration for {typeof(SimpleDemoServiceConfiguration)} missing",
Path.GetFileName(this.GetType().Assembly.Location));
}
serviceCollection.AddSingleton<ISimpleDemoServiceConfiguration>(
simpleDemoServiceConfiguration);
}
To Build a Website
-
Create a web API project, add reference of Oasis.MicroService
to it. Copy published micro-service binaries to expected paths (The BuildForDemo.ps1 script in the sample code does all publishing and copying work, please refer to it).
-
Create a MicroServices
section in the configuration file, listing all micro-service main assemblies to be hosted with the web API project.
-
Included
section lists all micro-service assemblies to be included, Oasis.MicroService
will consider all assemblies in Directory
and all its sub-directories whose file name matches SearchPattern
value to be micro-service assemblies. Root folder of the configuration is the directory of the assembly from which AddMicroServices
is called (usually should be the web API assembly). Note that wildcard characters "*
" and "?
" are supported in SearchPattern
(but not in Directory
) to provide the possibility for not having to manually add/delete a configuration entry whenever they add/delete a micro-service from the web API host. Environment
value will override the value of environment variable ASPNETCORE_ENVIRONMENT
if specified.
-
To exclude certain assemblies that may be wrongfully matched with wildcard characters in Included
section, Excluded
section is introduced to list such assemblies so Oasis.MicroService
will skip them (In the sample code Oasis.DemoServiceWithSqlite.dll will be matched by pattern *DemoService.dll, but it is a dependency assembly which doesn't contain micro-service required definitions. So, if we don't exclude it specifically or set flag IgnoreAssemblyLoadingErrors
to be true, relevant exceptions will be thrown upon the web API starting).
-
Oasis.MicroService
will throw exception if any micro-service assembly it tries to load got problems (e.g., assembly doesn't exist or is of wrong format, assembly doesn't properly implement relevant classes and so on), with value of the flag IgnoreAssemblyLoadingErrors
set to "true
" the exceptions will be swallowed (Excluded
section won't be necessary in this case), so such invalid assemblies are simply ignored. If there are a lot of assemblies wrongfully matched by wildcard characters, this flag is a convenient way to avoid listing them one by one in Excluded
section. But it's always recommended to leave the flag unconfigured (so the value is defaulted to false).
-
For detailed information for MicroServices
section, please refer to the document of the package.
-
Read the section from the configuration file, and call AddMicroServices
method to plug-in the micro-services (See program.cs file in Oasis.DemoWebApi
in the sample code for details).
{
"MicroServices":{
"Included": [
{ "Directory": "MicroServices", "SearchPattern": "*DemoService.dll" },
{ "Directory": "MicroServices", "SearchPattern":
"Oasis.DemoServiceWithSqlite.dll", "Environment": "Test" }
],
"Excluded": [
{ "Directory": "MicroServices", "SearchPattern":
"Oasis.CommonLibraryForDemoService.dll" }
],
"IgnoreAssemblyLoadingErrors": false
}
}
var builder = WebApplication.CreateBuilder(args);
var microServiceConfiguration = new MicroServiceConfiguration();
builder.Configuration.GetSection("MicroServices").Bind(microServiceConfiguration);
builder.AddMicroServices(microServiceConfiguration);
If everything is set up correctly, the controllers defined in the micro-service assemblies should be accessible from the website. That's all it takes to build up micro-services and deploy them by simply copying over the binaries.
To Try the Sample Code Attached
-
Execute BuildForDemo.ps1 script, it builds the web API project, publishes the three demo micro-services and copy over relevant binaries to expected locations.
-
Debug Oasis.DemoWebApi
project, the controllers defined in the three micro-services should be available at paths /EmptyDemoService/Test, /SimpleDemoService/Test and /DemoServiceWithSqlite/Test.
-
Oasis.EmptyDemoService
is an empty micro-service that has no configuration files, and to be included by *DemoService.dll search pattern; Oasis.SimpleDemoService
is a simple micro-service that has a configuration file and to be included by *DemoService.dll search pattern as well; Oasis.DemoServiceWithSqlite
is a microservice that has environment specific configuration files, accesses a sqlite
database, and to be included by the entry without any wildcard character; Oasis.CommonLibraryDemoService
is a dependency library that the three micro-service depends on, its existence helps to demonstrate how Excluded
section or IgnoreAssemblyLoadingErrors
flag should be used. Oasis.DemoWebApi
is the web API host.
Please note that the sample code and the package is built with .NET 8.0 preview version, which may not be supported by Visual Studio 2022 yet, so please install .NET 8.0 SDK and open the project and run it with Visual Studio Code.
Points of Interest
Since all micro-services are actually being executed in the same process, dependency version may become an issue if multiple hosted micro-services depend on the same assembly of different versions. It's highly recommended that all dependency libraries should be strong named.
For any suggestions or inquires, please leave a comment under this article or visit https://github.com/keeper013/Oasis.MicroService.
History
- Version 0.1.0: Expecting user to implement a
ServiceContext
class, which isn't so convenient. - Version 0.2.0: Eliminated
ServiceContext
to simplify user interface. - Version 0.3.0: Strong named
Oasis.MicroService
assembly, environment specific configuration feature implemented - Version 0.4.0: Simplified environment specific configuration user interface, so
MicroServiceContextBuilder
only has the default constructor now. - Version 0.5.0: Enhanced environment specific configuration so micro-services consider value of environment variable
ASPNETCORE_ENVIRONMENT
if not configured in MicroServices
section. Defaulted configuration file name to be appsettings.json instead of taking the assembly name. - Version 0.6.0: Enhanced
MicroServices
configuration section to allow matching assembly names with wildcard characters. Existing words refined a little.