Write to file from multiple threads async with C# and .NET Core

There are several patterns on how to allow multiple threads to write to the same file. the ReaderWriterLock class is invented for this purpose. Another classic is using semaphors and the lock statement to lock a shared resource.

This article explains how to use a ConcurrentQueue and a always running Task to accomplish the same feat.

The theory behind this is:

  • Threads deliver what to write to the file to the ConcurrentQueue.
  • A task running in the background will read from the ConcurrentQueue and do the actual file writing.

This allows the shared resource to be access from one thread only (the task running in the background) and everyone else to deliver their payload to a thread-safe queue.

But enough talk, lets code.

THE FILE WRITER CLASS

using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace MyCode
{
  public class MultiThreadFileWriter
  {
    private static ConcurrentQueue<string> _textToWrite = new ConcurrentQueue<string>();
    private CancellationTokenSource _source = new CancellationTokenSource();
    private CancellationToken _token;

    public MultiThreadFileWriter()
    {
      _token = _source.Token;
      // This is the task that will run
      // in the background and do the actual file writing
      Task.Run(WriteToFile, _token);
    }

    /// The public method where a thread can ask for a line
    /// to be written.
    public void WriteLine(string line)
    {
      _textToWrite.Enqueue(line);
    }

    /// The actual file writer, running
    /// in the background.
    private async void WriteToFile()
    {
      while (true)
      {
        if (_token.IsCancellationRequested)
        {
          return;
        }
        using (StreamWriter w = File.AppendText("c:\\myfile.txt"))
        {
          while (_textToWrite.TryDequeue(out string textLine))
          {
            await w.WriteLineAsync(textLine);
          }
          w.Flush();
          Thread.Sleep(100);
        }
      }
    }
  }
}

// Somewhere in the startup.cs or the Main.cs file
services.AddSingleton<MultiThreadFileWriter>();
// Now you can add the class using constructor injection
// and call the WriteLine() function from any thread without
// worrying about thread safety

Nothice that my code introduces a Thread.Sleep(100) statement. This is not needed, but it can be a good idea to give your application a little breathing space, especially if there are periods where nothing is written. Remove the line if your code requires an instant file write pattern.

MORE TO READ:

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

HttpClient retry on HTTP timeout with Polly and IHttpClientBuilder

The Polly retry library and the IHttpClientBuilder is a match made in heaven as it defines all the retry logic at startup. The actual HttpClient  calls are therefore untouched by any retry code.

The retry logic is called policies, and they determine how and in what circumstances a retry must be done.

Retrying on HTTP timeouts (where the caller does not respond) differs slightly from other HTTP errors (where the caller returns 404 Not Found or 500 errors). This is because the HttpClient does not receive an response code, but throws a TimeoutRejectedException when the call time outs.

This requires your configuration to make a retry policy and wrap this policy in a timeout policy.

But enough talk, lets code.

STEP 1: THE NUGET PACKAGES

You need the following packages:

  • Polly
  • Microsoft.Extensions.Http.Polly

STEP 2: CONFIGURE IHttpClientBuilder AND POLLY POLICIES IN THE STARTUP

In the startup.cs, add a HttpClient to the services and configure the retry policies, and then wrap the retry policies in a timeout policy. This is an example from a startup.cs file:

public static IHostBuilder CreateHostBuilder(string[] args)
{
  var host = Host.CreateDefaultBuilder(args);
  host.ConfigureServices((hostContext, services) =>
  {
    // ...
    // ...
    services.AddHttpClient("HttpClient")
      .AddPolicyHandler(GetRetryPolicy())
      .AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(5));
    });
    // ...
    // ...
  } 
  return host;
}

private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
  return HttpPolicyExtensions
    .HandleTransientHttpError()
    .Or<TimeoutRejectedException>()
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(30));
}

What’s happening here?

The services.AddHttpClient creates a new HttpClient.

The First policy handler added is the retry policy. Please note that the retry policy will also retry on TimeoutRejectedExceptions. This retry policy will retry 3 times with 30 seconds delay.

The next policy handler is the timeout handler. This handler will throw a TimeoutRejectedException when the url called have been unresponsive for 5 seconds.

