feat(storage): 添加目录操作功能和存档仓库系统

- 在IStorage接口中添加目录操作相关方法:ListDirectoriesAsync、
  ListFilesAsync、DirectoryExistsAsync、CreateDirectoryAsync
- 为FileStorage和GodotFileStorage实现目录操作功能
- 添加ScopedStorage的目录操作委托实现
- 新增ISaveRepository接口定义基于槽位的存档系统
- 实现SaveRepository类提供完整的存档管理功能
- 添加SaveConfiguration类用于存档系统配置
This commit is contained in:
GeWuYou 2026-02-24 13:19:55 +08:00 committed by gewuyou
parent 6a99b54d6e
commit 2d4527d066
8 changed files with 750 additions and 0 deletions

View File

@ -75,4 +75,32 @@ public interface IStorage : IUtility
/// <param name="key">要删除的键</param>
/// <returns>表示异步操作的Task</returns>
Task DeleteAsync(string key);
/// <summary>
/// 列举指定路径下的所有子目录名称
/// </summary>
/// <param name="path">要列举的路径,空字符串表示根目录</param>
/// <returns>子目录名称列表</returns>
Task<IReadOnlyList<string>> ListDirectoriesAsync(string path = "");
/// <summary>
/// 列举指定路径下的所有文件名称
/// </summary>
/// <param name="path">要列举的路径,空字符串表示根目录</param>
/// <returns>文件名称列表</returns>
Task<IReadOnlyList<string>> ListFilesAsync(string path = "");
/// <summary>
/// 检查指定路径的目录是否存在
/// </summary>
/// <param name="path">要检查的目录路径</param>
/// <returns>如果目录存在则返回true否则返回false</returns>
Task<bool> DirectoryExistsAsync(string path);
/// <summary>
/// 创建目录(递归创建父目录)
/// </summary>
/// <param name="path">要创建的目录路径</param>
/// <returns>表示异步操作的Task</returns>
Task CreateDirectoryAsync(string path);
}

View File

@ -0,0 +1,57 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using GFramework.Core.Abstractions.utility;
namespace GFramework.Game.Abstractions.data;
/// <summary>
/// 存档仓库接口,管理基于槽位的存档系统
/// </summary>
/// <typeparam name="TSaveData">存档数据类型必须实现IData接口并具有无参构造函数</typeparam>
public interface ISaveRepository<TSaveData> : IUtility
where TSaveData : class, IData, new()
{
/// <summary>
/// 检查指定槽位是否存在存档
/// </summary>
/// <param name="slot">存档槽位编号</param>
/// <returns>如果存档存在返回true否则返回false</returns>
Task<bool> ExistsAsync(int slot);
/// <summary>
/// 加载指定槽位的存档
/// </summary>
/// <param name="slot">存档槽位编号</param>
/// <returns>存档数据对象,如果不存在则返回新实例</returns>
Task<TSaveData> LoadAsync(int slot);
/// <summary>
/// 保存存档到指定槽位
/// </summary>
/// <param name="slot">存档槽位编号</param>
/// <param name="data">要保存的存档数据</param>
Task SaveAsync(int slot, TSaveData data);
/// <summary>
/// 删除指定槽位的存档
/// </summary>
/// <param name="slot">存档槽位编号</param>
Task DeleteAsync(int slot);
/// <summary>
/// 列出所有有效的存档槽位
/// </summary>
/// <returns>包含所有有效存档槽位编号的只读列表,按升序排列</returns>
Task<IReadOnlyList<int>> ListSlotsAsync();
}

View File

@ -0,0 +1,35 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.data;
/// <summary>
/// 存档系统配置
/// </summary>
public sealed class SaveConfiguration
{
/// <summary>
/// 存档根目录 (如 "user://saves")
/// </summary>
public string SaveRoot { get; init; } = "user://saves";
/// <summary>
/// 存档槽位前缀 (如 "slot_")
/// </summary>
public string SaveSlotPrefix { get; init; } = "slot_";
/// <summary>
/// 存档文件名 (如 "save.json")
/// </summary>
public string SaveFileName { get; init; } = "save.json";
}

