如何在干净的架构模式中正确地将 API 调用与文件存储服务分开?

How to properly separate API calls from File Storage Service in a Clean Architecture Pattern?

提问人:Vedran Knezevic 提问时间:2/25/2023 最后编辑:Sujith KumarVedran Knezevic 更新时间:2/25/2023 访问量:415

问:

我正在处理一个 .NET Core 项目,我需要在其中实现干净的架构模式。任务是:

  1. 向第三方 API 服务发出 HTTP 请求
  2. 以 Stream 形式读取响应内容
  3. 将流内容保存到文件存储

为了解决这个问题,我在基础结构层创建了两个类:

  1. 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);
                    }
                }
            }
        }
  1. 文件存储服务.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 语句时遇到了挑战。

如果您对如何解决上述问题有一些建议,我将不胜感激,以便:

  1. 明确地将 API 服务与文件服务分开,以便一个不知道另一个
  2. 正确释放 HttpResponseMessage 中的 Using 语句,并从 apiService.GetDataAsync 方法流,而不使用 Func(如果可能<>

提前致谢。

c# .net-core clean-architecture using-statement

评论

0赞 Chetan 2/25/2023
这些类不是通过接口抽象的吗?有了它们,你可以使它们松散耦合,而不依赖于实现。
0赞 Vedran Knezevic 2/25/2023
是的,我确实有在核心层定义并在基础设施层实现的这两个类的接口。我正在使用依赖注入,因此这两个类中的两个方法是通过 DI(接口)从第三个方法调用的。这些不依赖于实现,而是相互依赖,这是我试图解决的问题。
0赞 Selmir Aljic 2/25/2023
您能解释一下您在返回流和使用语句时面临的挑战吗?
0赞 Vedran Knezevic 2/25/2023
主要挑战是,如果您使用 return 关键字代替(在上面的情况下)Func<>您将在负责创建文件的第二种方法中处理对象。基本上,这里的目标是遵循 SRP(单一负责原则),其中一种方法负责处理 http 请求/响应,第二种方法负责您想对响应结果执行的任何操作(创建文件、保存到 Db、读取为字符串等)总之,有一个独立的流响应,它可以作为链中其他方法的参数,而没有 Func<>。
0赞 Selmir Aljic 2/25/2023
@VedranKnezevic 您正在处理异常,因为您正在方法中释放流,如果您想将流返回给调用方,则该方法不再负责释放它,并且应删除 using 语句。GetDataAsync()GetDataAsync()

答:

0赞 Emperor Eto 2/25/2023 #1

我不确定你是否考虑过类路由,但我首选的处理方式是有一个模仿 UWP/Metro/etc. 从一开始就拥有的 and 框架。事实上,dotnet/runtime Github 存储库中有一个非常古老但仍然悬而未决的问题。StorageFileStorageFolder

这个想法只是让抽象基类 - 、 及其父类 - 为要使用的每个存储平台实现,从本地文件系统到 Azure blob 再到其他任何平台。StorageFileStorageFolderStorageObject

这些是基本定义:

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
}

最重要的是,代码的其余部分都引用了基类 - 等,因此与实现完全无关。通过处理另一个高度抽象和可扩展的类,除了它之外,你不需要做任何特别的事情。当然,如果你有高度专业化的东西,你也可以子类化并从 和 返回任何你想要的东西。StorageFileStreamDisposeStreamOpenReadAsyncOpenWriteAsync

评论

0赞 Vedran Knezevic 2/25/2023
我不确定这是否回答了这里的主要挑战。我已经通过接口抽象了 FileService 类(本地存储、azure blob 等)。请查看我对上面 Selmir Aljic 问题的回答。
0赞 Emperor Eto 2/25/2023
对不起,我想我有点看不出你的确切问题是什么。但是,如果具体来说是处理 的问题,类模式仍然可以帮助您解决这个问题,因为您可以对 子类 包装 及其相应的读取方法,然后在子类化的方法中处理 。也许这有点过度设计,但它导致了一个很好的干净模式。HttpResponseMessageStreamHttpResposeMessageHttpResponseMessageStreamDispose
0赞 Selmir Aljic 2/25/2023 #2

在评论中讨论后,我认为问题在于流被 处置,然后抛出一个处置对象异常。apiServicefileStorageService

此问题的可能解决方案是删除 中的语句并返回流。usingapiService

    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);
}

评论

0赞 Emperor Eto 2/25/2023
您确定在释放后可以从中读取流吗?我不确定这两种方式,但我的猜测不是。HttpResponseMessage
0赞 Vedran Knezevic 2/25/2023
在这种情况下,我期望收到“无法访问已关闭的流”异常?
0赞 Selmir Aljic 2/25/2023
我还没有测试过它,但我的猜测是它应该可以正常工作,如果没有,那么有多种解决方案可以解决在处理流时需要处置流和 HttpResponse 的问题,但如果不需要,我们不要朝那个方向前进:))
0赞 Vedran Knezevic 2/25/2023
正如@JustAnswertheQuestion所指出的,它不应该完全因为已释放的 HttpResponseMessage 而工作。我不喜欢这种 Func <>方法,但我也同意 HttpResponseMessage Wrapper 对于这种情况可能有点矫枉过正。由于此内容为 <300 KB,内存不是问题,如果没有其他建议,我可能会将响应作为一个整体(字符串、字节、内存流等)读取,然后决定如何处理它:)
0赞 Selmir Aljic 2/25/2023
@VedranKnezevic如果是这种情况,那么您确实可以将流复制到内存流并返回该流,或者在流被释放时使用 RefCountDisposable 之类的东西来释放响应和流。但老实说,我认为您目前的方法没有问题,这两种服务之间没有直接的依赖关系。另一种方法是内存总线 IObservable<Stream>如果要分离流的读取和写入