Gestire i dati dell'audit del CRM

...risulta essere molto utile in contesti di business di medie e grandi dimensioni in cui più persone hanno accesso ai dati essenziali del CRM.

Purtroppo però l'interfaccia utente del CRM offre una sola modalità di visualizzazione di questi dati e non permette facilmente di effettuare reportistica o esportazioni.

Infatti dall'interno del CRM si hanno due visualizzazioni: una di sommario ed una di dettaglio: la prima mostra le date, gli utenti ed il tipo di operazione effettuata nonché l'attributo primario del record. La seconda i dettagli di una singola operazione con i valori vecchi e quelli nuovi inseriti dall'utente nel corso dell'operazione; sfortunatamente però tale visualizzazione, pure completa, è riservata ad un singolo record.

Il problema sostanziale risiede nel fatto che i dati di auditing sono memorizzati nel DB in maniera particolare allo scopo di non appesantire troppo la tabella che li contiene (in realtà il modo migliore per interrogarli è quello di utilizzare la vista "Audit"). Infatti i record di audit contengono il codice dell'entità interessata, la data, il GUID dell'utente, il GUID del record, il codice dell'azione e dell'operazione (sono optionsetvalue rintracciabili nell'SDK e nei metadati) i valori vecchi degli attributi separati da "," o, per gli inserimenti, i nomi degli attributi separati da tilde "~". Inoltre nell'attributo attribute mask ci sono i numeri delle colonne interessate dall'azione. Come si vede non è semplice andare a gestire questi dati.

Allora come fare se si desidera un cruscotto con i dettagli dei dati di audit per un insieme di record?

Sebbene sul web esistano suggerimenti su alcune query SQL non supportate io preferisco procedere in maniera supportata, sfruttando quindi l'SDK.
Esso infatti offre cinque messaggi la cui combinazione permette di avere in una collezione i dati che servono e poterli quindi salvare in qualsiasi formato voluto previo magari un filtraggio (es. file xml, csv, xls, tabella di db, ...).
I cinque messaggi sono:

RetrieveAttributeChangeHistory: recupera la storia delle modifiche di un singolo attributo

RetrieveAuditDetails: dato il GUID di un record di audit va a recuperare i dettagli sugli attributi modificati

 

Retrieve: Messaggio standard del CRM applicato sull'entità audit per recuperare un singolo record

 

RetrieveMultiple: messagio standard del CRM per recuperare record multipli e quindi effettuare interrogazioni, applicato all'entità di audit

 

RetrieveRecordChangeHistory: dato il GUI di un record ed il nome dell'entità di cui fa parte recupera la storia negli audit di quel record.

Quelli a mio parere più utili sono il quarto ed il secondo. L'idea è quella di creare una prima collezione con i dati principali di audit chiamando il quarto messaggio nel quale si possono specificare i filtri, ciclare sulla collezione di entità risultante e per ciascun record chiamare il secondo messaggio per farsi dare i dettagli ed inserirli nella collezione risultante. Si faccia attenzione in questi casi ad applicare sempre la paginazione dato che di solito i volumi non sono piccoli e la Platform del CRM di default restituisce un massimo di cinquemila record come risultato delle interrogazioni.

Sotto un esempio di codice c#.


Metodo che crea la prima collezione filtrando per un'entità ed un periodo.
private EntityCollection GetAuditData(string entityname, DateTime from, DateTime to, int page, int records, bool administrators, IOrganizationService crm)
{
var fetch = "<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false' page='{0}' count='{1}'>" +
"<entity name='audit'>" +
"<all-attributes />" +
"<order attribute='createdon' descending='false' />" +
"<filter type='and'>" +
"<condition attribute='objecttypecode' operator='eq' value='{2}' />" +
"<condition attribute='createdon' operator='on-or-after' value='{3}' />" +
"<condition attribute='createdon' operator='on-or-before' value='{4}' />" +
GetUserConditions(users, crm) +  //Metodo che fornisce il filtro sugli utenti
"</filter>" +
"</entity>" +
"</fetch>";
var query = string.Format(fetch, page.ToString(), records.ToString(), TransformEntityName(entityname), ConvertDateForFetchXml(from), ConvertDateForFetchXml(to));
return RunQuery(query, crm);
}

Metodo che crea la collezione dei dettagli ciclando record per record sulla collezione fornita dal metodo precedente.


private void GetAudit(EntityCollection maindata, IOrganizationService crm)
{
  foreach(Entity e in maindata.Entities)
  {
   try
   {
    req=new RetrieveAuditDetailsRequest() {
    AuditId=(Guid)e.Attributes["auditid"]
    };
    resp = (RetrieveAuditDetailsResponse)crm.Execute(req);
    det=resp.AuditDetail.AuditRecord;
    rec = new AuditRecord()  //classe o struttura contenente i dati di interesse come campi. Il risultato finale sarà una collezione di questi oggetti: auditdatalist
    {
     Action=det.FormattedValues["action"],
     AuditId=(Guid)e.Attributes["auditid"],
     EntityName=((EntityReference)e.Attributes["objectid"]).Name,
     ObjectId=((EntityReference)e.Attributes["objectid"]).Id,
     Operation=det.FormattedValues["operation"],
     UserName=((EntityReference)det.Attributes["userid"]).Name,
     Date=(DateTime)det.Attributes["createdon"]
    };
   audittype = resp.AuditDetail.GetType();
   if(audittype==typeof(AttributeAuditDetail))
   {
      auditdetail = (AttributeAuditDetail)resp.AuditDetail;
      sb = new StringBuilder("");
      try
      {
         foreach (KeyValuePair<String, object> attribute in auditdetail.NewValue.Attributes)
         {
            oldValue = "(no value)";
            newValue = "(no value)";
            if (auditdetail.OldValue.Contains(attribute.Key))
               oldValue = GetDataValue(auditdetail.OldValue[attribute.Key], entityname, attribute.Key, crm);
            newValue = GetDataValue(auditdetail.NewValue[attribute.Key], entityname, attribute.Key, crm);
            if (newValue != oldValue)
              sb.AppendLine(String.Format("Attribute: {0}, old value: {1}, new value: {2}",
            attribute.Key, oldValue, newValue));
         }
      }
      catch (Exception ex) { }
      try
      {
         foreach (KeyValuePair<String, object> attribute in auditdetail.OldValue.Attributes)
         {
            if (!auditdetail.NewValue.Contains(attribute.Key))
            {
               newValue = "(no value)";
               oldValue = GetDataValue(auditdetail.OldValue[attribute.Key], entityname, attribute.Key, crm);
               sb.AppendLine(string.Format("Attribute: {0}, old value: {1}, new value: {2}",
               attribute.Key, oldValue, newValue));
            }
         }
      }
      catch (Exception ex) { }
      rec.Details = sb.ToString();
   } //if
   rec.AdditionalData = GetAdditionalData(rec.ObjectId, entityname, crm);
   auditdatalist.Add(rec); 
  }
catch(Exception ex){}
}
CreateExcelFile(auditdatalist, path + @"\" + filename, entityname);  //Metodo che scrive un file di Excel
}


Dettagli...