View File

@ -0,0 +1,145 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Globalization;
using GFramework.Core.Abstractions.storage;
using GFramework.Core.utility;
using GFramework.Game.Abstractions.data;
using GFramework.Game.storage;
namespace GFramework.Game.data;
/// <summary>
/// 基于槽位的存档仓库实现
/// </summary>
/// <typeparam name="TSaveData">存档数据类型</typeparam>
public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository<TSaveData>
where TSaveData : class, IData, new()
{
private readonly SaveConfiguration _config;
private readonly IStorage _rootStorage;
/// <summary>
/// 初始化存档仓库
/// </summary>
/// <param name="storage">存储实例</param>
/// <param name="config">存档配置</param>
public SaveRepository(IStorage storage, SaveConfiguration config)
{
ArgumentNullException.ThrowIfNull(storage);
ArgumentNullException.ThrowIfNull(config);
_config = config;
_rootStorage = new ScopedStorage(storage, config.SaveRoot);
}
/// <summary>
/// 检查指定槽位是否存在存档
/// </summary>
/// <param name="slot">存档槽位编号</param>
/// <returns>如果存档存在返回true否则返回false</returns>
public async Task<bool> ExistsAsync(int slot)
{
var storage = GetSlotStorage(slot);
return await storage.ExistsAsync(_config.SaveFileName);
}
/// <summary>
/// 加载指定槽位的存档
/// </summary>
/// <param name="slot">存档槽位编号</param>
/// <returns>存档数据对象,如果不存在则返回新实例</returns>
public async Task<TSaveData> LoadAsync(int slot)
{
var storage = GetSlotStorage(slot);
if (await storage.ExistsAsync(_config.SaveFileName))
return await storage.ReadAsync<TSaveData>(_config.SaveFileName);
return new TSaveData();
}
/// <summary>
/// 保存存档到指定槽位
/// </summary>
/// <param name="slot">存档槽位编号</param>
/// <param name="data">要保存的存档数据</param>
public async Task SaveAsync(int slot, TSaveData data)
{
var slotPath = $"{_config.SaveSlotPrefix}{slot}";
// 确保槽位目录存在
if (!await _rootStorage.DirectoryExistsAsync(slotPath))
await _rootStorage.CreateDirectoryAsync(slotPath);
var storage = GetSlotStorage(slot);
await storage.WriteAsync(_config.SaveFileName, data);
}
/// <summary>
/// 删除指定槽位的存档
/// </summary>
/// <param name="slot">存档槽位编号</param>
public async Task DeleteAsync(int slot)
{
var storage = GetSlotStorage(slot);
await storage.DeleteAsync(_config.SaveFileName);
}
/// <summary>
/// 列出所有有效的存档槽位
/// </summary>
/// <returns>包含所有有效存档槽位编号的只读列表,按升序排列</returns>
public async Task<IReadOnlyList<int>> ListSlotsAsync()
{
// 列举所有槽位目录
var directories = await _rootStorage.ListDirectoriesAsync();
var slots = new List<int>();
foreach (var dirName in directories)
{
// 检查目录名是否符合槽位前缀
if (!dirName.StartsWith(_config.SaveSlotPrefix, StringComparison.Ordinal))
continue;
// 提取槽位编号
var slotStr = dirName[_config.SaveSlotPrefix.Length..];
if (!int.TryParse(slotStr, CultureInfo.InvariantCulture, out var slot))
continue;
// 检查槽位中是否存在存档文件
if (await ExistsAsync(slot))
slots.Add(slot);
}
return slots.OrderBy(x => x).ToList();
}
/// <summary>
/// 获取指定槽位的存储对象
/// </summary>
/// <param name="slot">存档槽位编号</param>
/// <returns>对应槽位的存储对象</returns>
private IStorage GetSlotStorage(int slot)
{
return new ScopedStorage(_rootStorage, $"{_config.SaveSlotPrefix}{slot}");
}
/// <summary>
/// 初始化逻辑
/// </summary>
protected override void OnInit()
{
}
}

