Skip to content

Commit 91395ae

Browse files
committed
refactor: 优化 HttpClient 使用并添加 AGENTS 指南
- 使用 IHttpClientFactory 替代直接创建 HttpClient 实例 - DownloadResource 方法添加临时文件清理逻辑 - 添加 AGENTS.md AI 协作指南 - 更新 Docker 镜像版本标签
1 parent 5ae2d90 commit 91395ae

9 files changed

Lines changed: 412 additions & 18 deletions

File tree

.github/workflows/docker-image.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ jobs:
1717
- name: Login docker regsitry
1818
run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_USER_PASSWORD }}
1919
- name: Build the Docker image
20-
run: docker build --target zserver-api -f API.Dockerfile -t zlzforever/zserver-api:20260304-2 .
20+
run: docker build --target zserver-api -f API.Dockerfile -t zlzforever/zserver-api:202603-3 .
2121
- name: Publish the Docker image
22-
run: docker push zlzforever/zserver-api:20260304-2
22+
run: docker push zlzforever/zserver-api:202603-3

AGENTS.md

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
# ZServer AGENTS.md
2+
3+
This file provides guidelines for AI agents working on the ZServer codebase.
4+
5+
## 1. Build, Test & Run Commands
6+
7+
### Build
8+
```bash
9+
# Build entire solution
10+
dotnet build ZServer.sln
11+
12+
# Build specific project
13+
dotnet build src/ZServer.API/ZServer.API.csproj
14+
15+
# Build for release
16+
dotnet build -c Release ZServer.sln
17+
```
18+
19+
### Run
20+
```bash
21+
# Run API (standalone mode)
22+
dotnet run --project src/ZServer.API/ZServer.API.csproj -- --Standalone true --Port 8200
23+
24+
# Run API (cluster mode)
25+
dotnet run --project src/ZServer.API/ZServer.API.csproj -- --Standalone false --ClusterSiloPort 10001 --ClusterGatewayPort 20001 --Port 8100
26+
```
27+
28+
### Test
29+
```bash
30+
# Run all tests
31+
dotnet test src/ZServer.Tests/ZServer.Tests.csproj
32+
33+
# Run single test class
34+
dotnet test src/ZServer.Tests/ZServer.Tests.csproj --filter "FullyQualifiedName~LayerStoreTests"
35+
36+
# Run single test method
37+
dotnet test src/ZServer.Tests/ZServer.Tests.csproj --filter "FullyQualifiedName~LayerStoreTests.ShouldRefresh"
38+
39+
# Run with verbose output
40+
dotnet test -v n src/ZServer.Tests/ZServer.Tests.csproj --filter "FullyQualifiedName~TestName"
41+
42+
# Run tests with coverage
43+
dotnet test src/ZServer.Tests/ZServer.Tests.csproj --collect:"XPlat Code Coverage"
44+
```
45+
46+
### Package
47+
```bash
48+
# Publish ZServer.API
49+
dotnet publish src/ZServer.API/ZServer.API.csproj -c Release -r linux-x64 --self-contained
50+
51+
# Publish interfaces
52+
./publish_interfaces.sh
53+
```
54+
55+
---
56+
57+
## 2. Code Style Guidelines
58+
59+
### 2.1 Language & Framework
60+
61+
- **Target Framework**: net10.0 (latest), with fallback to net9.0, net8.0
62+
- **C# Version**: Latest (primary constructors, pattern matching)
63+
- **Nullable**: Disabled (no `#nullable enable`)
64+
65+
### 2.2 Naming Conventions
66+
67+
| Element | Convention | Example |
68+
|---------|------------|---------|
69+
| Class/Interface | PascalCase | `LayerStore`, `ILayerStore` |
70+
| Method | PascalCase | `GetMapAsync`, `FindAsync` |
71+
| Property | PascalCase | `Cache`, `LayerName` |
72+
| Field (private static) | PascalCase | `PropertyCache`, `Logger` |
73+
| Local variable | camelCase | `layerName`, `configurations` |
74+
| Parameter | camelCase | `styleStore`, `resourceGroup` |
75+
| Namespace | PascalCase | `ZServer.Store`, `ZMap.Renderer` |
76+
| Enum | PascalCase | `ServiceType`, `GeometryType` |
77+
| Constant | PascalCase | `DefaultBufferSize` |
78+
79+
### 2.3 File Organization
80+
81+
- **One public class per file**: Match filename to class name
82+
- **Using statements**: Sorted alphabetically at top of file
83+
- **Implicit usings**: Not used - explicit `using` for each namespace
84+
- **Namespace**: Match folder structure (`src/ZServer/Store/LayerStore.cs``namespace ZServer.Store`)
85+
86+
```csharp
87+
using System;
88+
using System.Collections.Concurrent;
89+
using System.Collections.Generic;
90+
using System.Linq;
91+
using Microsoft.Extensions.Logging;
92+
using NetTopologySuite.Geometries;
93+
using ZMap;
94+
using ZMap.Infrastructure;
95+
using ZMap.Source;
96+
using ZMap.Style;
97+
98+
namespace ZServer.Store;
99+
```
100+
101+
### 2.4 Class Structure
102+
103+
```csharp
104+
// Primary constructor (C# 9+)
105+
public class LayerStore(
106+
IStyleGroupStore styleStore,
107+
IResourceGroupStore resourceGroupStore,
108+
ISourceStore sourceStore,
109+
ISldStore sldStore)
110+
: ILayerStore
111+
{
112+
// Private static readonly fields first
113+
private static readonly ConcurrentDictionary<Type, List<PropertyInfo>> PropertyCache = new();
114+
private static readonly Lazy<ILogger> Logger = new(Log.CreateLogger<LayerStore>());
115+
private static readonly ConcurrentDictionary<string, Layer> Cache = new();
116+
117+
// Public methods
118+
public async Task RefreshAsync(List<JObject> configurations) { }
119+
120+
// Private methods
121+
private async Task<Layer> BindLayerAsync(...) { }
122+
}
123+
```
124+
125+
### 2.5 Documentation
126+
127+
- **XML Documentation**: Required for all public APIs
128+
- **Language**: Chinese comments for user-facing APIs, English for internal
129+
130+
```csharp
131+
/// <summary>
132+
/// WMS 服务接口
133+
/// </summary>
134+
/// <param name="layers">图层名称,支持两种方式...</param>
135+
[HttpGet]
136+
public async Task GetAsync(...) { }
137+
```
138+
139+
### 2.6 Error Handling
140+
141+
- **Custom exceptions**: Use specific exception types, not generic `Exception`
142+
- **Global exception filter**: `GlobalExceptionFilter` in `ZServer.API/Filters/`
143+
- **Logging**: Always log errors with context
144+
- **Never swallow exceptions**: Never use empty catch blocks
145+
146+
```csharp
147+
// GOOD - specific exception with logging
148+
if (source == null)
149+
{
150+
Logger.Value.LogError("图层 {ResourceGroup}:{Name} 的数据源 {SourceName} 不存在",
151+
resourceGroup, name, sourceName);
152+
return null;
153+
}
154+
155+
// GOOD - throw specific exception
156+
throw new FileNotFoundException("Invariant sql is missing", scriptPath);
157+
158+
// BAD - generic exception
159+
throw new Exception("Something went wrong");
160+
161+
// BAD - empty catch
162+
catch (Exception) { }
163+
```
164+
165+
### 2.7 Async/Await
166+
167+
- **Always use async**: Never use `.Result` or `.Wait()` on async methods
168+
- **Use `ValueTask`**: For methods that may synchronously complete
169+
170+
```csharp
171+
// GOOD
172+
public async Task<Layer> FindAsync(string layerName)
173+
{
174+
return await Task.FromResult(Cache.TryGetValue(layerName) ? Cache[layerName] : null);
175+
}
176+
177+
// GOOD - ValueTask for potentially sync completion
178+
public ValueTask<Layer> FindAsync(string layerName)
179+
{
180+
return Cache.TryGetValue(layerName, out var item)
181+
? new ValueTask<Layer>(item.Clone())
182+
: new ValueTask<Layer>();
183+
}
184+
185+
// BAD - blocking on async
186+
var ips = Dns.GetHostAddressesAsync(hostname).Result; // DON'T DO THIS
187+
```
188+
189+
### 2.8 Dependency Injection
190+
191+
- **Constructor injection**: All dependencies via constructor (primary constructor)
192+
- **Singleton for static resources**: Use `Lazy<T>` for lazy initialization
193+
194+
```csharp
195+
public class LayerStore(
196+
IStyleGroupStore styleStore, // injected
197+
IResourceGroupStore resourceGroupStore,
198+
ISourceStore sourceStore)
199+
: ILayerStore
200+
{
201+
private static readonly Lazy<ILogger> Logger = new(Log.CreateLogger<LayerStore>());
202+
}
203+
```
204+
205+
### 2.9 HTTP Client
206+
207+
- **Use `IHttpClientFactory`**: Never instantiate `HttpClient` directly
208+
209+
```csharp
210+
// GOOD
211+
private readonly IHttpClientFactory _httpClientFactory;
212+
public MyService(IHttpClientFactory httpClientFactory)
213+
{
214+
_httpClientFactory = httpClientFactory;
215+
}
216+
public async Task MakeRequest()
217+
{
218+
var client = _httpClientFactory.CreateClient();
219+
// use client...
220+
}
221+
222+
// BAD - direct instantiation
223+
var httpClient = new HttpClient(); // DON'T DO THIS
224+
```
225+
226+
### 2.10 Caching
227+
228+
- **Always set size limit**: Never use unbounded `MemoryCache`
229+
- **Set expiration**: Use `AbsoluteExpiration` or `SlidingExpiration`
230+
231+
```csharp
232+
// GOOD - with limits
233+
private static readonly IMemoryCache MemoryCache = new MemoryCache(
234+
new MemoryCacheOptions
235+
{
236+
SizeLimit = 1024,
237+
ExpirationScanInterval = TimeSpan.FromMinutes(5)
238+
});
239+
240+
// Use with size and expiration
241+
MemoryCache.GetOrCreate(key, entry =>
242+
{
243+
entry.Size = 1;
244+
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
245+
return value;
246+
});
247+
```
248+
249+
### 2.11 Authentication & Authorization
250+
251+
- **Middleware order matters**: `UseAuthentication()` must come before `UseAuthorization()`
252+
- **Use `[Authorize]`**: Decorate controllers/actions with authorization attributes
253+
254+
```csharp
255+
// CORRECT middleware order
256+
app.UseRouting();
257+
app.UseAuthentication(); // FIRST
258+
app.UseAuthorization(); // SECOND
259+
app.UseEndpoints(...);
260+
```
261+
262+
### 2.12 Configuration
263+
264+
- **No hardcoded secrets**: Never hardcode passwords, keys, or connection strings
265+
- **Use `IConfiguration`**: Inject configuration via DI
266+
267+
```csharp
268+
// GOOD - from configuration
269+
public class MyService
270+
{
271+
private readonly string _connectionString;
272+
public MyService(IConfiguration configuration)
273+
{
274+
_connectionString = configuration.GetConnectionString("Postgre");
275+
}
276+
}
277+
278+
// BAD - hardcoded
279+
var connStr = "Host=localhost;Password=secret"; // DON'T
280+
```
281+
282+
### 2.13 Testing
283+
284+
- **Framework**: xUnit
285+
- **Pattern**: Arrange-Act-Assert
286+
- **Base class**: Extend `BaseTests` for integration tests
287+
288+
```csharp
289+
public class LayerStoreTests : BaseTests
290+
{
291+
[Fact]
292+
public async Task ShouldRefresh_WhenValidConfig()
293+
{
294+
// Arrange
295+
var store = Service.GetRequiredService<ILayerStore>();
296+
297+
// Act
298+
await store.RefreshAsync(configurations);
299+
300+
// Assert
301+
var layer = await store.FindAsync("test-layer");
302+
Assert.NotNull(layer);
303+
}
304+
}
305+
```
306+
307+
---
308+
309+
## 3. Project Structure
310+
311+
```
312+
src/
313+
├── ZServer.API/ # ASP.NET Core API entry point
314+
├── ZServer/ # Core business logic
315+
├── ZServer.Grains/ # Orleans grain implementations
316+
├── ZServer.Silo/ # Orleans silo configuration
317+
├── ZServer.Interfaces/ # Shared interfaces
318+
├── ZServer.Tests/ # xUnit tests
319+
├── ZMap/ # Core domain (rendering, styling)
320+
├── ZMap.Renderer.SkiaSharp/ # SkiaSharp rendering
321+
├── ZMap.Source.*/ # Data sources (PostgreSQL, ShapeFile, COG)
322+
├── ZMap.TileGrid/ # Tile grid definitions
323+
├── ZMap.SLD/ # SLD styling
324+
├── ZMap.DynamicCompiler/ # Runtime C# compilation
325+
├── Client/ # Client tools
326+
├── Console/ # Console utilities
327+
└── ZServer.Benchmark/ # Benchmarks
328+
```
329+
330+
---
331+
332+
## 4. Key Patterns
333+
334+
### 4.1 Store Pattern
335+
- `I[Entity]Store` interface in `ZServer.Store`
336+
- `[Entity]Store` implementation
337+
- Methods: `FindAsync`, `RefreshAsync`, `GetAllAsync`
338+
339+
### 4.2 Grain Pattern (Orleans)
340+
- Grain interfaces in `ZServer.Interfaces`
341+
- Grain implementations in `ZServer.Grains`
342+
- Stateless worker grains for tile rendering
343+
344+
### 4.3 Source Pattern
345+
- `ISource` interface in `ZMap.Source`
346+
- Implementations: `PostgreSource`, `ShapeFileSource`, `COGGeoTiffSource`
347+
348+
---
349+
350+
## 5. Common Tasks
351+
352+
### 5.1 Adding a New Data Source
353+
1. Create `ZMap.Source.[Name]/[Name]Source.cs` implementing `ISource`
354+
2. Add to `ZServer.API` project reference
355+
3. Register in `ServiceCollectionExtensions.cs`
356+
357+
### 5.2 Adding a New API Endpoint
358+
1. Create controller in `src/ZServer.API/Controllers/`
359+
2. Add `[Authorize]` if needed
360+
3. Inject services via constructor
361+
4. Use `HttpContext.WriteZServerResponseAsync()` for responses
362+
363+
### 5.3 Running a Single Test
364+
```bash
365+
dotnet test src/ZServer.Tests/ZServer.Tests.csproj \
366+
--filter "FullyQualifiedName~Namespace.ClassName.MethodName"
367+
```
368+
369+
---
370+

ZServer.sln

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ ProjectSection(SolutionItems) = preProject
3939
docker-entrypoint.sh = docker-entrypoint.sh
4040
ci.yml = .github/workflows/docker-image.yml
4141
design.md = design.md
42+
AGENTS.md = AGENTS.md
43+
docs\ZServer使用文档.md = docs\ZServer使用文档.md
4244
EndProjectSection
4345
EndProject
4446
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZServer.API", "src\ZServer.API\ZServer.API.csproj", "{C2B0CB75-1C1B-4541-BCCA-E7071342C2C8}"

src/Console/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ static async Task Cut()
6464

6565
await using var conn =
6666
new NpgsqlConnection(
67-
"User ID=postgres;Password=1qazZAQ!;Host=localhost;Port=5432;Database=hdy;Pooling=true;");
67+
"User ID=postgres;Password=;Host=localhost;Port=5432;Database=hdy;Pooling=true;");
6868

6969
// var extent = await conn.QuerySingleOrDefaultAsync<Extent>(
7070
// $"select min(st_xmin(geom)) xmin, min(st_ymin(geom)) ymin, max(st_xmax(geom)) xmax, max(st_ymax(geom)) ymax from sldc_2019");

0 commit comments

Comments
 (0)