Handling “415 Unsupported Media Type” in .NET Core API

The default content type for .NET Core API’s is application/json. So if the content-type is left out, or another content type is used, you will get a “415 Unsupported Media Type”:

415 Unsupported Media Type from Postman

This is for example true if you develop an endpoint to capture Content Security Policy Violation Reports. Because the violation report is sent with the application/csp-report content type.

To allow another content-type, you need to specify which type(s) to receive. In ConfigureServices, add the content-type to use to the SupportedMediaTypes:

public void ConfigureServices(IServiceCollection services)
{
  ...
  ...
  // Add MVC API Endpoints
  services.AddControllers(options =>
  {
    var jsonInputFormatter = options.InputFormatters
        .OfType<Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonInputFormatter>()
        .Single();
    jsonInputFormatter.SupportedMediaTypes.Add("application/csp-report");
  }
  );
  ...
  ...
}

Now your endpoint will allow both application/json and application/csp-report content types.

BUT WHAT IF THERE IS NO CONTENT TYPE?

To allow an endpoint to be called without any content-type, you also allow everything to be posted to the endpoint. The endpoint will read the posted content using a streamreader instead of receiving it from a strongly typed parameter.

The endpoint cannot be called using your Swagger documentation.

using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace MyCode
{
  [ApiController]
  [Route("/api")]
  public class TestController : ControllerBase
  {
    [HttpPost("test")]
    public async Task<IActionResult> Test()
    {
      using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
      {
        string message = await reader.ReadToEndAsync();
        // Do something with the received content. For 
        // test pusposes, I will just output the content:
        return base.Ok(message);
      }
    }
  }
}

MORE TO READ:

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

Sitecore AccessResultCache cache is cleared by Sitecore.Caching.Generics.Cache`1+DefaultScavengeStrategy[[Sitecore.Caching.AccessResultCacheKey

Are you getting a lot of these messages in your Sitecore log:

6052 2021:03:25 05:23:12 WARN AccessResultCache cache is cleared by Sitecore.Caching.Generics.Cache`1+DefaultScavengeStrategy[[Sitecore.Caching.AccessResultCacheKey, Sitecore.Kernel, Version=11.1.0.0, Culture=neutral, PublicKeyToken=null]] strategy. Cache running size was xxx MB.

This message can easily appear once every minute.

WHAT IS THE ACCESSRESULTCACHE?

Every time a user accesses an item, the security rights to that item is put into the accessresultcache.

WHY IS THE CACHE CLEARED SO OFTEN?

Sitecore have chosen a relatively low value as cache. This value suits smaller sites perfectly, but larger sites will suffer.

Also, remember that it is every item that is being read that is cached. So If you have a dropdown or a tree view, these items are read and cached too. Looking up one item in Sitecore might trigger a cascade reading of 100-s of items.

WHAT TO DO THEN?

You can increase the cache size easily:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" 
xmlns:set="http://www.sitecore.net/xmlconfig/set/">
    <sitecore>
        <settings>
              <setting name="Caching.AccessResultCacheSize" set:value="300MB"/>
        </settings>
    </sitecore>
</configuration>

CAN YOU DISABLE THE ACCESSRESULTCACHE?

Yes. If all of your Sitecore editors are admins anyway, you can disable the cache. You can also disable the cache on the CD servers if there is no security protected areas on your site. You disable the security per database:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <databases>
      <database id="web">
        <securityEnabled>false</securityEnabled>
      </database>
    </databases>
  </sitecore>
</configuration>

MORE TO READ:

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

C# Newtonsoft camelCasing the serialized JSON output

JSON love to be camelCased, while the C# Model class hates it. This comes down to coding style, which is – among developers – taken more seriously than politics and religion.

But fear not, with Newtonsoft (or is it newtonSoft – or NewtonSoft?) you have more than one weapon in the arsenal that will satisfy even the most religious coding style troll.

OPTION 1: THE CamelCasePropertyNamesContractResolver

The CamelCasePropertyNamesContractResolver is used alongside JsonSerializerSettings the serializing objects to JSON. It will – as the name implies – resolve any property name into a nice camelCasing:

// An arbitrary class
public MyModelClass 
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public int Age { get; set; }
}

// The actual serializing code:
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