STEP 3: USE THE IHttpClientFactory IN THE CALLING CLASS

There is no Polly code in the class that does the http calls:

namespace MyCode
{
  public class MyClass
  {
    private readonly IHttpClientFactory _clientFactory;
 
    public MyClass(IHttpClientFactory clientFactory)
    {
      _clientFactory = clientFactory;
    }
 
    public async Task<string> Get(string url)
    {
      string authUserName = "user";
      string authPassword = "password";
 
      var httpClient = _clientFactory.CreateClient("HttpClient");
      // If you do not have basic authentication, you may skip these lines
      var authToken = Encoding.ASCII.GetBytes($"{authUserName}:{authPassword}");
      httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(authToken));
 
      // The actual Get method
      using (var result = await httpClient.GetAsync($"{url}"))
      {
        string content = await result.Content.ReadAsStringAsync();
        return content;
      }
    }
  }
}

The httpClient.GetAsync() will retry the call automatically if any of the conditions described in the GetRetryPolicy() occurs. It will only return after the call is either successful or the retry count is met.

FINAL NOTE: DO NOT SET HttpClient.Timeout

The HttpClient.Timeout will set the global timeout, i.e. the overall timeout, including polly retries. So if you set this timeout you will receive a TaskCanceledException or OperationCanceledException instead of the TimeoutRejectedException, and those exceptions cannot be caught by the timeout policy.

MORE TO READ:

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

Azure Cognitive Search from .NET Core and C#

The Azure Cognitive Search engine is the search of choice in the Microsoft Azure. The search engine can be used in a myriad of ways and there are so many options that it can be difficult to find a starting point.

To help myself, I made this simple class that implements one of the simplest setups and a great starting point for more advanced searches.

The class is an example on how to do a free text search in one index.

THE NUGET REFERENCES: 

The code references the following packages:

STEP 1: DEFINE A MODEL CLASS FOR THE INDEX

You must define a model class that matches the fields you wish to have returned from the index. This is my sample index called “advert”:

Sample Advert Index

And I have defined the fields relevant for my search result:

public class Advert
{
  public string Id { get; set; }
  public string Title { get; set; }
  public string Description { get; set; }
}

STEP 2: THE SAMPLE SEARCH CLASS:

This is just an example search class that implement the most basic functions. You need to specify your own URL to the search engine and the proper API key.

using Azure;
using Azure.Search.Documents;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MyCode
{
  public class AzureSearch
  {
    public async Task<IEnumerable<string>> Search(string query)
    {
      SearchClient searchClient = CreateSearchClientForQueries("advert");
      SearchOptions options = new SearchOptions() { IncludeTotalCount = true };
      var results = await searchClient.SearchAsync<Advert>(query, options);

      List<string> documents = new List<string>();
      Console.WriteLine(results.Value.TotalCount);
      foreach (var s in results.Value.GetResults())
      {
        documents.Add(s.Document.Title);
      }
      return documents;
    }

    private static SearchClient CreateSearchClientForQueries(string indexName)
    {
      string searchServiceEndPoint = "https://mysearch.windows.net";
      string queryApiKey = "the api key";

      SearchClient searchClient = new SearchClient(new Uri(searchServiceEndPoint), indexName, new AzureKeyCredential(queryApiKey));
      return searchClient;
    }
  }
}

STEP 3: THE USAGE

Remember that the code above is just a sample on how to do a basic free-text search.

class Program
{
  static void Main(string[] args)
  {
    var result = new AzureSearch().Search("BrianCaos").Result;
    foreach (var r in result)
      Console.WriteLine(r);
  }
}

MORE ADVANCED SEARCHES:

To do more advanced searches, you usually modify the SearchOptions. For example, if you wish to apply a filter to the search, you can use the “Filter” property. This property takes a slightly different format, as “=” is written “eq”, “>” is “gt” and “<” is “lt”.

public async Task<IEnumerable<string>> Search(string query)
{
  SearchClient searchClient = CreateSearchClientForQueries("advert");
  SearchOptions options = new SearchOptions() 
  { 
    IncludeTotalCount = true, 
    Filter = "MyField eq true" 
  };
  var results = await searchClient.SearchAsync<Advert>(query, options);

  List<string> documents = new List<string>();
  Console.WriteLine(results.Value.TotalCount);
  foreach (var s in results.Value.GetResults())
  {
    documents.Add(s.Document.Title);
  }
  return documents;
}

