public partial class MainWindow : Window
{
private IAdaptiveClient<IUsersService> client;
public MainWindow(IAdaptiveClient<IUsersService> client)
{
this.client = client;
}
public async Task<User> Login(string userName, string password)
{
// AdaptiveClient will attempt to use the server you define as most preferred.
// Server may be SQL, MySQL, WCF, REST, etc. - AdaptiveClient will resolve the correct
// client and all dependencies.
// If the request fails AdaptiveClient will begin an orderly fall back to any other
// server that can handle the request:
User user = await client.TryAsync(x => x.GetUser(userName, password));
return user;
}
}
Easily allow internal desktop apps to access APIs using fast, in-process calls over the local area network. The same applications can make HTTP calls and access the same APIs when off-site. AdaptiveClient resolves the correct implementation of your API client based on the connection available at runtime.
If you maintain a shrink wrap app or you are migrating and you want to target multiple database providers you can make the process simple and transparent to the end user of your application. AdaptiveClient resolves the correct implementation of your service and repository based on properties of the connection string.
All you need to do is define multiple connection strings for the API you want to use. Give each connection string a preference and AdaptiveClient will attempt to use them in the order you define. Connection strings can be for any transport or data provider.
AdaptiveClient allows you to continue to write strongly typed n-tier applications using SOLID and DRY principals you have already embraced. If you are careful to encapsulate your business logic in your service layer you are well on your way to writing a truly scalable application. You don't have to buy into any new architecture, you don't need to write any tooling, there are no black boxes.
AdaptiveClient
is a design pattern that leverages n-tier architecture and a dependency injection container (Autofac). The classes included in this download assist you in implementing the pattern. In a nutshell, AdaptiveClient works by associating three keys with each connection string in your application. These three keys are API_Name, EndPointType, and ProviderName. You define the values for each of these keys. You register each of your connection strings (or API URLs) with AdaptiveClient using these keys. You also use the same keys to register implementations of your services. When you make an API call AdaptiveClient use the keys associated with the connection string (or URL) to resolve the specific dependencies required to communicate with the server.
The functionality provided by AdaptiveClient comes primarily from a class called EndPointConfiguration
which is a class that contains a connection string and a few extra properties. When you implement AdaptiveClient you move your connection strings and API URLs to a section within your appsettings.json file. The values you see for API_Name, EndPointType, and ProviderName are also defined as constants in your application. You register implementations of your services using these same values as keys. Doing so allows AdaptiveClient to match a service implementation to a connection string:
{
"EndPoints": [
{
"Name": "StoreFront_SQLServer",
"IsActive": "true",
"API_Name": "StoreFront",
"Preference": "10",
"EndPointType": "InProcess",
"ProviderName": "MSSQL",
"ConnectionString": "Data Source=.\\SQLSERVER;Initial Catalog=AdaptiveClientEF_StoreFront;"
},
{
"Name": "BackOffice_SQLServer",
"IsActive": "true",
"API_Name": "BackOffice",
"Preference": "10",
"EndPointType": "InProcess",
"ProviderName": "MSSQL",
"ConnectionString": "Data Source=.\\SQLSERVER;Initial Catalog=AdaptiveClientEF_BackOffice;"
},
{
"Name": "StoreFront_MySQL",
"IsActive": "true",
"API_Name": "StoreFront",
"Preference": "20",
"EndPointType": "InProcess",
"ProviderName": "MySQL",
"ConnectionString": "Server=localhost;Database=AdaptiveClientEF_StoreFront;Uid=x;Pwd=x;SslMode=none;"
},
{
"Name": "BackOffice_MySQL",
"IsActive": "true",
"API_Name": "BackOffice",
"Preference": "20",
"EndPointType": "InProcess",
"ProviderName": "MySQL",
"ConnectionString": "Server=localhost;Database=AdaptiveClientEF_BackOffice;Uid=x;Pwd=x;SslMode=none"
},
{
"Name": "StoreFront_WebAPI",
"IsActive": "true",
"API_Name": "StoreFront",
"Preference": "30",
"EndPointType": "HTTP",
"ProviderName": "WebAPI",
"ConnectionString": "http://localhost:59260/api/StoreFront/"
}
]
}
AdaptiveClient includes a utility for loading EndPoints from your appsettings.json file. By default only EndPoints where IsActive
is true are loaded.
In most cases you will use a single connection string for each API your application uses (which may be a url). However, as mentioned above, AdaptiveClient will fall back if necessary and attempt to contact multiple servers using different connection strings. Here is how that process works:
EndPointConfiguration
public class EndPointConfiguration : IEndPointConfiguration
{
public string Name { get; set; }
public string API_Name { get; set; }
public int Preference { get; set; }
public String EndPointType { get; set; }
public string ConnectionString { get; set; }
public string ProviderName { get; set; }
public Dictionary<string, string> Parameters { get; set; }
public bool IsActive { get; set; }
}
- Name: Name of the EndPoint: DevServer01, QASloth02, etc.
- API_Name: Name of the application or API exposed by the EndPoint: OurCompanyApp, xyz.com, etc.
- Preference: Number that allows AdaptiveClient to rank this EndPoint. Lower numbers are more preferred.
- EndPointType: User defined string that describes what kind of transport is used to access the EndPoint - Examples might be DBMS, HTTP, TCP.
- ProviderName: A string that further describes the connection string data provider or protocol. Examples might be MSSQL, MySQL, WCF, REST.
- ConnectionString: Valid connection string OR URL if pointing to a HTTP server.
- Parameters: Not used at this time.
- IsActive: Set this value to false to prevent AdaptiveClient fro using this
EndPointConfiguration
.
RegistrationHelper
RegistrationHelper
hides the complexity of registering EndPointConfiguration
objects and services with the DI container. The following is an example of registering three implementations of IOrdersService
. The first implementation contains code specific to Microsoft SQL Server. The second is specific to MySQL. The third makes API calls over HTTP. Note how values supplied as parameters to the RegisterService
method match values provided in the appsettings.json file shown above.
registrationHelper.RegisterService<MSSQL_OrdersService, IOrdersService>(EndPointType.DBMS, API_Name.StoreFront, ProviderName.MSSQL);
registrationHelper.RegisterService<MySQL_OrdersService, IOrdersService>(EndPointType.DBMS, API_Name.StoreFront, ProviderName.MySQL);
registrationHelper.RegisterService<WebAPI_OrdersService, IOrdersService>(EndPointType.HTTP, API_Name.StoreFront, ProviderName.WebAPI);
Additional usage is discussed in the Getting Started section.
AdaptiveClient
AdaptiveClient implements two methods you can use to call your services: Try and Call. These methods differ in how and when they identify a server as being available if they are called without a specific EndPointConfiguration
name.
- Call and CallAsync
string endPointName = "Production_Server";
var orders = await serviceClient.CallAsync(x => x.OrdersService.GetOrders(), endPointName);
In the example above we are calling a service and passing the name of a specific EndPointConfiguration
to use. AdaptiveClient will resolve components based on the properties of that specific EndPointConfiguration
in order to call the service.
var orders = await serviceClient.CallAsync(x => x.OrdersService.GetOrders());
In the example above we do not pass the name of an EndPointConfiguration
to Call or CallAsync. When invoked using this overload, Call and CallAsync will attempt to discover an available server the first time (only) the methods are called for each API_Name that is registered with AdaptiveClient. To determine if a server is available AdaptiveClient resolves an implemenation of IEndPointValidator
and calls IsInterfaceAlive
(You can use the implementations of IEndPointValidator found in the AdaptiveClient.Utilities nuget package or create your own). After an available server is identified AdaptiveClient makes no other attempt to identify available servers. If you use the Call or CallAsync method and the call to the server fails your application will fail. The Call and CallAsync methods do not provide fallback capabilities.
- Try and TryAsync
var orders = await serviceClient.TryAsync(x => x.OrdersService.GetOrders());
Like their names imply, Try and TryAsync execute your method inside a try block. AdaptiveClient does not use a validator to test if a server is alive when Try is called - it simply calls your method. If the call fails, AddaptiveClient will attempt to fall back to another server that is registered for the API. Servers are contacted in the order of the Preference property on the EndPointConfiguration class. If a call to a server fails the server is marked as "Offline" and no additional calls are made to it during the lifetime of the application. If AdaptiveClient runs out of connection strings it will throw an exception.
string primaryEndPoint = "Production Server";
string backupEndPoint = "Backup Server";
var orders = await serviceClient.TryAsync(x => x.OrdersService.GetOrders(), primaryEndPoint, backupEndPoint);
Try and TryAsync are overloaded to allow one or more EndPointConfiguration
names to be used.
- Install Nuget packages:
- Autofac
- AdaptiveClient
- AdaptiveClient.EntityFrameworkCore (optional)
- AdaptiveClient.Utilities (optional but highly recommended)
- Define keys - Define API_Name, EndPointType, and ProviderName as described above. These keys should be defined in the Domain layer of your app.
- Define Endpoints - Create a file called something like EndPoints.json to define your EndPointConfigurations. See one of the demo projects for an example.
- Register required components - You will need to register your EndPoints, your services, and possibly one or more
IEndPointValidator
implementations. If you use Entity Framework you will also need to registerDbContext
andDbContextOptions
. See the section below and the example application. - Register optional components - Registering a
ServiceManifest
is optional but recommended even if you have a small number of services. If you use EF migrations you should register one or moreMigrationContexts
. Use aMigrationContext
to easily drop and re-create your database and apply migrations as necessary. You may also want to register aDatabaseInitalizer
if you seed your database when it is created or when a migration is applied.
The Zamagon demo is written to run against both MySQL and MSSQL databases. The code below is taken from that project and shows how services and Entity Framework objects are registered.
AdaptiveClient supports the concept of Modules that are similar to the object of the same name in Autofac. The code below is taken from the Startup class of a dotnet core web application:
// This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Other services registered here...
// Autofac & AdaptiveClient
IEnumerable<IEndPointConfiguration> endPoints = EndPointUtilities.LoadEndPoints("EndPoints.json");
ContainerBuilder builder = new ContainerBuilder();
builder.Populate(services);
builder.RegisterModule(new LeaderAnalytics.AdaptiveClient.EntityFrameworkCore.AutofacModule());
RegistrationHelper registrationHelper = new RegistrationHelper(builder);
registrationHelper
.RegisterEndPoints(endPoints) // endPoints must be registered first
.RegisterModule(new Zamagon.Services.Common.AdaptiveClientModule())
.RegisterModule(new Zamagon.Services.BackOffice.AdaptiveClientModule())
.RegisterModule(new Zamagon.Services.StoreFront.AdaptiveClientModule());
var container = builder.Build();
IDatabaseUtilities databaseUtilities = container.Resolve<IDatabaseUtilities>();
// Create all databases or apply migrations
foreach (IEndPointConfiguration ep in endPoints.Where(x => x.EndPointType == EndPointType.DBMS))
databaseUtilities.CreateOrUpdateDatabase(ep).Wait();
return container.Resolve<IServiceProvider>();
}
The following is an example of how services might registered for an app that reads MSSQL, MySQL, or calls a WebAPI server. This code is also take from the Zamagon demo.
public class AdaptiveClientModule : IAdaptiveClientModule
{
public void Register(RegistrationHelper registrationHelper)
{
// --- StoreFront Services ---
registrationHelper
// MSSQL
.RegisterService<StoreFront.MSSQL.OrdersService, IOrdersService>(EndPointType.DBMS, API_Name.StoreFront, DataBaseProviderName.MSSQL)
.RegisterService<StoreFront.MSSQL.ProductsService, IProductsService>(EndPointType.DBMS, API_Name.StoreFront, DataBaseProviderName.MSSQL)
// MySQL
.RegisterService<StoreFront.MySQL.OrdersService, IOrdersService>(EndPointType.DBMS, API_Name.StoreFront, DataBaseProviderName.MySQL)
.RegisterService<StoreFront.MySQL.ProductsService, IProductsService>(EndPointType.DBMS, API_Name.StoreFront, DataBaseProviderName.MySQL)
// WebAPI
.RegisterService<StoreFront.WebAPI.OrdersService, IOrdersService>(EndPointType.HTTP, API_Name.StoreFront, DataBaseProviderName.WebAPI)
.RegisterService<StoreFront.WebAPI.ProductsService, IProductsService>(EndPointType.HTTP, API_Name.StoreFront, DataBaseProviderName.WebAPI)
// DbContexts
.RegisterDbContext<Database.Db>(API_Name.StoreFront)
// Migration Contexts
.RegisterMigrationContext<Database.Db_MSSQL>(API_Name.StoreFront, DataBaseProviderName.MSSQL)
.RegisterMigrationContext<Database.Db_MySQL>(API_Name.StoreFront, DataBaseProviderName.MySQL)
// Database Initializers
.RegisterDatabaseInitializer<SFDatabaseInitializer>(API_Name.StoreFront, DataBaseProviderName.MSSQL)
.RegisterDatabaseInitializer<SFDatabaseInitializer>(API_Name.StoreFront, DataBaseProviderName.MySQL)
// Service Manifests
.RegisterServiceManifest<SFServiceManifest, ISFServiceManifest>(EndPointType.DBMS, API_Name.StoreFront, DataBaseProviderName.MSSQL)
.RegisterServiceManifest<SFServiceManifest, ISFServiceManifest>(EndPointType.DBMS, API_Name.StoreFront, DataBaseProviderName.MySQL)
.RegisterServiceManifest<SFServiceManifest, ISFServiceManifest>(EndPointType.HTTP, API_Name.StoreFront, DataBaseProviderName.WebAPI);
}
}
-
AdaptiveClient
is designed to work with an n-tier architecture. Make sure your application has clean separation of layers. Generally this means your business logic should reside entirely in your service layer - not in controllers, code-behind, or view models. -
Create clients in their own assemblies as required. Clients must implement the same interfaces as their server counterparts and the services they access. Register clients the same way services are registered. See the Application.WebAPIClient project for an example.
-
Will
AdaptiveClient
make multiple hops to resolve a client? Yes, see the demo at the link below. -
Can I force
AdaptiveClient
to use a certain EndPoint and bypass the fallback logic? Yes. You can set the IsActive flag to false for EndPointConfigurations you dont want to use. You can also supply one or more EndPoint names in your call toAdaptiveClient
or ClientFactory:
User user = await client.CallAsync(x => x.GetUser(userID), "MyEndPointName");