28
\$\begingroup\$

There is a handful of LinkedIn clients written in C# on the NuGet package library, but as far as I can tell, most only do authentication via LinkedIn. I found a couple which offer a LinkedIn REST API interface. Of those two, one looks like it has gone inactive (the nuget pack even warns that it is elementary). The other one, by Spring, has a very wide IProfileOperations interface. I wondered what a more fluent interface would look like, so I tried my hand at rolling one over the weekend.

I've already written a couple of API clients (all together in one library), but this is my first one with OAuth. There are many more plugin points for this, and so my client's API is a little more complex. I am using DotNetOpenAuth in the background, but trying to hide that as an implementation detail (as much as possible anyway).

Here are my goals:

  • A default implementation that "just works" out of the box.
  • Ability to customize the default implementation either through an IoC container or by overriding classes and implementing interfaces.
    • The above should be accomplished using a reasonable level of granularity for swapping out different aspects of the implementation. Meaning, allow customization with the least number of interfaces.
  • A "fluent" API to consume LinkedIn data.
  • Only 1 or 2 classes / interfaces to work with in client code. By that I mean the starting interface, where you new something up and use IntelliSense to discover your options.

Let's start with the oauth consumer key and secret. It seems the most logical place for this is in the *.config file, but someone might want to get it from a database to make it easier to set. If you have different app names under a single config, you could also implement this as a factory that retrieves the appropriate consumer key & secret for the app name you want LinkedIn to display to the user. I based the property names on the actual fields from the LinkedIn developer form:

namespace LinkedN
{
    // wraps the oauth consumer key and secret
    public interface IAuthenticateLinkedInApp
    {
        string ApiKey { get; }
        string SecretKey { get; }
    }
}

DotNetOpenAuth does this in its IConsumerTokenManager interface, which is great, but is mixed with another concern: It contains methods for creating, expiring, and getting access token secrets. I'm not really sure whether these two need to be together. Should you store your access secrets and tokens along with the consumer key? Currently I am not, guess I'm hoping that the oauth people thought of this and make all tokens unique across all consumer keys. Either way, just in case, and to make this easier to wrap in a class that implements DNOA's IConsumerTokenManager, I have added the previous interface as a property of this one:

namespace LinkedN
{
    public interface IStoreLinkedInSecrets
    {
        IAuthenticateLinkedInApp AppCredentials { get; }
        void Create(string token, string secret);
        void Expire(string token);
        string Get(string token);
    }
}

But These methods require you to already know the token. Tokens are obtained per user, so care should be taken to make sure we send the correct token when sending an authorized request. For this I have implemented a separate interface just for storing tokens. The default implementation I have now stores it in an HTTP Cookie, so the IPrincipal arguments are ignored. But an application-level implementation of this could be configured to get this from session, database, cache, or whatever, as long as the accessed collection can be keyed on IPrincipal.Identity.Name:

namespace LinkedN
{
    public interface IStoreLinkedInTokens
    {
        void Create(IPrincipal principal, string token);
        void Expire(IPrincipal principal);
        string Get(IPrincipal principal);
    }
}

The previous 3 interfaces all provide infrastructure concerns for connecting to LinkedIn via oauth. In the process, you basically use the consumer key & secret (IAuthenticateLinkedInApp concerns) to push the user over to LinkedIn and get them asked if they want to allow access to your app. After user says yes, LinkedIn pushes the user back to your app with an authorization token that is unique to them (IStoreLinkedInTokens concerns, separate from consumer key and secret). I know I've skipped over a lot of stuff that happens behind the scenes, but that's the point. I only really need to do 2 things in order to get started:

namespace LinkedN
{
    public interface IConsumeLinkedInApi
    {
        void RequestUserAuthorization(LinkedInAuthorizationRequest request);
        LinkedInAuthorizationResponse ReceiveUserAuthorization(IPrincipal principal);
        // ... there is actually another method here, more on that later
    }
}

The first method is what pushes the user to linkedin. There's nothing special about the request arg, it's basically just a wrapper for the URL you want the user pushed back to after they authorize @linkedin. What I did struggle with a bit is the second method, which is used at the endpoint where linked in pushes the user back to the app. That is the point where you can obtain the access token (WITHOUT the secret). The LinkedInAuthorizationResponse class is basically just a wrapper for the token and the "extra data" key/value pairs, but again, it does not expose the user's secret. Secrets are stored separately by the IStoreLinkedInSecrets implementation, and its use is encapsulated out of the client's scope.