PAGINATION:

To do pages searches, you use the SearchOptions again. Use the “Size” and “Skip” parameters to specify paging. “Size” determines the number of results, “Skip” determines how many results to skip before returning results. This example implements a 1-based paging:

public async Task<IEnumerable<string>> Search(string query, int page, int pageSize)
{
  SearchClient searchClient = CreateSearchClientForQueries("advert");
  SearchOptions options = new SearchOptions() 
  { 
    IncludeTotalCount = true, 
    Size = pageSize,
    Skip = (page-1)*pageSize
  };
  var results = await searchClient.SearchAsync<Advert>(query, options);

  List<string> documents = new List<string>();
  Console.WriteLine(results.Value.TotalCount);
  foreach (var s in results.Value.GetResults())
  {
    documents.Add(s.Document.Title);
  }
  return documents;
}

MORE TO READ:

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

Programmatically create and delete Azure Cognitive Search Indexes from C# code

Azure Cognitive Search is the search engine of choice when using Microsoft Azure. It comes with the same search features as search engines like Elastic Search and SOLR Search (you can even use the SOLR search query language).

An example index

An example index

One cool feature is the ability to create and delete an index based off a C# model class. This is very useful, as it enables you to store the index definition in code alongside your application, and you can create a command line interface to do index modifications easily.

The code is slightly longer than usual, but hold on, it’s not what complicated at all.

STEP 1: THE NUGET PACKAGES AND RFERENCES

You need the following references:

STEP 2: CREATE A MANAGEMENT CONTEXT

This management context class is a class that will help create the index. It consists of an interface and a implementation.

namespace MyCode
{
  public interface IIndexManagementContext
  {
    /// <summary>
    /// Create or update the index
    /// </summary>
    /// <typeparam name="T">The type of the index definition for the index to create or update</typeparam>
    void CreateOrUpdateIndex<T>() where T : IIndexDefinition, new();

    /// <summary>
    /// Delete the index given by a given index definition. 
    /// </summary>
    /// <typeparam name="T">The type of the index definition for the index to delete</typeparam>
    void DeleteIndex<T>() where T : IIndexDefinition, new();
  }
}
using System;
using Microsoft.Azure.Search;
using Microsoft.Azure.Search.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace MyCode
{
  public class AzureIndexManagementContext : IIndexManagementContext
  {
    // Since the index name is stored in the index definition class, but should not
    // become an index field, the index name have been marked as "JSON ignore"
    // and the field indexer should therefore ignore the index name when
    // creating the index fields.
    private class IgnoreJsonIgnoreMarkedPropertiesContractResolver : DefaultContractResolver
    {
      protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
      {
        IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization);
        properties = properties.Where(p => !p.Ignored).ToList();
        return properties;
      }
    }

    private readonly ISearchServiceClient _searchServiceClient;

    public AzureIndexManagementContext(string searchServiceName, string adminApiKey)
    {
      _searchServiceClient = new SearchServiceClient(searchServiceName, new SearchCredentials(adminApiKey));
    }

    public void CreateOrUpdateIndex<T>() where T : IIndexDefinition, new()
    {
      string name = new T().IndexName;
      var definition = new Index
      {
        Name = name,
        Fields = FieldBuilder.BuildForType<T>(new IgnoreJsonIgnoreMarkedPropertiesContractResolver())
      };

      try
      {
        _searchServiceClient.Indexes.CreateOrUpdate(definition);
      }
      catch (Microsoft.Rest.Azure.CloudException e)
      {
        // TODO: Log the error and throw exception
      }
    }

    public void DeleteIndex<T>() where T : IIndexDefinition, new()
    {
      string name = new T().IndexName;
      try
      {
        _searchServiceClient.Indexes.Delete(name);
      }
      catch (Microsoft.Rest.Azure.CloudException e)
      {
        // TODO: Log the error and throw exception
      }
    }
  }
}

