How to recreate Filtered views in Dynamics 365 (code sample)

For all of us ‘CRM’ users who were used to writing SQL reports for CRM On Prem, one of the major features we’ve lost in moving to Online was the lack of SQL access. In December 2016, Microsoft gave us a fantastic tool to replicate the data to an Azure SQL Database (details on TechNet). With this tool you’re able to get the data from the cloud to a SQL database you have direct access to and everything is right in the world – except it’s really slow and it’s tough to write your SQL reports.

What’s the problem?

There are two key features included in the On Prem database that aren’t available when you replicate the data:

  1. Indexes aren’t automatically created so your queries are slow
  2. There are no Filtered views, so getting the display values for your report is a pain

Missing indexes are easy – when you’re writing queries that are underperforming, SQL will generate index suggestions. I used https://blog.sqlauthority.com/2011/01/03/sql-server-2008-missing-index-script-download/ and it got my contact query (1 million+ records) from 55 minutes to less than a second.

The big problem is that you don’t have Filtered views. In the On Prem, filtered views handled security for the user running the report, and it also provided extra columns to make reporting easier. Since Option Set fields are stored as an integer, you need to take extra steps if you want to display the label of the field.

What’s the solution?

I solved the display problem for one of my clients by writing code which mimics the Filtered views by generating views with extra columns for each of the labels. Here’s what you end up with:

  • The ‘contact’ table has a column ‘donotfax’ with value ‘0’
  • The ‘displaycontact’ view has a column ‘donotfax’ with value ‘0’, and ‘donotfax_display’ with value ‘Allow’.

Not only do these views give you all the original data with more details, but they’re just as fast querying the tables directly (at least, I can’t notice a difference). I spend a significant amount of time tailoring the views to get the best performance (it took me a while to track down a performance issue when you have too many join conditions).

How does the code work?

It’s pretty straightforward:

  1. Query the database to see what tables are there and match them up to entities in Dynamics 365 to determine the entities that need a view.
  2. Query for the existing views to see which ones need to be dropped and recreated.
  3. For each table that needs a view, find all the attributes that should have a display field (option set, Boolean, statecode, statuscode), and build up a column to get back the right value.

When do I need to run the code?

When you change the labels in Dynamics 365, the label is automatically updated in the database and your view will reflect the change immediately. The only time you need to regenerate the views is if you add new entities to the synchronization (requires a new view), or if you add new option set or Boolean fields to Dynamics 365 (the view needs a new column).

That’s it!

Now when you’re writing SQL against your replication database you’ll be able to query against the “displaycontact” and get back “donotfax_display” without having to write any other joins!

