Extracting Data From Dynamics CRM 2011

Dynamics CRM 2011 is an excellent CRM, business intelligence, and reporting platform.  It is typically the system of record for customer and sales statistics data.  Often times the data in Dynamics is critical for enterprise reporting.  There are a few ways to get to the raw data behind the Microsoft Dynamics CRM 2011 on-premise and online platforms.  I have personally been in different scenarios where I’ve needed to extract this data for integration into an enterprise reporting environment from both on-premise and online versions of Microsoft Dynamics CRM 2011.  My perspective comes from needing specific entities from Dynamics in their entirety for an incremental ETL process to load a star schema.  This blog post is intended to break down the different ways to source Dynamics data and extract the raw data into a staging database for a data warehouse solution.

Below is a matrix of the ways to connect to the source data for both the on-premise and online versions of Microsoft’s Dynamics CRM 2011:

firsttable_highlevel_features

Essentially, those are your main options for consuming database records from the Dynamics CRM 2011 application.  My client is leveraging SSIS as the tooling used to build and populate the data warehouse, but since so much data is stored in an online implementation of Dynamics that there was a big advantage to having simple data copies into SQL Server staging tables and not have to worry about interfacing with the web methods to retrieve the data, Scribe will do that work for us.

The main thing to note here is that the only way I found to extract raw data from an online version of Dynamics was by going through the Dynamics web services, no direct SQL query level access is available to my knowledge which limits the use of SSRS and complicates the use of SSIS for data integration, there is a query language that is fairly simple to understand, FetchXML which is sort of like T-SQL when you dive into it, but was challenging to use for data extraction.  I have experience with sourcing Dynamics CRM data from both online and on-premise platforms and I’d have to say I prefer working with the on-premise version more, just my 2 cents.  Mostly because the reporting capabilities and methods for data extraction are limited to FetchXML or interfacing with the Dynamics Web Services.

Here is a summary of the tooling or solution for getting table copies from an online database, at a high level:

Scribe – Capabilities for both online and on-premise, has a couple of products that make moving data in and out of Dynamics simple and easy and very cost effective.

SSIS Online – assumes a custom source script transformation will be developed with a source to target mapping in a data flow for each entity required, a web service will be the data source.

Custom Application – Capabilities for both online and on-premise, would interface with the web services and would be configurable and could do a high performing truncate-and-reload style copy of just about any Dynamics CRM 2011 database online or on-premise.  Developed in-house and took over 5 weeks to complete.

SSIS On-Premise – assumes a data flow is developed for each entity required using SQL level access to the filtered views in Dynamics CRM 2011.  These are standard source to target mappings, in data flows. 

Feature Breakdown (on-going list of features based on my personal experience, I will update this as I gain additional insight to the tools):

table_features_comparison

Conclusion

In the end I prefer to have data in a database versus data in a web service.  If I need data out of an online Dynamics implementation, I’d prefer to use the Scribe toolset simply because it copies data in it’s entirety and accounts for new and modified columns/DDL and gives me a means to pull table data instead of web service data.  I wish it performed a little better in my environment but for the cost and the productivity gain, it makes for a very valuable tooling.  The development effort for creating a custom application to do what Scribe is doing, or SSIS packages that interface with the web services directly are too great in comparison to purchasing a tool and sourcing physical tables for the ETL development.  The on-premise solution provides a very robust reporting environment from within the application, it also allows for very simple data integration through the SQL Server tables in the Dynamics data model, and also has the flexibility of using the web services if necessary. 

Run a program as a specific user… template

Launch files/programs as a specific user.
Sample:

 
    set WshShell = WScript.CreateObject("WScript.Shell")
    WshShell.run "runas /user:Administrator ""C:\Program Files\Internet Explorer\iexplore.exe""" 'Open command prompt  
    WScript.Sleep(1000)
    WshShell.SendKeys("AdminPassword") 'send password   
    WshShell.SendKeys("{ENTER}") 'send enter keystroke
    WScript.Sleep(1000)

    set wshshell = Nothing

Everyone should check out Hatch, it’s a great site and concept!

http://hatch.use.io/generate/run-a-program-as-a-specific-user/

Dynamics CRM 2011 Audit Report in SSRS

A client of mine had a requirement to report on the auditing of the Filtered Opportunity table, dbo.FilteredOpportunity.  Here is one way to create a report of audit data in Dynamics CRM 2011.  Since there is not a filtered view of the Audit table in CRM, one would need to create a query of the audit table and create a custom SSRS report in CRM.

For starters, the client’s report, need’s to look like this:

Audit Report Filtered Opportunity Custom Fields.

The custom fields that are required to be audited are (these are setup in CRM):

fields required to be audited

