IProfileOperations
接口。我想知道什么样的接口会更流畅,所以我在周末尝试了一个滚动接口。这是我第一次使用OAuth。与此相关的插件点很多,因此我客户的API稍微复杂一点。我在后台使用DotNetOpenAuth
,但尝试将其隐藏为实现细节(无论如何尽可能)。这是我的目标:
可以直接使用的默认实现。
可以通过IoC容器或通过重写类和实现接口来自定义默认实现。
以上内容应使用交换出实施的不同方面的合理粒度。意思是,允许使用最少的接口进行定制。
使用“流利”的API来使用LinkedIn数据。
在客户端代码中只能使用1或2个类/接口。 。我的意思是启动界面,在这里您可以进行一些新操作并使用IntelliSense来发现您的选择。
让我们从oauth使用者密钥和机密开始。似乎最合乎逻辑的地方是
*.config
文件中,但有人可能希望从数据库中获取它以使其更容易设置。如果您在单个配置下具有不同的应用程序名称,则也可以将其实现为工厂,该工厂检索要向LinkedIn显示给用户的应用程序名称的相应使用者密钥和秘密。我将属性名称基于LinkedIn开发人员表单中的实际字段: :它包含用于创建,过期和获取访问令牌机密的方法。我不确定这两个是否需要在一起。您是否应该将访问密钥和令牌与使用者密钥一起存储?目前我还不是,我想是希望oauth人们想到这一点,并使所有令牌在所有使用者密钥中都是唯一的。两种方法,以防万一,并使其更容易包装在实现DNOA的DotNetOpenAuth
的类中,我添加了以前的接口作为该属性的属性: />但是这些方法要求您已经知道令牌。令牌是按用户获取的,因此请务必确保在发送授权请求时我们发送正确的令牌。为此,我实现了一个单独的接口,仅用于存储令牌。我现在将默认实现存储在HTTP Cookie中,因此忽略了IConsumerTokenManager
参数。但是,只要将访问的集合可以在IConsumerTokenManager
上键入密钥,就可以将其配置为从会话,数据库,高速缓存等中获取此应用程序级别的实现: br />前面的3个接口都为通过oauth连接到LinkedIn提供了基础结构方面的考虑。在此过程中,您基本上使用了使用者密钥和秘密(请关注IPrincipal
)将用户推送到LinkedIn,并询问他们是否要允许访问您的应用程序。在用户说“是”之后,LinkedIn将使用对他们唯一的授权令牌将用户推回您的应用程序(请注意,与消费者密钥和机密分开)。我知道我已经跳过了幕后发生的很多事情,但这就是重点。我真的只需要做两件事就可以开始使用:namespace LinkedN
{
// wraps the oauth consumer key and secret
public interface IAuthenticateLinkedInApp
{
string ApiKey { get; }
string SecretKey { get; }
}
}
第一种方法是促使用户进入Linkedin的方法。请求arg没有什么特别的,它基本上只是您希望用户在授权@linkedin之后将其推回的URL的包装。我稍作努力的是第二种方法,该方法在端点处使用,该端点中的链接将用户推回应用程序。那就是您可以获取访问令牌(没有密码)的地方。
IPrincipal.Identity.Name
类基本上只是令牌和“额外数据”键/值对的包装,但同样,它不会暴露用户的秘密。秘密由IAuthenticateLinkedInApp
实现单独存储,其用法封装在客户端的范围之外。我苦苦挣扎的是,您可以通过两种方式设计客户端,而通过两种方式来设计客户端消耗它。
IStoreLinkedInTokens
中的第二种方法没有说明令牌如何进入LinkedInAuthorizationResponse
实现。这些中的一个比另一个更正确吗,为什么?为什么?调用IStoreLinkedInSecrets
的开发人员可以维护自己的IConsumeLinkedInApi
实现。这意味着他可能必须让构造函数注入/新建/最终使用两个接口实例。现在,我在接口的其余部分上以两种方式处理它:
namespace LinkedN
{
public interface IStoreLinkedInSecrets
{
IAuthenticateLinkedInApp AppCredentials { get; }
void Create(string token, string secret);
void Expire(string token);
string Get(string token);
}
}
在这里,您可以在主API接口上看到第3种方法以及第5种方法和我为该项目创建的最终接口。由于上述困难,我有2个
IStoreLinkedInTokens
重载。如果开发人员希望控制IConsumeLinkedInApi.ReceiveUserAuthorization(User)
实现,则可以使用它直接传递字符串令牌来发送端点请求。否则,他们可以传递一个IConsumeLinkedInApi.ReceiveUserAuthorization(User)
,例如MVC中的IStoreLinkedInTokens
属性,RequestUsing
或其他。然后,他们可以期望IStoreLinkedInTokens
方法在内部将IPrincipal
用作封装的Controller.User
实例的参数。访问API的流利的代码方法都是通过扩展方法来完成的。基本上,有一些扩展方法使用
Thread.CurrentPrincipal
来配置api调用详细信息。以下是RequestUsing
端点提供程序实现的一些扩展方法示例:namespace LinkedN
{
public interface IStoreLinkedInTokens
{
void Create(IPrincipal principal, string token);
void Expire(IPrincipal principal);
string Get(IPrincipal principal);
}
}
还有其他扩展方法也可以执行相同的操作,即在
IPrincipal
中设置值。然后,IStoreLinkedInTokens
实现使用这些设置来配置REST URL和HTTP标头,这就是为什么它不像其他4个接口那样被交换的原因。最终,您可以得到一个如下所示的客户端:namespace LinkedN
{
public interface IConsumeLinkedInApi
{
void RequestUserAuthorization(LinkedInAuthorizationRequest request);
LinkedInAuthorizationResponse ReceiveUserAuthorization(IPrincipal principal);
// ... there is actually another method here, more on that later
}
}
RequestBag
扩展的粒度可能有点太大。可以重载来映射某些字段集合。默认客户端执行以下操作以“开箱即用”:具有PersonProfile
的实现,使用密钥RequestBag
将使用者密钥和机密存储为IProvideLinkedInEndpoint
在配置文件中和SelectFields
。具有使用存储在
IAuthenticateLinkedInApp
目录中的XML文件的appSettings
的实现。具有
"LinkedInRestV1OAuth1aApiKey"
的实现,该实现使用cookie来设置和获取浏览器上的令牌。具有
"LinkedInRestV1OAuth1aSecretKey"
的实现,用于扫描程序集以查找通用IStoreLinkedInSecrets
的实现。 使用IoC时,开发人员可以选择非默认的
App_Data
类。它使用构造函数注入来解决上述依赖性。实际上,上面提到的IStoreLinkedInTokens
只是扩展了IServiceProvider
并将自定义args传递给它的基本构造函数。namespace LinkedN
{
public interface IConsumeLinkedInApi
{
// you already saw the other 2 methods above
IProvideLinkedInEndpoint<TResource> Resource<TResource>();
}
public interface IProvideLinkedInEndpoint<out TResource>
{
IDictionary<Enum, string> RequestBag { get; }
TResource RequestUsing(IPrincipal principal);
TResource RequestUsing(string token);
}
}
其他所有东西都只是管道。
IProvideLinkedInEndpoint<TResource>
类在内部使用其LinkedInClient
来构建URL和DNOA来发送请求/接收响应。在内部,他们使用DefaultLinkedInClient
并使用LinkedInClient
将响应字符串转换为POCO。回到有关如何使用API的问题,希望这可以说明我对该客户端接口的主要关注:
namespace LinkedN
{
public static class PersonProfileEndpointExtensions
{
public static IProvideLinkedInEndpoint<PersonProfile> Myself(
this IProvideLinkedInEndpoint<PersonProfile> endpoint)
{
endpoint.ThrowExceptionWhenSettingIdentificationTwice();
endpoint.SetOption(PersonProfileRequestOption.Identification, "~");
return endpoint;
}
public static IProvideLinkedInEndpoint<PersonProfile> MemberId(
this IProvideLinkedInEndpoint<PersonProfile> endpoint, string memberId)
{
endpoint.ThrowExceptionWhenSettingIdentificationTwice();
endpoint.SetOption(PersonProfileRequestOption.Identification,
string.Format("id={0}", memberId));
return endpoint;
}
public static IProvideLinkedInEndpoint<PersonProfile> MemberUrl(
this IProvideLinkedInEndpoint<PersonProfile> endpoint, string memberUrl)
{
endpoint.ThrowExceptionWhenSettingIdentificationTwice();
endpoint.SetOption(PersonProfileRequestOption.Identification,
string.Format("url={0}", memberUrl));
return endpoint;
}
internal static string GetOption(
this IProvideLinkedInEndpoint<PersonProfile> endpoint,
PersonProfileRequestOption option)
{
return endpoint.RequestBag.ContainsKey(option)
? endpoint.RequestBag[option] : null;
}
private static void SetOption(
this IProvideLinkedInEndpoint<PersonProfile> endpoint,
PersonProfileRequestOption option, string value)
{
endpoint.RequestBag[option] = value;
}
private static void ThrowExceptionWhenSettingIdentificationTwice(
this IProvideLinkedInEndpoint<PersonProfile> endpoint)
{
var option = endpoint.GetOption(PersonProfileRequestOption.Identification);
if (!string.IsNullOrWhiteSpace(option))
throw new InvalidOperationException(string.Format(
"The person profile endpoint has already been configured to " +
"identify resources for '{0}'.", option));
}
}
}
请查看我的代码。如果您有任何问题,请询问。如果您有任何意见,请给他们。
#1 楼
正如@Jeff指出的那样,这段代码是……美丽的。干得好!我很喜欢流畅的界面,但是代码本身乍一看...哇。
/>命名
所有标识符都遵循常规的大小写(对于局部变量和参数,请使用
camelCase
;对于类型及其成员,请使用PascalCase
)。 我喜欢您为字段名使用
_underscore
前缀,但这只是我的意见-您对此完全一致,这是一个客观事实。每个名称都是有意义的。 br />方法名称以动词开头,呈现的接口具有高度的内聚性和重点。 />如果您的代码中有机会,那就与注释有关。而不是这样:
// wraps the oauth consumer key and secret
public interface IAuthenticateLinkedInApp
您可能会有一个
<summary>
XML注释,而IntelliSense会选择它,并且编写客户端代码的人当然会喜欢-这之间的区别:那么:
如果您以编写自己的方式编写XML注释代码,所有公共成员都拥有XML文档将是锦上添花。并且,如果您真的对其进行了充实,则可以让构建过程为您生成XML文件,并在项目中包含所有文档-然后,您可以使用3rd-party工具围绕该文档生成整个网站,如果是MSDN风格的你喜欢。
异常
我喜欢
ThrowExceptionWhenSettingIdentificationTwice
扩展名抛出InvalidOperationException
,在这种情况下抛出异常是非常合适的。再次,XML注释将是一个不错的补充: > /// <summary>
/// Does something foo.
/// </summary>
/// <param name="foo">Any foo.</param>
/// <exception cref="InvalidOperationException">
/// Thrown when "bar" is specified for <c>foo</c>.
/// </exception>
public void DoSomethingFoo(string foo)
{
if (foo == "bar")
{
throw new InvalidOperationException("Invalid foo.");
}
}
其中的内容:
var option = endpoint.GetOption(PersonProfileRequestOption.Identification);
if (!string.IsNullOrWhiteSpace(option))
throw new InvalidOperationException(string.Format(
"The person profile endpoint has already been configured to " +
"identify resources for '{0}'.", option));
您的代码看起来像SOLID一样。作为依赖注入的狂热者,我喜欢您实现了一个与IoC容器一起使用的类。您已经完成了一项出色的工作。说真的我想保持这样编写的代码!
评论
我真的找不到任何评论要评论。这是编写良好的代码,并且显然经过深思熟虑。抱歉,您花了很长时间才找到问题的答案,我们当中有一大批人开始了Code Review的新起点,看来您在帮助该网站解决问题和发展方面可以提供很多帮助,请随时加入我们的第二台监视器,与我们交谈或引起您的疑问,
如果有人感兴趣,此代码仍位于我的github帐户中:github.com/danludwig/LinkedN