Tuesday, November 24, 2015

Accessing MS CRM Dynamics preview web api (CRM 2015 Update 1)...scala, dispatch, adal4j, etc.

I previously wrote about authenticating a native client app written in java against a Azure AD in order to access MS CRM Dynamics.
Once you authenticate, you can pass around your access token (using the OAuth2 model) and issues REST requests. The API uses the ODATAv4 protocol. ODATA is essentially what ODBC was to desktop computers in the 90s, a way to access data that is expressed in an object graph. The v4 specification finally gets things right by allowing navigation with links directly embedded in the returned data enabling a more robust hypermedia navigation style.
The actual entire protocol is fairly well documented here on MSDN. MS seems serious about allowing the polyglut programming world to finally access the CRM data.
We want to test our authentication. To do that, we'll first ask WhoAmI using the approach that Jason laid out in his java version located here.
We won't use the two leading ODATA libraries quite yet. The ODATA libraries (Apache Olingo and SDL ODATA) both assume an execution model in addition to a content composition model. We'll just use the scala library dispatch for execution. It uses a well proven and robust async HTTP library underneath called async-http-client.
Picking up from the last post on AD authentication for MS CRM. You can continue to use amm for your scala repl:
load.ivy("com.ning" % "async-http-client" % "latest.release")
load.ivy("net.databinder.dispatch" %% "dispatch-core" % "latest.release")
import dispatch._, Defaults._
If you need to refresh your authentication token, rerun two lines:
val resultFuture = context.acquireToken(resource, clientid, "username@yourdomain.onmicrosoft.com", "password", null)
val t = resultFuture.get
t.getExpiresOnDate
Now we can issue a request using the dispatch library:
val headers = Map("OData-MaxVersion" -> "4.0",
  "OData-Version" -> "4.0",
  "Accept" -> "application/json",
  "Authorization" -> ("Bearer " + t.getAccessToken))

val whoAmIRequest = (host("yourdomain.crm.dynamics.com").secure / "api" / "data" / "WhoAmI") <:< headers
val response = Http(whoAmIRequest OK as.String)
You can chain a handler onto the future so that when it completes, you print out the returned value. Or you could just wait a second (wall clock time) and then run
response.print
The userid, as a GUID, is in the response body.
If you want to get rid of having to add the headers to every request, you can define a filter:
// Setup the headers so they are automatically added
import com.ning.http.client.filter._
case class MSCRMFilter(val token: String) extends RequestFilter {
  def filter[T](ctx: FilterContext[T]): FilterContext[T] = {
    val newRequest = new RequestBuilder(ctx.getRequest)
    Map("OData-MaxVersion" -> "4.0",
      "OData-Version" -> "4.0",
      "Accept" -> "application/json",
      "Authorization" -> ("Bearer " + token)).foreach{ case (k,v) => newRequest.addHeader(k,v)}
    new FilterContext.FilterContextBuilder(ctx).request(newRequest.build).build
  }
}
// Setup a new AD aware HTTP client
val adhttp = Http.configure(_.addRequestFilter(new MSCRMFilter(t.getAccessToken)))
While this does not automatically handle the token timeout, it does automatically add the OAuth2 and OData headers to every request that goes out. Now we can issue the request without adding the headers:
val whoAmIRequest2 = host("yourdomain.crm.dynamics.com").secure / "api" / "data" / "WhoAmI"
val response2 = adhttp(whoAmIRequest2 OK as.String)
// Be lazy and check response2.isCompleted to ensure it completes :-) then print the result
response2.print
You will get the same response as before.
To get a list of accounts, you can issue another OData like request:
val crmhost = host("yourdomain.crm.dynamics.com").secure // setup a reusable host value
val accountRequest = (crmhost / "api" / "data" / "accounts") <<? Map("$select" -> "name", "$top" -> "20")
val accounts = adhttp(accountRequest OK as.String)
accounts.print
Or obtain the contacts:
val contactsRequest = (crmhost / "api" / "data" / "contacts") <<? Map("$top" -> "20")
val contacts = adhttp(contactsRequest OK as.String)
accounts.print
But you'll notice that you will want to specify more query parameters in order to customize the ODATA response and not just obtain foreign keys as values for some af the properties, but the actual object content. You'll neeed to look at the ODATA documentation for details on how to pull more values from related entities in the same query.
In the above URLs we are violating good design. Because ODATAv4 follows a much more adherent design to REST, you can navigate from the initial metadata and entity list to any object without ever having to construct the actual URL path. You will need to add query parameters and more headers to cusomize the ODATA request, but you should never really have to spell out the paths like we did above.

No comments:

Post a Comment