report

Hosting Power BI Content - 3/6

In the last post we saw how to use the PBI .NET API to make REST calls to the Power BI cloud services and get access to account content metadata. In this post I'll focus on the State module. The full

October 21, 2017

In the last post we saw how to use the PBI .NET API to make REST calls to the Power BI cloud services and get access to account content metadata. In this post I'll focus on the State module. The full project is in github.

State. Why?

The state contains the relevant data in the application. All the data in this application have a common origin which is the Power BI cloud service (PBICS). So why do we need a state in our application?

Well, strictly speaking we do not need that. We could simply go to PBICS every time we need such a data element. Keeping a state though gives our application a better performance and availability. How? Let's see the execution flow in both scenarios.

Without state

  1. The first user navigates to our application url: our application (app) is started up. App has to serve the list of PBI workspaces and reports in them so it proceeds by:

    • Authenticating with Azure

    • Acquiring access to PBICS

    • Downloading Workspaces and Reports from PBICSNotice that to complete this we are going to the cloud three times. Finally, we have all the information we need. So we proceed composing the presentation with those data (app renders them as a bootstrap accordion) and deliver our content as response to the requesting browser.

  2. The user clicks on one of the hyperlinks in the page to see a report. We need to build a page that embeds that report. To do so we need to acquire an embed token for that report, so we interrogate PBICS again. So we repeat all the work done in step 1 and as additional step we ask PBICS for the embed token, so 4 cloud requests. With the embed token we can now render an embedding page for the user.

  3. A second user browses to our app. We need to give him the same page as in step 1, hence we again go to the cloud with 4 consecutive cloud roundtrips before being able to respond. Notice that this occurs for each new user browsing our app.

  4. This second user or any other user clicks on a report already seen by a different user. To render an embed page for that report we need to ask PBICS for an embed token, even if we did this just a few seconds ago. So we repeat 4 roundtrips.

With state

  1. The first user navigates to our application url: our application (app) is started up. As in the previous scenario we interrogate PBICS to get the same data, but once we have them and before rendering the page back to the browser, we save them in our internal state.

  2. The user clicks on one of the hyperlinks in the page to see a report. We ask PBICS for an embed token but we we use the cache saved in step 1 so only 1 rountrip is required. Moreover, before delivering the embed token back to the browser, we save it in the state together with its expiration time.

  3. A second user browses to our app. We need to give him the same page as in step 1, so we simply take our cache and reuse it to render the page. No roundtrips.

  4. This second user or any other user clicks on a report already seen by a different user. As the embed token has already been requested, we already have a cached embed token in our app memory. Hence, we check whether the cached token has expired, and if not we simply answer back with that, with no need to involve PBICS again.

Of course using this approach means that if the source PBICS account is changed (for example, a new report is added or an existing is changed), our app won't show the update until a restart. So we could add an automatic refresh of the state to be executed periodically and/or when the server is not too busy.

In my implementation, it's ok to refresh the state daily, as the changes are less frequent than that.

[code language="fsharp"] //State.fs Let's see some code then.

type Report = { name : string id: string groupId: string embedUrl : string mutable embedToken : (string * DateTime) option }

type Workspace = { name : string id : string reports : Map<string, Report> }

type Workspaces = Map<string, Workspace> let mutable private workspaces = Map.empty [/code]

We model the state as F# maps of records. This makes things easier when we receive a request from the browser. The request will in fact contain 2 strings, a group id and a report id; the maps are built using the ids as key.

[code language="fsharp"] let private group2Workspace gId (group:Group) = { name = group.group.Name id = gId reports = group.reports |> Map.map (fun _ r -> { name = r.report.Name id = r.report.Id groupId = gId embedUrl = r.report.EmbedUrl embedToken = None }) }

let internal refresh () = workspaces <- PowerBI.getGroups() |> Map.map group2Workspace //lastRefresh <- DateTime.Now

let internal getWorkspaces () = //if (DateTime.Now - lastRefresh) > TimeSpan.FromHours(24.) then refresh() if workspaces.IsEmpty then refresh() workspaces

let internal getEmbedToken gId rId = try if workspaces.ContainsKey(gId) && workspaces.[gId].reports.ContainsKey(rId) then let rpt = workspaces.[gId].reports.[rId] match rpt.embedToken with | Some (token, expiration) when (DateTime.UtcNow < expiration) -> Ok token | _ -> match PowerBI.getEmbedToken gId rId with | Ok (token, exp) -> rpt.embedToken <- Some (token, (if exp.HasValue then exp.Value else DateTime.UtcNow) ); Ok token | Error msg -> Error msg else Error "Report not Found" with x -> Error x.Message [/code] To be noted:

  • refresh() is called only at startup when the state (workspaces) is empty, or after 24 hours from the last refresh (commented)

  • initially the embed token is set to None. The embed token is materialized and cached only when a user requests to see the report. It would be too long to populate all the embed tokens with the state materialization, and also unnecessary.

  • getEmbedToken requests PowerBI for an embed token only if the token is not already in the state or it is expired

Summary

The State F# module maintains a cache of the important metadata needed to run the application. The rendering part of our app is then developed on top of the state. We can think to the state as an intermediate layer between the rendering and the data source. The state is responsible to manage the expensive cloud roundtrips and deliver valid metadata to the rendering logic. To do so State.fs leverages PowerBI.fs (previous post), that wraps the details of accessing PBICS into easier utility functions.

Conclusion

This post completes the data modeling/sourcing part of the application. In the next post we'll see the server side rendering logic, Main.fs. The Main module takes the data from the State and renders HTML markup using WebSharper sitelets and UI.Next.

Related articles