var myModel = new MyModelClass() { FirstName = "Arthur", LastName = "Dent", Age = 42 };
var serializedOutput = JsonConvert.SerializeObject(
  myModel, 
  new JsonSerializerSettings
  {
    ContractResolver = new CamelCasePropertyNamesContractResolver()
  }
);

The resulting JSON string will now be camelCased, even when the MyModelClass properties are not:

{
  firstName: 'Arthur',
  lastName: 'Dent',
  age: 42
}

OPTION 2: USING THE JsonProperty ATTRIBUTE:

If you own the model class you can control not only how the class is serialized, but also how it is deserialized by uding the JsonProperty attribute:

using Newtonsoft.Json;

public MyModelClass 
{
  [JsonProperty("firstName")]
  public string FirstName { get; set; }

  [JsonProperty("lastName")]
  public string LastName { get; set; }

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

Both the JsonConvert.SerializeObject and the JsonConvert.DeserializeObject<T> methods will now use the JsonProperty name instead of the model class property name.

MORE TO READ:

 

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

Simple C# MemoryCache implementation – Understand the SizeLimit property

The .NET Core IMemoryCache is probably the simplest cache there is, and it is very easy to use, once you get your head around the weird SizeLimit property.

Especially when using the nice extension methods in this NuGet package:

But let’s code first, then discuss the SizeLimit.

SIMPLE MEMORY CACHE REPOSITORY:

using Microsoft.Extensions.Caching.Memory;
using System;

namespace MyCode
{
  public interface IMemoryCacheRepository
  {
    bool GetValue<T>(string key, out T value);
    void SetValue<T>(string key, T value);
  }

  public class MemoryCacheRepository : IMemoryCacheRepository
  {
    // We will hold 1024 cache entries
    private static _SIZELIMIT = 1024;
    // A cache entry expire after 15 minutes
    private static _ABSOLUTEEXPIRATION = 90;
    
    private MemoryCache Cache { get; set; }
    
    public MemoryCacheRepository()
    {
      Cache = new MemoryCache(new MemoryCacheOptions
      {
        SizeLimit = _SIZELIMIT
      });
    }

    // Try getting a value from the cache.
    public bool TryGetValue<T>(string key, out T value)
    {
      value = default(T);

      if (Cache.TryGetValue(key, out T result))
      {
        value = result;
        return true;
      }

      return false;
    }

    // Adding a value to the cache. All entries
    // have size = 1 and will expire after 15 minutes
    public void SetValue<T>(string key, T value)
    {
      Cache.Set(key, value, new MemoryCacheEntryOptions()
        .SetSize(1)
        .SetAbsoluteExpiration(TimeSpan.FromSeconds(_ABSOLUTEEXPIRATION))
      );
    }

    // Remove entry from cache
    public void Remove(string key)
    {
      Cache.Remove(key);
    }
  }
}

Usage:

MemoryCacheRepository cache = new MemoryCacheRepository();

// This is pseudocode. The cache can get any 
// type of object. You should define the object to 
// get.
string cacheKey = "somekey";
string objectToCache = new ArbitraryObject();

// Getting the object from cache:
if (cache.TryGetValue(cacheKey, out ArbitraryObject result))
  return result;
  
// Setting the object in the cache:
cache.SetValue(cacheKey, objectToCache);

WHAT IT IS WITH THE SIZELIMIT PROPERTY?

Once you create a new instance of a MemoryCache, you need to specify a sizelimit. In bytes? Kb? Mb? No, the SizeLimit is not an amount of bytes, but a number of cache entries your cache might hold.

Each cache entry you insert must specify a size (integer). The MemoryCache will then hold entries until that limit is met.

Example:

  • I specify a SizeLimit of 100.
  • I can then insert 100 entries with size = 1, or 50 entries with size = 2.
  • You can of course insert entries in different sizes, and when the sum reaches the SizeLimit, no more entries are being inserted.

The idea is that you know which entries are small and which are large, and you then control the memory usage of your cache this way, instead of having a hard ram-based limit.

Btw, if there is no more room in the cache, it is not the oldest entry that is being removed, making room for the new one. Instead, the new entry is not inserted, and the Set() method does not fail.

WHEN ARE ITEMS REMOVED FROM THE CACHE?

For each entry you specify the expiration time. In the example above, I use AbsoluteExpiration, but you can also use SlidingExpiration, or set a Priority. And you can even pin entries using the Priority = NeverRemove.

The actual entry is not removed with a background process, but rather when any activity on the cache is performed.

MORE TO READ:

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

C# get results from Task.WhenAll

The C# method Task.WhenAll can run a bunch of async methods in parallel and returns when every one finished.

But how do you collect the return values?

Imagine that you have this pseudo-async-method:

private async Task<string> GetAsync(int number)
{
  return DoMagic();
}

And you wish to call that method 20 times, and then collect all the results in a list?

That is a 3 step rocket:

  1. Create a list of tasks to run
  2. Run the tasks in parallel using Task.WhenAll.
  3. Collect the results in a list
// Create a list of tasks to run
List<Task> tasks = new List<Task>();
foreach (int i=0;i<20;i++)
{
  tasks.Add(GetAsync(i));
}

// Run the tasks in parallel, and
// wait until all have been run
await Task.WhenAll(tasks);

// Get the values from the tasks
// and put them in a list
List<string> results = new List<string>();
foreach (var task in tasks)
{
  var result = ((Task<string>)task).Result;
  results.Add(result);
}

MORE TO READ:

 

Posted in General .NET | Leave a comment

C# .NET Core Solr Search – Read from a Solr index

.NET Core has excellent support for doing searches in the Solr search engine. The search language is not always logical, but the search itself is manageable. Here’s a quick tutorial on how to get started.

STEP 1: THE NUGET PACKAGES

You need the following NuGet packages:

STEP 2: IDENTIFY THE FIELDS YOU WISH TO RETURN IN THE QUERY

You don’t need to return all the fields from the Solr index, but you will need to make a model class that can map the Solr field to a object field.

Solr Fields

Solr Fields

From the list of fields, I map the ones that I would like to have returned, in a model class:

using SolrNet.Attributes;

namespace MyCode
{
  public class MySolrModel
  {
    [SolrField("_fullpath")]
    public string FullPath { get; set; }

    [SolrField("advertcategorytitle_s")]
    public string CategoryTitle { get; set; }

    [SolrField("advertcategorydeprecated_b")]
    public bool Deprecated { 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:

using SolrNet;

private IServiceProvider InitializeServiceCollection()
{
  var services = new ServiceCollection()
    .AddLogging(configure => configure
      .AddConsole()
    )
    .AddSolrNet<MySolrModel>("https://[solrinstance]:8983/solr/[indexname]")
    .BuildServiceProvider();
  return services;
}

STEP 4: CREATE A SEARCH REPOSITORY TO DO SEARCHES:

Now onto the actual code. This is probably the simplest repository that can do a Solr search:

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

namespace MyCode
{
  public class MySolrRepository
  {
    private readonly ISolrReadOnlyOperations<MySolrModel> _solr;

    public AdvertCategoryRepository(ISolrReadOnlyOperations<MySolrModel> solr)
    {
      _solr = solr;
    }

    public async Task<IEnumerable<MySolrModel>> Search(string searchString)
    {
      var results = await _solr.QueryAsync(searchString);

      return results;
    }
  }
}

The Search method will do a generic search in the index that you specified when doing the dependency injection. It will not only search in the fields that your model class returns, but any field marked as searchable in the index.

You can do more complex searches by modifying the QueryAsync method. This example will do field based searches, and return only one row:

public async Task<MySolrModel> Search(string searchString)
{
  var solrResult = (await _solr.QueryAsync(new SolrMultipleCriteriaQuery(new ISolrQuery[]
    {
      new SolrQueryByField("_template", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"),
      new SolrQueryByField("_language", "da"),
      new SolrQueryByField("_latestversion", "true"),
      new SolrQueryByField("advertcategorydeprecated_b", "false"),
      new SolrQueryByField("_title", searchString)
    }, SolrMultipleCriteriaQuery.Operator.AND), new QueryOptions { Rows = 1 }))
    .FirstOrDefault();

  if (solrResult != null)
    return solrResult;

  return null;
}

That’s it for this tutorial. Happy coding!

MORE TO READ:

Posted in General .NET | Leave a comment

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 , , , | 2 Comments

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