Skip to content

Commit

Permalink
Resolved #726: Introduce IHttpClientProxy<T> and explicit usage of cl…
Browse files Browse the repository at this point in the history
…ient proxies.
  • Loading branch information
hikalkan committed Jan 10, 2019
1 parent 40f1e8d commit bb2b871
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 56 deletions.
96 changes: 54 additions & 42 deletions docs/en/AspNetCore/Dynamic-CSharp-API-Clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ ABP can dynamically create C# API client proxies to call remote HTTP services (R
Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project. Example:

````csharp
public interface IBookService : IApplicationService
public interface IBookAppService : IApplicationService
{
Task<List<BookDto>> GetListAsync();
}
````

Your interface should implement the `IRemoteService` interface. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookService` above satisfies this condition.
Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition.

Implement this class in your service application. You can use [auto API controller system](Auto-API-Controllers.md) to expose the service as a REST API endpoint.

Expand Down Expand Up @@ -45,13 +45,6 @@ public class MyClientAppModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
//Configure remote end point
context.Services.Configure<RemoteServiceOptions>(options =>
{
options.RemoteServices.Default =
new RemoteServiceConfiguration("http://localhost:53929/");
});

//Create dynamic client proxies
context.Services.AddHttpClientProxies(
typeof(BookStoreApplicationModule).Assembly
Expand All @@ -60,20 +53,34 @@ public class MyClientAppModule : AbpModule
}
````

`RemoteServiceOptions` is used to configure endpoints for remote services (This example sets the default endpoint while you can have different service endpoints used by different clients. See the "Multiple Remote Service Endpoint" section).

`AddHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, creates and registers proxy classes.

### Endpoint Configuration

`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. Simplest configuration is shown below:

````
{
"RemoteServices": {
"Default": {
"BaseUrl": "http://localhost:53929/"
}
}
}
````

See the "RemoteServiceOptions" section below for more detailed configuration.

## Usage

It's straightforward to use. Just inject the service interface in the client application code:

````csharp
public class MyService : ITransientDependency
{
private readonly IBookService _bookService;
private readonly IBookAppService _bookService;

public MyService(IBookService bookService)
public MyService(IBookAppService bookService)
{
_bookService = bookService;
}
Expand All @@ -89,40 +96,34 @@ public class MyService : ITransientDependency
}
````

This sample injects the `IBookService` service interface defined above. The dynamic client proxy implementation makes an HTTP call whenever a service method is called by the client.
This sample injects the `IBookAppService` service interface defined above. The dynamic client proxy implementation makes an HTTP call whenever a service method is called by the client.

## Configuration Details
### IHttpClientProxy Interface

### RemoteServiceOptions
While you can inject `IBookAppService` like above to use the client proxy, you could inject `IHttpClientProxy<IBookAppService>` for a more explicit usage. In this case you will use the `Service` property of the `IHttpClientProxy<T>` interface.

While you can configure `RemoteServiceOptions` as the example shown above, you can let it to read from your `appsettings.json` file. Add a `RemoteServices` section to your `appsettings.json` file:
## Configuration

````json
{
"RemoteServices": {
"Default": {
"BaseUrl": "http://localhost:53929/"
}
}
}
````
### RemoteServiceOptions

Then you can pass your `IConfigurationRoot` instance directly to the `Configure<RemoteServiceOptions>()` method as shown below:
`RemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can use `Configure` method to set or override it. Example:

````csharp
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();

context.Services.Configure<RemoteServiceOptions>(configuration);
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.Configure<RemoteServiceOptions>(options =>
{
options.RemoteServices.Default =
new RemoteServiceConfiguration("http://localhost:53929/");
});

//...
}
````

This approach is useful since it's easy to change the configuration later without touching to the code.
### Multiple Remote Service Endpoints

#### Multiple Remote Service Endpoint