View File

@ -0,0 +1,292 @@
# SaveRepository 使用指南
## 概述
`SaveRepository<TSaveData>` 是 GFramework 提供的通用存档仓库实现,基于 `IStorage` 抽象层,支持基于槽位的存档管理。
## 核心特性
- ✅ **完全解耦引擎依赖** - 框架层无 Godot 代码
- ✅ **配置对象带默认值** - 无需 ProjectSettings 也能工作
- ✅ **泛型设计** - 支持任意实现 `IData` 的存档类型
- ✅ **自动槽位管理** - 利用 `ScopedStorage` 实现路径隔离
- ✅ **异步操作** - 所有方法均为异步,避免阻塞主线程
## 快速开始
### 1. 定义存档数据类型
```csharp
using GFramework.Game.Abstractions.data;
public class GameSaveData : IData
{
public string PlayerName { get; set; } = "";
public int Level { get; set; } = 1;
public float PlayTime { get; set; } = 0f;
public DateTime SaveTime { get; set; } = DateTime.Now;
}
```
### 2. 在启动类中注册
```csharp
// 在 GameApp.cs 或类似的启动类中
protected override void OnRegisterUtility()
{
// 1. 注册序列化器
var serializer = new JsonSerializer();
this.RegisterUtility<ISerializer>(serializer);
// 2. 注册存储
this.RegisterUtility<IStorage>(new GodotFileStorage(serializer));
// 3. 创建存档配置
var saveConfig = new SaveConfiguration
{
SaveRoot = "user://saves", // 存档根目录
SaveSlotPrefix = "slot_", // 槽位前缀
SaveFileName = "save.json" // 存档文件名
};
// 4. 注册存档仓库
this.RegisterUtility<ISaveRepository<GameSaveData>>(
new SaveRepository<GameSaveData>(
this.GetUtility<IStorage>()!,
saveConfig
)
);
}
```
### 3. 使用存档仓库
```csharp
// 获取存档仓库
var saveRepo = this.GetUtility<ISaveRepository<GameSaveData>>();
// 保存存档到槽位 1
var saveData = new GameSaveData
{
PlayerName = "玩家1",
Level = 10,
PlayTime = 3600f
};
await saveRepo.SaveAsync(slot: 1, saveData);
// 加载槽位 1 的存档
var loadedData = await saveRepo.LoadAsync(slot: 1);
GD.Print($"玩家: {loadedData.PlayerName}, 等级: {loadedData.Level}");
// 检查槽位是否存在
if (await saveRepo.ExistsAsync(slot: 1))
{
GD.Print("槽位 1 存在存档");
}
// 列出所有有效槽位
var slots = await saveRepo.ListSlotsAsync();
GD.Print($"找到 {slots.Count} 个存档槽位");
// 删除槽位 1 的存档
await saveRepo.DeleteAsync(slot: 1);
```
## 高级用法
### 从 ProjectSettings 读取配置
```csharp
var saveConfig = new SaveConfiguration
{
SaveRoot = ProjectSettings.HasSetting("application/config/save/save_path")
? ProjectSettings.GetSetting("application/config/save/save_path").AsString()
: "user://saves",
SaveSlotPrefix = ProjectSettings.HasSetting("application/config/save/save_slot_prefix")
? ProjectSettings.GetSetting("application/config/save/save_slot_prefix").AsString()
: "slot_",
SaveFileName = ProjectSettings.HasSetting("application/config/save/save_file_name")
? ProjectSettings.GetSetting("application/config/save/save_file_name").AsString()
: "save.json"
};
```
### 支持版本迁移
```csharp
using GFramework.Game.Abstractions.data;
public class GameSaveData : IData, IVersionedData
{
public int Version { get; set; } = 1;
public string PlayerName { get; set; } = "";
public int Level { get; set; } = 1;
// 版本 2 新增字段
public int Experience { get; set; } = 0;
}
// 实现迁移逻辑(未来可扩展)
public class GameSaveDataMigration_1_to_2 : IDataMigration<GameSaveData>
{
public int FromVersion => 1;
public int ToVersion => 2;
public GameSaveData Migrate(GameSaveData oldData)
{
// 迁移逻辑:为旧存档添加默认经验值
oldData.Experience = oldData.Level * 100;
oldData.Version = 2;
return oldData;
}
}
```
### 多种存档类型
```csharp
// 游戏存档
this.RegisterUtility<ISaveRepository<GameSaveData>>(
new SaveRepository<GameSaveData>(storage, new SaveConfiguration
{
SaveRoot = "user://saves/game",
SaveSlotPrefix = "slot_",
SaveFileName = "save.json"
})
);
// 用户配置存档
this.RegisterUtility<ISaveRepository<UserProfileData>>(
new SaveRepository<UserProfileData>(storage, new SaveConfiguration
{
SaveRoot = "user://saves/profile",
SaveSlotPrefix = "profile_",
SaveFileName = "profile.json"
})
);
// 成就数据存档
this.RegisterUtility<ISaveRepository<AchievementData>>(
new SaveRepository<AchievementData>(storage, new SaveConfiguration
{
SaveRoot = "user://saves/achievements",
SaveSlotPrefix = "achievement_",
SaveFileName = "data.json"
})
);
```
## 文件结构
使用默认配置时,存档文件结构如下:
```
user://saves/
├── slot_1/
│ └── save.json
├── slot_2/
│ └── save.json
└── slot_3/
└── save.json
```
## API 参考
### ISaveRepository<TSaveData>
| 方法 | 说明 | 返回值 |
|------|------|--------|
| `ExistsAsync(int slot)` | 检查指定槽位是否存在存档 | `Task<bool>` |
| `LoadAsync(int slot)` | 加载指定槽位的存档 | `Task<TSaveData>` |
| `SaveAsync(int slot, TSaveData data)` | 保存存档到指定槽位 | `Task` |
| `DeleteAsync(int slot)` | 删除指定槽位的存档 | `Task` |
| `ListSlotsAsync()` | 列出所有有效的存档槽位 | `Task<IReadOnlyList<int>>` |
### SaveConfiguration
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `SaveRoot` | `string` | `"user://saves"` | 存档根目录 |
| `SaveSlotPrefix` | `string` | `"slot_"` | 槽位前缀 |
| `SaveFileName` | `string` | `"save.json"` | 存档文件名 |
## 从旧版 SaveStorageUtility 迁移
### 主要变化
1. **所有方法改为异步** - 使用 `async/await`
2. **接口名称变更** - `ISaveStorageUtility``ISaveRepository<TSaveData>`
3. **方法名称添加 Async 后缀**
4. **配置通过对象传递** - 支持默认值
### 迁移对照表
| 旧方法 | 新方法 |
|--------|--------|
| `Exists(int slot)` | `await ExistsAsync(int slot)` |
| `Load(int slot)` | `await LoadAsync(int slot)` |
| `Save(int slot, data)` | `await SaveAsync(int slot, data)` |
| `Delete(int slot)` | `await DeleteAsync(int slot)` |
| `ListSlots()` | `await ListSlotsAsync()` |
### 迁移示例
**旧代码:**
```csharp
var saveUtil = this.GetUtility<ISaveStorageUtility>();
saveUtil.Save(1, data);
var loadedData = saveUtil.Load(1);
```
**新代码:**
```csharp
var saveRepo = this.GetUtility<ISaveRepository<GameSaveData>>();
await saveRepo.SaveAsync(1, data);
var loadedData = await saveRepo.LoadAsync(1);
```
## 常见问题
### Q: 如何切换到云存档?
A: 只需替换 `IStorage` 实现即可:
```csharp
// 本地存档
this.RegisterUtility<IStorage>(new GodotFileStorage(serializer));
// 云存档(需要自行实现 CloudStorage
this.RegisterUtility<IStorage>(new CloudStorage(apiKey, serializer));
```
### Q: 如何加密存档?
A: 使用装饰器模式包装 `IStorage`
```csharp
var baseStorage = new GodotFileStorage(serializer);
var encryptedStorage = new EncryptedStorage(baseStorage, encryptionKey);
this.RegisterUtility<IStorage>(encryptedStorage);
```
### Q: 如何实现自动备份?
A: 使用 `DataRepository``AutoBackup` 选项(需要配合 `DataRepository` 使用)。
### Q: LoadAsync 返回的是新实例还是缓存实例?
A: 每次调用都会从存储读取并返回新实例,不会缓存。如果需要缓存,请在业务层实现。
## 最佳实践
1. **使用异步方法** - 避免阻塞主线程
2. **错误处理** - 使用 try-catch 捕获 IO 异常
3. **定期保存** - 不要等到游戏退出才保存
4. **槽位验证** - 加载前先检查槽位是否存在
5. **版本管理** - 为存档数据实现 `IVersionedData` 接口
## 相关文档
- [IStorage 接口文档](./IStorage.md)
- [ScopedStorage 使用指南](./ScopedStorage.md)
- [数据仓库模式](./DataRepository.md)

