设计模式:消费REST API

4
我正在制作一个DLL来在aspnetcore中调用REST API。
理想情况下,我希望可以通过以下方式访问它:
API api = new API(clientInfo);
api.Module.Entity.Action(params);

但我很难将这变成现实。我无法创建任何静态内容,因为可能会同时存在多个会话实例。如果不通过引用传递会话,则会话状态(cookies等)可能会在副本中更改。是否应该使用某个设计模式?

public class API
{
    private Session _session;
    public API(ClientInfo clientInfo)
    {
        _session = new Session(clientInfo);
    }
}

会话作为客户端的中间件,用于存储登录数据以便在需要时重复登录,并处理一些错误/重试并公开客户端方法。
public class Session
{
    private Client _client;
    private string _path;
    public Session(ClientInfo clientInfo)
    {
        _client= new Client(clientInfo);
        _path = clientInfo.Path;
    }
    public HttpResponseMessage Get(string name, string arguments = "")
    {
        return _client.Get(_path, name, arguments);
    }
    ...
}

客户端实际上执行这些调用。
public class Client
{
    public HttpResponseMessage Get(string path, string endpointName, string arguments)
    {
        return GetClient().GetAsync(path + endpointName + arguments).Result;
    }
    private HttpClient GetClient(){...}
    ...
}

2
如果你依赖会话,那么你已经违反了REST的无状态约束。除此之外,C#已经有相当多的REST客户端库,例如[RestSharp](http://restsharp.org/)。在重复造轮子前,值得研究一下。 - Tobias Tengler
这是一个安全的API,因为它公开了业务数据。我可以在每个请求中执行登录(这会严重影响请求速度),或者存储会话cookie。该DLL可用于大量请求,例如导入合作伙伴数据。 - Christopher
2
你可以在请求中传递一个身份验证令牌(Authentication-Token)。这不会违反REST原则,因为您不再需要服务器端的会话(Session)。然而,这与您最初的回答脱轨了,只是想告诉您您正在做的事情从技术上讲不符合REST原则。 - Tobias Tengler
你为什么要在异步操作上使用同步操作呢?建议你“全程异步”。而且,仅仅使用“.Result”会引发麻烦。 - Fildor
2个回答

4

个人而言,我只是为我的API构建了一个简单的客户端,该客户端具有与API端点对应的方法:

最初的回答:

我通常会为我的API构建一个简单的客户端,其中的方法与API的端点相对应。

public class FooClient
{
    private readonly HttpClient _httpClient;

    public FooClient(HttpClient httpClient)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    }

    public async Task<GetFooResult> Get(int id)
    {
        ...
    }

    // etc
}

在Startup.cs中注册一个类型化客户端即可提供HttpClient依赖项:

最初的回答:

services.AddHttpClient<FooClient>(c =>
{
    // configure client
});

我添加了一个IServiceCollection扩展来封装此设置逻辑和其他任何设置逻辑: "最初的回答"
public static class IServiceCollectionExtensions
{
    public static IServiceCollection AddFooClient(this IServiceCollection services, string uri)
    {
        ...
    }
}

然后,在我的启动应用程序中,我只需要执行类似以下的操作:
services.AddFooClient(Configuration.GetValue<string>("FooUri"));

这对于通过Polly设置自动错误处理、重试策略等非常有帮助,因为您可以在扩展中仅设置一次所有配置。
现在,针对持久化诸如身份验证令牌之类的内容,您有几种可能性。我倾向于将身份验证令牌作为声明进行持久化,这样您就可以简单地检索声明并将其传递到需要它的客户端方法中:
var foo = await _fooClient.Get(fooId, User.FindFirstValue("FooAuthToken"));

如果您这样处理,可以将客户端绑定在任何作用域中,包括单例。
另一种方法是实际在客户端中保存认证令牌,但必须小心处理。除非您使用像ConcurrentDictionary这样的东西,否则绝对要避免使用单例作用域,即使使用它,确保始终使用正确的令牌可能有点棘手。
假设您使用请求作用域,直接将令牌存储为 ivar 或其他内容,但仍需要将其持久化到其他地方,否则每个请求仍然需要重新进行身份验证。如果您将其存储在会话中,那么可以执行以下操作:
services.AddScoped<FooClient>(p =>
{
    var httpClientFactory = p.GetRequiredService<IHttpClientFactory>();
    var httpContextAccessor = p.GetRequiredService<IHttpContextAccessor>();

    var httpClient = httpClientFactory.Create("ClientName");
    var session = httpContextAccessor.HttpContext.Session;

    var client = new FooClient(httpClient);
    client.SetAuthToken(session["FooAuthToken"]);
});

然而,即使如此,我仍然认为将授权令牌传递到方法中比执行任何其他操作更好。这样更明确哪些操作需要授权,哪些不需要,并且您总是知道来自何处的数据。

最初的回答:

将授权令牌传递到方法中比执行其他操作更好,因为这样更明确哪些操作需要授权,哪些不需要,并且您总是知道来自何处的数据。


1

你面临的最大问题之一将是HttpClient的重复使用。这是“pre-Core”时代的已知问题。幸运的是,它已经得到解决,截至Net Core 2.1,我们现在拥有一个HttpClientFactory,允许您自由创建和管理HttpClients,而它们将作为框架的一部分为您处理。

https://www.stevejgordon.co.uk/introduction-to-httpclientfactory-aspnetcore

考虑到这一点,你可以使用 DI 来注入 IHttpClientFactory,从而为你提供访问所需管道的必要途径。除此之外,如何设计代码来“管理”你对 REST 资源的访问完全取决于你自己。也许可以采用某种 Repository 模式?(其实只是猜测,因为不知道你的架构等具体情况)


每个请求都会实例化一个新的HttpClient。在这种情况下,我尽量避免使用DI,因为下一个程序员可能不希望以这种方式使用DLL。但是,使用DI会更加简洁。 - Christopher
1
如果类需要它,那么类就需要它。因此,它应该作为构造函数的依赖项。如何“注入”该依赖项取决于您。但是,为了促进可测试的代码,这是做事情的首选方式。 - Robert Perry
好的,你编辑了评论,改变了上下文。我指出 HttpClientFactory 是为了解决这个确切问题。以前注入 HttpClient 会引起麻烦。然而,HttpClientFactory 是专门为了帮助你解决这个问题而设计的。 - Robert Perry
谢谢,我会使用您提供的链接中的信息来优化我的HttpClient使用方式。 - Christopher

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接