Talk about the combination of explicit interface call and Nacos

Keywords: Nacos

background

For the internal API interface of the company, after introducing the registry, it is inevitable to use the service to discover this thing.

Now the more popular interface call method should be based on the call of declarative interface, which makes the development more simple and fast.

. NET in the declarative interface call, WebApiClient and Refit can be selected.

Some time ago, a group friend asked Lao Huang if there was an example of WebApiClient and Nacos integration.

After looking around, I really didn't find it, so I had to do it myself.

Taking WebApiClient as an example, this paper briefly introduces its combination with Nacos's service discovery.

API interface

Create a minimal api based on. NET 6.

using Nacos.AspNetCore.V2;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddNacosAspNet(x =>
{
    x.ServerAddresses = new List<string> { "http://localhost:8848/" };
    x.Namespace = "cs";

    // The service name is in lowercase!!
    x.ServiceName = "sample";
    x.GroupName = "Some_Group";
    x.ClusterName = "DC_1";
    x.Weight = 100;
    x.Secure = false;
});

var app = builder.Build();
.
app.MapGet("/api/get", () =>
{
    return Results.Ok("from .net6 minimal API");
});

app.Run("http://*:9991");

This application is a provider. When it is started, it will register with Nacos and can be found and called by other applications.

Declarative interface call

Here is also to create a WEB API project of. NET 6 for demonstration. Here, a nuget package needs to be introduced.

<ItemGroup>
    <PackageReference Include="WebApiClientCore.Extensions.Nacos" Version="0.1.0" />
</ItemGroup>

First, declare this interface.

// [HttpHost("http://192.168.100.100:9991")]
[HttpHost("http://sample")]
public interface ISampleApi : IHttpApi
{
    [HttpGet("/api/get")]
    Task<string> GetAsync();
}

In fact, we should pay attention to the HttpHost feature. Under normal circumstances, the specific domain name or IP address is configured.

If we need to find the real address corresponding to this interface through nacos, we only need to configure its service name.

The next step is to register the interface so that the ISampleApi can move.

var builder = WebApplication.CreateBuilder(args);

// Add nacos service discovery module
// The current service is not registered with nacos here. Adjust it as needed
builder.Services.AddNacosV2Naming(x =>
{
    x.ServerAddresses = new List<string> { "http://localhost:8848/" };
    x.Namespace = "cs";
});

// Register the interface and enable the service discovery function of nacos
// Pay attention to grouping and cluster configuration
// builder.Services.AddNacosDiscoveryTypedClient<ISampleApi>("Some_Group", "DC_1");
builder.Services.AddNacosDiscoveryTypedClient<ISampleApi>(x => 
{
    // HttpApiOptions
    x.UseLogging = true;
}, "Some_Group", "DC_1");

var app = builder.Build();

app.MapGet("/", async (ISampleApi api) =>
{
    var res = await api.GetAsync();
    return $"client ===== {res}" ;
});

app.Run("http://*:9992");

Run and visit localhost:9992 to see the effect

From the above log, it requests http://sample/api/get , actually http://192.168.100.220:9991/api/get , it happens that this address is registered on nacos, that is, the service discovery is effective.

info: System.Net.Http.HttpClient.ISampleApi.LogicalHandler[100]
      Start processing HTTP request GET http://sample/api/get
info: System.Net.Http.HttpClient.ISampleApi.ClientHandler[100]
      Sending HTTP request GET http://sample/api/get
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET http://192.168.100.220:9991/api/get - -
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'HTTP: GET /api/get'

Let's take a look at what the WebApiClientCore.Extensions.Nacos package does.

Simple analysis

In essence, an httpclient handler is added, which depends on the INacosNamingService provided by the sdk.

public static IHttpClientBuilder AddNacosDiscoveryTypedClient<TInterface>(
    this IServiceCollection services,
    Action<HttpApiOptions, IServiceProvider> configOptions,
    string group = "DEFAULT_GROUP",
    string cluster = "DEFAULT")
    where TInterface : class, IHttpApi
{
    NacosExtensions.Common.Guard.NotNull(configOptions, nameof(configOptions));

    return services.AddHttpApi<TInterface>(configOptions)
            .ConfigurePrimaryHttpMessageHandler(provider =>
            {
                var svc = provider.GetRequiredService<INacosNamingService>();
                var loggerFactory = provider.GetService<ILoggerFactory>();

                if (svc == null)
                {
                    throw new InvalidOperationException(
                        "Can not find out INacosNamingService, please register at first");
                }

                return new NacosExtensions.Common.NacosDiscoveryHttpClientHandler(svc, group, cluster, loggerFactory);
            });
}

The SendAsync method is rewritten in the handler to replace the RequestUri of HttpRequestMessage, that is, the service name is replaced with the real service address.

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    var current = request.RequestUri;
    try
    {
        request.RequestUri = await LookupServiceAsync(current).ConfigureAwait(false);
        var res = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
        return res;
    }
    catch (Exception e)
    {
        _logger?.LogDebug(e, "Exception during SendAsync()");
        throw;
    }
    finally
    {
        // Should we reset the request uri to current here?
        // request.RequestUri = current;
    }
}

The specific search and replacement logic is as follows:

internal async Task<Uri> LookupServiceAsync(Uri request)
{
    // Call SelectOneHealthyInstance with subscribe
    // And the host of Uri will always be lowercase, it means that the services name must be lowercase!!!!
    var instance = await _namingService
        .SelectOneHealthyInstance(request.Host, _groupName, new List<string> { _cluster }, true).ConfigureAwait(false);

    if (instance != null)
    {
        var host = $"{instance.Ip}:{instance.Port}";

        // conventions here
        // if the metadata contains the secure item, will use https!!!!
        var baseUrl = instance.Metadata.TryGetValue(Secure, out _)
            ? $"{HTTPS}{host}"
            : $"{HTTP}{host}";

        var uriBase = new Uri(baseUrl);
        return new Uri(uriBase, request.PathAndQuery);
    }

    return request;
}

Here is to query a healthy instance first. If it exists, it will be assembled. Here is a convention on HTTPS, that is, whether there is Secure configuration in the metadata.

It is roughly shown in the figure below:

Write at the end

Declarative interface calls are very convenient for Http interface requests

If you are interested, you are welcome to join us and develop and improve together.

Address of Nacos SDK CSharp: https://github.com/nacos-group/nacos-sdk-csharp

Address of Nacos CSharp extensions: https://github.com/catcherwong/nacos-csharp-extensions

Address of the sample code for this article: https://github.com/catcherwong-archive/2021/tree/main/WebApiClientCoreWithNacos

Posted by DocUK on Thu, 11 Nov 2021 18:55:14 -0800