View File

@ -228,6 +228,71 @@ public sealed class FileStorage : IFileStorage
#endregion
#region Directory Operations
/// <summary>
/// 列举指定路径下的所有子目录名称
/// </summary>
/// <param name="path">要列举的路径,空字符串表示根目录</param>
/// <returns>子目录名称列表</returns>
public Task<IReadOnlyList<string>> ListDirectoriesAsync(string path = "")
{
var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path);
if (!Directory.Exists(fullPath))
return Task.FromResult<IReadOnlyList<string>>(Array.Empty<string>());
var dirs = Directory.GetDirectories(fullPath)
.Select(Path.GetFileName)
.Where(name => !string.IsNullOrEmpty(name))
.ToList();
return Task.FromResult<IReadOnlyList<string>>(dirs);
}
/// <summary>
/// 列举指定路径下的所有文件名称
/// </summary>
/// <param name="path">要列举的路径,空字符串表示根目录</param>
/// <returns>文件名称列表</returns>
public Task<IReadOnlyList<string>> ListFilesAsync(string path = "")
{
var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path);
if (!Directory.Exists(fullPath))
return Task.FromResult<IReadOnlyList<string>>(Array.Empty<string>());
var files = Directory.GetFiles(fullPath)
.Select(Path.GetFileName)
.Where(name => !string.IsNullOrEmpty(name))
.ToList();
return Task.FromResult<IReadOnlyList<string>>(files);
}
/// <summary>
/// 检查指定路径的目录是否存在
/// </summary>
/// <param name="path">要检查的目录路径</param>
/// <returns>如果目录存在则返回true否则返回false</returns>
public Task<bool> DirectoryExistsAsync(string path)
{
var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path);
return Task.FromResult(Directory.Exists(fullPath));
}
/// <summary>
/// 创建目录(递归创建父目录)
/// </summary>
/// <param name="path">要创建的目录路径</param>
/// <returns>表示异步操作的Task</returns>
public Task CreateDirectoryAsync(string path)
{
var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path);
Directory.CreateDirectory(fullPath);
return Task.CompletedTask;
}
#endregion
#region Write
/// <summary>