These are the fields required by the client to be audited.  So anytime an update action took place on the dbo.FilteredOpportunity view, there would be an occurence of an audit in the dbo.Audit view.

The audit table is a uniqe format, similar to CDC, the AttributeMask is also in numeric format, it doesn’t just list the column names in a comma separated string, making things even more fun to query.  In my case 3 records show up in the audit log for 1/11/2012:

audit sample results

As you can see, it’s not as simple as selecting out the 3 records and “plopping” the results into a table in the RDL layout.  There’s much more to it. For starters, the AttributeMask field is comma seperated but has a leading and trailing comma. The ChangeData field is ~ seperated and does not have any trailing delimiters. Further down in the stored procedure you will see the way I’m handling both of these fields in lines 50-65.

Ideally we want to get our data into a format that is easy for an RDL to consume.  So I have written the stored procedure below, I am leveraging a standard split funtion (lines 50-65) to create multiple rows out of the contents of the AttributeMask and ChangeData columns in the Audit table:

USE [Prod_MSCRM]
GO
/****** Object:  StoredProcedure [dbo].[custom_p_auditreport_filteredopportunity]    Script Date: 01/14/2012 03:40:34 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER proc [dbo].[custom_p_auditreport_filteredopportunity]--'1/10/2012'
(@pEndDate datetime)
as

BEGIN

declare @table table (id int identity(1,1), username varchar(max), attributemask varchar(max) null, changeddata varchar(max), objectid uniqueidentifier not null, CreatedOn datetime, OpportunityName varchar(max), AccountIDName varchar(max), SolicitationNumber varchar(max))
declare @endresult table (id int identity(1,1), v_num_cursor int, ColumnKey int, ColumnName varchar(max), DataValue varchar(max), objectid uniqueidentifier, username varchar(max), createdon datetime, OpportunityName varchar(max), AccountIDName varchar(max), SolicitationNumber varchar(max))
declare @reporttable table (id int identity(1,1), username varchar(max), v_num_cursor int, OpportunityName varchar(max), AccountIDName varchar(max), ColumnName varchar(max), NewValue varchar(max), OldValue varchar(max), SolicitationNumber varchar(max))

--daily audit records
insert into @table(username, attributemask, changeddata, objectid, CreatedOn, OpportunityName, accountidname, SolicitationNumber)
select a.UserIdName, a.AttributeMask, a.changedata, a.ObjectId, a.CreatedOn, fo.name as OpportunityName, fo.accountidname, fo.new_solicitation
   from dbo.Audit a with(nolock)
 INNER JOIN dbo.FilteredOpportunity fo
    ON a.ObjectId = fo.opportunityid
 WHERE 1=1 
 and cast(a.CreatedOn as DATE) = @pEndDate
 and a.AttributeMask <> ''
 and fo.new_opptypename = 'Strategic'
 order by a.CreatedOn asc
 
--ids and loop values for table variable style cursor
declare @v_max_cursor int, @v_num_cursor int
select @v_max_cursor = COUNT(*) from @table
select @v_num_cursor = 1

while @v_num_cursor <= @v_max_cursor
BEGIN

--cursor fields
declare @v_attributemask varchar(max), @v_changedata varchar(max), @v_objectid uniqueidentifier, @v_CreatedOn datetime, @v_UserName varchar(max)
declare @v_opportunityName varchar(max), @v_accountidname varchar(max), @v_new_solicitation varchar(max)

select @v_attributemask = a.AttributeMask, @v_changedata = a.changeddata, @v_objectid = a.ObjectId
, @v_CreatedOn = a.CreatedOn, @v_UserName = a.username, @v_opportunityName = a.OpportunityName, @v_accountidname = a.AccountIDName, @v_new_solicitation = a.SolicitationNumber
from @table a 
where id = @v_num_cursor

--use split function to create rows out of individual audit rows.
insert into @endresult(v_num_cursor, ColumnKey, ColumnName, DataValue, ObjectID, UserName, CreatedOn, AccountIDName, OpportunityName, SolicitationNumber)
SELECT iq.rowbatch, iq.ColumnKey, av.Name, iq.DataValue, iq.ObjectID, iq.UserName, iq.CreatedOn, iq.Accountidname, iq.OpportunityName, iq.SolicitationNumber
FROM (
	SELECT @v_num_cursor as rowbatch, rowid, @v_accountidname as Accountidname, @v_opportunityName as OpportunityName, MAX(ColumnKey) as ColumnKey, MAX(DataValue) as DataValue, @v_objectid as ObjectID, @V_username as UserName, @v_CreatedOn as CreatedOn, @v_new_solicitation as SolicitationNumber
	from (
		select ID as rowid, DATA as ColumnKey, null as DataValue
		from dbo.Split(',',LEFT(RIGHT(@v_attributemask, (LEN(@v_attributemask)-1)), (LEN(RIGHT(@v_attributemask, (LEN(@v_attributemask)-1)))-1))) 
		union all
		select ID as rowid, NULL as ColumnKey, DATA as DataValue
		from dbo.Split('~',@v_changedata)
	) iq 
	group by rowid
) iq
inner join dbo.AttributeView av
on iq.ColumnKey = av.ColumnNumber
where av.EntityId = '30B0CD7E-0081-42E1-9A48-688442277FAE'

--custom lookup, translate surrogate keys and GUIDs to string values or human legible values.
update @endresult set DataValue = case when ColumnName = 'new_businessgroup' then [dbo].[fnBusinessGroupLookup_FilteredOpportunity](DataValue) 
				   when ColumnName = 'ownerid' then [dbo].[fnOwnerIDLookup_SystemUserBase](LTRIM(RTRIM(Replace(case when DataValue = '' then NULL else DataValue end, 'systemuser,',''))))
				   when ColumnName = 'new_2ndownerid' then [dbo].[fnOwnerIDLookup_SystemUserBase](LTRIM(RTRIM(Replace(case when DataValue = '' then NULL else DataValue end, 'systemuser,',''))))
				   when ColumnName = 'new_3rdownerid' then [dbo].[fnOwnerIDLookup_SystemUserBase](LTRIM(RTRIM(Replace(case when DataValue = '' then NULL else DataValue end, 'systemuser,',''))))
				   when ColumnName = 'new_4thownerid' then [dbo].[fnOwnerIDLookup_SystemUserBase](LTRIM(RTRIM(Replace(case when DataValue = '' then NULL else DataValue end, 'systemuser,',''))))
				   when ColumnName = 'new_5thownerid' then [dbo].[fnOwnerIDLookup_SystemUserBase](LTRIM(RTRIM(Replace(case when DataValue = '' then NULL else DataValue end, 'systemuser,',''))))
				   when ColumnName = 'new_strategicphase' then [dbo].[fnStrategicPhaseNameLookup_FilteredOpportunity](LTRIM(RTRIM(DataValue)))
				   when ColumnName = 'customerid' then [dbo].[fnCustomerIDLookup_FilteredOpportunity](LTRIM(RTRIM(Replace(case when DataValue = '' then NULL else DataValue end, 'systemuser,',''))))
				   when ColumnName = 'new_naicsid' then [dbo].[fnNAICSIDLookup_FilteredOpportunity](LTRIM(RTRIM(Replace(case when DataValue = '' then NULL else DataValue end, 'systemuser,',''))))
				   when ColumnName = 'new_typeofbid' then dbo.fnTypeOfBidNameLookup_FilteredOpportunity(LTRIM(RTRIM(DataValue)))
				   when ColumnName = 'new_role' then [dbo].[fnRoleNameLookup_FilteredOpportunity](LTRIM(RTRIM(DataValue)))
				   when ColumnName = 'new_teaming' then [dbo].[fnTeamingNameLookup_FilteredOpportunity](LTRIM(RTRIM(DataValue)))
				   when ColumnName = 'new_nda' then [dbo].[fnNDALookup_FilteredOpportunity](LTRIM(RTRIM(DataValue))) 
else DataValue end	,
ColumnName = case when ColumnName = 'new_businessgroup' then 'new_businessgroupname'
										when ColumnName = 'ownerid' then 'owneridname'
										when ColumnName = 'new_2ndownerid' then 'new_2ndowneridname' 
										when ColumnName = 'new_3rdownerid' then 'new_3rdowneridname' 
										when ColumnName = 'new_4thownerid' then 'new_4thowneridname' 
										when ColumnName = 'new_5thownerid' then 'new_5thowneridname'
										when ColumnName = 'new_strategicphase' then 'new_strategicphasename' 
										when ColumnName = 'customerid' then 'customeridname'
										when ColumnName = 'new_naicsid' then 'new_naicsidname'
										when ColumnName = 'new_typeofbid' then 'new_typeofbidname'
										when ColumnName = 'new_role' then 'new_rolename'
										when ColumnName = 'new_teaming' then 'new_teamingname'
										when ColumnName = 'new_nda' then 'new_ndaname'
										else ColumnName end								
from @endresult

--unpivot FilteredOpportunity data to be in the same format as the audit records after split.  Join on audit.objectid = filteredopportunity.opportunityid
insert into @reporttable ( username, OpportunityName, AccountIDName, SolicitationNumber, v_num_cursor, ColumnName, NewValue, OldValue )
select distinct er.username, er.OpportunityName, er.AccountIDName, er.SolicitationNumber, er.v_num_cursor, cast(unpvt.ColumnName as varchar(Max)) as NewColumn, cast(unpvt.DataValue as varchar(max)) as NewValue, er.DataValue as OldValue
from (
select opportunityid, cast(accountidname as varchar(max)) as accountidname, cast(name as varchar(max)) as name, cast(new_strategicphasename as varchar(max)) new_strategicphasename, 
cast(new_solicitation as varchar(max)) new_solicitation, cast(customeridname as varchar(max)) customeridname, 
cast(owneridname as varchar(max)) owneridname, cast(new_2ndowneridname as varchar(max)) new_2ndowneridname, 
cast(new_3rdowneridname as varchar(max)) new_3rdowneridname, cast(new_4thowneridname as varchar(max))new_4thowneridname,
cast(new_5thowneridname as varchar(max)) new_5thowneridname, cast(new_estrfpreleasedate as varchar(max)) new_estrfpreleasedate, 
cast(new_rfprelease as varchar(max)) new_rfprelease, cast(new_nextaction as varchar(max)) new_nextaction, 
cast(new_naicsidname as varchar(max)) new_naicsidname, cast(new_setaside_s as varchar(max)) new_setaside_s, 
cast(new_contracttype_s as varchar(max)) new_contracttype_s, cast(new_awardtype_s as varchar(max)) new_awardtype_s,
cast(new_typeofbidname as varchar(max)) new_typeofbidname, cast(estimatedvalue as varchar(max)) estimatedvalue, 
cast(new_share as varchar(max)) new_share, cast(new_estpropduedate as varchar(max)) new_estpropduedate, 
cast(new_proposalduedate as varchar(max)) new_proposalduedate, cast(estimatedclosedate as varchar(max)) estimatedclosedate, 
cast(new_awarddate as varchar(max)) new_awarddate, cast(new_rolename as varchar(max)) as new_rolename,
cast(new_teamingname as varchar(max)) new_teamingname, cast(new_teamingntext as varchar(max)) new_teamingntext, 
cast(new_ndaname as varchar(max)) new_ndaname, cast(new_ndantext as varchar(max)) new_ndantext, 
cast(new_bidproposal as varchar(max)) new_bidproposal, cast(new_centurian as varchar(max)) new_centurian, 
cast(new_fedbizops as varchar(max)) new_fedbizops, cast(new_businessgroupname as varchar(max)) new_businessgroupname
from dbo.FilteredOpportunity where opportunityid = @v_objectid) p
UNPIVOT
(DataValue for ColumnName in (new_strategicphasename, new_solicitation  ,customeridname, owneridname, new_2ndowneridname, 
new_3rdowneridname, new_4thowneridname ,new_5thowneridname, new_estrfpreleasedate  , new_rfprelease, new_nextaction  ,new_naicsidname, 
new_setaside_s  ,new_contracttype_s, new_awardtype_s,new_typeofbidname, estimatedvalue, new_share, new_estpropduedate, new_proposalduedate, 
estimatedclosedate  , new_awarddate, new_rolename,new_teamingname, new_teamingntext  , new_ndaname, new_ndantext, new_bidproposal, 
new_centurian  , new_fedbizops, new_businessgroupname)
				 ) As unpvt
inner join @endresult er
on unpvt.ColumnName = er.ColumnName
and unpvt.opportunityid = er.objectid

select @v_num_cursor = 1 + @v_num_cursor

END

--return results for report
select distinct username as Modifiedbyname, OpportunityName as new_solicitation, SolicitationNumber, AccountIDName as Name, [dbo].[fnFriendlyColumnName_AuditReport](ColumnName) as Item, NewValue, OldValue 
from @reporttable
order by 2 asc, Item asc

END

This stored procedure will return the data in a tabular format. As I mentioned it uses the standard split table function found on the web, then based on the objectid (one of the fields in the audit view) looks up the FilteredOpportunity record and then unpivots to support a row like view of the data. If you copy this stored procedure make sure to either comment out lines 66-114 as these have additional custom lookup functions that help me return a string value as in many cases, the audit is tracking the surrogate key or the uniqueidentifier and not the string value in the FilteredOpportunity view.

The operator UNPIVOT helps us format the FilteredOpportunity records similarly to how we’ve transformed the Audit records so the UNPIVOT in combination with the audit data split to rows, it’s a simple join from the audit to the current record, here is one of the audit records returned in its final state:

report data set results

Don’t forget to grant the “NETWORK SERVICE” execute permissions to the stored procedure once you’re done! Also remember that this report only looks at the day’s audit and what’s currently in the FilteredOpportunity view. If you need to do accurate historical reporting there will need to be a few modifications, and the query will become further complex. In my case the ReportServer is caching this report daily so that there is a snapshot of the report for every day.