Create Sitecore items using PowerShell

You can use the Sitecore PowerShell Extensions for many trivial tasks. This script creates a “components” folder below all items from a root item:

// Get the root item
$root = Get-ChildItem -Path "master:/sitecore/content/site/root"
// Loop through all children of the root item
foreach ($item in $root) {
    // Skip if the folder already exists
    if (@(Get-Item -Path "master:" -Query "$($item.ItemPath)/components").count -gt 0) {
	    Continue
    // Create new item
	} else {
        New-Item -Path $item.ItemPath -Name "components" -ItemType "/sitecore/templates/Common/Folder"
    }
}

This script was created by my colleague Adam Honore, when I realized that I had forgot to include a “components” folder in my Sitecore master – after I had created my 100+ items. Thanks to Adam for the help.

MORE TO READ:

Advertisements
Posted in Sitecore, Sitecore 7, Sitecore 8, Sitecore 9 | Tagged , | Leave a comment

Sitecore get Context from fully qualified URL

One of the basic Sitecore concepts is the concept of a context. The context is resolved from the HTTP Context to Sitecore and determines the database, language, user domain, content start item etc. The context is defined in the App_Config/Sitecore.config configuration file in the <sites> section:

<sites>
  <site name="shell" ... rootPath="/sitecore/content" startItem="/home" language="en" contentLanguage="en" database="core" domain="sitecore" ... />
  ...
  <site name="website_en" ... hostName="www.site.com|www.site.eu|www.site.co.uk" language="en" database="web" domain="extranet" ... />
  <site name="website_da" ... hostName="www.site.dk" language="da" database="web" domain="extranet" ... />
</sites>

The context is resolved from a first-match algorithm where Sitecore goes through the list of sites and finds the first site that matches the criteria.

