提问人:Vedran Knezevic 提问时间:2/25/2023 最后编辑:Sujith KumarVedran Knezevic 更新时间:2/25/2023 访问量:415
如何在干净的架构模式中正确地将 API 调用与文件存储服务分开?
How to properly separate API calls from File Storage Service in a Clean Architecture Pattern?
问:
我正在处理一个 .NET Core 项目,我需要在其中实现干净的架构模式。任务是:
- 向第三方 API 服务发出 HTTP 请求
- 以 Stream 形式读取响应内容
- 将流内容保存到文件存储
为了解决这个问题,我在基础结构层创建了两个类:
- ApiService.cs
public async Task GetDataAsync(string url, Func<Stream, Task> func)
{
using (HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(url))
{
if (httpResponseMessage.IsSuccessStatusCode)
{
using (Stream stream = await httpResponseMessage.Content.ReadAsStreamAsync())
{
await func(stream);
}
}
}
}
- 文件存储服务.cs
public async Task CreateFileAsync(string path, Stream content)
{
using (FileStream fileStream = new(path, FileMode.Create))
{
await content.CopyToAsync(fileStream);
}
}
核心层中还有第三种方法,将上述两种方法组合并处理:
public async Task DownloadData(string url, string path)
{
await apiService.GetDataAsync(url, async stream =>
{
await fileStorageService.CreateFileAsync(path, stream);
});
}
尽管上述代码有效,但文件存储服务对 API 服务存在明显的直接依赖关系。我的期望是将文件和 API 服务完全分离,其中两个不需要相互了解。我希望将来能够选择实现流水线模式(如果需要),并能够以某种方式定义上述代码:
string url = "<some_url>";
Stream result = await apiService.GetDataAsync(url);
string path ="<some_path>";
await fileStorageService.CreateFileAsync(path, result);
我试图将 Func <>参数替换为 API 服务方法中的 Stream 返回类型,但我在正确处理 HttpResponseMessage 和 Stream 中的 using 语句时遇到了挑战。
如果您对如何解决上述问题有一些建议,我将不胜感激,以便:
- 明确地将 API 服务与文件服务分开,以便一个不知道另一个
- 正确释放 HttpResponseMessage 中的 Using 语句,并从 apiService.GetDataAsync 方法流,而不使用 Func(如果可能<>
提前致谢。
答:
我不确定你是否考虑过类路由,但我首选的处理方式是有一个模仿 UWP/Metro/etc. 从一开始就拥有的 and 框架。事实上,dotnet/runtime Github 存储库中有一个非常古老但仍然悬而未决的问题。StorageFile
StorageFolder
这个想法只是让抽象基类 - 、 及其父类 - 为要使用的每个存储平台实现,从本地文件系统到 Azure blob 再到其他任何平台。StorageFile
StorageFolder
StorageObject
这些是基本定义:
public abstract class StorageObject
{
public abstract string Name { get; }
public abstract Task RenameAsync();
public abstract Task<StorageProperties> GetPropertiesAsync(); // would retrieve metadata like creation/access dates, size, etc.
public abstract Task DeleteAsync();
public abstract Task CopyAsync (StorageFolder destination);
public abstract Task<IEnumerable<StorageObject>> GetChildrenAsync();
}
public abstract class StorageFolder : StorageObject
{
public abstract int Files { get; }
public abstract int Folders { get; }
public abstract Task<StorageFile> GetFileAsync (string filename, bool createIfNotExists);
public abstract Task<StorageFolder> GetSubfolderAsync (string subfolderName, bool createIfNotExists);
public virtual Task<IEnumerable<StorageFile>> FindFilesAsync (string criteria, bool includeSubfolders) { ... } // default implementation would use GetChildrenAsync but be overridable for more efficient implementations
public virtual Task<IEnumerable<StorageFolder>> FindSubfoldersAsync (string criteria, bool includeSubfolders) { ... } // same
}
public abstract class StorageFile : StorageObject
{
public abstract Task<Stream> OpenReadAsync(bool shared = false);
public abstract Task<Stream> OpenWriteAsync(bool shared = false);
}
因此,例如,一个基本的(不完整的)实现将如下所示:LocalStorageFile
public class LocalStorageFile : StorageFile
{
private readonly string _path;
public LocalStorageFile (string path)
{
_path = path;
}
public override Task<Stream> OpenReadAsync(bool shared = false)
{
var str = File.Open(_path, FileMode.Open, FileAccess.Read, shared ? FileShare.Read : FileShare.None);
return Task.FromResult(str);
}
// ...
}
然后你会这样消费它:
LocalStorageFile f = new LocalStorageFile("c:\\mypath\\file.bin");
using (var str = await f.OpenStreamForReadAsync())
{
// do your thing
}
最重要的是,代码的其余部分都引用了基类 - 等,因此与实现完全无关。通过处理另一个高度抽象和可扩展的类,除了它之外,你不需要做任何特别的事情。当然,如果你有高度专业化的东西,你也可以子类化并从 和 返回任何你想要的东西。StorageFile
Stream
Dispose
Stream
OpenReadAsync
OpenWriteAsync
评论
HttpResponseMessage
Stream
HttpResposeMessage
HttpResponseMessage
Stream
Dispose
在评论中讨论后,我认为问题在于流被 处置,然后抛出一个处置对象异常。apiService
fileStorageService
此问题的可能解决方案是删除 中的语句并返回流。using
apiService
public async Task<Stream> GetDataAsync(string url)
{
using (HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(url))
{
if (httpResponseMessage.IsSuccessStatusCode)
{
return await httpResponseMessage.Content.ReadAsStreamAsync();
}
}
}
然后,您可以通过以下方式使用它:
string url = "<some_url>";
Stream result = await apiService.GetDataAsync(url);
using(result)
{
string path ="<some_path>";
await fileStorageService.CreateFileAsync(path, result);
}
评论
HttpResponseMessage
评论
GetDataAsync()
GetDataAsync()