What I struggled with was the fact that there are 2 ways you can design the client, and 2 ways to consume it. The second method in the IConsumeLinkedInApi doesn't say anything about how the token gets into the IStoreLinkedInTokens implementation. Is one of these more correct than the other and, of so, why?

  1. The IConsumeLinkedInApi.ReceiveUserAuthorization(User) could store the token itself, by constructor injecting it into the implementation class.
  2. The developer invoking IConsumeLinkedInApi.ReceiveUserAuthorization(User) could maintain his own implementation of IStoreLinkedInTokens. This would mean he may have to have constructor inject / new up / ultimately work with TWO interface instances.

For now I am dealing with it in both ways over the remaineder of the interface:

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

Here you can see the 3rd method on the primary API interface along with the 5th and final interface I have created for the project. I have 2 overloads of RequestUsing because of the struggle I mentioned above. If a developer wants control over the IStoreLinkedInTokens implementation, they can use it to pass a string token directly to send the endpoint request. Otherwise, they can pass an IPrincipal like the Controller.User property in MVC, Thread.CurrentPrincipal, or whatever. They can then expect the RequestUsing method to internally use the IPrincipal as an argument to an encapsulated IStoreLinkedInTokens instance.

The fluent code methods for accessing the API are all done with extension methods. Basically there are extension methods that use the RequestBag to configure the api call details. Here are some example extension methods for the PersonProfile endpoint provider implementation:

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

There are other extension methods that do the same thing, setting values in the RequestBag. The IProvideLinkedInEndpoint implementation then uses those settings to configure the REST URL and HTTP headers, which is why it's not meant to be swapped like the other 4 interfaces are. Ultimately you can end up with a client that looks like this:

var linkedN = new DefaultLinkedInClient();
var personProfile = linkedN.Resource<PersonProfile>
    .Myself()
    .SelectFields(
        PersonProfileFieldSelector.FirstName,
        PersonProfileFieldSelector.LastName,
        PersonProfileFieldSelector.Educations
    )
    .InLanguages("en-US", "es-ES", "zh-CN")
    .RequestUsing(User)
;

The granularity of the SelectFields extension may be a little too much. This can be overloaded to map certain collections of fields though. The default client does the following to "work out of the box":

  1. Has an implementation of IAuthenticateLinkedInApp that stores the consumer key & secret as appSettings in the config file, using the keys "LinkedInRestV1OAuth1aApiKey" and "LinkedInRestV1OAuth1aSecretKey".
  2. Has an implementation of IStoreLinkedInSecrets that uses an XML file stored in the App_Data directory.
  3. Has an implementation of IStoreLinkedInTokens that uses cookies to set & get the token on the browser.
  4. Has an implementation of IServiceProvider used to scan the assembly for implementations of the generic IProvideLinkedInEndpoint<TResource>. This is needed when an IoC container is absent.

When using an IoC, developers can opt for the non-default LinkedInClient class. It uses constructor injection to resolve the above dependencies. In fact, DefaultLinkedInClient mentioned above just extends LinkedInClient and passes in custom args to its base constructor.

public class MyController : Controller
{
    // register IConsumeLinkedInApi to resolve to LinkedInClient instance,
    // NOT DefaultLinkedInClient instance, in IoC container
    private readonly IConsumeLinkedInApi _linkedN;

    public MyController(IConsumeLinkedInApi linkedN)
    {
        _linkedN = linkedN;
    }

    public ActionResult Consume()
    {
        var personProfile = _linkedN.MemberId("youknowthis").RequestUsing(User);
    }
}

Everything else is just plumbing. The IProvideLinkedInEndpoint classes internally use their RequestBags to build URL's and DNOA to send requests / receive responses. Internally they use ?format=json and convert the response string to a POCO using DataContactJsonSerializer.

Going back to the concern of how to consume the API, hopefully this should illustrate my major concern with this client interface:

