diff --git a/Dockerfile.dotnet b/Dockerfile.dotnet
new file mode 100644
index 0000000..ef0e590
--- /dev/null
+++ b/Dockerfile.dotnet
@@ -0,0 +1,23 @@
+# Build stage
+FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
+
+WORKDIR /src
+
+# Copy client library
+COPY kenaito-conf-dotnet-client/ ./kenaito-conf-dotnet-client/
+
+# Copy demo project
+COPY kenaito-conf-dotnet-client-demo/ ./kenaito-conf-dotnet-client-demo/
+
+# Build
+WORKDIR /src/kenaito-conf-dotnet-client-demo
+RUN dotnet restore
+RUN dotnet publish -c Release -o /app
+
+# Runtime stage
+FROM mcr.microsoft.com/dotnet/aspnet:6.0
+
+WORKDIR /app
+COPY --from=build /app .
+
+ENTRYPOINT ["dotnet", "KenaitoConfDemo.dll"]
diff --git a/Dockerfile.go b/Dockerfile.go
new file mode 100644
index 0000000..5e28ed0
--- /dev/null
+++ b/Dockerfile.go
@@ -0,0 +1,25 @@
+# Build stage
+FROM golang:1.25-alpine AS build
+
+# 设置国内 Go 代理
+ENV GOPROXY=https://goproxy.cn,direct
+
+WORKDIR /src
+
+# Copy client library
+COPY kenaito-conf-go-client/ ./kenaito-conf-go-client/
+
+# Copy demo project
+COPY kenaito-conf-go-client-demo/ ./kenaito-conf-go-client-demo/
+
+# Build
+WORKDIR /src/kenaito-conf-go-client-demo
+RUN go mod tidy && go build -o /app/demo .
+
+# Runtime stage
+FROM alpine:3.20
+
+WORKDIR /app
+COPY --from=build /app/demo .
+
+ENTRYPOINT ["./demo"]
diff --git a/Dockerfile.nodejs b/Dockerfile.nodejs
new file mode 100644
index 0000000..f8c37fb
--- /dev/null
+++ b/Dockerfile.nodejs
@@ -0,0 +1,27 @@
+# Build stage
+FROM node:18-alpine AS build
+
+WORKDIR /src
+
+# Copy client library
+COPY kenaito-conf-nodejs-client/ ./kenaito-conf-nodejs-client/
+
+# Copy demo project
+COPY kenaito-conf-nodejs-client-demo/ ./kenaito-conf-nodejs-client-demo/
+
+# Install client locally, then demo dependencies
+WORKDIR /src/kenaito-conf-nodejs-client
+RUN npm link
+
+WORKDIR /src/kenaito-conf-nodejs-client-demo
+RUN npm link kenaito-conf && npm install
+
+# Runtime stage
+FROM node:18-alpine
+
+WORKDIR /app
+COPY --from=build /src/kenaito-conf-nodejs-client-demo .
+
+EXPOSE 8083
+
+CMD ["node", "app.js"]
diff --git a/Dockerfile.python b/Dockerfile.python
new file mode 100644
index 0000000..bd39b4b
--- /dev/null
+++ b/Dockerfile.python
@@ -0,0 +1,29 @@
+# Build stage
+FROM python:3.12-slim AS build
+
+WORKDIR /src
+
+# Copy client library
+COPY kenaito-conf-python-client/ ./kenaito-conf-python-client/
+
+# Copy demo project
+COPY kenaito-conf-python-client-demo/ ./kenaito-conf-python-client-demo/
+
+# Install client library
+WORKDIR /src/kenaito-conf-python-client
+RUN pip install --no-cache-dir .
+
+# Install demo dependencies
+WORKDIR /src/kenaito-conf-python-client-demo
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Runtime stage
+FROM python:3.12-slim
+
+WORKDIR /app
+COPY --from=build /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
+COPY --from=build /src/kenaito-conf-python-client-demo .
+
+EXPOSE 8082
+
+CMD ["python", "app.py"]
diff --git a/Dockerfile.springboot2 b/Dockerfile.springboot2
new file mode 100644
index 0000000..993c493
--- /dev/null
+++ b/Dockerfile.springboot2
@@ -0,0 +1,24 @@
+# Build stage
+FROM maven:3.9-eclipse-temurin-8 AS build
+
+WORKDIR /src
+
+# Copy client library and install to local Maven repo
+COPY kenaito-conf-springboot2-client/ ./kenaito-conf-springboot2-client/
+WORKDIR /src/kenaito-conf-springboot2-client
+RUN mvn clean install -DskipTests
+
+# Copy demo project and build
+COPY kenaito-conf-springboot2-client-demo/ ./kenaito-conf-springboot2-client-demo/
+WORKDIR /src/kenaito-conf-springboot2-client-demo
+RUN mvn clean package -DskipTests
+
+# Runtime stage
+FROM eclipse-temurin:8-jre
+
+WORKDIR /app
+COPY --from=build /src/kenaito-conf-springboot2-client-demo/target/*.jar app.jar
+
+EXPOSE 8081
+
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/README.md b/README.md
index 78b217c..381140a 100644
--- a/README.md
+++ b/README.md
@@ -24,8 +24,8 @@ kenaito-conf/
├── kenaito-conf-nodejs-client-demo/ # Node.js 客户端示例(端口 8083)
├── kenaito-conf-php-client/ # (未支持)PHP 客户端 SDK(PHP 8.0+)
├── kenaito-conf-php-client-demo/ # (未支持)PHP 客户端示例
-├── kenaito-conf-dotnet-client/ # (未支持).NET 客户端 SDK(.NET 6.0+)
-├── kenaito-conf-dotnet-client-demo/ # (未支持).NET 客户端示例
+├── kenaito-conf-dotnet-client/ # .NET 客户端 SDK(.NET 6.0+)
+├── kenaito-conf-dotnet-client-demo/ # .NET 客户端示例
├── kenaito-conf-c-client/ # (未支持)C 客户端 SDK(libcurl + OpenSSL)
├── kenaito-conf-c-client-demo/ # (未支持)C 客户端示例
├── kenaito-conf-cpp-client/ # (未支持)C++ 客户端 SDK(libcurl + OpenSSL)
diff --git a/README_DOCKERFILE.md b/README_DOCKERFILE.md
new file mode 100644
index 0000000..505142e
--- /dev/null
+++ b/README_DOCKERFILE.md
@@ -0,0 +1,152 @@
+# Kenaito Config Center 客户端 Docker 部署指南
+
+本文档说明如何使用 Dockerfile 构建和测试各语言客户端 demo。
+
+## 前置条件
+
+- Docker 已安装
+- 配置中心服务端已启动(默认 `http://localhost:8080`)
+- 已在管理后台创建对应应用和环境
+
+## 快速构建
+
+在项目根目录执行:
+
+```bash
+# Go 客户端
+docker build -f Dockerfile.go -t kenaito-conf-go-demo .
+
+# Node.js 客户端
+docker build -f Dockerfile.nodejs -t kenaito-conf-nodejs-demo .
+
+# Python 客户端
+docker build -f Dockerfile.python -t kenaito-conf-python-demo .
+
+# .NET 客户端
+docker build -f Dockerfile.dotnet -t kenaito-conf-dotnet-demo .
+
+# Spring Boot 2 客户端
+docker build -f Dockerfile.springboot2 -t kenaito-conf-springboot2-demo .
+```
+
+## 运行测试
+
+### Go 客户端
+
+```bash
+docker run -it --rm \
+ -e KENAITO_BASE_URL=http://host.docker.internal:8080 \
+ -e KENAITO_APP_NAME=kenaito-springboot-demo \
+ -e KENAITO_ENV=daily \
+ kenaito-conf-go-demo
+```
+
+### Node.js 客户端
+
+```bash
+docker run -d --rm --name nodejs-demo \
+ -p 8083:8083 \
+ -e KENAITO_BASE_URL=http://host.docker.internal:8080 \
+ -e KENAITO_APP_NAME=kenaito-springboot-demo \
+ -e KENAITO_ENV=daily \
+ kenaito-conf-nodejs-demo
+
+# 查看日志
+docker logs -f nodejs-demo
+
+# 测试接口
+curl http://localhost:8083/
+curl http://localhost:8083/health
+
+# 停止
+docker stop nodejs-demo
+```
+
+### Python 客户端
+
+```bash
+docker run -d --rm --name python-demo \
+ -p 8082:8082 \
+ -e KENAITO_BASE_URL=http://host.docker.internal:8080 \
+ -e KENAITO_APP_NAME=kenaito-springboot-demo \
+ -e KENAITO_ENV=daily \
+ kenaito-conf-python-demo
+
+# 查看日志
+docker logs -f python-demo
+
+# 测试接口
+curl http://localhost:8082/
+curl http://localhost:8082/health
+
+# 停止
+docker stop python-demo
+```
+
+### .NET 客户端
+
+```bash
+docker run -it --rm \
+ -e KENAITO_BASE_URL=http://host.docker.internal:8080 \
+ -e KENAITO_APP_NAME=kenaito-springboot-demo \
+ -e KENAITO_ENV=daily \
+ kenaito-conf-dotnet-demo
+```
+
+### Spring Boot 2 客户端
+
+```bash
+docker run -d --rm --name springboot2-demo \
+ -p 8081:8081 \
+ -e KENAITO_BASE_URL=http://host.docker.internal:8080 \
+ -e KENAITO_APP_NAME=kenaito-springboot-demo \
+ -e KENAITO_ENV=daily \
+ kenaito-conf-springboot2-demo
+
+# 查看日志
+docker logs -f springboot2-demo
+
+# 测试接口
+curl http://localhost:8081/
+curl http://localhost:8081/health
+
+# 停止
+docker stop springboot2-demo
+```
+
+## 环境变量说明
+
+| 变量名 | 说明 | 默认值 |
+|--------|------|--------|
+| `KENAITO_BASE_URL` | 配置中心地址 | `http://localhost:8080` |
+| `KENAITO_APP_NAME` | 应用名称 | `kenaito-springboot-demo` |
+| `KENAITO_ENV` | 环境标识 | `daily` |
+| `KENAITO_VERSION` | 配置版本 | `latest` |
+| `KENAITO_TOKEN` | 应用 Token | 空 |
+| `KENAITO_SYNC_INTERVAL` | 同步间隔(秒) | `10` |
+| `KENAITO_CACHE_DIR` | 缓存目录 | `.` |
+
+## 注意事项
+
+1. **host.docker.internal**:容器内访问宿主机服务时使用 `host.docker.internal:8080`,Linux 系统可能需要添加 `--add-host host.docker.internal:host-gateway`
+
+2. **网络模式**:如果配置中心也运行在 Docker 中,建议使用自定义网络:
+ ```bash
+ docker network create kenaito-net
+ docker run --network kenaito-net --name kenaito-server ...
+ docker run --network kenaito-net -e KENAITO_BASE_URL=http://kenaito-server:8080 ...
+ ```
+
+3. **配置缓存**:各客户端会在运行目录生成 `config-cache.json` 文件,容器重启后可从缓存加载配置
+
+4. **日志查看**:
+ - Go/.NET:直接输出到 stdout
+ - Node.js/Python/Spring Boot:使用各自日志框架
+
+## 测试配置变更
+
+1. 启动客户端容器
+2. 在管理后台修改对应应用的配置
+3. 等待同步间隔(默认 10 秒)
+4. 观察客户端日志是否输出配置变更
+5. 访问 HTTP 接口验证配置是否更新(Node.js/Python/Spring Boot)
diff --git a/kenaito-conf-dotnet-client-demo/KenaitoConfDemo.csproj b/kenaito-conf-dotnet-client-demo/KenaitoConfDemo.csproj
new file mode 100644
index 0000000..f1a9f54
--- /dev/null
+++ b/kenaito-conf-dotnet-client-demo/KenaitoConfDemo.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/kenaito-conf-dotnet-client-demo/Program.cs b/kenaito-conf-dotnet-client-demo/Program.cs
new file mode 100644
index 0000000..46e19ee
--- /dev/null
+++ b/kenaito-conf-dotnet-client-demo/Program.cs
@@ -0,0 +1,52 @@
+using Kenaito.Conf.Client;
+
+var config = new Config
+{
+ BaseUrl = "http://localhost:8080",
+ AppName = "kenaito-springboot-demo",
+ Env = "daily",
+ Version = "latest",
+ SyncInterval = 10,
+ CacheDir = "."
+};
+
+var client = new Client(config);
+
+client.OnChange(changed =>
+{
+ Console.WriteLine("Config changed:");
+ foreach (var kv in changed)
+ {
+ Console.WriteLine($" {kv.Key} = {kv.Value}");
+ }
+});
+
+await client.StartAsync();
+
+Console.WriteLine("Current configs:");
+foreach (var kv in client.GetAll())
+{
+ Console.WriteLine($" {kv.Key} = {kv.Value}");
+}
+
+var dbHost = client.Get("database.host");
+Console.WriteLine($"\ndatabase.host = {dbHost}");
+
+Console.WriteLine("\nListening for config changes... (press Ctrl+C to stop)");
+var cts = new CancellationTokenSource();
+Console.CancelKeyPress += (s, e) =>
+{
+ e.Cancel = true;
+ cts.Cancel();
+};
+
+try
+{
+ await Task.Delay(Timeout.Infinite, cts.Token);
+}
+catch (TaskCanceledException)
+{
+}
+
+Console.WriteLine("\nShutting down...");
+client.Stop();
diff --git a/kenaito-conf-dotnet-client-demo/README.md b/kenaito-conf-dotnet-client-demo/README.md
new file mode 100644
index 0000000..ac7dd73
--- /dev/null
+++ b/kenaito-conf-dotnet-client-demo/README.md
@@ -0,0 +1,41 @@
+# Kenaito Config Center .NET Demo
+
+演示如何使用 kenaito-conf-dotnet-client 客户端库。
+
+## 本地运行
+
+```bash
+cd kenaito-conf-dotnet-client-demo
+dotnet run
+```
+
+## Docker 运行
+
+```bash
+# 在项目根目录执行
+docker build -f Dockerfile.dotnet -t kenaito-conf-dotnet-demo .
+
+docker run -it --rm \
+ -e CONFIG_CENTER_BASE_URL=http://host.docker.internal:8080 \
+ kenaito-conf-dotnet-demo
+```
+
+## 配置项
+
+修改 `Program.cs` 中的配置:
+
+```csharp
+var config = new Config
+{
+ BaseUrl = "http://localhost:8080", // 配置中心地址
+ AppName = "kenaito-dotnet-demo", // 应用名称
+ Env = "daily", // 环境
+ SyncInterval = 10 // 同步间隔(秒)
+};
+```
+
+## 功能演示
+
+- 启动时拉取全量配置并打印
+- 监听配置变更,实时输出变更内容
+- 按 Ctrl+C 优雅退出
diff --git a/kenaito-conf-dotnet-client/Cache.cs b/kenaito-conf-dotnet-client/Cache.cs
new file mode 100644
index 0000000..ae2032b
--- /dev/null
+++ b/kenaito-conf-dotnet-client/Cache.cs
@@ -0,0 +1,41 @@
+using Newtonsoft.Json;
+
+namespace Kenaito.Conf.Client;
+
+internal class Cache
+{
+ private readonly string _cacheFile;
+
+ public Cache(string cacheDir)
+ {
+ _cacheFile = Path.Combine(cacheDir, "config-cache.json");
+ }
+
+ public Dictionary? Read()
+ {
+ try
+ {
+ if (!File.Exists(_cacheFile))
+ return null;
+ var json = File.ReadAllText(_cacheFile);
+ return JsonConvert.DeserializeObject>(json);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public void Write(Dictionary data)
+ {
+ try
+ {
+ var json = JsonConvert.SerializeObject(data, Formatting.Indented);
+ File.WriteAllText(_cacheFile, json);
+ }
+ catch
+ {
+ // ignore cache write errors
+ }
+ }
+}
diff --git a/kenaito-conf-dotnet-client/Client.cs b/kenaito-conf-dotnet-client/Client.cs
new file mode 100644
index 0000000..85f4447
--- /dev/null
+++ b/kenaito-conf-dotnet-client/Client.cs
@@ -0,0 +1,222 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Kenaito.Conf.Client;
+
+public class Client
+{
+ private readonly Config _config;
+ private readonly Cache _cache;
+ private readonly Listener _listener;
+ private readonly HttpClient _httpClient;
+ private readonly Dictionary _configs;
+ private readonly object _lock = new();
+ private long _lastTime;
+ private bool _started;
+ private CancellationTokenSource? _cts;
+
+ public Client(Config config)
+ {
+ _config = config;
+ if (string.IsNullOrEmpty(_config.Version))
+ _config.Version = "latest";
+ if (_config.SyncInterval <= 0)
+ _config.SyncInterval = 10;
+ if (string.IsNullOrEmpty(_config.CacheDir))
+ _config.CacheDir = ".";
+
+ _cache = new Cache(_config.CacheDir);
+ _listener = new Listener();
+ _configs = new Dictionary();
+ _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
+ }
+
+ public void OnChange(ChangeHandler handler)
+ {
+ _listener.OnChange(handler);
+ }
+
+ public string? Get(string key)
+ {
+ lock (_lock)
+ {
+ return _configs.TryGetValue(key, out var value) ? value : null;
+ }
+ }
+
+ public Dictionary GetAll()
+ {
+ lock (_lock)
+ {
+ return new Dictionary(_configs);
+ }
+ }
+
+ public async Task StartAsync()
+ {
+ if (_started)
+ return;
+ _started = true;
+
+ try
+ {
+ await FetchAllAsync();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ConfigClient] fetch all configs error: {ex.Message}, trying cache");
+ var cached = _cache.Read();
+ if (cached != null)
+ {
+ ApplyConfigs(cached);
+ Console.WriteLine($"[ConfigClient] loaded {CountConfigs(cached)} configs from cache");
+ }
+ else
+ {
+ throw new Exception("fetch configs failed and no cache", ex);
+ }
+ }
+
+ _cts = new CancellationTokenSource();
+ _ = SyncLoopAsync(_cts.Token);
+ }
+
+ public void Stop()
+ {
+ _cts?.Cancel();
+ }
+
+ private async Task FetchAllAsync()
+ {
+ var url = $"{_config.BaseUrl.TrimEnd('/')}/api/fetchAllConfigs";
+ var payload = new
+ {
+ appName = _config.AppName,
+ env = _config.Env,
+ version = _config.Version,
+ ignoreGlobal = _config.IgnoreGlobal,
+ token = _config.Token
+ };
+ var content = new StringContent(JsonConvert.SerializeObject(payload), System.Text.Encoding.UTF8, "application/json");
+ var response = await _httpClient.PostAsync(url, content);
+ response.EnsureSuccessStatusCode();
+
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject>(body);
+ if (result == null)
+ throw new Exception("empty response");
+
+ if (result.TryGetValue("code", out var codeObj))
+ {
+ var code = Convert.ToInt64(codeObj);
+ if (code != 0)
+ {
+ var msg = result.TryGetValue("msg", out var msgObj) ? msgObj?.ToString() : "unknown error";
+ throw new Exception($"server error: {msg}");
+ }
+ }
+
+ if (result.TryGetValue("latest.fetch.timestamp", out var tsObj))
+ {
+ long.TryParse(tsObj?.ToString(), out _lastTime);
+ }
+
+ _cache.Write(result);
+ ApplyConfigs(result);
+
+ Console.WriteLine($"[ConfigClient] fetched {CountConfigs(result)} configs");
+ }
+
+ private async Task FetchChangedAsync()
+ {
+ var url = $"{_config.BaseUrl.TrimEnd('/')}/api/fetchChangedConfigs";
+ var payload = new
+ {
+ appName = _config.AppName,
+ env = _config.Env,
+ version = _config.Version,
+ lastTime = _lastTime,
+ ignoreGlobal = _config.IgnoreGlobal
+ };
+ var content = new StringContent(JsonConvert.SerializeObject(payload), System.Text.Encoding.UTF8, "application/json");
+ var response = await _httpClient.PostAsync(url, content);
+ response.EnsureSuccessStatusCode();
+
+ var body = await response.Content.ReadAsStringAsync();
+ var wrapper = JsonConvert.DeserializeObject(body);
+ if (wrapper == null)
+ return;
+
+ if (wrapper.Code != 0)
+ throw new Exception($"server error code: {wrapper.Code}");
+
+ if (wrapper.Data == null)
+ return;
+
+ if (wrapper.Data.TryGetValue("latest.fetch.timestamp", out var ts))
+ {
+ long.TryParse(ts, out _lastTime);
+ wrapper.Data.Remove("latest.fetch.timestamp");
+ }
+
+ if (wrapper.Data.Count > 0)
+ {
+ lock (_lock)
+ {
+ foreach (var kv in wrapper.Data)
+ {
+ _configs[kv.Key] = kv.Value;
+ }
+ }
+ _listener.Notify(wrapper.Data);
+ Console.WriteLine($"[ConfigClient] {wrapper.Data.Count} configs changed");
+ }
+ }
+
+ private void ApplyConfigs(Dictionary data)
+ {
+ lock (_lock)
+ {
+ foreach (var kv in data)
+ {
+ if (kv.Key.StartsWith("kenaito.") || kv.Key == "latest.fetch.timestamp")
+ continue;
+ if (kv.Value is string strVal)
+ {
+ _configs[kv.Key] = strVal;
+ }
+ }
+ }
+ }
+
+ private int CountConfigs(Dictionary data)
+ {
+ return data.Count(kv => !kv.Key.StartsWith("kenaito.") && kv.Key != "latest.fetch.timestamp" && kv.Value is string);
+ }
+
+ private async Task SyncLoopAsync(CancellationToken ct)
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ try
+ {
+ await Task.Delay(_config.SyncInterval * 1000, ct);
+ await FetchChangedAsync();
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ConfigClient] sync error: {ex.Message}");
+ }
+ }
+ }
+
+ private class FetchChangedResponse
+ {
+ public int Code { get; set; }
+ public Dictionary? Data { get; set; }
+ }
+}
diff --git a/kenaito-conf-dotnet-client/Config.cs b/kenaito-conf-dotnet-client/Config.cs
new file mode 100644
index 0000000..b0a8f31
--- /dev/null
+++ b/kenaito-conf-dotnet-client/Config.cs
@@ -0,0 +1,13 @@
+namespace Kenaito.Conf.Client;
+
+public class Config
+{
+ public string BaseUrl { get; set; } = "";
+ public string AppName { get; set; } = "";
+ public string Env { get; set; } = "dev";
+ public string Version { get; set; } = "latest";
+ public string? Token { get; set; }
+ public int SyncInterval { get; set; } = 10;
+ public string CacheDir { get; set; } = ".";
+ public bool IgnoreGlobal { get; set; } = false;
+}
diff --git a/kenaito-conf-dotnet-client/KenaitoConfClient.csproj b/kenaito-conf-dotnet-client/KenaitoConfClient.csproj
new file mode 100644
index 0000000..584cbed
--- /dev/null
+++ b/kenaito-conf-dotnet-client/KenaitoConfClient.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net6.0
+ enable
+ enable
+ Kenaito.Conf.Client
+ 1.0.0
+ odboy
+ www.odboy.cn
+ Kenaito Config Center .NET Client
+
+
+
+
+
+
+
diff --git a/kenaito-conf-dotnet-client/Listener.cs b/kenaito-conf-dotnet-client/Listener.cs
new file mode 100644
index 0000000..082273e
--- /dev/null
+++ b/kenaito-conf-dotnet-client/Listener.cs
@@ -0,0 +1,28 @@
+namespace Kenaito.Conf.Client;
+
+public delegate void ChangeHandler(Dictionary changed);
+
+internal class Listener
+{
+ private readonly List _handlers = new();
+
+ public void OnChange(ChangeHandler handler)
+ {
+ _handlers.Add(handler);
+ }
+
+ public void Notify(Dictionary changed)
+ {
+ foreach (var handler in _handlers)
+ {
+ try
+ {
+ handler(changed);
+ }
+ catch
+ {
+ // ignore handler errors
+ }
+ }
+ }
+}
diff --git a/kenaito-conf-dotnet-client/README.md b/kenaito-conf-dotnet-client/README.md
new file mode 100644
index 0000000..7cac6f1
--- /dev/null
+++ b/kenaito-conf-dotnet-client/README.md
@@ -0,0 +1,75 @@
+# Kenaito Config Center .NET Client
+
+kenaito-conf 配置中心的 .NET 客户端 SDK。
+
+## 安装
+
+### 本地编译
+
+```bash
+cd kenaito-conf-dotnet-client
+dotnet build
+```
+
+### 作为本地依赖
+
+在 demo 项目的 csproj 中引用:
+
+```xml
+
+
+
+```
+
+## 快速开始
+
+```csharp
+using Kenaito.Conf.Client;
+
+var config = new Config
+{
+ BaseUrl = "http://localhost:8080",
+ AppName = "kenaito-dotnet-demo",
+ Env = "daily",
+ SyncInterval = 10
+};
+
+var client = new Client(config);
+
+client.OnChange(changed =>
+{
+ Console.WriteLine("Config changed:");
+ foreach (var kv in changed)
+ {
+ Console.WriteLine($" {kv.Key} = {kv.Value}");
+ }
+});
+
+await client.StartAsync();
+
+var dbHost = client.Get("database.host");
+Console.WriteLine($"database.host = {dbHost}");
+
+Console.ReadLine();
+client.Stop();
+```
+
+## 配置项
+
+| 配置项 | 说明 | 默认值 | 必填 |
+|--------|------|--------|------|
+| BaseUrl | 配置中心地址 | - | 是 |
+| AppName | 应用名称 | - | 是 |
+| Env | 环境标识 | dev | 否 |
+| Version | 配置版本 | latest | 否 |
+| Token | 认证令牌 | null | 否 |
+| SyncInterval | 同步间隔(秒) | 10 | 否 |
+| CacheDir | 缓存目录 | . | 否 |
+| IgnoreGlobal | 是否忽略全局配置 | false | 否 |
+
+## 功能特性
+
+- 启动时拉取全量配置
+- 定时轮询增量配置变更
+- 本地文件缓存,服务不可用时从缓存加载
+- 配置变更回调通知
diff --git a/kenaito-conf-go-client-demo/go.mod b/kenaito-conf-go-client-demo/go.mod
index 24afd60..d512ad4 100644
--- a/kenaito-conf-go-client-demo/go.mod
+++ b/kenaito-conf-go-client-demo/go.mod
@@ -2,6 +2,9 @@ module kenaito-conf-go-client-demo
go 1.25.0
-require kenaito-conf/go-client v0.0.0
+require (
+ kenaito-conf/go-client v0.0.0
+ github.com/joho/godotenv v1.5.1
+)
replace kenaito-conf/go-client => ../kenaito-conf-go-client
diff --git a/kenaito-conf-go-client-demo/go.sum b/kenaito-conf-go-client-demo/go.sum
new file mode 100644
index 0000000..ce90c15
--- /dev/null
+++ b/kenaito-conf-go-client-demo/go.sum
@@ -0,0 +1 @@
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
diff --git a/kenaito-conf-go-client-demo/main.go b/kenaito-conf-go-client-demo/main.go
index 2ecd341..45e9ab6 100644
--- a/kenaito-conf-go-client-demo/main.go
+++ b/kenaito-conf-go-client-demo/main.go
@@ -6,19 +6,40 @@ import (
"log"
"os"
"os/signal"
+ "strconv"
"syscall"
+
+ "github.com/joho/godotenv"
)
+func getEnv(key, defaultValue string) string {
+ if value := os.Getenv(key); value != "" {
+ return value
+ }
+ return defaultValue
+}
+
+func getEnvInt(key string, defaultValue int) int {
+ if value := os.Getenv(key); value != "" {
+ if i, err := strconv.Atoi(value); err == nil {
+ return i
+ }
+ }
+ return defaultValue
+}
+
func main() {
- // create config
+ _ = godotenv.Load()
+
+ // create config from environment variables
cfg := &client.Config{
- BaseURL: "http://localhost:8080",
- AppName: "kenaito-springboot-demo",
- Env: "daily",
- Version: "latest",
- //Token: "6f6966fe196572954b176c0e2a97bc399b050c6be31454b8d6b0f7f622b5d6f1",
- SyncInterval: 10, // sync every 10 seconds
- CacheDir: ".",
+ BaseURL: getEnv("KENAITO_BASE_URL", "http://localhost:8080"),
+ AppName: getEnv("KENAITO_APP_NAME", "kenaito-springboot-demo"),
+ Env: getEnv("KENAITO_ENV", "daily"),
+ Version: getEnv("KENAITO_VERSION", "latest"),
+ Token: getEnv("KENAITO_TOKEN", ""),
+ SyncInterval: getEnvInt("KENAITO_SYNC_INTERVAL", 10),
+ CacheDir: getEnv("KENAITO_CACHE_DIR", "."),
}
// create client