Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
470bee5
add nacose
MiaoShuYo Mar 19, 2025
643c478
Block folder. idea
MiaoShuYo Mar 19, 2025
732fec4
delete .idea
MiaoShuYo Mar 19, 2025
7899622
Update .gitignore
MiaoShuYo Mar 19, 2025
589cf4d
Migrate file location, delete subfolders
MiaoShuYo Mar 19, 2025
2100626
Add blank lines
MiaoShuYo Mar 19, 2025
ffbf1b5
Migrate test project location.
MiaoShuYo Mar 19, 2025
f39757d
Organize solution correctly
raman-m Mar 19, 2025
30a0f34
Use file-scoped namespaces
raman-m Mar 19, 2025
0a3f426
CI/CD: add workflows aka GitHub Actions
raman-m Mar 19, 2025
306196c
Modified code
MiaoShuYo Mar 20, 2025
31308ca
Update Ocelot.Discovery.Nacos.UnitTests.csproj
MiaoShuYo Mar 20, 2025
1eed642
Update pr.yml
MiaoShuYo Mar 20, 2025
de2b498
Fix warning
MiaoShuYo Mar 20, 2025
ece0185
Fix warning
MiaoShuYo Mar 20, 2025
71d81bc
Fix VS IDE messages
raman-m Mar 21, 2025
c7c0460
Test coverage 100%
MiaoShuYo Mar 21, 2025
d7c7c33
Review tests
raman-m Mar 22, 2025
d962f9e
No anonymous delegates: use named delegate
raman-m Mar 22, 2025
b82594c
Replace the log component with IOcelotLoggerFactory
MiaoShuYo Mar 22, 2025
6767b6e
Remove classes that are no longer on trial.
MiaoShuYo Mar 24, 2025
4547c55
Add detailed notes to _reservedKeys
MiaoShuYo Mar 24, 2025
bea32f4
Code review by @raman-m
raman-m Mar 24, 2025
7912f62
Add acceptance testing project
raman-m Mar 24, 2025
7ac2cae
Update workflows to run acceptance tests
raman-m Mar 24, 2025
1a16f83
Update src/NacosMiddlewareConfigurationProvider.cs
raman-m Mar 24, 2025
eec6bad
_reservedKeys changed to public
MiaoShuYo Mar 24, 2025
950c5ae
Temporarily disable acceptance testing due to the draft status of the…
raman-m May 27, 2025
dd05752
Update package references
raman-m May 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net

name: Main

on:
push:
branches: [ "main" ]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
dotnet-version: [ '8.0', '9.0' ]

steps:
- uses: actions/checkout@v4
- name: Setup .NET ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ matrix.dotnet-version }}.x
- name: Restore
run: dotnet restore ./Ocelot.Discovery.Nacos.sln -p:TargetFramework=net${{ matrix.dotnet-version }}
- name: Build
run: dotnet build --no-restore ./Ocelot.Discovery.Nacos.sln --framework net${{ matrix.dotnet-version }}
- name: Unit Tests
run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }} ./unit/Ocelot.Discovery.Nacos.UnitTests.csproj
22 changes: 22 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net

name: PR

on: pull_request

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET 9.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: 9.0.x
- name: Restore
run: dotnet restore ./Ocelot.Discovery.Nacos.sln -p:TargetFramework=net9.0
- name: Build
run: dotnet build --no-restore ./Ocelot.Discovery.Nacos.sln --framework net9.0
- name: Unit Tests
run: dotnet test --no-restore --no-build --verbosity normal --framework net9.0 ./unit/Ocelot.Discovery.Nacos.UnitTests.csproj
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,5 @@ FodyWeavers.xsd

