oxygen domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /home/sites/20b/2/2c6dbbae55/public_html/wp-includes/functions.php on line 6260wishlist-member domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /home/sites/20b/2/2c6dbbae55/public_html/wp-includes/functions.php on line 6260ws-form domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /home/sites/20b/2/2c6dbbae55/public_html/wp-includes/functions.php on line 6260ws-form-post domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /home/sites/20b/2/2c6dbbae55/public_html/wp-includes/functions.php on line 6260ws-form-user domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /home/sites/20b/2/2c6dbbae55/public_html/wp-includes/functions.php on line 6260In this, the third part of our series on logging, we’ll expose the really powerful feature of Metalama, the ability to log an entire solution with relatively little effort and most importantly this is really easy to implement time and time again, producing consistent results that you can rely on. We’ll do this with the aid of Metalama’s TransitiveProjectFabric, a fabric which as it’s name suggests is capable of transiting over multiple projects.
To make this as efficient as possible, and easy to maintain the obvious way to use our TransitiveProjectFabric is from a NuGet Package. This package will be saved to our own local NuGet Server so that we can always quickly add logging when we need to. Whilst a private NuGet Server may seem a little OTT for individual developers you’ll some come to appreciate just how useful they can be, especially when you have your own set of XAF modules that you find yourself routinely applying to XAF solutions.
You can find part one of the series here and part two here.
In Visual Studio (2022 or above) create a new class library. Base this on .Net Standard. Add two NuGet packages to the solution, Metalama.Framework and, in this case Serilog. In this example we are going to be using Serilog structured logging however if you intend to use a different logging framework then your choice here will be different.
As this is a blog that deals primarily with matters XAF related it seemed only sensible that we should now turn our attention to adding logging to an XAF application and before commencing work on this third part of the blog I ran into a couple of issues I hadn’t expected to. As was stated very clearly in the first part of this series Metalama was still in preview at the time of writing and the transition of XAF to fully support .Net Core whilst well advanced still has its foibles.
Preparing the samples for this part of the series led to a couple of issues which I had not anticipated and which forced me to rethink the requirements of the proposed library. In addition to providing a means to log all of the methods that would be found in a project there would be a new requirement, the ability to specifically avoid logging certain methods.
The first class that will be added to the library is going to be the Fabric that we need. In essence it’s the same as the one in the last part save for one small but crucial difference, this one will be based on TransitiveProjectFabric.
public class Fabric : TransitiveProjectFabric
{
#region Public Methods
public override void AmendProject(IProjectAmender amender)
{ amender.With(p => p.Types.SelectMany(t => t.Methods)).AddAspect<LogAttribute>(); }
#endregion
}The next class to be added is the attribute that will essentially prevent a method from being logged if there is no need for it to be logged.
[AttributeUsage(AttributeTargets.Method)]
public class NoLogAttribute : Attribute
{
}This attribute is there to do no more than serve as a simple flag to tell the aspect that will apply logging to methods to skip one decorated with [NoLog].
The last class to be added is the LogAttribute. Whilst similar to examples you have seen before there are some subtle differences worthy of discussion.
[AttributeUsage(AttributeTargets.Method)]
public class LogAttribute : Aspect, IAspect<IMethod>
{
#region Public Methods
public void BuildAspect(IAspectBuilder<IMethod> builder)
{
if(!builder.Target.Attributes.Any(a => a.Type.Is(typeof(NoLogAttribute))))
{
builder.Advices.OverrideMethod(builder.Target, nameof(this.OverrideMethod));
}
}
public void BuildEligibility(IEligibilityBuilder<IMethod> builder)
{
}
[Template]
public dynamic? OverrideMethod()
{
string? methodName = $"{meta.Target.Type}.{meta.Target.Method.Name}";
if(meta.Target.Parameters.Count == 0)
{
Log.Information("Entering {MethodName}", methodName);
} else
{
Dictionary<string, object> parameters = new Dictionary<string, object>();
foreach(var p in meta.Target.Parameters)
{
if(p.RefKind != RefKind.Out)
{
parameters.Add($"{p.Type} {p.Name}", p.Value);
} else
{
//Metalame can't serialise an out parameter but it would help if we know it's there.
parameters.Add($"{p.Type} {p.Name}", " = <out>");
}
}
Log.Information("Entering {MethodName} with these parameters: {parameters}", methodName, parameters);
}
Stopwatch watch = Stopwatch.StartNew();
try
{
var result = meta.Proceed();
watch.Stop();
if(meta.Target.Method.ReturnType.Is(SpecialType.Void))
{
Log.Information(
"Leaving {MethodName} having taken {elapsedTime}ms.",
methodName,
watch.ElapsedMilliseconds);
} else
{
Log.Information(
"Leaving {MethodName} and returning this result: {result} having taken {elapsedTime}ms.",
methodName,
result,
watch.ElapsedMilliseconds);
}
return result;
} catch(Exception e)
{
watch.Stop();
Log.Error(
"An error has occured in {MethodName} with the following message {stack}",
methodName,
(e.Message, e.StackTrace));
throw;
}
}
#endregion
}Let’s begin our examination of this with the method signature itself which inherits from Aspect and implements the Interface IAspect<IMethod> (which itself requires the implementation of IEligibilityBuilder<IMethod>).
Next is the BuildAspect method. In essence this is performing a relatively simple check. If the method that is being looked at is not decorated with the [NoLog] attribute then go ahead and add the code required to implement logging to the method via the OverrideMethod method, which in this case is pretty much a carbon copy of our previous LogAttribute code.
Now for our immediate purposes this is sufficient but if you take some time to examine the Metalama documentation and samples you’ll see that you have just been introduced to some of the more powerful things that Metalama is capable of doing.
Save and build the project, and if creating a NuGet Package do that as well and push it to your NuGet Server.
Open Visual Studio 2022 and create a new XAF application using the XAF wizard. For the sake of simplicity base it on .Net core, Windows and XPO selecting basic security.
Then add a simple Customer Business object.
And finally add our newly created NuGet Logging package.
Because we are using Serilog we are going to need to configure the logger. Add the following NuGet Packages to the XafLogging.Win project.
The additional NugetPackages, over and above those we used in the second part of this series will allow us to add additional information to the structured logging (especially in Seq) and also allow us to use a Json configuration file for the logger which can be configured outside the application in the runtime environment.
In the XafLogging.Win project add a new text file called AppSettings.json. set its ‘Copy To Output Directory’ property to Copy Always. Add the following to the file.
{
"Serilog": {
"Using": [],
"MinimumLevel": "Debug",
"WriteTo": [
{
"Name": "File",
"Args": { "path": "Logs/log.txt" }
},
{
"Name": "Seq",
"Args": { "serverUrl": "http://localhost:5341" }
}
],
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ,"WithProcessId"],
"Properties": {
"Application": "Real World Logging with external configuration"
}
}
}Now open up the Program.cs file in the same project and amend the Main method it to resemble the code below.
[STAThread]
static void Main()
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile(path: "AppSettings.json", optional: false, reloadOnChange: true)
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
try
{
DevExpress.ExpressApp.FrameworkSettings.DefaultSettingsCompatibilityMode = DevExpress.ExpressApp.FrameworkSettingsCompatibilityMode.Latest;
#if EASYTEST
DevExpress.ExpressApp.Win.EasyTest.EasyTestRemotingRegistration.Register();
#endif
WindowsFormsSettings.LoadApplicationSettings();
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
DevExpress.Utils.ToolTipController.DefaultController.ToolTipType = DevExpress.Utils.ToolTipType.SuperTip;
if(Tracing.GetFileLocationFromSettings() ==
DevExpress.Persistent.Base.FileLocation.CurrentUserApplicationDataFolder)
{
Tracing.LocalUserAppDataPath = Application.LocalUserAppDataPath;
}
Tracing.Initialize();
XafLoggingWindowsFormsApplication winApplication = new XafLoggingWindowsFormsApplication();
winApplication.GetSecurityStrategy().RegisterXPOAdapterProviders();
if(System.Configuration.ConfigurationManager.ConnectionStrings["ConnectionString"] != null)
{
winApplication.ConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings[
"ConnectionString"].ConnectionString;
}
#if EASYTEST
if(ConfigurationManager.ConnectionStrings["EasyTestConnectionString"] != null) {
winApplication.ConnectionString = ConfigurationManager.ConnectionStrings["EasyTestConnectionString"].ConnectionString;
}
#endif
#if DEBUG
if(System.Diagnostics.Debugger.IsAttached &&
winApplication.CheckCompatibilityType == CheckCompatibilityType.DatabaseSchema)
{
winApplication.DatabaseUpdateMode = DatabaseUpdateMode.UpdateDatabaseAlways;
}
#endif
try
{
winApplication.Setup();
winApplication.Start();
} catch(Exception e)
{
winApplication.StopSplash();
winApplication.HandleException(e);
}
} finally
{
Log.CloseAndFlush();
}
}Amend the App.Config file Connection string to match that of whichever database you intend to use., then build and run the application.
If you had opted for XAF security then you will by now have experienced an error in the ApplicationUser Class in the main module at the CreateUserLoginInfo method. Simply decorate this method with the [NoLog] attribute and build and run the solution. This time it will run successfully and if, once you have closed it you examine the bin folder of the startup project you will find a logs directory and inside of that a detailed log.