NuGet软件包库上有一些用C#编写的LinkedIn客户端,但据我所知,大多数客户端仅通过LinkedIn进行身份验证。我发现一对夫妇提供了LinkedIn REST API接口。在这两个中,一个看起来好像已经不活动了(nuget pack甚至警告它是基本的)。另一个由Spring提供,具有非常宽的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));
        }
    }
}


请查看我的代码。如果您有任何问题,请询问。如果您有任何意见,请给他们。

评论

我真的找不到任何评论要评论。这是编写良好的代码,并且显然经过深思熟虑。

抱歉,您花了很长时间才找到问题的答案,我们当中有一大批人开始了Code Review的新起点,看来您在帮助该网站解决问题和发展方面可以提供很多帮助,请随时加入我们的第二台监视器,与我们交谈或引起您的疑问,

如果有人感兴趣,此代码仍位于我的github帐户中:github.com/danludwig/LinkedN

#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容器一起使用的类。您已经完成了一项出色的工作。说真的我想保持这样编写的代码!