# JetBrains Rider
*.sln.iml
.idea/*
/Ocelot.Discovery.Nacos/.idea
31 changes: 31 additions & 0 deletions Ocelot.Discovery.Nacos.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35527.113
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Discovery.Nacos", "src\Ocelot.Discovery.Nacos.csproj", "{CEF24699-3E41-D971-ACCA-FEF5CCB2011C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Discovery.Nacos.UnitTests", "unit\Ocelot.Discovery.Nacos.UnitTests.csproj", "{FAD17C0B-4F8F-99A6-1419-9665E4210346}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CEF24699-3E41-D971-ACCA-FEF5CCB2011C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEF24699-3E41-D971-ACCA-FEF5CCB2011C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEF24699-3E41-D971-ACCA-FEF5CCB2011C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEF24699-3E41-D971-ACCA-FEF5CCB2011C}.Release|Any CPU.Build.0 = Release|Any CPU
{FAD17C0B-4F8F-99A6-1419-9665E4210346}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FAD17C0B-4F8F-99A6-1419-9665E4210346}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FAD17C0B-4F8F-99A6-1419-9665E4210346}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FAD17C0B-4F8F-99A6-1419-9665E4210346}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0E583663-7152-4575-9AE5-FF3D78AEAA1C}
EndGlobalSection
EndGlobal
64 changes: 64 additions & 0 deletions src/Nacos.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Ocelot.Values;
using Service = Ocelot.Values.Service;

namespace Ocelot.Discovery.Nacos;

public class Nacos : IServiceDiscoveryProvider
{
private readonly INacosNamingService _client;
private readonly string _serviceName;

public Nacos(string serviceName,INacosNamingService client)
{
_client = client;
_serviceName = serviceName;
}

public async Task<List<Service>> GetAsync()
{
try
{
var instances = await _client.GetAllInstances(_serviceName)
.ConfigureAwait(false);

return instances?
.Where(i => i.Healthy && i.Enabled && i.Weight > 0) // 健康检查过滤
.Select(TransformInstance)
.ToList() ?? new List<Service>();
}
catch (NacosException ex)
{
throw ex;

Check warning on line 31 in src/Nacos.cs

View workflow job for this annotation

GitHub Actions / build

Re-throwing caught exception changes stack information (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2200)
}
}

private Service TransformInstance(Instance instance)
{
var metadata = instance.Metadata ?? new Dictionary<string, string>();

return new Service(
id: instance.InstanceId,
hostAndPort: new ServiceHostAndPort(instance.Ip, instance.Port),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hostAndPort: new ServiceHostAndPort(instance.Ip, instance.Port),
hostAndPort: new(instance.Ip, instance.Port),

What is the scheme? Is the scheme known at this stage of building service?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a client fetches a service, it will access the corresponding service based on the address information at the time of registration, so if the registration is for an HTTPS address, then the fetched service is HTTPS, and vice versa.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, in the Nacos service discovery, scheme (protocol) management is not required, right?
For example, in Kubernetes we can manage the protocol while registering a service instance in the K8s registry.
In the next round of development, we will write acceptance tests that will show real usage in practice.

name: instance.ServiceName,
version: metadata.GetValueOrDefault("version", "default"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot get this default value. Could you show me in Nacos docs please that default version is marked as "default"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version value is not explicitly defined as "default" in the Nacos documentation. The "default" value in the code is a custom fallback in case there is no "version" key in the metadata dictionary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You confirmed that this is the issue. I am not certain but these values may be applicable: current, latest, default.
Question: How can the default be consumed by Ocelot or Nacos client?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version can be customized through the configuration file.

tags: ProcessMetadataTags(metadata)
);
}

private List<string> ProcessMetadataTags(IDictionary<string, string> metadata)
{
return metadata
.Where(kv => !_reservedKeys.Contains(kv.Key))
.Select(kv => FormatTag(kv))
.ToList();
}

private string FormatTag(KeyValuePair<string, string> kv)
{
var encodedKey = WebUtility.UrlEncode(kv.Key);
var encodedValue = WebUtility.UrlEncode(kv.Value);
return $"{encodedKey}={encodedValue}";
}

private static readonly string[] _reservedKeys = { "version", "group", "cluster", "namespace", "weight" };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to make this definition public.
Could you provide some links to metadata documentation that explain this?
I suggest writing an XML documentation (in the code) for the definition of the array.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the nacos java version of the sdk, these are included by default, but in the nacos .net sdk these are not included by default and need to be added by yourself.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this array publicly accessible, then.

}
27 changes: 27 additions & 0 deletions src/NacosMiddlewareConfigurationProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Ocelot.Configuration.Repository;
using Ocelot.Middleware;

namespace Ocelot.Discovery.Nacos;

public class NacosMiddlewareConfigurationProvider
{
public static OcelotMiddlewareConfigurationDelegate Get { get; } = builder =>
{
var internalConfigRepo = builder.ApplicationServices.GetService<IInternalConfigurationRepository>();
var log =builder.ApplicationServices.GetService<ILogger<NacosMiddlewareConfigurationProvider>>();
var config = internalConfigRepo.Get();

Check warning on line 12 in src/NacosMiddlewareConfigurationProvider.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 12 in src/NacosMiddlewareConfigurationProvider.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

if (UsingNacosServiceDiscoveryProvider(config.Data))
{
log.LogInformation("Using Nacos service discovery provider.");

Check warning on line 16 in src/NacosMiddlewareConfigurationProvider.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'logger' in 'void LoggerExtensions.LogInformation(ILogger logger, string? message, params object?[] args)'.

Check warning on line 16 in src/NacosMiddlewareConfigurationProvider.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'logger' in 'void LoggerExtensions.LogInformation(ILogger logger, string? message, params object?[] args)'.
}

return Task.CompletedTask;
};

private static bool UsingNacosServiceDiscoveryProvider(IInternalConfiguration configuration)
{
return configuration?.ServiceProviderConfiguration != null
&& configuration.ServiceProviderConfiguration.Type?.ToLower() == "nacos";
}
}
26 changes: 26 additions & 0 deletions src/NacosProviderFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Ocelot.ServiceDiscovery;

namespace Ocelot.Discovery.Nacos;

public static class NacosProviderFactory
{
/// <summary>
/// String constant used for provider type definition.
/// </summary>
public const string Nacos = nameof(Discovery.Nacos.Nacos);

public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider;

private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route)
{
var client = provider.GetService<INacosNamingService>();
if (client == null)
{
throw new NullReferenceException($"Cannot get an {nameof(INacosNamingService)} service during {nameof(CreateProvider)} operation to instantiate the {nameof(Nacos)} provider!");
}

return Nacos.Equals(config.Type, StringComparison.OrdinalIgnoreCase)

Check warning on line 22 in src/NacosProviderFactory.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.

Check warning on line 22 in src/NacosProviderFactory.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.
? new Nacos(route.ServiceName, client)
: null;
}
}
13 changes: 13 additions & 0 deletions src/Ocelot.Discovery.Nacos.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="nacos-sdk-csharp" Version="1.3.10" />
<PackageReference Include="nacos-sdk-csharp.AspNetCore" Version="1.3.10" />
<PackageReference Include="Ocelot" Version="23.4.3" />
</ItemGroup>
</Project>
16 changes: 16 additions & 0 deletions src/OcelotBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Ocelot.DependencyInjection;
using Nacos.AspNetCore.V2;

namespace Ocelot.Discovery.Nacos;

public static class OcelotBuilderExtensions
{
public static IOcelotBuilder AddNacos(this IOcelotBuilder builder, string section = "nacos")
{
builder.Services
.AddNacosAspNet(builder.Configuration,section)
.AddSingleton(NacosProviderFactory.Get)
.AddSingleton(NacosMiddlewareConfigurationProvider.Get);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a word of caution!
It might be premature to define this since you were inspired by the Consul provider and ConsulMiddlewareConfigurationProvider implementation. However, please note that this Consul config provider was developed specifically for the AddConfigStoredInConsul() feature, correct?
Therefore, let's keep it in the code for now, the next phase of acceptance testing will reveal the subsequent steps.

return builder;
}
}
11 changes: 11 additions & 0 deletions src/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Logging;
global using Ocelot.Configuration;
global using Ocelot.ServiceDiscovery.Providers;
global using Nacos.V2;
global using Nacos.V2.Exceptions;
global using Nacos.V2.Naming.Dtos;
global using System.Net;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
1 change: 1 addition & 0 deletions unit/MSTestSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
141 changes: 141 additions & 0 deletions unit/NacosProviderFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Ocelot.Discovery.Nacos;
using Ocelot.Configuration;
using Nacos.V2;

[TestClass]
public class NacosProviderFactoryTests
{
[TestMethod]
public void CreateProvider_ShouldReturnNacosProvider_WhenConfigTypeIsNacos()
{
// Arrange
var serviceProviderMock = new Mock<IServiceProvider>();
var nacosNamingServiceMock = new Mock<INacosNamingService>();
serviceProviderMock.Setup(sp => sp.GetService(typeof(INacosNamingService)))
.Returns(nacosNamingServiceMock.Object);

var config = new ServiceProviderConfiguration(
type: "nacos",
scheme: "http",
host: "localhost",
port: 8848,
token: null,
configurationKey: "nacos",
pollingInterval: 5000
);

var route = new DownstreamRoute(
key: "testKey",
upstreamPathTemplate: null,
upstreamHeadersFindAndReplace: null,
downstreamHeadersFindAndReplace: null,
downstreamAddresses: null,
serviceName: "testService",
serviceNamespace: null,
httpHandlerOptions: null,
useServiceDiscovery: false,
enableEndpointEndpointRateLimiting: false,
qosOptions: null,
downstreamScheme: null,
requestIdKey: null,
isCached: false,
cacheOptions: null,
loadBalancerOptions: null,
rateLimitOptions: null,
routeClaimsRequirement: null,
claimsToQueries: null,
claimsToHeaders: null,
claimsToClaims: null,
claimsToPath: null,
isAuthenticated: false,
isAuthorized: false,
authenticationOptions: null,
downstreamPathTemplate: null,
loadBalancerKey: null,
delegatingHandlers: null,
addHeadersToDownstream: null,
addHeadersToUpstream: null,
dangerousAcceptAnyServerCertificateValidator: false,
securityOptions: null,
downstreamHttpMethod: null,
downstreamHttpVersion: null,
downstreamHttpVersionPolicy: default,
upstreamHeaders: null,
metadataOptions: null
);

// Act
var provider = NacosProviderFactory.Get(serviceProviderMock.Object, config, route);

// Assert
Assert.IsNotNull(provider);
Assert.IsInstanceOfType(provider, typeof(Ocelot.Discovery.Nacos.Nacos));
}

[TestMethod]
[ExpectedException(typeof(NullReferenceException))]
public void CreateProvider_ShouldThrowException_WhenNacosNamingServiceIsNull()
{
// Arrange
var serviceProviderMock = new Mock<IServiceProvider>();
serviceProviderMock.Setup(sp => sp.GetService(typeof(INacosNamingService)))
.Returns(null);

Check warning on line 84 in unit/NacosProviderFactoryTests.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.

var config = new ServiceProviderConfiguration(
type: "nacos",
scheme: "http",
host: "localhost",
port: 8848,
token: null,
configurationKey: "nacos",
pollingInterval: 5000
);

var route = new DownstreamRoute(
key: "testKey",
upstreamPathTemplate: null,
upstreamHeadersFindAndReplace: null,
downstreamHeadersFindAndReplace: null,
downstreamAddresses: null,
serviceName: "testService",
serviceNamespace: null,
httpHandlerOptions: null,
useServiceDiscovery: false,
enableEndpointEndpointRateLimiting: false,
qosOptions: null,
downstreamScheme: null,
requestIdKey: null,
isCached: false,
cacheOptions: null,
loadBalancerOptions: null,
rateLimitOptions: null,
routeClaimsRequirement: null,
claimsToQueries: null,
claimsToHeaders: null,
claimsToClaims: null,
claimsToPath: null,
isAuthenticated: false,
isAuthorized: false,
authenticationOptions: null,
downstreamPathTemplate: null,
loadBalancerKey: null,
delegatingHandlers: null,
addHeadersToDownstream: null,
addHeadersToUpstream: null,
dangerousAcceptAnyServerCertificateValidator: false,
securityOptions: null,
downstreamHttpMethod: null,
downstreamHttpVersion: null,
downstreamHttpVersionPolicy: default,
upstreamHeaders: null,
metadataOptions: null
);

// Act
var provider = NacosProviderFactory.Get(serviceProviderMock.Object, config, route);
// Assert
Assert.IsNull(provider);
}
}
Loading