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?
- The
IConsumeLinkedInApi.ReceiveUserAuthorization(User)
could store the token itself, by constructor injecting it into the implementation class. - The developer invoking
IConsumeLinkedInApi.ReceiveUserAuthorization(User)
could maintain his own implementation ofIStoreLinkedInTokens
. 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":
- Has an implementation of
IAuthenticateLinkedInApp
that stores the consumer key & secret asappSettings
in the config file, using the keys"LinkedInRestV1OAuth1aApiKey"
and"LinkedInRestV1OAuth1aSecretKey"
. - Has an implementation of
IStoreLinkedInSecrets
that uses an XML file stored in theApp_Data
directory. - Has an implementation of
IStoreLinkedInTokens
that uses cookies to set & get the token on the browser. - Has an implementation of
IServiceProvider
used to scan the assembly for implementations of the genericIProvideLinkedInEndpoint<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 RequestBag
s 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.