STEP 3: CREATE A MODEL CLASS THAT DEFINES THE INDEX

This class will define the actual index. It uses attributes like IsFilterable and IsSearchable to define the properties for the index. You create one model class per index, and this is just one example of such a model class.

This also consists of one interface and one implementation.

using Newtonsoft.Json;

namespace MyCode
{
  public interface IIndexDefinition
  {
    // The name of the index. 
    // Property is ignored when serialized to JSON
    [JsonIgnore]
    string IndexName { get; }
  }
}
using System;
using Microsoft.Azure.Search;
using System.ComponentModel.DataAnnotations;
using Sitecore.Configuration;

namespace MyCode
{
  // This is just an example index. You must create your own class
  // to define your index.
  public class UserIndexDefinition : IIndexDefinition
  {
    public string IndexName = "MyUserIndex";

    // All indexes needs a key. 
    [Key]
    public string IndexKey { get; set; }

    [IsFilterable]
    public string UserID { get; set; }

    [IsFilterable, IsSearchable]
    public string Firstname { get; set; }

    [IsFilterable, IsSearchable]
    public string LastName { get; set; }

    [IsFilterable, IsSearchable]
    public string FullName { get; set; }

    [IsFilterable, IsSearchable]
    public string Email { get; set; }

    [IsSortable]
    public DateTime CreatedDate { get; set; }
  }
}

STEP 4: USE THE MANAGEMENT CONTEXT TO CREATE OR DELETE THE INDEX

First you need to create the context and the index model class. The “name” is the search index instance name, and the apikey1 is the “Primary Admin Key” as found in your Azure Search Index:

Azure Search Keys

Azure Search Keys

IIndexManagementContext indexManagementContext => new AzureIndexManagementContext("name", "apikey1");
IIndexDefinition userIndexDefinition => new UserIndexDefinition();

To create the index, use the following code:

var methodInfo = typeof(IIndexManagementContext).GetMethod("CreateOrUpdateIndex");
var genericMethod = methodInfo.MakeGenericMethod(userIndexDefinition.GetType());
genericMethod.Invoke(indexManagementContext, null);

To delete the index, use the following code:

var methodInfo = typeof(IIndexManagementContext).GetMethod("DeleteIndex");
var genericMethod = methodInfo.MakeGenericMethod(userIndexDefinition.GetType());
genericMethod.Invoke(indexManagementContext, null);

MORE TO READ:

 

Posted in General .NET | 1 Comment

Sitecore LinkField TargetItem is NULL – what’s wrong?

An ancient topic, that pops up once or twice every year. The TargetItem of Sitecore.Data.Links.LinkField returns NULL and you are CERTAIN that the item is published. What to do?

CASE 1: THERE IS SECURITY SET ON THE TARGETITEM

Items where extranet\Anonymous does not have read access, the item will exist in the WEB database, but is not available by the calling user, and the return value is NULL.

The solution is simple, use the SecurityDisabler before reading the LinkField:

using (new SecurityDisabler())
{
    LinkField linkField = myItem.Fields["myfield"];
    if (linkField != null && linkField.TargetItem != null)
    {
      // do stuff
    }
  }
}

CASE 2: YOU FROGOT TO PUBLISH THE TEMPLETE

The item is published, but the item template is not. Go to the WEB database and find the item. If there is no fields on the item, the template is most likely missing. Remember to publish the template.

CASE 3: THE LINKFIELD IS POINTING TO AN EXTERNAL URL

The LinkField have a LinkType. If the LinkType is “internal“, the targetitem is valid. If the LinkType is “external“, you have added an external url, and you need to read the “Url” property instead.

Click here to get a method that gives you the correct link regardless of the linktype.

CASE 4: THE LINKFIELD IS POINTING TO A MEDIA LIBRARY ITEM

The TargetItem is not NULL, but it is poiting to the media libray item which have no URL. Instead, you need to use the MediaManager to get the URL of the media item.

Click here to see how to use the MediaManager.

CASE 5: THE ITEM IS ACTUALLY NOT PUBLISHED

Ahh, the most embarrassing situation. You published the item with the LinkField, but not the item that the LinkField is pointing to.

