Introduction
Avalanche.Message unifies various coding facets, [git], [www], [licensing].
Add package reference to .csproj.
<PropertyGroup>
<RestoreAdditionalProjectSources>https://avalanche.fi/Avalanche.Core/nupkg/index.json</RestoreAdditionalProjectSources>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalanche.Message"/>
</ItemGroup>
IMessageDescription is message template.
// ArgumentNull message description
IMessageDescription argumentNull = new MessageDescription("ArgumentNull", 0xA3450267, "'{parameterName}' cannot be null.")
.SetHResult(0x80070057)
.SetSeverity(MessageLevel.Error)
.SetException(typeof(System.ArgumentException));
Arguments are assigned in IMessage.
// Create message
IMessage message = new Message()
.SetMessageDescription(argumentNull)
.SetArguments("name")
.SetReadOnly();
Messages descriptions are managed in IMessageDescriptions tables.
IMessage message = SystemMessages.ArgumentNull.Generic.New();
Messages can be used as IValidable statuscodes.
// Observable
IObservable<IMessage> observable = new MyObservable();
// Observer
MyObserver observer = new MyObserver();
// Observe
observable.Subscribe(observer);
// Print status codes
foreach (IMessage statuscode in observer.StatusCodes) WriteLine(statuscode);
Messages can be used as events.
// Create event description
IMessageDescription fileCreatedEventDescription = new MessageDescription("FileCreated", 0x20012301, "File '{filename}' was created.");
// Create event
IMessage @event = fileCreatedEventDescription
.New("filename.txt")
.SetTime(DateTime.Now)
.SetId(IdGenerators.Guid.Next)
.SetUserData("EventSource", "Server-45")
.SetReadOnly();
Messages are throwable.
throw SystemMessages.ArgumentNull.Generic.New("argumentName").SetTime(DateTime.Now).NewException();
Messages are string.Format() printable.
// ArgumentNull message description
IMessageDescription argumentNull = new MessageDescription("ArgumentNull", 0xA3450267, "'{parameterName}' cannot be null.")
.SetHResult(0x80070057)
.SetSeverity(MessageLevel.Error)
.SetException(typeof(System.ArgumentException));
// Create message
IMessage message = argumentNull.New("name");
// Get system.Format() compatible template
string formatText = message.MessageDescription.Template.Breakdown.FormatTemplate();
// Formulate with system.Format()
string formulation = string.Format(formatText, message.Arguments);
// Print
WriteLine(formulation);
Messages are loggable.
SystemMessages.Argument.EnumValueNotFound.New("Value").LogTo(logger);
Messages are localizable (with Avalanche.Localization).
// Create message description
IMessageDescription ok = new MessageDescription("NULL.S_OK", 0, "Operation successful")
.SetHResult(0x00000000).SetSeverity(MessageLevel.Debug);
// Create localization
ILocalization localization = new Localization()
.AddLine("fi", ok.Key, "Detect", "Operaatio onnistui");
// Decorate to localize
ok = ok.Localized(localization);
// "Operation successful"
WriteLine(ok.New().Print(CultureInfo.GetCultureInfo("en")));
// "Operaatio onnistui"
WriteLine(ok.New().Print(CultureInfo.GetCultureInfo("fi")));
Message texts are pluralizable (with Avalanche.Localization).
// Create message description.
MessageDescription filesDeleted = new MessageDescription("System.IO.FilesDeleted", 0x25110001, "{count} file(s) were deleted.");
// Create localization
ILocalization localization = new Localization()
.AddLine("", filesDeleted.Key, "Detect", "No files were deleted.", "Unicode.CLDR", "count:cardinal:zero:en")
.AddLine("", filesDeleted.Key, "Detect", "One file was deleted.", "Unicode.CLDR", "count:cardinal:one:en")
.AddLine("", filesDeleted.Key, "Detect", "{count} files were deleted.", "Unicode.CLDR", "count:cardinal:other:en");
// Localize message description
filesDeleted.Localize(localization).SetReadOnly();
// "No files were deleted."
WriteLine(filesDeleted.New(0).Print());
// "One file was deleted."
WriteLine(filesDeleted.New(1).Print());
// "2 files were deleted."
WriteLine(filesDeleted.New(2).Print());
Messages descriptions can define http statuses (HttpStatusCode).
// Create message template: OK
IMessageDescription ok = new MessageDescription("Http.Ok", null, "OK")
.SetHttpStatusCode(HttpStatusCode.OK)
.SetSeverity(MessageLevel.Information);
// Create message template: Forbidden
IMessageDescription forbidden = new MessageDescription("Http.Forbidden", null, "'{user}' is not authorized to '{action}'.")
.SetHttpStatusCode(HttpStatusCode.Forbidden)
.SetSeverity(MessageLevel.Error)
.SetException((IMessage m) => new HttpRequestException(m.Print(), m.Error, (HttpStatusCode)m.HttpStatusCode()));
// Create error status "'UserName' is not authorized to 'Post'."
IMessage status = forbidden.New("UserName", "Post");
try
{
// Throw
status.Throw();
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.Forbidden)
{
// System.Net.Http.HttpRequestException: 'UserName' is not authorized to 'Post'.
// at index.Run() in index.cs:line 147
WriteLine(e);
}
Messages descriptions can define HResult code.
try
{
throw SystemMessages.InvalidOperation.Exception.NewException();
}
catch (Exception e) when (e.HResult == HResultIds.COR_E_INVALIDOPERATION)
{
}
Messages are processable in-process and out-of-process.
// Create message description
IMessageDescription ok = new MessageDescription("NULL.S_OK", 0, "Operation successful")
.SetHResult(0x00000000)
.SetSeverity(MessageLevel.Debug);
// Create message
IMessage message = ok.New().SetId(IdGenerators.Integer.Next);
// Serialize as csv line
string[] cells = { message.MessageDescription.Code.ToString()!, message.Id!.ToString()!, Escaper.Comma.EscapeJoin(message.Arguments.Select(a => a?.ToString() ?? "")) };
string csvLine = Escaper.Semicolon.EscapeJoin(cells);
The following GlobalUsings.cs can be used to include extension methods.
global using Avalanche.Message;
global using Avalanche.Utilities;
global using Avalanche.Template;
Class libraries:
- Avalanche.Message.dll contains implementations.
- Avalanche.Message.Abstractions.dll contains interfaces.
- Avalanche.Message.Logging.dll contains Microsoft.Extensions.Logging extensions.
Dependency libraries, direct and indirect:
- Avalanche.Template.dll
- Avalanche.Template.Abstractions.dll
- Avalanche.Tokenizer.dll
- Avalanche.Tokenizer.Abstractions.dll
- Avalanche.Utilities.dll
- Avalanche.Utilities.Abstractions.dll
Tangential libraries:
- Avalanche.StatusCode.HResult.dll contains various public status codes (see more).
- Avalanche.StatusCode.System.dll contains various public status codes.
- Avalanche.StatusCode.Http.dll contains various public status codes.
- Avalanche.StatusCode.OpcUa.dll contains various public status codes.
- Avalanche.Localization.dll
- Avalanche.Localization.Abstractions.dll
- Avalanche.Localization.Cldr.dll
- Avalanche.Localization.Extensions.dll
- Avalanche.Localization.Asp.dll
- Avalanche.Message.Localization.dll
Full Example
Full example
using System.Collections;
using System.Globalization;
using System.Net;
using Avalanche.Localization;
using Avalanche.Message;
using Avalanche.StatusCode;
using Avalanche.Template;
using Avalanche.Utilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using static System.Console;
class index
{
public static void Run()
{
{
// <01>
// ArgumentNull message description
IMessageDescription argumentNull = new MessageDescription("ArgumentNull", 0xA3450267, "'{parameterName}' cannot be null.")
.SetHResult(0x80070057)
.SetSeverity(MessageLevel.Error)
.SetException(typeof(System.ArgumentException));
// </01>
// <02>
// Create message
IMessage message = new Message()
.SetMessageDescription(argumentNull)
.SetArguments("name")
.SetReadOnly();
// </02>
}
{
// <03>
IMessage message = SystemMessages.ArgumentNull.Generic.New();
// </03>
try
{
// <08>
throw SystemMessages.ArgumentNull.Generic.New("argumentName").SetTime(DateTime.Now).NewException();
// </08>
}
catch (Exception) { }
}
{
// <09>
// Observable
IObservable<IMessage> observable = new MyObservable();
// Observer
MyObserver observer = new MyObserver();
// Observe
observable.Subscribe(observer);
// Print status codes
foreach (IMessage statuscode in observer.StatusCodes) WriteLine(statuscode);
// </09>
}
{
// <10>
// ArgumentNull message description
IMessageDescription argumentNull = new MessageDescription("ArgumentNull", 0xA3450267, "'{parameterName}' cannot be null.")
.SetHResult(0x80070057)
.SetSeverity(MessageLevel.Error)
.SetException(typeof(System.ArgumentException));
// Create message
IMessage message = argumentNull.New("name");
// Get system.Format() compatible template
string formatText = message.MessageDescription.Template.Breakdown.FormatTemplate();
// Formulate with system.Format()
string formulation = string.Format(formatText, message.Arguments);
// Print
WriteLine(formulation);
// </10>
}
{
// Service collection
IServiceCollection serviceCollection = new ServiceCollection();
// Add logging
InitializeLogging(serviceCollection);
// Service provider
using var service = serviceCollection.BuildServiceProvider();
// Create ILogger
var logger = service.GetRequiredService<Microsoft.Extensions.Logging.ILogger<index>>();
// <11>
SystemMessages.Argument.EnumValueNotFound.New("Value").LogTo(logger);
// </11>
}
{
// <14>
// Create message description
IMessageDescription ok = new MessageDescription("NULL.S_OK", 0, "Operation successful")
.SetHResult(0x00000000).SetSeverity(MessageLevel.Debug);
// Create localization
ILocalization localization = new Localization()
.AddLine("fi", ok.Key, "Detect", "Operaatio onnistui");
// Decorate to localize
ok = ok.Localized(localization);
// "Operation successful"
WriteLine(ok.New().Print(CultureInfo.GetCultureInfo("en")));
// "Operaatio onnistui"
WriteLine(ok.New().Print(CultureInfo.GetCultureInfo("fi")));
// </14>
}
{
// <15>
// Create message description.
MessageDescription filesDeleted = new MessageDescription("System.IO.FilesDeleted", 0x25110001, "{count} file(s) were deleted.");
// Create localization
ILocalization localization = new Localization()
.AddLine("", filesDeleted.Key, "Detect", "No files were deleted.", "Unicode.CLDR", "count:cardinal:zero:en")
.AddLine("", filesDeleted.Key, "Detect", "One file was deleted.", "Unicode.CLDR", "count:cardinal:one:en")
.AddLine("", filesDeleted.Key, "Detect", "{count} files were deleted.", "Unicode.CLDR", "count:cardinal:other:en");
// Localize message description
filesDeleted.Localize(localization).SetReadOnly();
// "No files were deleted."
WriteLine(filesDeleted.New(0).Print());
// "One file was deleted."
WriteLine(filesDeleted.New(1).Print());
// "2 files were deleted."
WriteLine(filesDeleted.New(2).Print());
// </15>
}
{
// <21>
// Create event description
IMessageDescription fileCreatedEventDescription = new MessageDescription("FileCreated", 0x20012301, "File '{filename}' was created.");
// Create event
IMessage @event = fileCreatedEventDescription
.New("filename.txt")
.SetTime(DateTime.Now)
.SetId(IdGenerators.Guid.Next)
.SetUserData("EventSource", "Server-45")
.SetReadOnly();
// </21>
}
{
// <31>
// Create message template: OK
IMessageDescription ok = new MessageDescription("Http.Ok", null, "OK")
.SetHttpStatusCode(HttpStatusCode.OK)
.SetSeverity(MessageLevel.Information);
// Create message template: Forbidden
IMessageDescription forbidden = new MessageDescription("Http.Forbidden", null, "'{user}' is not authorized to '{action}'.")
.SetHttpStatusCode(HttpStatusCode.Forbidden)
.SetSeverity(MessageLevel.Error)
.SetException((IMessage m) => new HttpRequestException(m.Print(), m.Error, (HttpStatusCode)m.HttpStatusCode()));
// Create error status "'UserName' is not authorized to 'Post'."
IMessage status = forbidden.New("UserName", "Post");
try
{
// Throw
status.Throw();
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.Forbidden)
{
// System.Net.Http.HttpRequestException: 'UserName' is not authorized to 'Post'.
// at index.Run() in index.cs:line 147
WriteLine(e);
}
// </31>
}
{
// <32>
try
{
throw SystemMessages.InvalidOperation.Exception.NewException();
}
catch (Exception e) when (e.HResult == HResultIds.COR_E_INVALIDOPERATION)
{
}
// </32>
}
{
// <41>
// Create message description
IMessageDescription ok = new MessageDescription("NULL.S_OK", 0, "Operation successful")
.SetHResult(0x00000000)
.SetSeverity(MessageLevel.Debug);
// Create message
IMessage message = ok.New().SetId(IdGenerators.Integer.Next);
// Serialize as csv line
string[] cells = { message.MessageDescription.Code.ToString()!, message.Id!.ToString()!, Escaper.Comma.EscapeJoin(message.Arguments.Select(a => a?.ToString() ?? "")) };
string csvLine = Escaper.Semicolon.EscapeJoin(cells);
// </41>
WriteLine(csvLine); // "0;0;"
}
}
/// <summary></summary>
public class MyObservable : IObservable<IMessage>
{
/// <summary></summary>
public IDisposable Subscribe(IObserver<IMessage> observer)
{
observer.OnNext(new Message());
observer.OnCompleted();
return null!;
}
}
/// <summary></summary>
public class MyObserver : IObserver<IMessage>
{
/// <summary></summary>
public readonly List<IMessage> StatusCodes = new();
/// <summary></summary>
public void OnCompleted() { }
/// <summary></summary>
public void OnError(Exception error) { }
/// <summary></summary>
public void OnNext(IMessage statusCode) { StatusCodes.Add(statusCode); }
}
static IServiceCollection InitializeLogging(IServiceCollection serviceCollection)
{
// Initial configuration
MemoryConfiguration memConfig = new MemoryConfiguration()
.Set("Logging:LogLevel:Default", "Debug")
.Set("Serilog:WriteTo:0:Name", "Console")
.Set("Serilog:WriteTo:0:Args:OutputTemplate", "[{EventIdHex} {Level:u1}] {Message:lj}{NewLine}{Exception}")
.Set("Serilog:WriteTo:0:Args:RestrictedToMinimumLevel", "Verbose")
.Set("Serilog:WriteTo:0:Args:Theme", "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console");
// Read configuration
IConfigurationRoot configuration = new ConfigurationBuilder()
.Add(memConfig)
.Build();
//
var switchableLogger = new Serilog.SwitchableLogger();
// Logging
serviceCollection.AddLogging(loggingBuilder =>
loggingBuilder
#if DEBUG
.SetMinimumLevel(LogLevel.Trace)
#else
.SetMinimumLevel(LogLevel.Information)
#endif
.AddSerilog(switchableLogger, true)
.AddSerilogConfigurationLoader(configuration, switchableLogger,
c => new Serilog.LoggerConfiguration()
#if DEBUG
.MinimumLevel.Verbose()
#else
.MinimumLevel.Information()
#endif
.Enrich.With(new EventIdEnricher())
.ReadFrom.Configuration(configuration)
.CreateLogger())
);
//
return serviceCollection;
}
/// <summary>Memory configuration</summary>
public class MemoryConfiguration : ConfigurationProvider, IEnumerable<KeyValuePair<string, string>>, IConfigurationSource
{
/// <summary>Expose inner configuration data</summary>
public new IDictionary<String, String> Data => base.Data;
/// <summary>Configuration data</summary>
public string this[string key] { get => base.Data[key]; set => base.Data[key] = value; }
/// <summary>Create memory configuration</summary>
public MemoryConfiguration() : base() { }
/// <summary>Assign <paramref name="value"/> to <paramref name="key"/></summary>
public new MemoryConfiguration Set(string key, string value) { base.Data[key] = value; return this; }
/// <summary>Build configuration provider.</summary>
public IConfigurationProvider Build(IConfigurationBuilder builder) => this;
/// <summary>Enumerate</summary>
public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => Data.GetEnumerator();
/// <summary>Enumerate</summary>
IEnumerator IEnumerable.GetEnumerator() => Data.GetEnumerator();
}
/// <summary>Serilog enricher that reduces "EventId" to its "id" field.</summary>
public class EventIdEnricher : ILogEventEnricher
{
/// <summary>Reduce to EventId.id</summary>
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
// Get properties
if (!logEvent.Properties.TryGetValue("EventId", out LogEventPropertyValue? eventIdStructure)) return;
// Not structure field
if (eventIdStructure is not StructureValue structureValue || structureValue.Properties == null) return;
// Get list
IReadOnlyList<LogEventProperty> list = structureValue.Properties;
// Process each
for (int i = 0; i < list.Count; i++)
{
// Get property
LogEventProperty logEventProperty = list[i];
// Not id
if (logEventProperty.Name != "Id") continue;
// New property
LogEventProperty eventId = propertyFactory.CreateProperty("EventId", logEventProperty.Value);
// Add as new key
logEvent.AddOrUpdateProperty(eventId);
// Print as hex
string hex = logEventProperty.Value.ToString("X8", CultureInfo.InvariantCulture);
// New property
LogEventProperty eventIdHex = propertyFactory.CreateProperty("EventIdHex", hex);
// Add as new key
logEvent.AddOrUpdateProperty(eventIdHex);
// Completed
return;
}
}
}
}