The examples above have configured the "Default" remote service endpoint. You may have different endpoints for different services (as like in a microservice approach where each microservice has different endpoint). In this case, you can add other endpoints to your configuration file:
The examples above have configured the "Default" remote service endpoint. You may have different endpoints for different services (as like in a microservice approach where each microservice has different endpoints). In this case, you can add other endpoints to your configuration file:

````json
{
Expand All @@ -137,10 +138,6 @@ The examples above have configured the "Default" remote service endpoint. You ma
}
````

See the next section to learn how to use this new endpoint.

### AddHttpClientProxies Method

`AddHttpClientProxies` method can get an additional parameter for the remote service name. Example:

````csharp
Expand All @@ -150,4 +147,19 @@ context.Services.AddHttpClientProxies(
);
````

`remoteServiceName` parameter matches the service endpoint configured via `RemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint.
`remoteServiceName` parameter matches the service endpoint configured via `RemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint.

### As Default Services

When you create a service proxy for `IBookAppService`, you can directly inject the `IBookAppService` to use the proxy client (as shown in the usage section). You can pass `asDefaultServices: false` to the `AddHttpClientProxies` method to disable this feature.

````csharp
context.Services.AddHttpClientProxies(
typeof(BookStoreApplicationModule).Assembly,
asDefaultServices: false
);
````

If you disable `asDefaultServices`, you can only use `IHttpClientProxy<T>` interface to use the client proxies (see the related section above).

