C# Using Dapper as your SQL framework in .NET Core

Dapper is a easy to use object mapper for .NET and .NET Core, an it can be used a variety of ways. I use Dapper instead of Entity Framework because it makes my code less complex.

BASICS OF DAPPER: THE OBJECT MAPPER

Basically, Dapper is an object mapper. This means that Dapper will map SQL rows to C# model classes 1-1. So if you wish to select data from an SQL table, you create a class containing the exact same fields as the SQL table:

Contact Form Table

So for that contact form table above, I can create a corresponding ContactForm model class:

using System;
using Dapper.Contrib.Extensions;

namespace MyCode
{
  [Table("dbo.ContactForms")]
  public class ContactForm
  {
    [Key]
    public int Id { get; set; }

    public DateTime Created { get; set; }
	public string Name { get; set; }
	public string Phone { get; set; }
	public string Email { get; set; }
	public int? ZipCode { get; set; }
	public string Comment { get; set; }
	public string IpAddress { get; set; }
	public string UserAgent { get; set; }
  }
}

By including the Dapper.Contrib.Extensions, I can mark the table key in my code, and the table itself. Nullable fields like the zipcode are also nullable in my class.

SIMPLE SQL SELECT WITH DAPPER

Now with the mapping in place, selecting and returning a class is super easy:

using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using Dapper;
using Dapper.Contrib.Extensions;

public class ContactFormRepository
{
  public IEnumerable<ContactForm> Get()
  {
    using var connection = new SqlConnection("some_sql_connection_string");
    return connection.Query<ContactForm>("select * from ContactForms").ToList();
  }
}

Dapper will map the ContactForms table to my ContactForm model class.

Selecting with parameters are equally easy, presented here in 2 different forms; one method returning only one row, another method returning all matching the parameter:

public ContactForm Get(int id)
{
    using var connection = new SqlConnection("some_sql_connection_string");
    return connection.QuerySingleOrDefault<ContactForm>("select * from ContactForms where id = @Id", 
      new { Id = id } 
    );
}

public IEnumerable<ContactForm> Get(string email)
{
    using var connection = new SqlConnection("some_sql_connection_string");
    return connection.Query<ContactForm>("select * from ContactForms where email = @Email", 
      new { Email = email } 
    ).ToList();
}

INSERT STATEMENT WITH DAPPER:

With inserting you decide if you wish to use your model class (great for exact inserts) or if you wish to use a dynamic class. The latter is great when you have fields that are autogenerated by the SQL server like auto-incrementing keys or dates that is set to GETDATE():

public void Insert(ContactForm contactForm)
{
    using var connection = new SqlConnection("some_sql_connection_string");
    connection.Insert(contactForm);
}

public void Insert(string name, string email)
{
    using var connection = new SqlConnection("some_sql_connection_string");
    connection.Execute("insert into ContactForms (name, email) values (@Name, @Email)", new { Name = name, Email = email });
}

USING STORED PROCEDURES:

This is also easy, just give the name of the stored procedure. In this example I will also use the DynamicParameters just to be fancy:

public IEnumerable<ContactForm> Get(string email)
{
    using var connection = new SqlConnection("some_sql_connection_string");
    var parameters = new DynamicParameters();
    parameters.Add("@Email", email);
    return connection.Query<ContactForm>("Stored_Procedure_Name", parameters, commandType: CommandType.StoredProcedure).ToList();
}

The same goes with insert using a stored procedure:

public void Insert(string name, string email)
{
    using var connection = new SqlConnection("some_sql_connection_string");
    var parameters = new DynamicParameters();
    parameters.Add("@Name", name);
    parameters.Add("@Email", email);
    connection.Execute("Stored_Procedure_Name", parameters, commandType: CommandType.StoredProcedure);
}

That’s basically it. Very easy to use.

MORE TO READ:

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

Sitecore KeepLockAfterSave – Configuring Security Policies Per-Role Based

Now here is a nifty Sitecore trick. You have probably learned about the AutomaticLockOnSave feature that allows Sitecore to lock an item when it is saved. The feature is enabled or disabled using configuration setting (and can be negated with the inverse AutomaticUnlockOnSaved setting).

But did you know that you can set the lock on save properties on a per-role?

Yes, Sitecore have a section in the core database where a lot of the security properties are stored. Users who have access the a particular item, have that property.

The policies are stored here: /sitecore/system/Settings/Security/Policies

Sitecore Security Policies

For “KeepLockAfterSave“, you will need to modify the config file, and allow Sitecore to read the CORE database setting:

<!-- 
    The "Keep Lock After Save" item is serialized in order to deploy permission on it, 
    making the roles SSTEditor + SSTAdmin unable to read it, thus making them not keep locks on 
    datasource items after doing page editing 
-->
<include name="KeepLockAfterSave" database="core" path="/sitecore/system/Settings/Security/Policies/Page Editor/Keep Lock After Save"/>

To set the property to false for certain groups, block the access to the item for that group:

My manager group does not have access to this item, meaning that the “Keep Lock After Save” is false.

MORE TO READ:

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

C# Remove specific Querystring parameters from URL

These 2 extension methods will remove specific query string parameters from an URL in a safe manner.

METHOD #1: SPECIFY THE PARAMETERS THAT SHOULD GO (NEGATIVE LIST):

using System;
using System.Linq;
using System.Web;