You can go through the sites list yourself using the Sitecore.Configuration.Factory.GetSiteInfoList() method. So with a little guesswork you can get the context from any URL, assuming that:

  • The url must be fully qualified (i.e. https://www.site.com/url)
  • The site section contains the URL in the hostName property
using Sitecore.Configuration;
using Sitecore.Sites;
using Sitecore.Web;

public static SiteContext GetSiteContext(Uri url)
{
  string hostName = url.Host;

  foreach (SiteInfo site in Factory.GetSiteInfoList())
  {
    foreach(string sitecoreHostName in site.HostName.Split('|'))
    {
      if (string.Equals(sitecoreHostName, hostName, StringComparison.InvariantCultureIgnoreCase))
      {
        return new SiteContext(site);
      }
    }
  }

  return null;
}

WHAT IS THE HostName VS TargetHostName?

Sitecore uses 2 definitions to determine URL’s:

  • The hostName is the incoming URL. You can define a list of incoming URL’s in a pipe separated list, and each URL will be resolved to that site context.
  • The targetHostName is the outgoing URL and determines which URL to create when asking for a fully qualified URL.

MORE TO READ:

Posted in .net, c#, General .NET, Sitecore 6, Sitecore 7, Sitecore 8, Sitecore 9 | Tagged , , , | Leave a comment

Deserialize XML array to string[] and how to allow JSON to be deserialized into the same POCO class

How do you deserialize the classic XML array to an string[] type?

The XML representation of an array is a list of elements:

<?xml version="1.0" encoding="UTF-8"?>
<root>
   <MyXml>
      <Id>657467</Id>
	  <MyArray>
	    <element>A</element>
	    <element>B</element>
	    <element>C</element>
	  </MyArray>
   </MyXml>
</root>

And the MyArray would normally be deserialized to an annoying class:

// I don't want this class
[XmlRoot(ElementName="MyArray")]
public class MyArray 
{
  [XmlElement(ElementName="element")]
  public List<string> Element { get; set; }
}

// I Just want a string array:
public string[] MyArray { get; set; }

The solution is to use the XmlArray and XmlArrayItem attributes. These properties specifies that the XmlSerializer must serialize the XML structure as an array:

[XmlArray(ElementName = "MyArray", IsNullable = true)]
[XmlArrayItem(ElementName = "element", IsNullable = true)]
public string[] Myarray 
{
  get; set;
}

The XmlArray attribute determines that the MyArray XML element is an array, and the XmlArrayItem attribute determines that each array element is called element in the XML.

Now, the XmlSerializer will serialize the element to an array:

string contents = "this is my xml string";

XmlSerializer serializer = new XmlSerializer(typeof(MyXml));
using (TextReader reader = new StringReader(contents))
{
  var myclass = (MyXml)serializer.Deserialize(reader);
}

SO WHY IS THIS IMPORTANT?

This is important if you import XML and JSON, because you would rather not have to define this JSON just to cope with the XML array format:

"MyArray": {
  "element": [
    "A",
    "B",
    "C"
  ]

Instead, if you just decorate your POCO class with both XML and JSON properties:

[JsonProperty(PropertyName = "MyArray")]
[XmlArray(ElementName = "MyArray", IsNullable = true)]
[XmlArrayItem(ElementName = "element", IsNullable = true)]
public string[] Myarray 
{
  get; set;
}

You can use the proper JSON array notation:

"MyArray": [    
    "A",
    "B",
    "C"
]

MORE TO READ:

Posted in .net, c#, General .NET | Tagged , , , , | Leave a comment

Read from Azure Queue with Azure WebJobs 3.0 and .NET Core

From WebJobs 2.0 to WebJobs 3.0 Microsoft have completely rewritten the way Azure WebJobs is used. The reasons are probably noble, but they require you to redo your work when upgrading. So I made this template that allows me to start up a Azure WebJob in .NET Core.

STEP 1: THE PROJECT

The project is a .NET Core Console Application.

STEP 2: THE NUGET PACKAGES

These packages change with the next upgrade but for WebJobs 3.0 you will need these packages:

<ItemGroup>
  <PackageReference Include="WindowsAzure.Storage" Version="9.3.3" />
  <PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.5" />
  <PackageReference Include="Microsoft.Azure.WebJobs.Extensions" Version="3.0.2" />
  <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="3.0.4" />
  <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
  <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.0" />
  <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
  <PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
  <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.2.0" />
  <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
</ItemGroup>

STEP 3: THE APPSETTINGS.JSON

Remember to include your AzureWebJobsStorage connection string in the appsettings.json:

{
  "ConnectionStrings": {
    "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=xxx;AccountKey=xxx=="
  }
}

STEP 4: THE PROGRAM.CS

This program.cs will start your console application as a Azure Webjob:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace FeedTestEventReceiver
{
  class Program
  {
    public static void Main(string[] args)
    {
      var host = new HostBuilder()
        .ConfigureServices(
        (hostContext, services) =>
        {
          services.Configure<HostOptions>(
            option =>
            {
              option.ShutdownTimeout = System.TimeSpan.FromSeconds(20);
            }
          );
        }
        )
        .ConfigureLogging( (context, b) => { b.AddConsole(); })
        .ConfigureWebJobs( c => { c.AddAzureStorage(); } 
        )
        .ConfigureServices( (context, services) => 
        {
          services.AddTransient<QueueService, QueueService>();
        }
      )
      .Build();

      host.RunAsync().Wait();
    }

  }
}

The major differences between 2.0 and 3.0 are:

  • The HostBuilder is used to configure the WebJob
  • Your Webjob endpoints are now configured using ConfigureServices. You  will need to change the highlighted line of code and include your own endpoint.

STEP 5: THE QUEUSERVICE

This is just an example on how to implement a WebJob endpoint. This class is added in the AddTransient line of code in the program.cs and will listen to a queue for events:

using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage;

namespace FeedTestEventReceiver
{
  public class QueueService
  {
    public async Task ReadFromQueue([QueueTrigger("queuename")] string message, ILogger log)
    {
        log.LogInformation($"{message}");
    }
  }
}

STEP 6: THE TEST

To test the WebJob, simply start the console application with F5, and the WebJob will run locally listening for events int the specified queue:

Azure WebJob

Azure WebJob

MORE TO READ:

 

Posted in .NET Core, c# | Tagged , , , | Leave a comment

Read blob file from Microsoft Azure Storage with .NET Core

In order to read a blob file from a Microsoft Azure Blob Storage, you need to know the following:

  • The storage account connection string. This is the long string that looks like this:
    DefaultEndpointsProtocol=https;
    AccountName=someaccounfname;
    AccountKey=AVeryLongCrypticalStringThatContainsALotOfChars==
  • The blob storage container name. This is the name in the list of “Blobs”.
  • The blob file name. This is the name of the blob inside the container. A file name can be in form of a path, as blobs are structured as a file structure inside the container. For ecample: folder/folder/file.extension

You also need this NuGet package:

Windows.Azure.Storage

The code is pretty simple:

using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;

public string GetBlob(string containerName, string fileName)
{
  string connectionString = $"yourconnectionstring";

  // Setup the connection to the storage account
  CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);
  
  // Connect to the blob storage
  CloudBlobClient serviceClient = storageAccount.CreateCloudBlobClient();
  // Connect to the blob container
  CloudBlobContainer container = serviceClient.GetContainerReference($"{containerName}");
  // Connect to the blob file
  CloudBlockBlob blob = container.GetBlockBlobReference($"{fileName}");
  // Get the blob file as text
  string contents = blob.DownloadTextAsync().Result;
  
  return contents;
}

The usage is equally easy:

GetBlob("containername", "my/file.json");

MORE TO READ:

Posted in .NET Core, Microsoft Azure | Tagged , | Leave a comment

Sitecore Pipelines – The great walkthrough

I have previously stated my enthusiasm for the Sitecore Pipeline concept. This article describes how to create your own, how to add parameters, and which properties to use.

WHAT IS A SITECORE PIPELINE?

It is best described as a simple form of dependency injection pattern, or maybe more of a config-file-configurable method chaining pattern. The idea is, that if you have a task to perform, and this task requires several independent sub-tasks to run in sequence, you can split these sub-tasks into separate methods (called processors), and then run them in sequence using the same parameters for each method.

The best known Sitecore pipeline is the httpRequestBegin pipeline that runs for every request:

<httpRequestBegin>
  <processor type="Sitecore.Pipelines.PreprocessRequest.CheckIgnoreFlag, Sitecore.Kernel" />
  ..
  <processor type="Sitecore.Pipelines.HttpRequest.SiteResolver, Sitecore.Kernel" />
  <processor type="Sitecore.Pipelines.HttpRequest.UserResolver, Sitecore.Kernel" />
  ...
  <processor type="Sitecore.Pipelines.HttpRequest.ExecuteRequest, Sitecore.Kernel" />
</httpRequestBegin>

WHY NOT JUST MAKE ONE BIG METHOD? OR ONE CLASS THAT USES DEPENDENCY INJECTION?

Pipelines give you the following advantages:

  • Extensibility: You can easily add another processor to any pipeline, thus adding to the functionality of that task.
  • Replaceability: You can easily replace any processor with one of your own, replacing the way Sitecore works.
  • Testability: Pipelines allow you to break your code into very small processors. Any change of one processor does not interfere with any of the other processors, making testing easier.
  • It’s easy: It’s really easy to work with. It’s standard .NET code.

ENOUGH SMALL TALK, LETS MAKE A PIPELINE:

A pipeline consists of a class or Arguments, inheriting from Sitecore.Pipelines.PipelineArgs, and a configuration section that describes which tasks (processors) to execute in which sequence.

The arguments class is usually a POCO class with getters and setters:

public class MyArgs : PipelineArgs
{
  public string Argument1
  {
    get; set;  
  }

  public int Argument2
  {
    get; set;
  }
}

The configuration section determines the pipeline name, and which processors to run:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:env="http://www.sitecore.net/xmlconfig/env/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
  <sitecore>
    <pipelines>
      <my_pipeline>
        <processor type="MyCode.MyPipeline.MyProcessor1, MyDll"/>
        <processor type="MyCode.MyPipeline.MyProcessor2, MyDll"/>
        <processor type="MyCode.MyPipeline.MyProcessor3, MyDll"/>
      </my_pipeline>
    </pipelines>
  </sitecore>
</configuration>

HOW DOES THE PROCESSOR LOOK LIKE?

The processor needs a method called “Process” that takes one argument, “args” of the “MyArgs” type:

namespace MyCode.MyPipeline
{
  public class MyProcessor1
  {
    public void Process(MyArgs args)
    {
      try
      {
        DoSomething(args.Argument1);
        args.Argument2 = DoAnotherThing();
      }
      catch (Exception ex)
      {
        args.AddMessage("An error occurred");
        args.AbortPipeline();
      }
    }
  }
}

HOW TO EXECUTE A PIPELINE:

The CorePipeline.Run() method executes a pipeline:

MyArgs args = new MyArgs();
args.Argument1 = "my string";
args.Argument2 = 42;

CorePipeline.Run("my_pipeline", args);
if (args.Aborted)
{
  foreach (var message in args.GetMessages())
    Log.Error(message.Text, this);
}

HOW TO ADD PARAMETERS TO A PIPELINE:

If you add a subitem to the configuration:

<processor type="MyCode.MyPipeline.MyProcessor1, MyDll">
  <MyParameter>value</MyParameter>
</processor

This subitem will by magic appear in any property of the same name in the processor:

namespace MyCode.MyPipeline
{
  public class MyProcessor1
  {
    public string MyParameter { get; set; }
	
	public void Process(MyArgs args)
    {
	   // value of MyParameter is "value" 
    }
  }
}

In fact the same advanced configuration rules apply to pipelines as all other Sitecore items.

PROCESSOR ATTRIBUTES:

You can decorate your processors with attributes:

<processor type="MyCode.MyPipeline.MyProcessor1, MyDll" method="MyMethod" name="MyName" runIfAborted="false" resolve="false" reusable="false"/>
  • type: The class to run
  • method: The method name to run. If not included, you use the name “Process”
  • name: Just a descriptive name
  • runIfAborted: Processors with this attribute set to true will run even if the pipeline is aborted. This is useful as a cleanup property if something went wrong.
  • resolve: Set this property to true to allow the Sitecore Dependency Injection engine to resolve the type name to run.
  • reusable: This property allows your processor to be run in a transient scope.

GOOD PRACTISES:

  • If you throw an exception in any processor, the pipeline will stop its execution, and the CorePipeline.Run() also throws an exception. This means that your “runIfAborted=true” processor will not run. You should therefore catch any exception in the processor, and use the args.AbortPipeline() method to control execution flow.
  • Make your processors as atomic as possible. Some of my best processors consists of one line of code. Your processor should do one task and one task only.
  • You can debug the performance of your processors, but this slows down the performance of all pipelines, so use this with caution.
  • Processors are only created once and kept alive for the entire application lifetime. Any constructors are only called once and local variables are not reset, so treat your processors as a stateless piece of code.
    This is true unless you use the reusable=”false” property, but beware that object construction takes time and this feature will slow down your pipeline.
  • Any configuration error will result in an exception when CorePipeline.Run() is called. If the method or class is not found for one processor, the whole pipeline is not executed.

MORE TO READ:

Posted in .net, c#, General .NET, Sitecore 5, Sitecore 6, Sitecore 7, Sitecore 8, Sitecore 9 | Tagged , , , , , , , | Leave a comment

Your session ID is being reused and there is nothing you can do about it

In many years we have been used to the fact that the ASP.Net session ID uniquely identifies one session, and one session only. Back in 2006, Margaret Rouse from TechTarget even wrote in the definition of a session ID that:

Every time an Internet user visits a specific Web site, a new session ID is assigned. Closing a browser and then reopening and visiting the site again generates a new session ID. However, the same session ID is sometimes maintained as long as the browser is open, even if the user leaves the site in question and returns. In some cases, Web servers terminate a session and assign a new session ID after a few minutes of inactivity.

Margaret Rouse, TechTarget

But a new feature in modern browsers changes this paradigm. If you in the Google Chrome browser selects the “Continue where you left off” feature:

Google Chrome Setting

Google Chrome Setting

… the browser will remember the session ID forever, even when a new session is started.

EXAMPLE:

I have enabled the “Continue where you left off” feature in my Google Chrome.
This is my current session ID.

Current Session ID

Current Session ID

For the sake of this test, my session will expire in 1 minute. I am using a SQL server as my session provider, so I can find my session:

Session in SQL

Session in SQL

My ASP.Net Global.asax Session_Start() event is fired when my session is created:

protected void Session_Start(object sender, EventArgs e)
{
  // do stuff
}

I now close my browser, and wait 1 minute. After 1 minute of inactivity, my session expires, and the session is removed from the Session state provider:

Session Provider is empty

Session Provider is empty

When reopen my browser, I would have expected my session ID to be recreated. But no, a new session with the same session ID is created:

New session created with same ID

New session created with same ID

ASP.Net will fire the Global.asax Session_Start() event as expected, since a new session have been created.

WHY IS THIS IMPORTANT TO KNOW?

This behavior means that you cannot use the session ID to identify one user session. Any data collection cannot rely on the session ID as the unique identifier. Collecting anonymous user statistics based on session ID will become skewed because you will have multiple session_start events for one session ID.

As an edge case, we risk that the session ID will no longer be considered as anonymous. The GDPR rules clearly states that any user-identifiable data cannot be collected without the consent of the user. When the session ID does not change, one could argue that the session ID is user identifiable just as the IP address or the user name, because it will live on the user machine forever.

WHAT CAN YOU DO ABOUT IT?

It is possible to generate a new session ID, but it requires to do some postbacks, so this solution is only feasible as a method to generate a new session ID when you log in, or when a user have finished a shopping transaction.

This StackOverflow thread discusses the possibilities of creating a new session ID.

If you plan on using the session ID as a data collection key, don’t. Instead, create your own key, and use that. For ASP.Net solutions that have a Global.asax file, you can create a new “session key” every time a new session starts:

protected void Session_Start(object sender, EventArgs e)
{
  HttpContext.Current.Session.Add("s_id", Guid.NewGuid().ToString());
}

In your data collection methods, replace SessionID with the custom property:

public static void Collect(HttpContext context, string behavior)
{
  if (context != null && context.Session != null)
  {
    // Don't do this
    context.Session.SessionID;
    // Use this instead
    context.Session["s_id"].ToString()
  }
}

MORE TO READ:

 

Posted in .net, General .NET | Tagged , , , | Leave a comment

Sitecore disable automatic lock on create items

From Sitecore 9, Sitecore changed the default behavior when creating items in the content editor. As per default, any item is locked when created, and will only be unlocked if the user unlocks it, or the user saves the item, and the AutomaticUnlockOnSaved config property is true.

Item is always locked on create

Item is always locked on create

Unfortunately, Sitecore forgot to include a DoNotLockOnCreate property, so it is impossible to get rid of this feature – unless we do some coding.

This piece of code is a collaboration between Sitecore Support and myself, and will work ONLY WHEN CREATING ITEMS FROM TEMPLATES. Items created based on a branch will stay locked.

STEP 1: EXTEND THE addFromTemplate pipeline

The addFromTemplate pipeline runs every time an item is created. We add a new processor that will create the new item, and then call the item:checkin command to unlock the item.

using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.ItemProvider.AddFromTemplate;
using System;

namespace MyCode
{
  public class UnlockItem : AddFromTemplateProcessor
  {
    public override void Process(AddFromTemplateArgs args)
    {
      Assert.IsNotNull(args.FallbackProvider, "FallbackProvider is null");

      try
      {
        var item = args.FallbackProvider.AddFromTemplate(args.ItemName, args.TemplateId, args.Destination, args.NewId);
        if (item == null)
          return;
        Context.ClientPage.SendMessage(this, "item:checkin");
        args.ProcessorItem = args.Result = item;
      }
      catch (Exception ex)
      {
        Log.Error(this.GetType() + " failed. Removing item " + args.NewId, ex, this);
        var item = args.Destination.Database.GetItem(args.NewId);
        item?.Delete();
        args.AbortPipeline();
      }
    }
  }
}

STEP 2: ADD THE PROCESSOR TO THE addFromTemplate pipeline

<sitecore>
  <pipelines>
    <group>
      <pipelines>
        <addFromTemplate>
          <processor type="MyCode.UnlockItem, MyDll"/>
        </addFromTemplate>
      </pipelines>
    </group>
  </pipelines>
</sitecore>

The code will now run every time you create a new item, unlocking the item immediately after. As stated before, this hack will only work when creating new items based on templates. Items created from a branch will keep a lock on the top branch item.

MORE TO READ:

Posted in .net, c#, General .NET, Sitecore 9 | Tagged , , , | 2 Comments

Sitecore delete old item versions

Are your Sitecore editors version-trigger-happy? Is your master database items drowning in old versions that will never be used – or is just exact replicas of the previous version?

Many versions of the same item

Many versions of the same item

Fear not, you can in fact – with a little bit of code of course – recycle versions of items.

This tiny class will recursively run through items from the root of your choice, and recycle versions, except those you wish to keep:

namespace MyCode
{
  private class Cleanup
  {
    public void CleanUpItemVersions(Item item, int maxVersionCount)
    {
      int latestVersionNumber = item.Versions.GetLatestVersion().Version.Number;
      if (latestVersionNumber > maxVersionCount)
      {
        int latestVersionToKeep = latestVersionNumber - maxVersionCount;
        RecycleVersions(item, latestVersionToKeep);
      }

      foreach (Item child in item.Children)
      {
        CleanUpItemVersions(child, maxVersionCount);
      }
    }

    private static void RecycleVersions(Item item, int latestVersionToKeep)
    {
      Item[] versions = item.Versions.GetVersions();
      foreach (Item version in versions)
      {
        if (version.Version.Number <= latestVersionToKeep)
        {
          version.RecycleVersion();
        }
      }
    }
  }
}

USAGE:

This method will clean all but the latest 3 version from all english items below /sitecore/content/home:

publicvoid CleanUpItemVersions()
{
  Database master = Database.GetDatabase("master");
  Item rootItem = master.GetItem("(sitecore/content/home", LanguageManager.GetLanguage("en"));
  var cleanup = new Cleanup();
  cleanup.CleanUpItemVersions(rootItem, 3);
}

MORE TO READ:

 

Posted in .net, c#, General .NET, Sitecore 5, Sitecore 6, Sitecore 7, Sitecore 8, Sitecore 9 | Tagged , , | 2 Comments

Serilog – Add thread ID to the log file

Serilog is more or less the default file logger when developing .NET Core applications. The simple logger can easily be modified to – for example – add the thread ID.

FIRST THINGS FIRST: THE SERILOG NUGET PACKAGES

You need these packages in order for Serilog to appear before you:

Serilog.AspNetCore
Serilog.Sinks.File

STEP 1: ADD SERILOG TO THE LOGGING:

You need to add Serilog when initializing the services:

private static IServiceProvider InitializeServiceCollection()
{
  var services = new ServiceCollection()
    .AddLogging(configure => configure
      .AddSerilog()
      .AddConsole()
    )
    .BuildServiceProvider();
  return services;
}

STEP 2: CREATE A ILOGEVENTENRICHER

The ILogEventEnricher is the Serilog way of adding properties that can be used when logging:

public class ThreadIDEnricher : ILogEventEnricher
{
  public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
  {
    logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
      "ThreadID", Thread.CurrentThread.ManagedThreadId.ToString("D4")));
  }
}

STEP 3: ADD THE ENRICHER AND USE IT WHEN LOGGING:

public static void Initialize(IServiceProvider services)
{
  string logFileName = "log.log";

  Serilog.Log.Logger = new LoggerConfiguration()
    // Adding the enricher
    .Enrich.With(new ThreadIDEnricher())
    .WriteTo.File("log.log", 
                  rollingInterval: RollingInterval.Day, 
                  // Adding the threadID with {ThreadID} to the output string
                  outputTemplate: "{Timestamp:HH:mm:ss.fff} ({ThreadID}) {Message:lj}{NewLine}{Exception}")
    .CreateLogger();
}

THE USAGE:

// Replace "Program" with your own instance name
var logger = services.GetService<ILoggerFactory>().CreateLogger<Program>();
logger.LogError("This is an error");

The output will look like this, with the thread ID in parenthesis just after the timestamp:

11:22:48.149 (0001) Application.Started
11:22:48.232 (0001) An error happened.
11:22:49.236 (0002) Another error happened.
11:22:49.239 (0003) Application.Ended

MORE TO READ:

Posted in .NET Core, c#, Microsoft Azure | Tagged , , | Leave a comment