protected void GenerateViews(string sqlConnectionString, string crmConnectionString)
{
 // Adjust these to have different view or columns names
 string displayViewPrefix = "display";
 string displayViewSuffix = String.Empty;
 string displayFieldPrefix = String.Empty;
 string displayFieldSuffix = "_display";

 this.SqlConnectionString = sqlConnectionString;
 // Testing SQL connection
 string sqlError = this.ValidateSqlString();
 if (!String.IsNullOrEmpty(sqlError))
 {
   throw new ConfigurationErrorsException("Connection String error: " + sqlError);
 }


 CrmServiceClient service = new CrmServiceClient(crmConnectionString);

 // Retrieve existing tables from the replication database
 DataTable databaseTables = this.ExecuteQuery("SELECT [name] FROM sys.tables order by name");
 List existingTable = new List();
 for (int i = 0; i < databaseTables.Rows.Count; i++)
 {
   existingTable.Add(databaseTables.Rows[i][0].ToString());
 }

 // Retrieve the existing views
 DataTable databaseViews = this.ExecuteQuery($"SELECT [name] FROM sys.views where name like '{displayViewPrefix}%'");
 List views = new List();
 for (int i = 0; i < databaseViews.Rows.Count; i++)  {    views.Add(databaseViews.Rows[i][0].ToString());  }  // Retrieve the entities   RetrieveAllEntitiesRequest request = new RetrieveAllEntitiesRequest() { EntityFilters = EntityFilters.Entity | EntityFilters.Attributes };  RetrieveAllEntitiesResponse response = service.Execute(request) as RetrieveAllEntitiesResponse;  foreach (string tableName in existingTable)  {    // Ensure the table represents an entity and isn't an extra table in the database    EntityMetadata metadata = response.EntityMetadata.FirstOrDefault(e => e.SchemaName.Equals(tableName, StringComparison.CurrentCultureIgnoreCase));
   if (metadata == null)
   {
     continue;
   }

   // Drop the existing view
   string viewName = $"{displayViewPrefix}{tableName}{displayViewSuffix}";
   if (views.Contains(viewName))
   {
     this.ExecuteNonQuery($"DROP VIEW {viewName}");
   }

   // Build up the new view command
   StringBuilder sb = new StringBuilder($"select {tableName}.*");
   foreach (AttributeMetadata attribute in metadata.Attributes.OrderBy(a => a.SchemaName))
   {
     if (attribute.AttributeType == AttributeTypeCode.State)
     {
       sb.Append($", (select {attribute.SchemaName}_metadata.localizedlabel from ");
       sb.Append($"StateMetadata {attribute.SchemaName}_metadata with(nolock) ");
       sb.Append($"where {attribute.LogicalName} is not null and ");
       sb.Append($"{attribute.LogicalName} = {attribute.LogicalName}_metadata.[State] and ");
       sb.Append($"{attribute.LogicalName}_metadata.entityname = '{tableName}') as {displayFieldPrefix}{attribute.SchemaName}{displayFieldSuffix}");
     }
     else if (attribute.AttributeType == AttributeTypeCode.Status)
     {
       sb.Append($", (select {attribute.SchemaName}_metadata.localizedlabel from ");
       sb.Append($"StatusMetadata {attribute.SchemaName}_metadata with(nolock) ");
       sb.Append($"where {attribute.LogicalName} is not null and ");
       sb.Append($"{attribute.LogicalName} = {attribute.LogicalName}_metadata.[Status] and ");
       sb.Append($"{attribute.LogicalName}_metadata.entityname = '{tableName}') as {displayFieldPrefix}{attribute.SchemaName}{displayFieldSuffix}");
     }
     else if (attribute.AttributeType == AttributeTypeCode.Boolean ||
       (attribute.AttributeType == AttributeTypeCode.Picklist && ((Microsoft.Xrm.Sdk.Metadata.EnumAttributeMetadata)attribute).OptionSet.IsGlobal != null && !((Microsoft.Xrm.Sdk.Metadata.EnumAttributeMetadata)attribute).OptionSet.IsGlobal.Value))
     {
       sb.Append($", (select {attribute.SchemaName}_metadata.localizedlabel from ");
       sb.Append($"OptionSetMetadata {attribute.SchemaName}_metadata with(nolock) ");
       sb.Append($"where {attribute.SchemaName} is not null and ");
       sb.Append($"{attribute.SchemaName} = {attribute.SchemaName}_metadata.[Option] and ");
       sb.Append($"{attribute.SchemaName}_metadata.entityname = '{tableName}' and ");
       sb.Append($"{attribute.SchemaName}_metadata.optionsetname = '{attribute.SchemaName}') as {displayFieldPrefix}{attribute.SchemaName}{displayFieldSuffix}");
     }
     else if (attribute.AttributeType == AttributeTypeCode.Picklist && ((Microsoft.Xrm.Sdk.Metadata.EnumAttributeMetadata)attribute).OptionSet.IsGlobal != null &&
       ((Microsoft.Xrm.Sdk.Metadata.EnumAttributeMetadata)attribute).OptionSet.IsGlobal.Value)
     {
       sb.Append($", (select {attribute.SchemaName}_metadata.localizedlabel from ");
       sb.Append($"GlobalOptionSetMetadata {attribute.SchemaName}_metadata with(nolock) ");
       sb.Append($"where {attribute.SchemaName} is not null and ");
       sb.Append($"{attribute.SchemaName} = {attribute.SchemaName}_metadata.[Option] and ");
       sb.Append($"{attribute.SchemaName}_metadata.optionsetname = '{attribute.SchemaName}') as {displayFieldPrefix}{attribute.SchemaName}{displayFieldSuffix}");
     }
   }
   sb.Append($" from {tableName}");
   this.ExecuteNonQuery($"CREATE VIEW[{viewName}] as {sb.ToString()}");
 }
}
protected string ValidateSqlString()
{
  string errorMessage = String.Empty;
  if (String.IsNullOrEmpty(this.SqlConnectionString))
  {
    errorMessage = "Connection String is empty";
  }
  else
 {
   try
   {
     using (SqlConnection connection = new SqlConnection(this.SqlConnectionString))
     {
       try
       {
         connection.Open();
       }
       catch (Exception ex)
       {
         errorMessage = "Error establishing connection to SQL server: " + ex.Message;
       }
     }
   }
   catch (Exception ex)
   {
     errorMessage = "Configuration error in sql connection string: " + ex.Message;
   }
 }
 return errorMessage;
}