Don’t worry. This happens to all of us. More than once.

MORE TO READ:

 

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

Filtering Application Insights telemetry using a ITelemetryProcessor

Application Insights is a wonderful tool. Especially when you have a microservice or multi-application environment and you need one place for all the logs and metrics. But it’s not free, and the costs can run wild if you are not careful.

Although the message logging usually is the most expensive cost, remote dependencies and requests can take up a lot of the costs too.

You can suppress the telemetry data by using a ITelemetryProcessor. The ITelemetryProcessor processes the telemetry information before it is send to Application Insights, and can be useful in many situations, including as a filter.

Take a look at this graph, the red part are my dependencies, and you can see the drop in tracking after the filter was applied:

Application Insights Estimated Costs

Application Insights Estimated Costs

This is an example on the dependency telemetry filter that will exclude all successful dependencies to be tracked, but allow all those that fails:

using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;

namespace MyCode
{
  public class DependencyTelemetryFilter : ITelemetryProcessor
  {
    private readonly ITelemetryProcessor _nextProcessor;

    public DependencyTelemetryFilter(ITelemetryProcessor nextProcessor)
    {
      _nextProcessor = nextProcessor;
    }
    
    public void Process(ITelemetry telemetry)
    {
      if (telemetry is DependencyTelemetry dependencyTelemetry)
      {
        if (dependencyTelemetry.Success == true)
        {
          return;
        }
      }

      _nextProcessor.Process(telemetry);
    }
  }
}

To add the filter, simply call the AddApplicationInsightsTelemetryProcessor method in your startup code:

private void ConfigureApplicationInsights(IServiceCollection services)
{
  services.AddApplicationInsightsTelemetryProcessor&lt;DependencyTelemetryFilter&gt;();
}

MORE TO READ:

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

Add a UserAgent to the IHttpClientFactory in .NET Core

Using a IHttpClientFactory to create HttpClient connections have a number of advantages, as you can configure several httpclients on startup. Each client will be reused, including the properties attached to that client.

In a previous post I showed how to create a Polly retry mechanism for a HttpClient. Adding a UserAgent to a HttpClient is even easier.

In the ConfigureServices()  (in the Startup.cs file), add the following code:

services.AddHttpClient("HttpClient", 
  client => 
  client.DefaultRequestHeaders.UserAgent.ParseAdd("my-bot/1.0")
);

This imaginary image service will get an image using the “HttpClient” connection. Every time a GET request is made, the UserAgent will be “my-bot/1.0“:

using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;

namespace MyCode
{
  public class ImageService
  {
    private readonly IHttpClientFactory _clientFactory;

    public ImageService(IHttpClientFactory clientFactory)
    {
      _clientFactory = clientFactory;
    }

    public async Task<string> GetImage(string imageUrl)
    {
      try
      {
        var httpClient = _clientFactory.CreateClient("HttpClient");
        using var response = await httpClient.GetAsync(imageUrl);
        if (!response.IsSuccessStatusCode)
          throw new Exception($"GET {imageUrl} returned {response.StatusCode}");
        if (response.Content.Headers.ContentLength == null)
          throw new Exception($"GET {imageUrl} returned zero bytes");
        // ...
        // Do something with the image being fetched
        // ...
      }
      catch (Exception exception)
      {
        throw new Exception($"Failed to get image from {imageUrl}: {exception.Message}", exception);
      }
    }
  }
}

MORE TO READ:

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

Sending JSON with .NET Core QueueClient.SendMessageAsync

In .NET Core, Microsoft.Azure.Storage.Queue have been replaced with Azure.Storage.Queues, and the CloudQueueMessage that you added using queue.AddMessageAsync() have been replaced with the simpler queue.SendMessageAsync(string) method.

But this introduces a strange situation, when adding serialized JSON objects. If you just add the serialized object to the queue:

using Azure.Storage.Queues;
using Newtonsoft.Json;
using System;

public async Task SendObject(object someObject)
{
  await queueClient.SendMessageAsync(JsonConvert.SerializeObject(someObject));
}

The queue cannot be opened from Visual Studio. You will get an error that the string is not Base 64 encoded.