Using `asDefaultServices: false` may only be needed if your application has multiple implementation of the service, so you want to distinguish the HTTP client proxy and do not want to override the other implementation.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using System.Reflection;
using Castle.DynamicProxy;
using JetBrains.Annotations;
using Volo.Abp;
using Volo.Abp.Castle.DynamicProxy;
using Volo.Abp.Http.Client;
Expand All @@ -13,8 +14,28 @@ public static class ServiceCollectionDynamicHttpClientProxyExtensions
{
private static readonly ProxyGenerator ProxyGeneratorInstance = new ProxyGenerator();

public static IServiceCollection AddHttpClientProxies(this IServiceCollection services, Assembly assembly, string remoteServiceName = RemoteServiceConfigurationDictionary.DefaultName)
/// <summary>
/// Registers HTTP Client Proxies for all public interfaces
/// extend the <see cref="IRemoteService"/> interface in the
/// given <paramref name="assembly"/>.
/// </summary>
/// <param name="services">Service collection</param>
/// <param name="assembly">The assembly containing the service interfaces</param>
/// <param name="remoteServiceConfigurationName">
/// The name of the remote service configuration to be used by the HTTP Client proxies.
/// See <see cref="RemoteServiceOptions"/>.
/// </param>
/// <param name="asDefaultService">
/// True, to register the HTTP client proxy as the default implementation for the services.
/// </param>
public static IServiceCollection AddHttpClientProxies(
[NotNull] this IServiceCollection services,
[NotNull] Assembly assembly,
[NotNull] string remoteServiceConfigurationName = RemoteServiceConfigurationDictionary.DefaultName,
bool asDefaultService = true)
{
Check.NotNull(services, nameof(assembly));

//TODO: Make a configuration option and add remoteServiceName inside it!
//TODO: Add option to change type filter

Expand All @@ -24,36 +45,101 @@ public static IServiceCollection AddHttpClientProxies(this IServiceCollection se

foreach (var serviceType in serviceTypes)
{
services.AddHttpClientProxy(serviceType, remoteServiceName);
services.AddHttpClientProxy(
serviceType,
remoteServiceConfigurationName,
asDefaultService
);
}

return services;
}

public static IServiceCollection AddHttpClientProxy<T>(this IServiceCollection services, string remoteServiceName = RemoteServiceConfigurationDictionary.DefaultName)
/// <summary>
/// Registers HTTP Client Proxy for given service type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">Type of the service</typeparam>
/// <param name="services">Service collection</param>
/// <param name="remoteServiceConfigurationName">
/// The name of the remote service configuration to be used by the HTTP Client proxy.
/// See <see cref="RemoteServiceOptions"/>.
/// </param>
/// <param name="asDefaultService">
/// True, to register the HTTP client proxy as the default implementation for the service <typeparamref name="T"/>.
/// </param>
public static IServiceCollection AddHttpClientProxy<T>(
[NotNull] this IServiceCollection services,
[NotNull] string remoteServiceConfigurationName = RemoteServiceConfigurationDictionary.DefaultName,
bool asDefaultService = true)
{
return services.AddHttpClientProxy(typeof(T), remoteServiceName);
return services.AddHttpClientProxy(
typeof(T),
remoteServiceConfigurationName,
asDefaultService
);
}

public static IServiceCollection AddHttpClientProxy(this IServiceCollection services, Type type, string remoteServiceName = RemoteServiceConfigurationDictionary.DefaultName)
/// <summary>
/// Registers HTTP Client Proxy for given service <paramref name="type"/>.
/// </summary>
/// <param name="services">Service collection</param>
/// <param name="type">Type of the service</param>
/// <param name="remoteServiceConfigurationName">
/// The name of the remote service configuration to be used by the HTTP Client proxy.
/// See <see cref="RemoteServiceOptions"/>.
/// </param>
/// <param name="asDefaultService">
/// True, to register the HTTP client proxy as the default implementation for the service <paramref name="type"/>.
/// </param>
public static IServiceCollection AddHttpClientProxy(
[NotNull] this IServiceCollection services,
[NotNull] Type type,
[NotNull] string remoteServiceConfigurationName = RemoteServiceConfigurationDictionary.DefaultName,
bool asDefaultService = true)
{
Check.NotNull(services, nameof(services));
Check.NotNull(type, nameof(type));
Check.NotNull(remoteServiceConfigurationName, nameof(remoteServiceConfigurationName));

services.Configure<AbpHttpClientOptions>(options =>
{
options.HttpClientProxies[type] = new DynamicHttpClientProxyConfig(type, remoteServiceName);
options.HttpClientProxies[type] = new DynamicHttpClientProxyConfig(type, remoteServiceConfigurationName);
});

var interceptorType = typeof(DynamicHttpProxyInterceptor<>).MakeGenericType(type);
services.AddTransient(interceptorType);

var interceptorAdapterType = typeof(CastleAbpInterceptorAdapter<>).MakeGenericType(interceptorType);
return services.AddTransient(
type,
serviceProvider => ProxyGeneratorInstance
.CreateInterfaceProxyWithoutTarget(
type,
(IInterceptor) serviceProvider.GetRequiredService(interceptorAdapterType)
)
);

if (asDefaultService)
{
services.AddTransient(
type,
serviceProvider => ProxyGeneratorInstance
.CreateInterfaceProxyWithoutTarget(
type,
(IInterceptor)serviceProvider.GetRequiredService(interceptorAdapterType)
)
);
}

services.AddTransient(
typeof(IHttpClientProxy<>).MakeGenericType(type),
serviceProvider =>
{
var service = ProxyGeneratorInstance
.CreateInterfaceProxyWithoutTarget(
type,
(IInterceptor) serviceProvider.GetRequiredService(interceptorAdapterType)
);

return Activator.CreateInstance(
typeof(HttpClientProxy<>).MakeGenericType(type),
service
);
});

return services;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Volo.Abp.Http.Client.DynamicProxying
{
public class HttpClientProxy<TRemoteService> : IHttpClientProxy<TRemoteService>
{
public TRemoteService Service { get; }

public HttpClientProxy(TRemoteService service)
{
Service = service;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Volo.Abp.Http.Client.DynamicProxying
{
public interface IHttpClientProxy<out TRemoteService>
{
TRemoteService Service { get; }
}
}

0 comments on commit bb2b871

Please sign in to comment.