View File

@ -105,6 +105,46 @@ public sealed class ScopedStorage(IStorage inner, string prefix) : IScopedStorag
await inner.DeleteAsync(Key(key));
}
/// <summary>
/// 列举指定路径下的所有子目录名称
/// </summary>
/// <param name="path">要列举的路径,空字符串表示根目录</param>
/// <returns>子目录名称列表</returns>
public Task<IReadOnlyList<string>> ListDirectoriesAsync(string path = "")
{
return inner.ListDirectoriesAsync(Key(path));
}
/// <summary>
/// 列举指定路径下的所有文件名称
/// </summary>
/// <param name="path">要列举的路径,空字符串表示根目录</param>
/// <returns>文件名称列表</returns>
public Task<IReadOnlyList<string>> ListFilesAsync(string path = "")
{
return inner.ListFilesAsync(Key(path));
}
/// <summary>
/// 检查指定路径的目录是否存在
/// </summary>
/// <param name="path">要检查的目录路径</param>
/// <returns>如果目录存在则返回true否则返回false</returns>
public Task<bool> DirectoryExistsAsync(string path)
{
return inner.DirectoryExistsAsync(Key(path));
}
/// <summary>
/// 创建目录(递归创建父目录)
/// </summary>
/// <param name="path">要创建的目录路径</param>
/// <returns>表示异步操作的Task</returns>
public Task CreateDirectoryAsync(string path)
{
return inner.CreateDirectoryAsync(Key(path));
}
/// <summary>
/// 为给定的键添加前缀
/// </summary>