System.Private.CoreLib: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.

So you need to Base 64 encode the serialized object before adding it to the queue:

using Azure.Storage.Queues;
using Newtonsoft.Json;
using System;

public async Task SendObject(object someObject)
{
  await queueClient.SendMessageAsync(Base64Encode(JsonConvert.SerializeObject(someObject)));
}

private static string Base64Encode(string plainText)
{
  var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
  return System.Convert.ToBase64String(plainTextBytes);
}

When reading the serialized JSON string, you do not need to Base 64 decode the string, it will be directly readable.

MORE TO READ:

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

Sitecore Publish item when moved or dragged using uiMoveItems and uiDragItemTo pipelines

Sometimes you have items that needs to be published immediately if moved to a new location in the content tree. Sitecore supports this – of course – via the uiMoveItems and uiDragItemTo pipelines.

Move item to new location

Move item to new location

This technique really applies to whatever you wish to do with items that are moved or dragged to a new location.

You can move an item in Sitecore 2 ways, either by clicking the “Move to” button or by simply dragging an item to a new location. There are 2 separate pipelines handling these actions. The uiMoveItems handles the button click, and the uiDragItemTo handles the drag operation. Both args are almost the same, but not quite, which is why we need to entrances to the actual method.

But enough talk, lets code. The function that asks for the item to be published looks like this:

using Sitecore;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Publishing;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.Sheer;
using System;
using System.Collections.Generic;
using System.Linq;

namespace MyCode
{
  public class ItemMoved
  {
    // Only do something if it's this particular
    // item type that is moved. Change this to your Template ID
    private static ID _TEMPLATE_ID = "some item id";

    // Entrance for the UiMoveItems pipeline
    public void UiMoveItems(ClientPipelineArgs args)
    {
      DoProcess(args, "target", "items");
    }

    // Entrance for the UiDragItemTo pipeline
    public void UiDragItemTo(ClientPipelineArgs args)
    {
      DoProcess(args, "target", "id");
    }

    // The actual method
    private void DoProcess(ClientPipelineArgs args, string targetParam, string sourceParam)
    {
      Assert.ArgumentNotNull(args, "args");

      // Get the master database from the args
      Database db = Factory.GetDatabase(args.Parameters["database"]);
      Assert.IsNotNull(db, "db");

      // Get the target item we are moving to
      Item targetItem = GetTargetItem(args, db, targetParam);
      Assert.IsNotNull(targetItem, "targetItem");

      // Get the source items being moved. The first item 
      // is the root item of the items moved.
      IEnumerable<Item> sourceItems = GetSourceItems(args, db, sourceParam);
      Assert.IsNotNull(sourceItems, "sourceItems");
      Assert.IsTrue(sourceItems.Count() != 0, "sourceItems are empty");
      Item sourceItem = sourceItems.First();

      if (!args.IsPostBack)
      {
        // No one clicked anything yet. Check if it's the item
        // in question that is being moved
        if (sourceItem.TemplateID == _TEMPLATE_ID)
        {
          // If the item is not published at the moment, ignore the item
          if (!sourceItem.Publishing.IsPublishable(DateTime.Now, false))
            return;
          // The item is published. Ask the user to publish the item for them
          SheerResponse.Confirm($"You have moved {sourceItem.Name}. You need to publish the item immediately. Would you like to publish it now?");
          args.WaitForPostBack();
          return;
        }
        return;
      }

      Context.ClientPage.Modified = false;
      if (args.Result == "yes")
      {
        // The user clicked "yes" to publish the item. Publish the item now.
        PublishOptions publishOptions = new PublishOptions(sourceItem.Database, Database.GetDatabase("web"), publishMode, sourceItem.Language, DateTime.Now);
        publishOptions.RootItem = sourceItem;
        publishOptions.Deep = true;
        publishOptions.PublishRelatedItems = false;
        publishOptions.CompareRevisions = false;
        var handle = PublishManager.Publish(new PublishOptions[] { publishOptions });
        PublishManager.WaitFor(handle);     
      }
      return;
    }