// how best to design this interface?
public class LinkedInController : Controller
{
    private readonly IConsumeLinkedInApi _linkedN;
    //private readonly IStoreLinkedInTokens _tokens;

    public LinkedInController(IConsumeLinkedInApi linkedN
        // , IStoreLinkedInTokens tokens
    )
    {
        _linkedN = linkedN;
        //_tokens = tokens;
    }

    public ActionResult RequestUserAuthorization()
    {
        var callback = Url.Action("ReceiveUserAuthorization");
        _linkedN.RequestUserAuthorization(
            new LinkedInAuthorizatonRequest(Request.Url, callback));
        return View();
    }

    public ActionResult ReceiveUserAuthorization()
    {
        var response = _linkedN.ReceiveUserAuthorization();
        // _tokens.Create(response.Token);
        return View();
    }

    public ActionResult Resource()
    {
        // this implementation assumes the client is smart & can find the token
        // by the IPrincipal.Identity.Name.
        var resource = _linkedN.Resource<PersonProfile>.Myself().RequestUsing(User);

        //// this implementation assumes the client is dumb or not trustworthy, and 
        //// that token storage should be controlled separately from API calls.
        // var token = _tokens.Get(User);
        // var token = "test token i got from cookie, session, cache, wherever";
        // var resource = _linkedN.Resource<PersonProfile>.Myself()
        //     .RequestUsing(token);
        return View();
    }
}

Please review my code. If you have any questions, please ask. If you have any comments, please give them.

\$\endgroup\$
3
  • 5
    \$\begingroup\$ I really can't find anything to comment on in a review. This is well written code, and obviously well thought out. \$\endgroup\$ May 16, 2014 at 21:53
  • \$\begingroup\$ Sorry it took so long for you to get an answer to your question, there is a large group of us starting a new beginning for Code Review, it looks like you have plenty to offer in terms of helping this site grow with questions and answers, please feel free to join us in The Second Monitor either to converse with us or bring attention your questions \$\endgroup\$
    – Malachi
    Jul 8, 2014 at 13:48
  • 1
    \$\begingroup\$ If anyone is interested, this code is still sitting in my github account: github.com/danludwig/LinkedN \$\endgroup\$
    – danludwig
    Jul 8, 2014 at 15:19

1 Answer 1

15
\$\begingroup\$

As @Jeff pointed out, this code is ...beautiful. Well done!


I like the fluent interface a lot, but the code itself is at first glance... wow.

  • Naming

    • All identifiers follow conventional casing (camelCase for locals and parameters, PascalCase for types and their members).
    • I like that you're using an _underscore prefix for field names, but that's just my opinion - you're perfectly consistent about it, and that is an objective fact.
    • Every single name is meaningful.
    • Method names start with a verb, the presented interfaces are highly cohesive, focused.
  • Comments

    • If there's an opportunity in your code, it has be about comments. Instead of this:

      // wraps the oauth consumer key and secret
      public interface IAuthenticateLinkedInApp
      

      You could have a <summary> XML comment and IntelliSense would pick it up, and people writing the client code would certainly appreciate - it's the difference between this:

      interface CR.Sandbox.Program.IFoo

      And that:

      interface CR.Sandbox.Program.IFoo / Does something foo.

      If you write your XML comments the same way you crafted your code, having XML documentation on all public members would be the icing on the cake. And if you really flesh it up, you can have the build process generate an XML file for you, with all the documentation in your project - then you can use 3rd-party tools to generate a whole website around that documentation, MSDN-style if you like.

  • Exceptions

    • I like that the ThrowExceptionWhenSettingIdentificationTwice extension is throwing an InvalidOperationException, it's a perfectly appropriate exception to be thrown in these circumstances. Again, XML comments would be a nice addition:

      /// <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.");
          }
      }
      
    • Throwing your own custom OptionAlreadySetException would allow you to turn this:

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

      Into that:

      var option = endpoint.GetOption(PersonProfileRequestOption.Identification);
      if (!string.IsNullOrWhiteSpace(option))
          throw new OptionAlreadySetException(option);
      

Your code looks as SOLID as can be. Being a fan of Dependency Injection, I like that you've implemented a class for use with an IoC container. You've done a ...solid job. Seriously. I want to be maintaining code written like that!

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.