It's devoted to a class i created recently in my current project. We plan to use Enterprise Library
in our projects and want our MS CRM users able to watch through the log entries via MS CRM. So my article describes
a process of creation custom listener for Enterprise Library that is able to write entries in MS CRM. I used
Enterpise library Beta 2 version, and it has poor documentation, so in several places i used a workaround code because
i failed to use all power of EL API for creating custom listener... So lets start!
Show/Hide
We start from declaring our class, it's inherited from CustomTraceListener class and marker with
ConfigurationElementType to mark configuration section we will use in our projects to configure our listener:
[ConfigurationElementType(typeof(CustomTraceListenerData))]
public class MSCRMTraceListener : CustomTraceListener
{
}
I skip description of connection to MS CRM webservice, just say that i have a property Servise that returns object of
CrmService type. Something like this:
private CrmService Service
{
get
{
}
}
We create property SeveritySetting to get appropriate severity level from the listener's configuration setting. This is my
cheat-workaround-code, because i failed to receive "native configuration"'s severity setting from config, so i was needed to use
attributes of the listener to keep appropriate severity. The property looks like:
private TraceEventType SeveritySetting
{
get
{
return GetSeverityFromSettings();
}
}
And GetSeverityFromSettings method contains attribute processing:
///
/// Receives maximum severity for logging from configuration settings.
/// If settings doesn't contain SeverityAttributeKey key, it returns Verbose.
/// If attribute contains integer value - we parse it to understand what range 1-2-4-8-16 it belongs to.
/// If attribute contains non-integer value - try to compare with enumeration members' names,
/// if fails - return Verbose.
///
///
private TraceEventType GetSeverityFromSettings()
{
/// If settings doesn't contain SeverityAttributeKey key, it returns Verbose
if (!Attributes.ContainsKey(SeverityAttributeKey)) return TraceEventType.Verbose;
var severitySetting = Attributes[SeverityAttributeKey];
int severity;
/// If attribute contains integer value - we parse it to understand what range 1-2-4-8-16 it belongs to.
if (Int32.TryParse(severitySetting, out severity))
{
if (severity <= 1) return TraceEventType.Critical;
if (severity <= 2) return TraceEventType.Error;
if (severity <= 4) return TraceEventType.Warning;
return severity <= 8 ? TraceEventType.Information : TraceEventType.Verbose;
} /// If attribute contains non-integer value - try to compare with enumeration members' names /// if fails - return Verbose. switch (severitySetting.ToLower())
{
case "critical":
return TraceEventType.Critical;
case "error":
return TraceEventType.Error;
case "warning":
return TraceEventType.Warning;
case "information":
return TraceEventType.Error;
default:
return TraceEventType.Verbose;
}
}
We need to override TraceData and Write methods to log messages. TraceData processes incoming data object and if it's LogEntry object, it calls asynchroniously method to write it to MS CRM. I use asynchronious call because calling crm's webservice can take significant amount of time and i don't want external callers suffered from this. If data is not LogEntry object i just call simple writing, it calls asyncronious writing itself public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data)
{
if (data is LogEntry)
{
/// If data has insufficient severity level - just return
if ((data as LogEntry).Severity > SeveritySetting) return;
/// start asynchronious thread for logging
var traceThread = new Thread(WriteAsync);
traceThread.Start(data);
}
else
{
/// Severity of the message is not defined, we count it as a Verbose by default
if (TraceEventType.Verbose > SeveritySetting) return;
WriteLine(data.ToString());
}
}
Write and WriteLine methods are also pretty simple:
public override void Write(string message)
{
/// Severity of the message is not defined, we count it as a Verbose by default
if (TraceEventType.Verbose > SeveritySetting) return;
WriteAsync(message);
}
public override void WriteLine(string message)
{
/// Severity of the message is not defined, we count it as a Verbose by default
if (TraceEventType.Verbose > SeveritySetting) return;
Write(message);
}
WriteAsync method containts simple realization, that wraps input string into LogEntry object and
calls full implementation(asynchroniously as i promised):
private void WriteAsync(string message)
{
/// вызываем все ту же запись
var data = new LogEntry
{
AppDomainName = String.Empty,
EventId = 1,
MachineName = Environment.MachineName,
Message = message,
Title = string.Empty,
Priority = 1,
Severity = TraceEventType.Verbose,
TimeStamp = DateTime.Now,
Categories = new[] { "General" }
};
var traceThread = new Thread(WriteAsync);
traceThread.Start(data);
}
Before describe full implementation of WriteAsync method, i need to say a few words about corresponding object model
in MS CRM. We have new_logentry entity, which contains all LogEntry fields: appdomainname(new_appdomainname),
eventid(new_eventid) and so on. We also create new_logentrycategory entity to store possible entry's categories
and it's linked with new_logentry category by N:N relationship.
So WriteAsync method gets all existed categories from MS CRM (i won't write this simple CRM webservice call here),
and if new entry's categories aren't presented among them, it adds new categories to MS CRM. After that it calls
SaveLogEntry method to save the entry:
private void WriteAsync(object data)
{
var entry = data as LogEntry;
/// actually, at this time all our data objects are LogEntry
if (entry == null) return;
/// get all categories from MS CRM
var categories = GetAllCategories();
/// run througn entry categories
foreach (var dataCategory in entry.Categories)
{
/// skip if category exists in MS CRM
if (categories.ContainsKey(dataCategory)) continue;
/// otherwise - create it
var id = CreateNewLogEntryCategory(dataCategory);
categories.Add(dataCategory, id);
}
///сохраняем запись
SaveLogEntry(entry, categories);
}
Even though i don't think it's needed to present SaveLogEntry method here, i also write it to make your work simplier;)
private void SaveLogEntry(LogEntry data, IDictionary
{
/// заполняем объект записи
var entry = new new_logentry
{
new_appdomainname = data.AppDomainName,
new_eventid = data.EventId.ToString(),
new_machinename = data.MachineName,
new_message = data.Message,
new_name = data.Title,
new_priority = new CrmNumber { Value = data.Priority },
new_severity = GetSeverity(data.Severity),
new_timestamp =
new CrmDateTime { Value = data.TimeStamp.ToString("u").Replace(" ", "T").Replace("Z", " Z") }
};
var target = new TargetCreateNew_logentry
{
New_logentry = entry
};
/// создаем запись
var response = (CreateResponse)Service.Execute(
new CreateRequest
{
Target = target
}
);
/// сохраняем связи между созданным объектом записи и категориями
foreach (var category in data.Categories)
{
if (categories.ContainsKey(category))
AddEntryCategoryLink(response.id, categories[category]);
}
}
GetSeverity method above just converts TraceEventType to appropriate Picklist value. AddEntryCategoryLink creates
N:N link between new_logentry and new_logentrycategory objects (if you feel yourself embarrassed here, look at
AssociateEntitiesRequest class in MS CRM documentation. There is also scaring line above:
new_timestamp = new CrmDateTime { Value = data.TimeStamp.ToString("u").Replace(" ", "T").Replace("Z", " Z") }
but it just converts DateTime to string presentation that CrmDateTime's Value can parse.
After that our class is completed and i hust want to say a few words about configuration part of the work. I won't
describe full logging section that EL Congigurator creates for you, i just show how "add" element, that adds
our listener to listeners collection, looks:
CrmServise and OrganizationName are attributes i used to create CrmService object (i skipped this code above). Severity is a value of
SeverityAttributeKey constant, it's used in GetSeverityFromSettings method. Name is used later to add listener to the sources. And don't
forget to add this listener to "Unprocessed Category" source, otherwise you will get an exception when trying to write entry with new category.
There is nothing more to add, i hope this sample was useful to you. Next post i'll describe one System.Net.Mail problem and way to solve it. C ya:)
No comments:
Post a Comment