Update all SSRS reports to apply consistent header / footer on demand

Teaching good old SSRS a new trick…

Background

Not too long ago I had a client with a very simple, yet practical requirement: they wanted all of their SSRS reports to have standard headers and footers. Nothing ground breaking there, however, the twist that they put on this requirement was a very useful one in that they wanted to use a single template file to apply the headers and footers. Even more, they wanted this template to be easily updatable, meaning any update to the template would apply the changes to all reports simultaneously. Ah, now we’re on to something more interesting.

The original solution that I developed with a colleague to meet this requirement was based on local report definitions (.rdlc – or SSRS without the reportserver), but I thought it would be interesting, and hopefully helpful to others to offer a solution that could be applied against the full version of SSRS (stand alone and SharePoint Integrated). So, here we go…

Overview

A few points of information about SSRS architecture before we get to specifics (for those who might not already be familiar). The .rdl files that define report definitions are generally authored in either visual studio or report builder. These files are actually xml documents that reference a published schema provided by Microsoft (for SSRS 2012 and 2014, the schema is Report Definition Language (RDL) 2010: http://schemas.microsoft.com/sqlserver/reporting/2010/01/reportdefinition/

SSRS also offers a robust API… and even better for the less .Net savvy such as myself, SSRS comes complete with a command line executable utility that will compile and run a “Visual Basic .NET code file, written against a proxy that is built on Web Service Description Language (WSDL), which defines the Reporting Services SOAP API”. This is called the rs Utility, or (rs.exe): http://msdn.microsoft.com/en-us/library/ms157252(v=sql.110).aspx

In the past, I have only used the rs Utility for scripting deployments and backups; but, why not use it to update report definitions? It already exposes the necessary web service method: SetItemDefinition.

Solution

The implementation is actually fairly straight forward (after I racked my brain for a couple of days). As I said before, the original solution ran within a custom app that implemented LocalReport. We simply updated the appropriate .rdlc XML elements at run time against the defined template document. Transposing to the full report server, we just need to identify the template document on the server, then apply the appropriate XML elements to all of the report files in a loop instead.

A couple of things to be aware of about your template and report .rdl files.

1) Make sure all of the items in your Custom.rdl file are named creatively, as in something that a report developer wouldn’t normally use. When the script runs, it will replace items in the header and footer with your report items in the template. SSRS requires that all items within a report definition have unique names, otherwise the definition is not valid. So, If you have a textbox in your template named Textbox1 and you try update all of your report files, there’s an excellent chance that you’ll end up with the following error:

rs exec header error

I suggest naming each report item in the header like “CustomHeaderImageLeft20141114”.

2) Don’t use an embedded image in your template file. This is because the script just won’t work If you do, but also, if you’re applying the image to all of your report definitions then you’re better off using an external reference image anyways. The image itself can just be uploaded to your report server.

So, here’s the code for the script file (.rss):

'Script to apply custom header and footer from Custom.rdl file saved to ReportServer to all report files on ReportServer
' uses Mgmt2010 endpoint / executable against stand alone or SharePoint integrated instance
' Save file as .rss extension and run using rs.exe from command line.
'Authored by Jared Zagelbaum 11/2014    jaredzagelbaum.wordpress.com
Sub Main()
Dim reportDefinition As Byte() = Nothing
Dim doc As New System.Xml.XmlDocument
Dim nsmanager As New XmlNamespaceManager(doc.NameTable)
Dim templateHeader As System.Xml.XmlElement
Dim templateFooter As System.Xml.XmlElement
Dim reportHeader As System.Xml.XmlElement
Dim reportFooter As System.Xml.XmlElement
nsmanager.AddNamespace("rd", "http://schemas.microsoft.com/sqlserver/reporting/2010/01/reportdefinition")
Dim items As CatalogItem() = rs.ListChildren("/", True)

'find template file
For Each item As CatalogItem In items
If item.TypeName = "Report" And item.Name = "Custom" Then
reportDefinition = rs.GetItemDefinition(item.Path)
Dim stream As New MemoryStream(reportDefinition)
doc.Load(stream)
Exit For
End If
Next

'load template header / footer into var
templateHeader = doc.SelectSingleNode("/rd:Report/rd:ReportSections/rd:ReportSection/rd:Page/rd:PageHeader", nsmanager)
templateFooter = doc.SelectSingleNode("/rd:Report/rd:ReportSections/rd:ReportSection/rd:Page/rd:PageFooter", nsmanager)

'iterate through catalog items and replace report headers with template
For Each item As CatalogItem In items
If item.TypeName = "Report" And item.Name <> "Custom" Then
reportDefinition = rs.GetItemDefinition(item.Path)
Dim stream As New MemoryStream(reportDefinition)
Dim outstream As New MemoryStream()
doc.Load(stream)
reportHeader = doc.SelectSingleNode("/rd:Report/rd:ReportSections/rd:ReportSection/rd:Page/rd:PageHeader", nsmanager)
reportFooter = doc.SelectSingleNode("/rd:Report/rd:ReportSections/rd:ReportSection/rd:Page/rd:PageFooter", nsmanager)
reportHeader.InnerXml = templateHeader.InnerXml
reportFooter.InnerXml = templateFooter.InnerXml
doc.Save(outstream)
reportDefinition = outstream.ToArray()
rs.SetItemDefinition(item.Path, reportDefinition, Nothing)
stream.Dispose()
outstream.Dispose()

End If
Next

End Sub

Save this script using .rss extension and execute from the command line on your reportserver from any directory.

Parting Thoughts

The great part about coding against the Mgmt2010 endpoint is that the script can run against stand alone or integrated SharePoint mode without change.  However, If you have an older version of SSRS, then you will have to alter the name space reference and xpath to the appropriate version / schema. There may / may not be different API calls involved as well.

One could get even more detailed with this script and add common items to anywhere in a report; datasets, etc. In those cases, it would also be better to create a reference to a shared dataset instead of scripting a new one into every single report.

I originally said that we were teaching SSRS a new trick, but the API / rs.exe utility has been around for several years now. Just like training a dog, most of the work is what the human has to learn. Hope this helped!

Advertisements

Let’s get started

Just back from PASS Summit 2014 and I am feeling inspired about stepping it up a notch and starting to give back to the great community that has provided me with the knowledge to build my career over the years. Hopefully, I’ll be able to help a frustrated person out there somewhere with a tidbit or two from some of the solutions I’ve created.

I’m primarily a business intelligence practitioner, which means I’m really all over the stack. You can expect to see posts here about cloud services (mostly Azure because I currently get free MSDN credits), Data Architecture / Modeling, Data Visualization (all the new stuff), SSIS / SSAS / SSRS (the old stuff), DAX & BISM (my new personal favorite), and whatever else comes out next week that makes this post sound old already.

I’m working on some interesting projects right now that I hope to have published patterns for in the near future (comparable sales logic in DAX and creating and maintaining a standard report template in SSRS). In the meantime, I thought I’d start like a real pro, and post some of the stuff I’ve done in the past again…

Monitoring scheduled report executions in SSRS using SSRS

Implementing SSO with per-user identity using Claims, SharePoint 2013 Business Intelligence Services, and Windows 2012 Server Web Application Proxy

As I mentioned before, really, all over the stack…