    // Returns the item we move to
    private Item GetTargetItem(ClientPipelineArgs args, Database db, string paramName)
    {
      var targetId = args.Parameters[paramName];
      Assert.IsNotNullOrEmpty(targetId, "targetId");
      var targetItem = db.GetItem(targetId);
      return targetItem;
    }

    // Returns the items we move
    private IEnumerable<Item> GetSourceItems(ClientPipelineArgs args, Database db, string paramName)
    {
      var sourceIds = args.Parameters[paramName].Split('|').ToList();
      Assert.IsTrue(sourceIds.Any(), "sourceIds.Any()");
      var sourceItems = sourceIds.Select(id => db.GetItem(id)).ToList();
      return sourceItems;
    }
  }
}

The method needs to be hooked up in your pipelines:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
      <processors>
        <uiMoveItems>
          <processor patch:after="*[@method='RepairLinks']" mode="on" type="MyCode.ItemMoved, MyDll" method="UiMoveItems" />
        </uiMoveItems>
        <uiDragItemTo>
          <processor patch:after="*[@method='RepairLinks']" mode="on" type="MyCode.ItemMoved, MyDll" method="UiDragItemTo" />
        </uiDragItemTo>
      </processors>
    </sitecore>
</configuration>

Did you notice how the ItemMoved method contains 2 entry methods, UiMoveItems and UiDragItemTo? This is because the parameters are not the same when pressing the move to button and when dragging. The targetItem is stored in 2 different parameters (items vs id).

That’s it. Happy coding.

MORE TO READ:

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

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

If you have several tasks that can be run in parallel, but still need to wait for all the tasks to end, you can easily achieve this using the Task.WhenAll() method in .NET Core.

Imagine you have this imaginary method that takes a URL and uploads the file to some location:

private async Task UploadFile(string fileName)
{
  // pseudo code, you just need to imagine
  // that this metod executes a task
  if (file exists)
    await _fileRepository.UploadFile(fileName);
}

RUN ONCE:

This method can be called from your main method:

private static async Task Main(string[] args)
{
  await UploadFile("c.\\file.txt");
}

RUN IN SEQUENCE:

If you have 2 files to be uploaded you can call it twice:

private static async Task Main(string[] args)
{
  await UploadFile("c.\\file.txt");
  await UploadFile("c.\\file2.txt");
}

This will upload the first file, then the next file. There is no parallelism here, as the “async Task” does not automatically make something run in in parallel.

RUN IN PARALLEL:

But with Task.WhenAll() you can run both at the same time in parallel:

private static async Task Main(string[] args)
{
  var task1 = UploadFile("c.\\file.txt");
  var task2 = UploadFile("c.\\file2.txt");
  await Task.WhenAll(task1, task2);
}

This will spawn 2 threads, run them simultaneously, and return when both threads are done.

RUN IN PARALLEL THE FLEXIBLE WAY:

If you want even more flexibility, you can call it using an IEnumerable list of objects:

private static async Task Main(string[] args)
{
  List<string> fileNames = new List<string>();
  fileNames.Add("c:\\file.txt");
  fileNames.Add("c:\\file2.txt");
  var tasks = fileNames.Select(f => UploadFile(f));
  await Task.WhenAll(tasks);
}

This will create a list of Tasks to be run at the same time. You can add many filename to the fileNames list and have them run, each of them in their own thread.

RUN IN PARALLEL IN BATCHES:

Beware of the limitations of threading. Spawning a thread have a small but significant overhead, and running too many threads at once could be slower than running them in sequence. If you have 100’s of files to be uploaded, you should run the tasks in batches:

private static async Task Main(string[] args)
{
  List<string> fileNames = new List<string>();
  fileNames.Add("c:\\file.txt");
  fileNames.Add("c:\\file2.txt");
  // ... adding 100's of files

  var batchSize = 10;
  int batchCount = (int)Math.Ceiling((double)userIds.Count() / batchSize);
  for(int i = 0; i < batchCount; i++)
  {
    var filesToUpload = fileNames.Skip(i * batchSize).Take(batchSize);
    var tasks = filesToUpload.Select(f => UploadFile(f));
    await Task.WhenAll(tasks));
  }
}

This will spawn 10 threads and wait for them to finish before taking the next 10.

MORE TO READ:

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