C# Lists in Lists – Getting all inner values, or the unique inner values

Getting the inner value of a list inside a list can seem complicated in C#, but LINQ makes it easier. But first let’s make a list in a class, and then make a list of the class with the list of classes – you get the drift:

using System;
using System.Collections.Generic;
using System.Linq;

public class OuterClass 
{
	public IEnumerable<InnerClass> InnerClasses { get; set; }
}

public class InnerClass
{
	public string InnerValue { get; set; }	
}

Then let’s make a list of the OuterClass, and assign some values:

// The starting point: A list of OuterClasses
List<OuterClass> outerClasses = new List<OuterClass>();

// Make a list of InnerClasses, and add 3 values, A,B,C
List<InnerClass> innerClasses = new List<InnerClass>();
innerClasses.Add(new InnerClass() { InnerValue = "A" });
innerClasses.Add(new InnerClass() { InnerValue = "B" });
innerClasses.Add(new InnerClass() { InnerValue = "C" });

// Add a OuterClasses, each with a list of 3 InnerClasses
outerClasses.Add(new OuterClass() { InnerClasses = innerClasses });
outerClasses.Add(new OuterClass() { InnerClasses = innerClasses });
outerClasses.Add(new OuterClass() { InnerClasses = innerC

Now we have a list of 3 OuterClasses, and each OuterClass have a list of 3 InnerClasses, a total of 9 items, and a list of InnerValue like this: A,B,C,A,B,C,A,B,C

THE NON-LINQ WAY:

Without LINQ, you should make 2 loops:

foreach (var outerClass in outerClasses)
{
	foreach (var innerClass in innerClasses)
	{
		Console.WriteLine(innerClass.InnerValue);	
	}
}

This returns A,B,C,A,B,C,A,B,C

THE LINQ WAY:

Use the SelectMany to get all InnerClasses:

var allInnerClasses = outerClasses.SelectMany(o => o.InnerClasses);
foreach (var innerClass in allInnerClasses)
{
	Console.WriteLine(innerClass.InnerValue);	
}

This also returns A,B,C,A,B,C,A,B,C

GETTING ONLY THE UNIQUE VALUES:

To return only the unique values, do this:

var allUniqueInnerValues = outerClasses.SelectMany(o => o.InnerClasses)
	.GroupBy(i => i.InnerValue)
	.Select(i => i.First())
	.Select(i => i.InnerValue);
	
foreach (var innerValue in allUniqueInnerValues)
{
	Console.WriteLine(innerValue);
}

This will return only A,B,C.

MORE TO READ:

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

Sitecore create custom Content Editor Warnings using Conditions and Rules

The Content Editor Warnings are these yellow boxes that hover over your content in the Content Editor in Sitecore:

Content Editor Warning
Content Editor Warning

In the old days we would make them by hooking into the getContentEditorWarnings pipeline. Nowadays we use the Sitecore Rules engine. So to make your own Content Editor Warning you need to:

  • Create a custom Condition.
  • Create the code for the custom Condition
  • Create a new Rule

STEP 1: CREATE A CUSTOM CONDITION

Custom Condition
Custom Condition

Conditions are placed under:

/sitecore/system/Settings/Rules/Definitions/Elements

You can create your own folder in this structure and create a “Condition“. The “Text” field should contain the text displayed in the Rule editor, and the “Type” field should contain the reference to the class containing the code, for example:

MyCode.Rules.MyRule, MyDll

STEP 2: CREATE THE CODE

A condition inherits from a RuleContext. Rules can return different things, my return true or false, but you can create rules that return values like integers or strings.

This is my Rule:

using Sitecore.Diagnostics;
using Sitecore.Data.Items;
using Sitecore.Rules;
using Sitecore.Rules.Conditions;

namespace MyCode.Rules
{
  public class MyRule<T> : WhenCondition<T> where T : RuleContext   
  {
    protected override bool Execute(T ruleContext)
    {
      Assert.ArgumentNotNull(ruleContext, "ruleContext");
      // This is them item being clicked on
	  Item item = ruleContext.Item;
	  // Pseudocode, imagine that you are checking
	  // something
	  if (something == true)
        return true;
      return false;
    }
  }
}

STEP 3: CREATE A NEW RULE

Content Editor Warning Rule
Content Editor Warning Rule

Content Editor Warning rules are placed in:

/sitecore/system/Settings/Rules/Content Editor Warnings/Rules

And you need to create a new Content Editor Warning Rule.

Then you create the rule using the rules editor:

Rules Editor
Rules Editor

My rule is fancy, as a first check for a specific template type (built in rule), then check for my own rule.

Lastly, the rule ends with what should happen. Here you type in the text that should appear in the content editor warning.

That’s it. You are now a Sitecore expert.

MORE TO READ:

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

Sitecore poor Index Update performance linked to missing Index in the Links database

Suddenly my index updates took forever to finish. I mean, one index update would take 2 minutes. And I have a lot of index updates. A lot.

UPDATE: 2022-01-19: Sitecore have suggested the same changes to the links database in KB1000639 – How to reduce query execution time for the Link Database.

Running jobs in Sitecore
Index Updates in Sitecore

After some panic and some SQL debugging we were lead to some long running SQL statements that looked like this:

SELECT *
  FROM [Sitecore_Web].[dbo].[Links]
  where TargetItemID = '562F77DD-6C00-4BE1-AF0E-9F9EEAA8CCEF' 
  and TargetDatabase = 'master'

One of these would take almost 2 minutes to finish.

Now, my master database contains 2.000.000+ items, increasing with at least 5.000 items per week. Each of my items points to a category item, and each category item can have 250.000+ links to them. So there is a lot of rows in the Links database. 36.000.000 to be exact.

Inside the Links database there is several indexes, but none of them on TargetItemID and TargetDatabase. So we tried to create the index ourselves:

CREATE NONCLUSTERED INDEX [IX_TargetDatabaseTargetItemID] ON [dbo].[Links]
(
	[TargetDatabase] ASC,
	[TargetItemID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

And voila! After a system restart, my index updates was back on track.

FINAL NOTES:

First of all, I usually don’t create indexes on databases that I do not own. And I cannot guarantee that the index solved all of my problems. It just so happens that after the index was introduced, my system is running fine.

MORE TO READ:

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

C# and Microsoft.ML. Removing words from sentence. Getting started with Machine Learning

Microsoft.ML is the NuGet package for ML.Net, Microsoft’s open-source Machine Learning framework.

In this introduction I will create a stopword engine, capable of removing unwanted words from a sentence. I know that it is overkill to use machine learning to do this, but it serves as a great introduction as how to initialize and call Microsoft.ML.

STEP 1: THE NUGET PACKAGE

You need the following NuGet package:

STEP 2: CREATE A LIST OF STOPWORDS

We need a list or unwanted words to remove from the list:

public class StopWords
{
  internal static readonly string[] Custom =
  {
    "profanity",
    "swearing",
    "degrading"
  };
}

STEP 3: CREATE THE TEXTPROCESSING SERVICE

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.ML;
using Microsoft.ML.Transforms.Text;

public class TextProcessingService
{
  // The PredictionEngineis part of the Microsoft.ML package
  private readonly PredictionEngine<InputData, OutputData> _stopWordEngine;

  // The PredictionEngine receives an array of words
  private class InputData
  {
    public string[] Words { get; set; }
  }

  // The PredictionEngine returns an array of words
  private class OutputData : InputData
  {
    public string[] WordsWithoutStopWords { get; set; }
  }

  public TextProcessingService()
  {
    var context = new MLContext();

    // Getting the list of words to remove from our sentece
	var stopWords =
      StopWords.Custom.ToArray();

    // Define the transformation
	var transformerChain = context.Transforms.Text
      .RemoveDefaultStopWords(
        inputColumnName: "Words",
        outputColumnName: "WordsWithoutDefaultStopWords",
        language: StopWordsRemovingEstimator.Language.English)
      .Append(context.Transforms.Text.RemoveStopWords(
        inputColumnName: "WordsWithoutDefaultStopWords",
        outputColumnName: "WordsWithoutStopWords",
        stopwords: stopWords));

    var emptySamples = new List<InputData>();
    var emptyDataView = context.Data.LoadFromEnumerable(emptySamples);
    var textTransformer = transformerChain.Fit(emptyDataView);

    _stopWordEngine = context.Model.CreatePredictionEngine<InputData, OutputData>(textTransformer);
  }

  public string[] ExtractWords(string text)
  {
      // This will remove stopwords
	  var withoutStopWords = _stopWordEngine.Predict(new InputData { Words = text.Split(' ')}).WordsWithoutStopWords;
      if (withoutStopWords == null)
        return null;
      return withoutStopWords;
  }
}

USAGE:

public static void Main()
{
  var textProcessing = new TextProcessingService();
  var newString = textProcessing.ExtractWords("my code removes swearing and degrading language");
  Console.WriteLine(String.Join(' ',newString));
}

The code above will generate the following output:

  • code removes language

But why does it do that? The input string is “my code removes swearing and degrading language” and I have only defined “swearing” and “degrading” as words that needs to be removed?

The answer lies within line 37 in the TextProcessingService. I use a StopWordsRemovingEstimator, and the language is set to English. The RemoveDefaultStopWords method will add these default stop words to my list of words. The Microsoft class is pre-loaded with a number of stopwords, among those “my“, “and“. My list of words just adds to that list.

That’s it. Happy coding.

MORE TO READ:

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

Sitecore high CPU usage – is the SQL Session State Provider the villain?

My massive Sitecore 9.1 installation started having CPU spikes, even when the servers did not seem to receive more requests. When CPU spiked, I could see that the requests queue would build up, until it crashed with the following error:

System.Web.HttpException (0x80004005): The request queue limit of the session is exceeded.
at System.Web.SessionState.SessionStateModule.QueueRef()
at System.Web.SessionState.SessionStateModule.PollLockedSession()
at System.Web.SessionState.SessionStateModule.GetSessionStateItem()
at System.Web.SessionState.SessionStateModule.BeginAcquireState(Object source, EventArgs e, AsyncCallback cb, Object extraData)
at System.Web.HttpApplication.AsyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.<>c__DisplayClass285_0.b__0()
at System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step)
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

My internet search let me to believe that my SQL Session State Provider could be the issue.

Sitecore have some settings regarding session management. One is the pollingInterval:

PollingInterval – Specifies the time interval in seconds that the session-state provider uses to check if any sessions have expired.

The default polling interval is 2 seconds, which can lead to high load on the SQL server, and therefore a high CPU load, leading to longer response times, leading to the request queue not being processed.

Changing the pollingInterval is done 2 places, the SQL Session State Provider itself in the web.config, and the \App_Config\Sitecore\Marketing.Tracking\Sitecore.Analytics.Tracking.config config file

I changed my values to 60 seconds like this in the web.config:

<sessionState mode="Custom" cookieless="false" timeout="20" customProvider="mssql">
  <providers>
    <add name="mssql" type="Sitecore.SessionProvider.Sql.SqlSessionStateProvider, Sitecore.SessionProvider.Sql" 
	sessionType="private" 
	connectionStringName="session" 
	pollingInterval="60" 
	compression="true" />
  </providers>
</sessionState>

And in Sitecore.Analytics.Tracking.config:

<sharedSessionState defaultProvider="mssql">
  <providers>
    <add name="mssql" type="Sitecore.SessionProvider.Sql.SqlSessionStateProvider,Sitecore.SessionProvider.Sql" 
     connectionStringName="sharedsession" 
	 pollingInterval="60" 
	 compression="true" 
	 sessionType="shared" />
  </providers>
</sharedSessionState>

After this change, CPU usage have dropped, response times have dropped, the request queue does not build up, and I have not received any more request queue limit errors.

MORE TO READ:

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

Run parallel tasks in batches using .NET Core, C# and Async coding

If you have several tasks that you need to run, but each task takes up resources, it can be a good idea to run the tasks in batches. There are a few tools out there, and one of them is the SemaphoreSlim class. The SemaphoreSlim limits the number of concurrent threads with a few easy lines of code.

But first we need an imaginary method that we need to run in parallel:

private async Task DoStuff(string value)
{
  // pseudo code, you just need to imagine
  // that this method executes a task
  await something(value);
}

And we need a bunch of DoStuff values that need to run:

List<string> values = new List<string>();
values.Add("value1");
values.Add("value2");
values.Add("value3");
...
...
...

Now, to start “DoStuff()” with all the values from the “values” list, but run them in batches, we can do this:

using System.Threading;

public async Task BatchRun(List<string> values)
{
    // Limit the concurrent number of 
    // threads to 5
    var throttler = new SemaphoreSlim(5);

    // Create a list of tasks to run
    var tasks = values.Select(async value =>
    {
        // ... but wait for each 5 tasks
        // before running the next 5 tasks
        await throttler.WaitAsync();
        try
        {
            // Run the task
            return await DoStuff(value);
        }
        catch (Exception exception)
        {
            // handle the exception if any
        }
        finally
        {
            // Always release the semaphore
            // when done
            throttler.Release();
        }
    });

    // Now we actually run the tasks
    await Task.WhenAll(tasks);
}

The code will first setup a SemaphoreSlim and ask it to throttle the number of threads to 5. Next it will generate a list of tasks, and use the WaitAsync() to ask the SemaphoreSlim to wait for the first 5 tasks to finish before starting the next 5 tasks. Finally, the Task.WhenAll() method is running the tasks.

The greatness of the SemaphoreSlim lies within the simplicity of the code. You control the batch count one place, and all you need to do is to call WaitAsync() and Release() as extra lines of code. The rest looks like any other task runner.

MORE TO READ:

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

C# Working with Namespaces in XDocument and XML

XDocument is your old XML friend in C#, and it allows you to create or manipulate XML documents. To work with namespaces, you must use the XNamespace class.

This is an imaginary XML document (a Google Merchant Feed formatted one) that we wish to manipulate:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
	<channel>
		<item>
			<g:id>42069</g:id>
			<title>Brian Caos</title>
			<description>A developer</description>
			<link>https://briancaos.wordpress.com/</link>
			<g:image_link>https://briancaos.files.wordpress.com/2019/09/bp-small-e1567426409278.png</g:image_link>
			<g:availability>in stock</g:availability>
			<g:price>42.00 USD</g:price>
		</item>
	</channel>
</rss>

Notice that the XML have the namespace http://base.google.com/ns/1.0, but not all fields are namespaced. This is because the document is basically a RSS 2.0 feed with additional fields, and it’s only the additional fields that have the namespace.

READ THE DOCUMENT

To read the document from a string, you need an XDocument, and a XNamespace:

// Get the RSS 2.0 XML data
string feedData = {{feed string}}
 
// Convert the data into an XDocument
var document = XDocument.Parse(feedData);
// Specify the Google namespace
XNamespace g = "http://base.google.com/ns/1.0";

The {{feed string}} is of course the feed as described above.

The trick is that we now have the namespace in the variable called “g”. To add a namespace, you simply add the “g” variable in front of the element name, like this:

ADD AN ELEMENT WITH A NAMESPACE

If I would like to add a <g:availability>in_stock</g:availability> to every item in the XML document, I can do so like this:

// Get all elements with the name "item"
var items = document.Descendants().Where(node => node.Name == "item");
foreach (var item in items)
{
    // Create new element with namespace http://base.google.com/ns/1.0
    var id = new XElement(g + "availability", "in stock");
    // Add it to the document
    item.Add(id);
}

GET AN ELEMENT WITH A NAMESPACE

This will get all items with the element is <g:availability>out of stock</g:availability>:

items = document.Descendants()
  .Where(node => node.Name == "item"
         && node.Descendants()
         .Any(desc => desc.Name == g + "availability"
              && desc.Value == "out of stock"));

MORE TO READ:

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

Why Sitecore Composable DXP is great news for developers and consultants

Sitecore announced that they will embrace the Composable DXP approach. DXP is, according to Gartner:

A digital experience platform (DXP) is an integrated set of core technologies that support the composition, management, delivery and optimization of contextualized digital experiences.

Gartner

The composable DXP is when your DXP does not consist of a single monolithic system, but of a series of systems that speak together using API’s. And preferably running in the cloud, because it’s 2021.

In other words, the solution you make is build by picking systems and platforms, making microservices with custom code, and allow these systems and platforms to talk to each other using API’s. All of this is running in the cloud.

The Sitecore CMS is not the sole provider of functionality. If there is a better product out there, you integrate that product into your solution.

But wait!” I hear you mumble from your basement office. “Isn’t that the way we have being building solutions for our customers for the last 15 years?“. And, yes my fellow cynical software developer, it is indeed. But why is this a big deal then?

REASON 1: It now has a name

Someone has to sell the thing you do. And selling it is much easier if you have a name for it. Your sales guys can now go to your customers and say “We’re embracing the new composable DXP“. Even Gardner have rubber stamped your approach. And you can find whitepapers on composable DXP with buzzwords like “Putting your customers first”, and “Easy to customize” (like that’s not what we’re trying to achieve every day).

Yes, you have being doing composable DXP forever. But without the buzzwords, it is a hard sell.

REASON 2: Finally the Sitecore CMS will be praised for their integrationability

I just made up the word “integrationability“. But seriously, the strongest feature of the Sitecore CMS is it’s approach to integrations. Nobody talks about how easy and great it is to put data into Sitecore, and to pull data out of. And yet this is the single most used feature of all the features in Sitecore CMS.

I haven’t built a single solution in my 18 years of development that did not use integration into other systems. The very first system, a Sitecore 4.1 solution used an Oracle database and a custom VB script, and my latest project uses a full cloud based microservice infrastructure using Azure services and custom integrations to at least 5 other systems to provide the user experience.

You need a custom personalization rule? You just build it. You need to send data to external systems? You just build it. With composable DXP, Sitecore will finally be able to show off how great an integration platform it is.

REASON 3: Using the best system available will become the default

Sometimes the bigger hammer is not the better solution. Sometimes personalization can be done using a simpler system. Or maybe the customer prefers his own analytics platform. With the composable DXP, you can choose a more lightweight approach, if that’s what suits your project better.

No one will call you a premature optimizer if you suggest using Uniform in the project. Or if you feel that a Javascript based frontend is the best solution. This gives you the flexibility to select your weapon of choice, which in the end will speed up the development process.

REASON 4: Standalone services are easier to develop

Composable also means that one component has one job. Developing microservices are easier to understand, develop and debug. And they can be deployed independently from other services.

If one service becomes obsolete, you no longer have to dismantle a monolith. And as long as you remember to build your services so their status can be observed, you can build dashboards that will tell you which system is failing.

REASON 5: It is fun!

Cloud based development is fun! Building API’s and integrating systems are great! You get to actually make architecture. And code. And at the same time give more value to the customer. And to do it faster. And better.

Final thoughts

The composable DXP is not a new thing. But it is now acknowledged by non-developers. And that’s a new thing. Sure, not all systems have great API’s. Or run in the cloud. Or are easy to host. But they will be better over time. The open world is here.

Happy coding.

MORE TO READ:

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

Write to SOLR from .NET Core

.NET Core supports the SOLR index through the SolrNet.Core NuGet packages.

Here is how you update the index.

STEP 1: THE NUGET PACKAGES

You need the following NuGet packages:

STEP 2: CREATE A MODEL CLASS CONTAINING THE PROPERTIES OF YOUR SOLR INDEX

Use the SolrNet.Attributes to specify the properties of the document to index:

using System;
using SolrNet.Attributes;

namespace MyCode
{
  public class MySolrDocument
  {
    [SolrField("_uniqueid")]
    [SolrUniqueKey]
    public int RowID { get; set; }

    [SolrField("name")]
    public string Name { get; set; }

    [SolrField("age")]
    public int Age { get; set; }
  }
}

STEP 3: INJECT SOLR INTO YOUR SERVICECOLLECTION

Your code needs to know the Solr URL and which model to return when the Solr instance is queried. This is an example on how to inject Solr, your method might differ slightly:

private IServiceProvider InitializeServiceCollection()
{
    ...
    ...
    .AddSolrNet<MySolrDocument>("https://myurl:8983/solr/myindex")
    .AddSingleton<MySolrRepository>()
    .BuildServiceProvider();
    return services;
}

STEP 4: IMPLEMENT A REPOSITORY THAT CAN UPDATE THE INDEX

This is a simple repository that can update the SOLR index:

using System;
using System.Collections.Generic;
using SolrNet;
using SolrNet.Commands.Parameters;
using System.Linq;
using System.Threading.Tasks;

namespace MyCode
{
  public class MySolrRepository
  {
    private readonly ISolrOperations<MySolrDocument> _solr;

    public MappedValueRepository(ISolrOperations<MySolrDocument> solr)
    {
      _solr = solr;
    }

    public async Task Update(int id, string name, int age)
    {
        MySolrDocument solrDoc = new MySolrDocument()
        {
            RowId = id,
            Name = name,
            Age = age
        };
        await _solr.AddAsync(solrDoc);
        await _solr.CommitAsync();
    }

  }
}

Explanation:

SOLR does not have separate create and update statements. If a document with the same unique key exists, the document is updated. If not, it is created.

SOLR updates are protected using transactions. You need to call CommitAsync() before your changes are committed. SOLR also provides a RollbackAsync() method in case you need to roll back.

To delete documents, simply call DeleteAsync() with a model class containing the unique key specified in the model class.

That’s it. You are now a SOLR expert.

MORE TO READ:

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

Sitecore publishItem pipeline – handling missing delete when publishing

When publishing items in Sitecore, the publishItem pipeline is called for each item that must be published, or unpublished. Except for children of items that have been deleted. Let’s take this example:

Items To Publish
Items to publish

If I publish the top item, the publishItem pipeline will be called for every child with the PublishAction.PublishVersion property in the context.

If I then delete “Page1” and republish the top item, the publishItem pipeline is run again. This time the “Page1” will be called with PublishAction.DeleteTargetItem property in the context. But there is no publishItem being called for each of the children of “Page1“.

This is because Sitecore assumes that when a parent is being unpublished, it will automatically clean up any child items, because the relationship between parent and child have been broken.

SO THAT IS THE CHALLENGE THEN?

In my solution, we synchronize certain Sitecore items with an external database. This database have no parent-child relation. so when the “Page1” is deleted, the children below is not deleted from the external database, because no one tells the database to do so.

HOW CAN THIS BE FIXED:

My colleague Odin Jespersen helped me fix this issue. He created a new pipeline step for the publishItem pipeline that compares the web database with the master database to find items that have been deleted from the master database but not in the web database. These items are deemed deleteable and we then delete the items from the external database.

ENOUGH TALK, SHOW ME THE CODE:

This is how it looks like:

STEP 1: ADD THE STEP TO THE PUBLISHITEM PIPELINE:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:env="http://www.sitecore.net/xmlconfig/env/">
  <sitecore>
    <pipelines>
      <publishItem>
        <processor patch:before="*[@type='Sitecore.Publishing.Pipelines.PublishItem.DetermineAction, Sitecore.Kernel']" type="MyCode.DeleteFromExternalDatabase, MyDll" />
      </publishItem>
    </pipelines>
  </sitecore>
</configuration>

STEP 2: THE PROCESSOR:

using System.Linq;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using Sitecore.Publishing;
using Sitecore.Publishing.Pipelines.PublishItem;

namespace MyCode
{
  public class DeleteFromExternalDatabase : PublishItemProcessor
  {
    private readonly MasterDatabaseFactory _sourceDatabaseFactory = Sitecore.Data.Database.GetDatabase("master");
    private readonly WebDatabaseFactory _targetDatabaseFactory = Sitecore.Data.Database.GetDatabase("web");

    public override void Process([CanBeNull] PublishItemContext context)
    {
      if (context == null)
        return;
      if (context.Aborted)
        return;
      if (context?.PublishContext == null)
        return;
      
      Item sourceItem = context.PublishHelper.GetSourceItem(context.ItemId);
      if (sourceItem == null)
        return;
      
      if (!sourceItem.Paths.IsContentItem)
        return;
      
	  // Only do this trick for the items that are being synchronized with the 
	  // external database
	  if (sourceItem.TemplateID != MyTemplate)
        return;
      
	  Item targetFolder = _targetDatabaseFactory.Get().GetItem(sourceItem.ID);
      if (targetFolder == null)
        return;
      
      var targetItems = targetFolder.GetChildrenDerivedFrom(MyTemplate);
      if (targetItems != null)
      {
        foreach (Item targetItem in targetItems)
        {
          if (_sourceDatabaseFactory.Get().GetItem(targetItem.ID) != null)
          {
            continue;
          }
          var items = targetItem.GetChildrenDerivedFrom(MyTemplate);
          if (items == null)
            continue;
          foreach (Item item in items)
          {
            if (_sourceDatabaseFactory.Get().GetItem(item.ID) == null)
            {
              // Do the delete from the external database
            }
          }
        }
      }
    }
  }
}

MORE TO READ:

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