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

问题描述 投票:0回答:2

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

  1. 向第三方 API 服务发出 HTTP 请求
  2. 以流的形式读取响应内容
  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);
            }
        }

Core Layer中还有第三种方法将以上两种方法结合处理:

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

我试图用 API 服务方法中的 Stream 返回类型替换 Func<> 参数,但我在正确处理 HttpResponseMessage 和 Stream 中的 using 语句方面面临挑战。

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

  1. API Service 和 File Service 明确分开,互不相识
  2. 正确处理 HttpResponseMessage 中的 Using 语句和来自 apiService.GetDataAsync 方法的 Stream,而不(如果可能)使用 Func<>

提前致谢。

c# .net .net-core clean-architecture using-statement
2个回答
0
投票

我不确定你是否考虑过类路线,但我更喜欢处理这个问题的方法是有一个

StorageFile
StorageFolder
框架来模仿 UWP/Metro/等。从一开始就有。事实上,dotnet/runtime Github repo 中有一个非常古老但仍然悬而未决的问题。

这个想法只是简单地拥有抽象基类——

StorageFile
StorageFolder
和它们的父类
StorageObject
——为你想要使用的每个存储平台实现,从本地文件系统到 Azure blob 到任何其他东西。

这些是基本定义:

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
返回任何你想要的东西。


0
投票

在评论中讨论后,我认为问题在于流由

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);
}
© www.soinside.com 2019 - 2024. All rights reserved.