protected DataTable ExecuteQuery(string queryString)
{
  DataTable table = new DataTable();
  using (SqlConnection connection = new SqlConnection(this.SqlConnectionString))
  {
    SqlCommand command = new SqlCommand(queryString, connection) { CommandTimeout = 1200 };
    connection.Open();
    try
    {
      SqlDataReader dr = command.ExecuteReader(CommandBehavior.CloseConnection);
      table.Load(dr);
    }
    finally
    {
      connection.Close();
    }
  }
  return table;
}

protected bool ExecuteNonQuery(string queryString)
{
  bool successful = false;
  using (SqlConnection connection = new SqlConnection(this.SqlConnectionString))
  {
   SqlCommand command = new SqlCommand(queryString, connection) { CommandTimeout = 1200 };
   connection.Open();
   try
   {
     command.ExecuteNonQuery();
     successful = true;
   }
   finally
   {
     // Always call Close when done reading.
     connection.Close();
   }
 }
 return successful;
}

 

7 thoughts on “How to recreate Filtered views in Dynamics 365 (code sample)

  1. Hi Aiden,
    Would you need to regenerate the views if you restore a copy of a production CRM instance to a sandboxed UAT CRM? We are using the views as you have described to run our old SSRS reports with our new online 365 CRM instance. The reports were working fine until we restored our production instance to our UAT instance. We have recreated the DES profile but the reports are blank. Should we regenerate the views as well?

    Thanks

    Phil

    Like

    • Hi Phil,

      The views are only expecting table names and columns to exist, it’s independent of the CRM after you generate them. Really, moving your CRM shouldn’t matter as far as the views are concerned. I’d regenerate them and make sure you’re hitting the right database and prefixes (from the export profile).

      Thanks,
      Aiden

      Like

      • Hi Aiden,

        Thanks for your reply. I didn’t think that regenerating the views would make a difference but the reports stopped working after the restore so I thought it would be worth a try. I regenerated the views and the reports are now working so not sure why I need to but at least I know now for future restores.

        Thanks

        Phil

        Like

    • Hi Allison,

      I glossed over it by saying that filtered views handle security. The Data Export Service does not handle any sort of security and your SQL database is unaware of security setting in Dynamics. You would have to implement security restrictions on your reporting tool.

      Thanks,
      Aiden

      Like

      • That’s what I’d seen elsewhere, but when you said you were recreating the filtered views I got hopeful you’d found a way to do this part of them as well (since that’s their primary advantage, for my usage anyway). Ah well. Thanks!

        Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s