namespace MyCode
{
  public static class UrlExtension
  {
    public static string RemoveQueryStringsFromUrl(this string url, string[] keys)
    {
      if (!url.Contains("?"))
        return url;

      string[] urlParts = url.ToLower().Split('?');
      try
      {
        var querystrings = HttpUtility.ParseQueryString(urlParts[1]);
        foreach (string key in keys)
          querystrings.Remove(key.ToLower());

        if (querystrings.Count > 0)
          return urlParts[0] 
            + "?" 
            + string.Join("&", querystrings.AllKeys.Select(c => c.ToString() + "=" + querystrings[c.ToString()]));
        else
          return urlParts[0];
      }
      catch (NullReferenceException)
      {
        return urlParts[0];
      }
    }
  }
}

Usage/Test cases:

string url = "https://briancaos.wordpress.com/page/?id=1&p=2";
string url2 = url.RemoveQueryStringsFromUrl(url, new string[] {"p"});
string url3 = url.RemoveQueryStringsFromUrl(url, new string[] {"p", "id"});

//Result: 
// https://briancaos.wordpress.com/page/?id=1
// https://briancaos.wordpress.com/page

METHOD #2: SPECIFY THE PARAMETERS THAT MAY STAY (POSITIVE LIST):

using System;
using System.Linq;
using System.Web;

namespace MyCode
{
  public static class UrlExtension
  {
    public static string RemoveQueryStringsFromUrlWithPositiveList(this string url, string[] allowedKeys)
    {
      if (!url.Contains("?"))
        return url;

      string[] urlParts = url.ToLower().Split('?');
      try
      {
        var querystrings = HttpUtility.ParseQueryString(urlParts[1]);
        var keysToRemove = querystrings.AllKeys.Except(allowedKeys);

        foreach (string key in keysToRemove)
          querystrings.Remove(key);

        if (querystrings.Count > 0)
          return urlParts[0] 
		    + "?" 
			+ string.Join("&", querystrings.AllKeys.Select(c => c.ToString() + "=" + querystrings[c.ToString()]));
        else
          return urlParts[0];
      }
      catch (NullReferenceException)
      {
        return urlParts[0];
      }
    }
  }
}

Usage/Test cases:

string url = "https://briancaos.wordpress.com/page/?id=1&p=2";
string url2 = url.RemoveQueryStringsFromUrl(url, new string[] {"p"});
string url3 = url.RemoveQueryStringsFromUrl(url, new string[] {"p", "id"});

//Result: 
// https://briancaos.wordpress.com/page/?p=2
// https://briancaos.wordpress.com/page/?id=1&p=2

MORE TO READ:

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

Sitecore ComputedIndexField extends your SOLR index

The Sitecore SOLR index is your quick access to Sitecore content. And you can extend this access by adding computed index fields. This is a way of enriching your searches with content that is not part of your Sitecore templates, but is needed when doing quick searches.

THE SIMPLE SCENARIO: GET A FIELD FROM THE PARENT ITEM

This is a classic scenario, where the content in Sitecore is organized in a hierarchy, for example by Category/Product, and you need to search within a certain category:

Category/Product Hierarcy

In order to make a direct search for products within a certain category, you will need to extend the product template with the category ID, so you can do a search in one take. So lets add the category ID to the product template SOLR search using a computed index field.

STEP 1: THE CONFIGURATION:

<?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>
    <contentSearch>
      <indexConfigurations>
        <defaultSolrIndexConfiguration>
          <fieldMap>
            <fieldNames hint="raw:AddFieldByFieldName">
              <field fieldName="CategoryId" returnType="guid" />
            </fieldNames>
          </fieldMap>
          <documentOptions>
            <fields hint="raw:AddComputedIndexField">          
              <field fieldName="CategoryId" returnType="string">MyCode.ComputedIndexFields.CategoryId, MyDll</field>
            </fields>
          </documentOptions>
        </defaultSolrIndexConfiguration>
      </indexConfigurations>
    </contentSearch>
  </sitecore>
 </configuration>  

The configuration is a 2 step process. The “fieldMap” maps field names (CategoryId in this case) to output types, in this case a GUID. The documentOptions maps the field name to a piece of code that can compute the field value. Please note that the documentOptions claims that the output type is a string, not a Guid. But don’t worry, as long as our code returns a Guid, everything will be fine.

STEP 2: THE CODE

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;
using Sitecore.Data.Items;

namespace MyCode.ComputedIndexFields
{
  public class CategoryId : IComputedIndexField
  {
    public object ComputeFieldValue(IIndexable indexable)
    {
      Item item = indexable as SitecoreIndexableItem;

      if (item == null)
        return null;

      if (item.TemplateName != "Product")
        return null;

      Item categoryItem = item.Parent;
      if (categoryItem.TemplateName != "Category")
        return null;

      return categoryItem.ID.ToGuid();
    }

    public string FieldName
    {
      get;
      set;
    }

    public string ReturnType
    {
      get;
      set;
    }
  }
}

The code is equally straight forward. If the code returns NULL, no value will be added.

The code first checks to see if the item being indexed is a product. If not, the code is skipped. Also, if the parent item is not a category, we also skip the code. Only if the item is a product and the parent is a category, the category ID is added to the index.

You will need to re-index your SOLR index. When the index is updated, you will find a “CategoryId” field on all of the “Product” templates in the SOLR index.

MORE TO READ:

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

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 | 1 Comment