If you look into tracing for the old CrmServiceClient and the XRM Tooling you come across documentation which refers to setting some switches in a web.config file. If you have done only modern .NET Core development you might be like web.config what?! With the documentation a little old and if you are using the new CdsServiceClient as part of the alpha SDK or even the old CrmServiceClient with .NET Core you need to be aware of how to set tracing with the .NET Core logging API and without a web.config.
Set the tracing in .NET Core projects:
Found within Microsoft.PowerPlatform.Cds.Client you will find TraceControlSettings and within there a property called TraceLevel. Set this to any of the enumeration values found in System.Diagnostics.SourceLevels and it will set the tracing of the CdsServiceClient instances instantiated thereafter.
Microsoft.PowerPlatform.Cds.Client.TraceControlSettings.TraceLevel =
System.Diagnostics.SourceLevels.All;
Available enumerations of System.Diagnostics.SourceLevels are: All, Off, Critical, Error, Warning, Information, Verbose, ActivityTracing.
Setup tracing to work using the .NET Core logging API (ILogger):
You have set the tracing level but now how to get it written to the application logs.
CRM Tip of the Day covered this back in 2017 with Azure Functions which were based on .NET Core 1.0 and the pattern still applies today.
Basically the tracing in CrmServiceClient and CdsServiceClient uses the old System.Diagnostics.TraceListener. Not so plug and play with the .NET Core logging ILogger which is the standard logging found in Microsoft.Extensions.Logging.
So what you can do is create a wrapping class that inherits from TraceListener and has an ILogger property, then override the abstract methods of TraceListener and pump out the messages with the ILogger property.
public class LoggerTraceListener : TraceListener
{
private readonly ILogger _logger;
public LoggerTraceListener(string name, ILogger logger) : base(name)
{
_logger = logger;
}
public override void Write(string message)
{
_logger.LogInformation(message);
}
public override void WriteLine(string message)
{
_logger.LogInformation(message);
}
}
With our new TraceListener based class we can add it as a trace listener to the TraceControlSettings of Microsoft.PowerPlatform.Cds.Client and pass an available ILogger to it.
TraceControlSettings.AddTraceListener(new LoggerTraceListener(
"Microsoft.PowerPlatform.Cds.Client",
logger);
Setup tracing with ASP.NET Core:
What is so different about ASP.NET Core you might ask. Not much other than the logging API isn’t available in version 3.x ConfigureServices, which is where you would typically add the CdsServiceClient.
The documentation for 3.0+ states that logger injection into Startup constructor and Startup.ConfigureServices is not supported. Logging isn’t actually available until the Configure method as a parameter. In the previous post I covered adding CdsServiceClient within the ConfigureServices, so how can we getting logging enabled before registering and instantiating CdsServiceClient.
To do so we configure a service in ConfigureServices that depends on ILogger. By doing so it makes it available to that new service. Our class previously of LoggerTraceListener depends on ILogger so we just register LoggerTraceListener as a singleton service while getting ILogger from the Service Provider (see sp.GetRequiredService<ILogger>()
). Then it is available to be added as a TraceListener.
Register LoggerTraceListener in ConfigureServices:
services.AddSingleton(sp =>
new LoggerTraceListener(
"Microsoft.PowerPlatform.Cds.Client",
sp.GetRequiredService<ILogger<LoggerTraceListener>>())
);
Get LoggerTraceListener to use in AddTraceListener:
TraceControlSettings.AddTraceListener(
services.BuildServiceProvider().GetRequiredService<LoggerTraceListener>());
Improving the AddCdsServiceClient extension method with logging:
This can be added to AddCdsServiceClient extension method from the previous blog post about setting up CdsServiceClient with ASP.NET Core, as well extend options definition so that tracing can be something that is set from the appsettings.
To do this and setup so we don’t have to build the service provider (BuildServiceProvider()
) as shown in the previous code snippet during the configure services we move the logging initiation to the CdsServiceClientWrapper class and make ILogger a required parameter as suggested by the documentation of using ILogger in ConfigureServices. Since we are already registering that class why not also make it dependent on ILogger so it is injected and handles logging.
Updated CdsServiceClientWrapper class:
public class CdsServiceClientWrapper
{
public readonly CdsServiceClient CdsServiceClient;
public CdsServiceClientWrapper(string connectionString, ILogger<CdsServiceClientWrapper> logger, string traceLevel = "Off")
{
TraceControlSettings.TraceLevel =
(System.Diagnostics.SourceLevels)Enum.Parse(
typeof(System.Diagnostics.SourceLevels), traceLevel);
TraceControlSettings.AddTraceListener(
new LoggerTraceListener(
"Microsoft.PowerPlatform.Cds.Client", logger
)
);
CdsServiceClient = new CdsServiceClient(connectionString ?? throw new ArgumentNullException(nameof(connectionString)));
}
}
You can see added constructor parameters for ILogger and the trace level. Then creating the LoggerTraceListener in the constructor and setting it as the TraceListener prior to creating CdsServiceClient.
Updated AddCdsServiceClient extension:
public static void AddCdsServiceClient(this IServiceCollection services, Action<CdsServiceClientOptions> configureOptions)
{
CdsServiceClientOptions cdsServiceClientOptions = new CdsServiceClientOptions();
configureOptions(cdsServiceClientOptions);
services.AddSingleton(sp =>
new CdsServiceClientWrapper(
cdsServiceClientOptions.ConnectionString,
sp.GetRequiredService<ILogger<CdsServiceClientWrapper>>(),
cdsServiceClientOptions.TraceLevel)
);
services.AddTransient<IOrganizationService, CdsServiceClient>(sp =>
sp.GetService<CdsServiceClientWrapper>().CdsServiceClient.Clone());
if (cdsServiceClientOptions.IncludeOrganizationServiceContext)
{
services.AddTransient(sp =>
new OrganizationServiceContext(sp.GetService<IOrganizationService>()));
}
}
Updated CdsServiceClientOptions:
public class CdsServiceClientOptions
{
/// <summary>
/// <see cref="CdsServiceClient"/> constructors for connection string
/// </summary>
public string ConnectionString { get; set; }
/// <summary>
/// Parameter to allow for transient OrganizationServiceContext service based on Clone
/// of singleton CdsServiceClient.
/// </summary>
/// <see cref="OrganizationServiceContext"/>
public bool IncludeOrganizationServiceContext { get; set; }
/// <summary>
/// Define a Trace Level for the CdsServiceClient
/// Values are: All, Off, Critical, Error, Warning, Information, Verbose, ActivityTracing
/// </summary>
/// <see cref="System.Diagnostics.SourceLevels"/>
public string TraceLevel { get; set; }
}
Updated appsettings.json parameters:
"CdsServiceClient": {
"ConnectionString": "",
"IncludeOrganizationServiceContext": true,
"TraceLevel": "Off"
}
All of this is updated and included in CdsWeb an ASP.NET Core 3.1 project available in the following Git repo:
One thought on “Tracing CdsServiceClient in .NET Core and ASP.NET Core”