using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Wayne.Lib.IO;
namespace Wayne.Lib.Log
{
///
/// Logger is a class used to create log objects.
///
public class Logger
{
#region Fields
///
/// Default date and time format to use in log files.
///
public const string DefaultDateTimeFormat = "HH':'mm':'ss'.'fff'[#]'";
private const int DefaultDebugMarkerBroadcastPort = 11001;
private readonly List OutstandingLogObjects = new List();
private readonly object OutstandingLogObjectsLock = new object(); // Locking object to safely access the OutstandingLogObjects.
private readonly Dictionary EventSubscriberDict = new Dictionary();
private readonly object EventSubscriberDictSyncObj = new object();
private readonly Dictionary ExternalLogWriterNameDict = new Dictionary();
private readonly Dictionary ExternalLogWriterWrapperDict = new Dictionary();
private readonly object ExternalLogWriterDictSyncObj = new object();
private readonly byte[] DebugMarkerBroadcastReceiverBuffer = new byte[100];
private readonly List DebugConfigFileNameList = new List();
private readonly List EventConfigFileNameList = new List();
private readonly List DebugConfigFilesList = new List();
private readonly List EventConfigFilesList = new List();
private readonly List DebugConfigBuilderList = new List();
private readonly List EventConfigBuilderList = new List();
private bool active = false;
private LoggerThread loggerThread;
private LoggerThread eventLoggerThread;
private LogConfig debugConfig;
private LogConfig eventConfig;
private DotNetLog dotNetLog;
private GlobalLogEntryCounter globalLogEntryCounter;
private IPEndPoint debugMarkerBroadcastAddress;
private System.Net.Sockets.Socket debugMarkerBroadcastReceiverSocket;
#endregion
#region Construction
Logger()
{
ThreadPriority=ThreadPriority.Lowest;
Reset();
}
#endregion
#region Internal Properties
///
/// Tells whether someone has called the Close() method.
///
public bool IsClosed
{
get { return !active; }
}
///
/// Internal access to the DebugConfig.
///
internal LogConfig DebugConfig
{
get { return debugConfig; }
}
///
/// Internal access to the GlobalLogEntryCounter.
///
internal GlobalLogEntryCounter GlobalLogEntryCounter
{
get { return globalLogEntryCounter; }
}
#endregion
#region Internal Methods
///
/// This method is used to fire an event instead of throwing an exception if something
/// "crashes" in a thread (in order to keep the thread alive but still "report" exceptions).
///
///
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
[SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
internal void FireOnThreadException(LogException exception)
{
try
{
if (OnThreadException != null)
OnThreadException(null, new EventArgs(exception));
}
catch (Exception) { }
}
///
/// Internal method that is used by the EventLogSubscriptionLogWriter to publish an
/// event entry to a specific subscriber.
///
///
///
internal void PublishEventLog(string eventSubscriberId, EventLogEntry eventLogEntry)
{
//eventLogStorage.Add(eventSubscriberId, eventLogEntry, storageType);
IEventSubscriber eventSubscriber;
lock (EventSubscriberDictSyncObj)
{
EventSubscriberDict.TryGetValue(eventSubscriberId, out eventSubscriber);
}
if (eventSubscriber != null)
eventSubscriber.HandleEvent(eventLogEntry);
}
#endregion
#region Internal Methods: Persistent log objects
internal void RegisterPersistentLogObject(DebugLogger debugLogger)
{
lock (OutstandingLogObjectsLock)
{
if (!OutstandingLogObjects.Contains(debugLogger))
OutstandingLogObjects.Add(debugLogger);
}
}
internal void UnregisterPersistentLogObject(DebugLogger debugLogger)
{
lock (OutstandingLogObjectsLock)
{
if (OutstandingLogObjects.Contains(debugLogger))
OutstandingLogObjects.Remove(debugLogger);
}
}
#endregion
#region Public Events
///
/// An event that is fired when the logging thread is catching an exception.
///
public event EventHandler> OnThreadException;
#endregion
#region Public Properties
///
/// Is any debug logging configured?
///
public bool DebugLoggingConfigured
{
get { return debugConfig.IsConfigured; }
}
///
/// Is any event logging configured?
///
public bool EventLoggingConfigured
{
get { return eventConfig.IsConfigured; }
}
///
/// Should the logger be synchronized or not?
/// Default is false (the log writing is performed in another thread).
///
public bool Synchronized { get; set; }
///
/// Priority for the Logger threads. Defaults to Lowest.
///
public ThreadPriority ThreadPriority { get; set; }
#endregion
#region Public Methods: ConfigFile handling
///
/// Clear all loaded log configuration .
///
public void ClearConfigFiles()
{
if (!active)
return;
DebugConfigFileNameList.Clear();
EventConfigFileNameList.Clear();
DebugConfigFilesList.Clear();
EventConfigFilesList.Clear();
DebugConfigBuilderList.Clear();
EventConfigBuilderList.Clear();
RefreshConfig();
}
///
/// Reloads the configuration from the specified configuration file.
///
/// Log configuration for the debug logging.
/// Log configuration for the event logging.
public void SetConfigFile(string debugConfigFileName, string eventConfigFileName)
{
if (!active)
return;
ClearConfigFiles();
AddDebugConfigFile(debugConfigFileName);
AddEventConfigFile(eventConfigFileName);
}
///
/// Reloads the configuration from the specified configuration file for the debug logging. To activate the event logging
/// SetConfigFile(string,string) should be called.
///
/// Log configuration for the debug logging.
public void SetConfigFile(string debugConfigFileName)
{
if (!active)
return;
ClearConfigFiles();
AddDebugConfigFile(debugConfigFileName);
}
///
/// Reloads the configuration, adding the given debug log config file.
///
/// Log configuration for the debug logging.
public void AddDebugConfigFile(string debugConfigFileName)
{
if (!active)
return;
DebugConfigFileNameList.Add(debugConfigFileName);
RefreshConfig();
}
///
/// Reloads the configuration, adding the given debug log config file.
///
/// Log configurations for the debug logging.
public void AddDebugConfigFile(params LogConfigBuilder[] logConfigBuilders)
{
if (!active)
return;
DebugConfigBuilderList.AddRange(logConfigBuilders);
RefreshConfig();
}
///
/// Reloads the configuration, adding the given debug log config file.
///
/// Log configuration for the debug logging.
public void AddDebugConfigFile(Stream debugConfigFileStream)
{
AddDebugConfigFile(debugConfigFileStream, System.Text.Encoding.UTF8);
}
///
/// Reloads the configuration, adding the given debug log config file.
///
/// Log configuration for the debug logging.
/// The encoding of the stream.
public void AddDebugConfigFile(Stream debugConfigFileStream, System.Text.Encoding encoding)
{
if (!active)
return;
DebugConfigFilesList.Add(GetFileLines(debugConfigFileStream, encoding));
RefreshConfig();
}
///
/// Reloads the configuration, adding the given event log config file.
///
/// Log configuration for the event logging.
public void AddEventConfigFile(string eventConfigFileName)
{
if (!active)
return;
EventConfigFileNameList.Add(eventConfigFileName);
RefreshConfig();
}
///
/// Reloads the configuration, adding the given event log config file.
///
/// Log configurations for the event logging.
public void AddEventConfigFile(params LogConfigBuilder[] logConfigBuilders)
{
if (!active)
return;
EventConfigBuilderList.AddRange(logConfigBuilders);
RefreshConfig();
}
///
/// Reloads the configuration, adding the given event log config file.
///
/// Log configuration for the event logging.
public void AddEventConfigFile(Stream eventConfigFileStream)
{
AddDebugConfigFile(eventConfigFileStream, System.Text.Encoding.UTF8);
}
///
/// Reloads the configuration, adding the given event log config file.
///
/// Log configuration for the event logging.
/// The encoding of the stream.
public void AddEventConfigFile(Stream eventConfigFileStream, System.Text.Encoding encoding)
{
if (!active)
return;
EventConfigFilesList.Add(GetFileLines(eventConfigFileStream, encoding));
RefreshConfig();
}
private string[] GetFileLines(Stream fileStream, System.Text.Encoding encoding)
{
List lines = new List();
using (StreamReader reader = new StreamReader(fileStream, encoding))
{
while (!reader.EndOfStream)
lines.Add(reader.ReadLine());
}
return lines.ToArray();
}
#endregion
#region Public Methods: Refresh
///
/// Re-loads the configuration for the logging.
///
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
[SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
public void RefreshConfig()
{
if (!active)
return;
InitDebugMarkerBroadcastListener();
// Refresh the DebugConfig.
List debugConfigFiles = new List(DebugConfigFilesList);
foreach (string fileName in DebugConfigFileNameList)
debugConfigFiles.Add(FileSupport.LoadToStringArray(fileName));
if (DebugConfigBuilderList.Count > 0)
debugConfigFiles.Add(LogConfigBuilder.GetLogConfigFileLines(DebugConfigBuilderList.ToArray()));
DebugConfig.Refresh(debugConfigFiles, new LogConfigOutput[0], new LogConfigOutput[0]);
// Refresh the EventConfig.
List eventConfigFiles = new List(EventConfigFilesList);
foreach (string fileName in EventConfigFileNameList)
eventConfigFiles.Add(FileSupport.LoadToStringArray(fileName));
if (EventConfigBuilderList.Count > 0)
eventConfigFiles.Add(LogConfigBuilder.GetLogConfigFileLines(EventConfigBuilderList.ToArray()));
eventConfig.Refresh(eventConfigFiles, new LogConfigOutput[0], new LogConfigOutput[0]);
// Start the thread if it's not yet started (first time it's null).
if ((loggerThread == null) && (debugConfigFiles.Count > 0))
loggerThread = new LoggerThread(LogType.Debug, debugConfig);
if ((eventLoggerThread == null) && (eventConfigFiles.Count > 0))
eventLoggerThread = new LoggerThread(LogType.Event, eventConfig);
// To prevent locking the list while iterating, take a copy and iterate through the copy.
// The danger of doing this is that an object could be removed after the copy and before its
// Invalidate-method is called. But nothing really bad can happen due to this...
List outstandingLogObjectsCopy;
lock (OutstandingLogObjectsLock)
{
outstandingLogObjectsCopy = new List(OutstandingLogObjects);
}
foreach (DebugLogger debugLogger in outstandingLogObjectsCopy)
{
try
{
debugLogger.Invalidate();
}
catch (Exception) { }
}
// Check if the dotNetLog should be active or not.
dotNetLog.CheckActive();
}
#endregion
#region Public Methods: Reset
///
/// Resets the logger completely.
///
public void Reset()
{
Close();
active = true;
debugConfig = new LogConfig();
eventConfig = new LogConfig();
dotNetLog = new DotNetLog();
globalLogEntryCounter = new GlobalLogEntryCounter();
}
#endregion
#region Public Methods: Close
///
/// Closes the logger. This should be done as the last things before the application terminates.
///
public void Close()
{
if (active)
{
// Shut down the config.
if (debugConfig != null)
{
debugConfig.Dispose();
debugConfig = null;
}
if (eventConfig != null)
{
eventConfig.Dispose();
eventConfig = null;
}
if (dotNetLog != null)
{
// Shut down the dotNetLog.
dotNetLog.Dispose();
dotNetLog = null;
}
ExternalLogWriterNameDict.Clear();
ExternalLogWriterWrapperDict.Clear();
// Now we're closed.
active = false;
// Shut down the loggerThread.
if (loggerThread != null)
{
loggerThread.Dispose();
loggerThread = null;
}
if (eventLoggerThread != null)
{
eventLoggerThread.Dispose();
eventLoggerThread = null;
}
if (debugMarkerBroadcastReceiverSocket != null)
{
try
{
if (debugMarkerBroadcastReceiverSocket.Connected)
{
debugMarkerBroadcastReceiverSocket.Shutdown(SocketShutdown.Both);
}
}
catch { }
try
{
debugMarkerBroadcastReceiverSocket.Close();
}
catch { }
debugMarkerBroadcastReceiverSocket = null;
}
if (globalLogEntryCounter != null)
{
globalLogEntryCounter.Dispose();
globalLogEntryCounter = null;
}
debugMarkerBroadcastAddress = null;
DebugConfigFileNameList.Clear();
DebugConfigFilesList.Clear();
EventConfigFileNameList.Clear();
EventConfigFilesList.Clear();
DebugConfigBuilderList.Clear();
EventConfigBuilderList.Clear();
OutstandingLogObjects.Clear();
EventSubscriberDict.Clear();
Synchronized = false;
}
}
#endregion
#region Public Methods: LogEntry
///
/// Logs the given LogEntry.
///
/// The LogEntry to log.
public void AddEntry(LogEntry logEntry)
{
if (!active)
return;
if (loggerThread != null)
loggerThread.AddEntry(logEntry);
if ((eventLoggerThread != null) && (logEntry is EventLogEntry))
eventLoggerThread.AddEntry(logEntry); // eventThread has higher priority?
}
///
/// Logs the given ExceptionLogEntry.
///
/// The LogEntry to log.
public void AddExceptionLogEntry(ExceptionLogEntry logEntry)
{
AddEntry(logEntry);
}
///
/// Logs the given ErrorLogEntry.
///
/// The LogEntry to log.
public void AddErrorLogEntry(ErrorLogEntry logEntry)
{
AddEntry(logEntry);
}
///
/// Logs the given EventLogEntry.
///
/// The LogEntry to log.
public void AddEventLogEntry(EventLogEntry logEntry)
{
AddEntry(logEntry);
}
#endregion
#region Public Methods: Unregister entity
///
/// Removes the specified entity from the internal filter buffers.
///
///
public void UnregisterEntity(IIdentifiableEntity entity)
{
if (loggerThread != null)
{
loggerThread.AddEntry(new UnregisterEntity(entity));
}
else
{
if (debugConfig != null)
{
debugConfig.UnregisterEntity(entity);
}
}
if (eventLoggerThread != null)
{
eventLoggerThread.AddEntry(new UnregisterEntity(entity));
}
else
{
if (eventConfig != null)
{
eventConfig.UnregisterEntity(entity);
}
}
}
#endregion
#region Public Methods for Event Subscribers
///
/// Register an IEventSubscriber, so it can start receiving events. The event subscriber will be sent the pending events that
/// has been stored since the subscriber was registered the last time.
///
///
public void RegisterEventSubscriber(IEventSubscriber eventSubscriber)
{
if (eventSubscriber == null)
throw new ArgumentNullException("eventSubscriber");
lock (EventSubscriberDictSyncObj)
{
EventSubscriberDict[eventSubscriber.SubscriberId] = eventSubscriber;
}
}
///
/// Unregister a registered IEventSubscriber.
///
///
public void UnregisterEventSubscriber(IEventSubscriber eventSubscriber)
{
if (eventSubscriber == null)
throw new ArgumentNullException("eventSubscriber");
lock (EventSubscriberDictSyncObj)
{
EventSubscriberDict.Remove(eventSubscriber.SubscriberId);
}
}
#endregion
#region ExternalLogging
private string GetExternalLogWriterKey(ExternalLogWriter externalLogWriter)
{
return string.Format("{0}#{1}", externalLogWriter.ExternalLogType, externalLogWriter.ExternalLogName);
}
private string GetExternalLogWriterKey(ExternalLogWriterWrapper externalLogWriterWrapper)
{
return string.Format("{0}#{1}", externalLogWriterWrapper.ExternalLogType, externalLogWriterWrapper.ExternalLogName);
}
///
/// Register an ExternalLogWriter.
///
/// The external log writer to register.
public void RegisterExternalLogger(ExternalLogWriter externalLogWriter)
{
if (ExternalLogWriterDictSyncObj != null)
lock (ExternalLogWriterDictSyncObj)
ExternalLogWriterNameDict[GetExternalLogWriterKey(externalLogWriter)] = externalLogWriter;
}
///
/// Unregister an ExternalLogWriter.
///
/// The external log writer to unregister.
public void UnregisterExternalLogger(ExternalLogWriter externalLogWriter)
{
if (ExternalLogWriterDictSyncObj != null)
lock (ExternalLogWriterDictSyncObj)
{
ExternalLogWriterNameDict.Remove(GetExternalLogWriterKey(externalLogWriter));
bool found;
do
{
found = false;
foreach (KeyValuePair pair in ExternalLogWriterWrapperDict)
{
if (pair.Value == externalLogWriter)
{
ExternalLogWriterWrapperDict.Remove(pair.Key);
found = true;
break;
}
}
}
while (found);
}
}
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
internal void PerformExternalLogEntry(ExternalLogWriterWrapper externalLogWriterWrapper, LogEntry logEntry)
{
if (ExternalLogWriterDictSyncObj != null)
{
lock (ExternalLogWriterDictSyncObj)
{
ExternalLogWriter externalLogWriter;
if (!ExternalLogWriterWrapperDict.TryGetValue(externalLogWriterWrapper, out externalLogWriter))
{
if (ExternalLogWriterNameDict.TryGetValue(GetExternalLogWriterKey(externalLogWriterWrapper), out externalLogWriter))
{
externalLogWriter.InitParametersInternal(externalLogWriterWrapper.Parameters);
ExternalLogWriterWrapperDict[externalLogWriterWrapper] = externalLogWriter;
}
}
if (externalLogWriter != null)
{
try
{
if (externalLogWriter.Active)
{
string text = LogTextWriting.GetLogEntryText(externalLogWriterWrapper, logEntry,
externalLogWriterWrapper.WritingParameters);
externalLogWriter.LogInternal(logEntry, text);
}
}
catch { }
}
}
}
}
///
/// Get the LogTextWritingParameters for the requested ExternalLogWriter.
///
/// The ExternalLogWriter to get the parameters of.
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")]
internal LogTextWritingParameters GetExternalLoggerWritingParameters(ExternalLogWriter externalLogWriter)
{
if (ExternalLogWriterDictSyncObj != null)
{
lock (ExternalLogWriterDictSyncObj)
{
foreach (KeyValuePair pair in ExternalLogWriterWrapperDict)
{
if (pair.Value == externalLogWriter)
{
return pair.Key.WritingParameters;
}
}
}
}
return null;
}
#endregion
#region Debug Marker Broadcasting
///
/// Broadcasts a debug marker to be inserted into all active logfiles.
///
/// The text to insert into all the log files.
public void InjectDebugMarker(string debugMarker)
{
try
{
System.Net.Sockets.Socket transmitterSocket = null;
try
{
try
{
transmitterSocket = new System.Net.Sockets.Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.IP);
transmitterSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
var lingerOption = new LingerOption(true, 0);
transmitterSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Linger, lingerOption);
transmitterSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, 1);
transmitterSocket.Bind(new IPEndPoint(IPAddress.Any, 0)); // Bind the socket to the "any" end point.
byte[] data = System.Text.Encoding.UTF8.GetBytes(debugMarker);
int offset = 0;
int size = data.Length;
if (size > DebugMarkerBroadcastReceiverBuffer.Length)
size = DebugMarkerBroadcastReceiverBuffer.Length;
int currentSentSize;
do
{
currentSentSize = transmitterSocket.SendTo(data, offset, size, SocketFlags.None, GetDebugMarkerBroadcastIPEndPoint());
offset += currentSentSize;
size -= currentSentSize;
}
while ((size > 0) && (currentSentSize > 0));
}
catch (Exception) { }
}
finally
{
if (transmitterSocket != null)
{
try
{
transmitterSocket.Shutdown(SocketShutdown.Both);
}
finally
{
transmitterSocket.Close();
}
}
}
}
catch (Exception) { }
}
private void InitDebugMarkerBroadcastListener()
{
if (debugMarkerBroadcastReceiverSocket == null)
{
try
{
debugMarkerBroadcastReceiverSocket = new System.Net.Sockets.Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.IP);
debugMarkerBroadcastReceiverSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1);
debugMarkerBroadcastReceiverSocket.Bind(new IPEndPoint(IPAddress.Any, GetDebugMarkerBroadcastIPEndPoint().Port)); // Bind the socket to the "any" end point.
debugMarkerBroadcastReceiverSocket.BeginReceive(DebugMarkerBroadcastReceiverBuffer, 0, DebugMarkerBroadcastReceiverBuffer.Length, 0, ReceiveCallback, debugMarkerBroadcastReceiverSocket); // Start to listen.
}
catch
{
try
{
debugMarkerBroadcastReceiverSocket.Close();
}
catch (Exception)
{
}
debugMarkerBroadcastReceiverSocket = null;
}
}
}
private void ReceiveCallback(IAsyncResult ar)
{
if (debugMarkerBroadcastReceiverSocket != null)
{
try
{
try
{
System.Net.Sockets.Socket socket = (System.Net.Sockets.Socket)ar.AsyncState;
int bytesReceived = 0;
try
{
try
{
bytesReceived = socket.EndReceive(ar);
}
catch { }
}
finally
{
if (bytesReceived > 0)
{
string debugMarker = System.Text.Encoding.UTF8.GetString(DebugMarkerBroadcastReceiverBuffer, 0, bytesReceived);
AddEntry(new DebugMarkerLogEntry(debugMarker));
}
}
}
finally
{
if (active)
debugMarkerBroadcastReceiverSocket.BeginReceive(DebugMarkerBroadcastReceiverBuffer, 0, DebugMarkerBroadcastReceiverBuffer.Length, 0, ReceiveCallback, debugMarkerBroadcastReceiverSocket);
}
}
catch { }
}
}
[System.Diagnostics.DebuggerStepThrough()]
private IPEndPoint GetDebugMarkerBroadcastIPEndPoint()
{
try
{
if (debugMarkerBroadcastAddress == null)
{
//using (Microsoft.Win32.RegistryKey key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"Software\Dresser Wayne\ISM Nucleus\TCP IP\UDP Broadcast", false))
{
IPAddress broadcastAddress;
int broadcastPort;
//if (key != null)
//{
// broadcastAddress = IPAddress.Parse((string)key.GetValue("BroadcastAddress", "255.255.255.255"));
// broadcastPort = (int)key.GetValue("LogMarkerInjectorPort", DefaultDebugMarkerBroadcastPort);
//}
//else
{
broadcastPort = DefaultDebugMarkerBroadcastPort;
broadcastAddress = IPAddress.Broadcast;
}
if (broadcastAddress != null)
debugMarkerBroadcastAddress = new IPEndPoint(broadcastAddress, broadcastPort);
}
}
}
catch
{
if (debugMarkerBroadcastAddress == null)
debugMarkerBroadcastAddress = new IPEndPoint(IPAddress.Broadcast, DefaultDebugMarkerBroadcastPort);
}
return debugMarkerBroadcastAddress;
}
#endregion
#region DateTimeToString
internal string DateTimeToString(DateTime dateTime, string dateTimeFormat)
{
if (GlobalLogEntryCounter != null)
return DateTimeToString(dateTime, dateTimeFormat, GlobalLogEntryCounter.GetNextValue());
return DateTimeToString(dateTime, dateTimeFormat, 0);
}
internal string DateTimeToString(DateTime dateTime, string dateTimeFormat, UInt64 logEntryIndex)
{
return FormatDateTime(dateTime) + "[" + logEntryIndex.ToString("00000") + "]";
}
private string FormatDateTime(DateTime dt)
{
char[] chars = new char[12];
Write2Chars(chars, 0, dt.Hour);
chars[2] = ':';
Write2Chars(chars, 3, dt.Minute);
chars[5] = ':';
Write2Chars(chars, 6, dt.Second);
chars[8] = '.';
Write2Chars(chars, 9, dt.Millisecond / 10);
chars[11] = Digit(dt.Millisecond % 10);
return new string(chars);
}
private void Write2Chars(char[] chars, int offset, int value)
{
chars[offset] = Digit(value / 10);
chars[offset + 1] = Digit(value % 10);
}
private char Digit(int value)
{
return (char)(value + '0');
}
#endregion
}
public class UnregisterEntity : LogEntry
{
public UnregisterEntity(IIdentifiableEntity entity)
: base(entity, null)
{
}
}
}