View File

@ -263,6 +263,94 @@ public sealed class GodotFileStorage : IStorage
#endregion
#region Directory Operations
/// <summary>
/// 列举指定路径下的所有子目录名称
/// </summary>
/// <param name="path">要列举的路径,空字符串表示根目录</param>
/// <returns>子目录名称列表</returns>
public async Task<IReadOnlyList<string>> ListDirectoriesAsync(string path = "")
{
return await Task.Run(() =>
{
var fullPath = string.IsNullOrEmpty(path) ? "user://" : ToAbsolutePath(path);
var dir = DirAccess.Open(fullPath);
if (dir == null) return Array.Empty<string>();
dir.ListDirBegin();
var result = new List<string>();
while (true)
{
var name = dir.GetNext();
if (string.IsNullOrEmpty(name)) break;
if (dir.CurrentIsDir() && !name.StartsWith(".", StringComparison.Ordinal))
result.Add(name);
}
dir.ListDirEnd();
return (IReadOnlyList<string>)result;
});
}
/// <summary>
/// 列举指定路径下的所有文件名称
/// </summary>
/// <param name="path">要列举的路径,空字符串表示根目录</param>
/// <returns>文件名称列表</returns>
public async Task<IReadOnlyList<string>> ListFilesAsync(string path = "")
{
return await Task.Run(() =>
{
var fullPath = string.IsNullOrEmpty(path) ? "user://" : ToAbsolutePath(path);
var dir = DirAccess.Open(fullPath);
if (dir == null) return Array.Empty<string>();
dir.ListDirBegin();
var result = new List<string>();
while (true)
{
var name = dir.GetNext();
if (string.IsNullOrEmpty(name)) break;
if (!dir.CurrentIsDir())
result.Add(name);
}
dir.ListDirEnd();
return (IReadOnlyList<string>)result;
});
}
/// <summary>
/// 检查指定路径的目录是否存在
/// </summary>
/// <param name="path">要检查的目录路径</param>
/// <returns>如果目录存在则返回true否则返回false</returns>
public Task<bool> DirectoryExistsAsync(string path)
{
var fullPath = ToAbsolutePath(path);
return Task.FromResult(DirAccess.DirExistsAbsolute(fullPath));
}
/// <summary>
/// 创建目录(递归创建父目录)
/// </summary>
/// <param name="path">要创建的目录路径</param>
/// <returns>表示异步操作的Task</returns>
public async Task CreateDirectoryAsync(string path)
{
await Task.Run(() =>
{
var fullPath = ToAbsolutePath(path);
if (!DirAccess.DirExistsAbsolute(fullPath))
DirAccess.MakeDirRecursiveAbsolute(fullPath);
});
}
#endregion
#region Write
/// <summary>