Tuesday, May 8, 2018

Asynchronously Throttling EPiFind Request To Stay under EPiFind Limit On Azure

ThrottledSemaphore


/// <summary>
    /// Throttled semaphore to allow maximum number of request concurrently per time period.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         In order to allow N no. of request concurrently for given time period <see cref="ThrottledSemaphore"/>
    ///         the caller should call the <see cref="TryLock"/> thorugh <see cref="using"/> as TryLock returns the IDisposable <see cref="ThrottledLock"/>
    ///         which tells caller whether the lock <see cref="ThrottledLock.IsLocked"/> has been taken or not.
    ///     </para>
    ///     <para>
    ///         <code>
    ///            private static ThrottledSemaphore throttledSemaphore = new ThrottledSemaphore();
    ///            using (var @lock = await throttledSemaphore.TryLock().ConfigureAwait(false))
    ///                   {
    ///                     if (@lock.IsLocked)
    ///                        return query.Track().Skip((page - 1) * count).Take(count).GetResult();
    ///                     throw new HttpResponseException((HttpStatusCode)429);
    ///                   }
    ///         </code>
    ///     </para>
    /// </remarks>
    public class ThrottledSemaphore
    {
        private ConcurrentQueue<DateTime> times;

        private SemaphoreSlim semaphore;

        public int MaxConcurrency;

        private TimeSpan Period;

        public ThrottledSemaphore(TimeSpan period, int maxConcurrency = 20)
        {
            Period = period;
            MaxConcurrency = maxConcurrency;
            semaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency);
            times = new ConcurrentQueue<DateTime>();
        }

        public async Task<ThrottledLock> TryLock(int timeoutMilliseconds = 500)
        {
            if (await semaphore.WaitAsync(timeoutMilliseconds).ConfigureAwait(false))
            {
                await Wait().ConfigureAwait(false);

                return new ThrottledLock(ReleaseLock) { IsLocked = true };
            }

            return new ThrottledLock(null);
        }

        private void ReleaseLock()
        {
            lock (semaphore)
            {
                times.Enqueue(DateTime.UtcNow);
                semaphore.Release();
            }
        }

        private async Task Wait()
        {
            var now = DateTime.UtcNow;
            var lastTime = DateTime.MinValue;

            if (times.Count >= MaxConcurrency)
                times.TryDequeue(out lastTime);

            var until = lastTime.Add(Period);
            if (MaxConcurrency - semaphore.CurrentCount + times.Count >= MaxConcurrency && until > now)
            {
                await Task.Delay(until - now).ConfigureAwait(false);
            }
        }

        /// <summary>
        /// The disposable releaser tasked with releasing the semaphore.
        /// </summary>
        public sealed class ThrottledLock : IDisposable
        {
            /// <summary>
            /// A value indicating whether this instance of the given entity has been disposed.
            /// </summary>
            /// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
            /// <remarks>
            /// If the entity is disposed, it must not be disposed a second
            /// time. The isDisposed field is set the first time the entity
            /// is disposed. If the isDisposed field is true, then the Dispose()
            /// method will not dispose again. This help not to prolong the entity's
            /// life in the Garbage Collector.
            /// </remarks>
            private bool isDisposed;

            public bool IsLocked { get; set; }

            public delegate void TaskDisposeCallBack();

            /// <summary>
            /// Task dispose call back to release or dispose any resources requried for this runner.
            /// </summary>
            private TaskDisposeCallBack taskDisposeCallBack;

            public ThrottledLock(TaskDisposeCallBack callBack)
            {
                taskDisposeCallBack = callBack;
            }

            /// <summary>
            /// Finalizes an instance of the <see cref="ThrottledLock"/> class.
            /// </summary>
            ~ThrottledLock()
            {
                // Do not re-create Dispose clean-up code here.
                // Calling Dispose(false) is optimal in terms of
                // readability and maintainability.
                this.Dispose(false);
            }

            /// <summary>
            /// Disposes of the resources (other than memory) used by the module that implements <see cref="T:System.Web.IHttpModule"/>.
            /// </summary>
            public void Dispose()
            {
                this.Dispose(true);

                // This object will be cleaned up by the Dispose method.
                // Therefore, you should call GC.SuppressFinalize to
                // take this object off the finalization queue
                // and prevent finalization code for this object
                // from executing a second time.
                GC.SuppressFinalize(this);
            }

            /// <summary>
            /// Disposes the object and frees resources for the Garbage Collector.
            /// </summary>
            /// <param name="disposing">
            /// If true, the object gets disposed.
            /// </param>
            private void Dispose(bool disposing)
            {
                if (this.isDisposed)
                {
                    return;
                }

                if (disposing)
                {
                    taskDisposeCallBack?.Invoke();
                }

                // Call the appropriate methods to clean up
                // unmanaged resources here.
                // Note disposing is done.
                this.isDisposed = true;
            }
        }
    }



Content Search Service/Usage


public static class ContentSearchService
    {
        private static IClient client = SearchClient.Instance;

        private static ThrottledSemaphore searchThrottle;

        static ContentSearchService()
        {
            // Throttle the user search requests to to avoid exceeding our allowed requests/sec to Episerver Find
            searchThrottle = new ThrottledSemaphore(
                period: TimeSpan.FromSeconds(1),
                maxConcurrency: CalculateSearchConcurrency(ServerStateEvent.GetServerCount()));
            ServerStateEvent.OnServerCountChanged += count => searchThrottle.MaxConcurrency = CalculateSearchConcurrency(count);
        }
  
 public static async Task<UnifiedSearchResults> GetSearchResults(string keyword, IList<Type> types = null, SortBy sort = SortBy.Relevance, DateTime? before = null, int page = 1, int count = 24)
        {
            var query = client.UnifiedSearch().For(keyword) as ITypeSearch<ISearchContent>;

            // Filter for specific page types
            if (!types.IsNullOrEmpty())
                query = query.FilterByExactTypes(types);

            // Filter before a specific date time
            if (before.HasValue)
                query = query.Filter(t => t.SearchPublishDate.Before(before.Value.RoundToMinute(RoundingDirection.Floor)));

            switch (sort)
            {
                case SortBy.Earliest:
                    query = query.OrderBy(o => o.SearchPublishDate);
                    break;
                case SortBy.Latest:
                    query = query.OrderByDescending(o => o.SearchPublishDate);
                    break;
                default:
                    query = query
                        .BoostMatching(s => s.SearchTitle.Match(keyword), 2)
                        .BoostMatching(s => s.SearchSection.Match(keyword), 1.5);
                    break;
            }

            using (var @lock = await searchThrottle.TryLock().ConfigureAwait(false))
            {
                if (@lock.IsLocked)
                    return query.Track().Skip((page - 1) * count).Take(count).GetResult();

                throw new HttpResponseException((HttpStatusCode)429);
            }
        }

        private static int CalculateSearchConcurrency(int serverCount)
        {
            // Divide the maximum concurrent user searches between the active servers (excluding the CMS admin server).
            return serverCount > 1 ? (Settings.Search.MaxConcurrency / (serverCount - 1)) : 1;
        }
    }



Monitoring Azure Servers Turning On and Shutting down


 public static class ServerStateEvent
    {
        // HACK: Guid copied from EPiServer.Events.Clients.Internal.ServerStateService.StateEventId and may be subject to change without notice
        private static readonly Guid EventId = new Guid("{51da5053-6af8-4a10-9bd4-8417e48f38bd}");

        private static readonly Guid RaiserId = Guid.NewGuid();

        private static Event serverEvent;

        private static ILogger logger = LogManager.GetLogger(typeof(ServerStateEvent));

        private static IServerStateService stateService = LicensingServices.Instance.GetService<IServerStateService>();

        private static int? serverCount = null;

        /// <summary>
        /// Event occurs when there has been a change to the number of server instances running in the application
        /// </summary>
        public static event Action<int> OnServerCountChanged;

        public static void Register()
        {
            var registry = ServiceLocator.Current.GetInstance<IEventRegistry>();
            serverEvent = registry.Get(EventId);
            serverEvent.Raised += OnRaised;
        }

        public static int GetServerCount()
        {
            if (serverCount == null)
                serverCount = stateService.ActiveServers();

            return serverCount.Value;
        }

        private static void OnRaised(object sender, EventNotificationEventArgs args)
        {
            if (args.Param is StateMessage message && (message.Type == StateMessageType.Hello || message.Type == StateMessageType.Bye))
            {
                // TODO: Remove when logging is removed
                var serverCountBefore = serverCount;

                // Calculate the new server count and fire the change event
                serverCount = stateService.ActiveServers();
                OnServerCountChanged?.Invoke(serverCount.Value);

                // TODO: Remove logging when confident that event is firing when needed
                logger.Information($"A ServerStateEvent was raised. Application: {message.ApplicationName} | Type: {message.Type} | Servers Before: {serverCountBefore} | Servers After: {serverCount}");
            }
        }
    }




 [InitializableModule]
    [ModuleDependency(typeof(EventsInitialization))]
    public class ServerStateConfig : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            ServerStateEvent.Register();
        }

        public void Uninitialize(InitializationEngine context) { }
    }


Sunday, May 6, 2018

FIFO Semaphore

In one of our project requirement we had to built the max concurrency requirement for which we decided to use Semaphore but we had requirement to build FIFO Semaphore as by default Semaphore does not guarantee FIFO access to waiting thread.

Therefore, I have use ConcurrentQueue together with Semaphore to come with FIFO Sempahore




using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace NRL.Shared.Locking
{
    public class ThreadPoolAsyncLock
    {
        private ConcurrentQueue<(SemaphoreSlim Semaphore, DateTime Time)> queue = new ConcurrentQueue<(SemaphoreSlim, DateTime)>();

        private (DateTime start, int requests) requestTime = (DateTime.UtcNow, 0);

        private SemaphoreSlim semaphoreSlims;

        public static TimeSpan Timeout = TimeSpan.FromMilliseconds(500);

        public static int MaxRequest = 30;

        public ThreadPoolAsyncLock(int maxThread = 30)
        {
            semaphoreSlims = new SemaphoreSlim(maxThread, maxThread);
        }

        public async Task<TaskRunner<TResult>> ScheduleTask<TResult>()
        {
            /* This lock will make sure no one enter in main semaphore unless some one from main semaphore releases this */
            await AcquireOrQueue().WaitAsync(Timeout).ConfigureAwait(false);

            /* Main semaphore wait */
            if (await semaphoreSlims.WaitAsync(Timeout).ConfigureAwait(false))
               {
                 new TaskRunner<TResult>(ReleaseLock) { IsLocked = true };
               }


            return new TaskRunner<TResult>(ReleaseLock);
        }

        private SemaphoreSlim AcquireOrQueue()
        {
            SemaphoreSlim slim = null;
            lock (semaphoreSlims)
            {
                if (semaphoreSlims.CurrentCount > 0 && (DateTime.UtcNow.Subtract(requestTime.start).Seconds > 1 || requestTime.requests < MaxRequest))
                {
                    slim = new SemaphoreSlim(1, 1);
                    Interlocked.Increment(ref requestTime.requests);
                }
                else
                {
                    slim = new SemaphoreSlim(0, 1);
                    queue.Enqueue((slim, DateTime.UtcNow));
                }
            }

            return slim;
        }

        /// <summary>
        /// Dequeue the thread waiting to enter main semaphore
        /// </summary>
        /// <remarks>
        /// It only allows the dequeue if the main semaphore is not full Or Max requests hasn't been reached within last second.
        /// </remarks>
        private void DeQueue()
        {
            lock (semaphoreSlims)
            {
                if (semaphoreSlims.CurrentCount == 0 || (requestTime.requests >= MaxRequest && DateTime.UtcNow.Subtract(requestTime.start).Seconds <= 1))
                    return;

                Interlocked.Decrement(ref requestTime.requests);
                if (queue.TryDequeue(out var semaphore))
                {
                    semaphore.Semaphore.Release();
                    requestTime.start = semaphore.Time;
                }
            }
        }

        private void ReleaseLock()
        {
            lock (semaphoreSlims)
            {
                semaphoreSlims.Release();
                DeQueue();
            }
        }

        /// <summary>
        /// The disposable releaser tasked with releasing the semaphore.
        /// </summary>
        public sealed class TaskRunner<TResult> : IDisposable
        {
            /// <summary>
            /// A value indicating whether this instance of the given entity has been disposed.
            /// </summary>
            /// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
            /// <remarks>
            /// If the entity is disposed, it must not be disposed a second
            /// time. The isDisposed field is set the first time the entity
            /// is disposed. If the isDisposed field is true, then the Dispose()
            /// method will not dispose again. This help not to prolong the entity's
            /// life in the Garbage Collector.
            /// </remarks>
            private bool isDisposed;

            
            public bool IsLocked { get; set; }

            /// <summary>
            /// A Task to run after acquiring and locking the thread.
            /// </summary>
            /// <remarks>
            /// If the entity acquires the lock, then entity shoule call <see cref="RunAsycn" /> or <see cref="Run"/> to run the task />
            /// </remarks>
            private Task<TResult> task;

            public delegate void TaskDisposeCallBack();

            /// <summary>
            /// Task dispose call back to release or dispose any resources requried for this runner.
            /// </summary>
            private TaskDisposeCallBack taskDisposeCallBack;

            public TaskRunner(TaskDisposeCallBack callBack)
            {
                taskDisposeCallBack = callBack;
            }

            /// <summary>
            /// Finalizes an instance of the <see cref="TaskRunner{TResult}"/> class.
            /// </summary>
            ~TaskRunner()
            {
                // Do not re-create Dispose clean-up code here.
                // Calling Dispose(false) is optimal in terms of
                // readability and maintainability.
                this.Dispose(false);
            }

            /// <summary>
            /// Disposes of the resources (other than memory) used by the module that implements <see cref="T:System.Web.IHttpModule"/>.
            /// </summary>
            public void Dispose()
            {
                this.Dispose(true);

                // This object will be cleaned up by the Dispose method.
                // Therefore, you should call GC.SuppressFinalize to
                // take this object off the finalization queue
                // and prevent finalization code for this object
                // from executing a second time.
                GC.SuppressFinalize(this);
            }

            /// <summary>
            /// Disposes the object and frees resources for the Garbage Collector.
            /// </summary>
            /// <param name="disposing">
            /// If true, the object gets disposed.
            /// </param>
            private void Dispose(bool disposing)
            {
                if (this.isDisposed)
                {
                    return;
                }

                if (disposing)
                {
                    taskDisposeCallBack?.Invoke();
                }

                // Call the appropriate methods to clean up
                // unmanaged resources here.
                // Note disposing is done.
                this.isDisposed = true;
            }
        }
    }
}


Usage


    using (var @lock = await new ThreadPoolAsyncLock().ScheduledTask().ConfigureAwait(false))
            {
                if (@lock.IsLocked)
                    return query.Track().Skip((page - 1) * count).Take(count).GetResult();

                throw new HttpResponseException((HttpStatusCode)429);
            }


EPiFind Whole Word Matching

By Default EPiFind tokenizes the query word. If the word is composed of multiple words and we need to search the whole word instead of tokenized words  then its bit tricky. We found it bit difficult initially to search for whole given word.

At the end we figure out to use the regex to specifically tell EPiFind to match whole word instead of tokenizing.


query = query.Search(x => x.For(legacyVideoId, q =>
      {
        q.Query = $"*{legacyVideoId}*";
      }).InField(a => a.LegacyVideoId));

Tuesday, April 17, 2018

Web API Default Behaviour Overriding

Overriding Text Encoding

In one of our project we had an issue where one of invalid text character in web api was throwing exception instead of removing or replacing that bad character. After investigating we figure out that the default Encoding was UTF8 but EncoderFallback was set to throw Exception for invalid character instead of replacing with default Character.

Therefore, we just added this one below line to replace the text encoding behaviour

            config.Formatters.JsonFormatter.SupportedEncodings[0] = Encoding.UTF8;

The default system UTF8 Encoding fallback set to replace invalid character with "?"

public static void ConfigureWebApi(HttpConfiguration config)
        {
            ...
            ...

        // HACK: To allow JSON serialization to handle invalid characters
            config.Formatters.JsonFormatter.SupportedEncodings[0] = Encoding.UTF8;
            config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
            config.Services.Replace(typeof(IExceptionLogger), new GlobalExceptionLogger());
        }

Overriding GlobalExceptionHandler

We are using Azure Telemetry to log all of our errors therefore, we wanted to override the global exception handler to log all errors to telemetry. This is similar concept of Global.ascx function for web api.

the below line will replace the GlobalExceptionHandler for WebApi

            config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());


   public class GlobalExceptionHandler : ExceptionHandler
    {
        public override void Handle(ExceptionHandlerContext context)
        {
            var exception = context.Exception;
            var message = context.Exception.GetType().Name + ": " + context.Exception.Message;

            if (exception is HttpException httpException)
            {
                context.Result = new CustomErrorResult(context.Request, (HttpStatusCode)httpException.GetHttpCode(), message);
                return;
            }

            // Return HttpStatusCode for other types of exception.
            context.Result = new CustomErrorResult(context.Request, HttpStatusCode.BadRequest, message);
        }
    }
 


public class CustomErrorResult : IHttpActionResult
    {
        private readonly string _errorMessage;
        private readonly HttpRequestMessage _requestMessage;
        private readonly HttpStatusCode _statusCode;

        public CustomErrorResult(HttpRequestMessage requestMessage, HttpStatusCode statusCode, string errorMessage)
        {
            _requestMessage = requestMessage;
            _statusCode = statusCode;
            _errorMessage = errorMessage;
        }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            return Task.FromResult(_requestMessage.CreateErrorResponse(_statusCode, _errorMessage));
        }
    }
  


Overriding GlobalExceptionLogger

We also wanted to replace the GlobalExceptionLogger to be our ExceptionLogger to push all errors to Azure Telemetry.

            config.Services.Replace(typeof(IExceptionLogger), new GlobalExceptionLogger());


public class GlobalExceptionLogger : ExceptionLogger
    {
        public override void Log(ExceptionLoggerContext context)
        {
            new TelemetryClient().TrackException(context.Exception);
        }
    }
 

Tuesday, December 19, 2017

Azure Storage Account File Storage Wrapper For Basic Functions

In one of our project we had to upload/download files to azure file storage.
Therefore, I build this small wrapper classes to make use of Azure file storage easy.

AzureStorageAccountClient

This client is supposed to provide function to connect to different storage account such as (Blob, File and etc storage)


    public class AzureStorageAccountClient
    {
        private readonly ILogger logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        public AzureStorageAccountClient(string connectionString)
        {
            ConnectionString = connectionString;
        }

        public string ConnectionString { get; }

        public IAzureStorageShareClient FileClientConnect()
        {
            string message = string.Empty;
            try
            {
                return new AzureStorageShareClient(CloudStorageAccount.Parse(ConnectionString).CreateCloudFileClient());
            }
            catch (Exception ex)
            {
                logger.Critical("Error in Azure Client", ex);
                throw;
            }
        }
    }


AzureStorageShareClient

This class provides functions to perform on share storage client.


   public interface IAzureStorageShareClient
    {
        CloudFileClient CloudFileClient { get; }
    }

    public class AzureStorageShareClient : IAzureStorageShareClient
    {
        public AzureStorageShareClient(CloudFileClient cloudFileClient)
        {
            CloudFileClient = cloudFileClient;
        }

        public CloudFileClient CloudFileClient { get; }
    }   



AzureStorageFileDirectoryClient

This class provides functionality to different Directoy related operations.


    public interface IAzureStorageFileDirectoryClient
    {
        CloudFileShare CloudFileShare { get; }

        CloudFileDirectory CloudFileDirectory { get; set; }
    }

    public class AzureStorageFileDirectoryClient : IAzureStorageFileDirectoryClient
    {
        public AzureStorageFileDirectoryClient(CloudFileShare cloudFileShare)
        {
            CloudFileShare = cloudFileShare;
        }

        public CloudFileShare CloudFileShare { get; }

        public CloudFileDirectory CloudFileDirectory { get; set; }
    }


AzureStorageFileClient

This class provides functions to different File related operations


    public interface IAzureStorageFileClient
    {
        CloudFileDirectory CloudFileDirectory { get; set; }

        CloudFile CloudFile { get; }
    }

    public class AzureStorageFileClient : IAzureStorageFileClient
    {
        public AzureStorageFileClient(CloudFile cloudFile)
        {
            CloudFile = cloudFile;
        }

        public CloudFileDirectory CloudFileDirectory { get; set; }

        public CloudFile CloudFile { get; }
    }



As you have noted none of the above classes have any functions.
Below is the extension class that provides functions available to those interfaces.

AzureStorageAccountExtensions


    public static class AzureStorageAccountExtensions
    {
        public static async Task GetOrAddShare(this IAzureStorageShareClient client, string shareName)
        {
            var cloudFileShare = client.CloudFileClient.GetShareReference(shareName);
            await cloudFileShare.CreateIfNotExistsAsync().ConfigureAwait(false);
            return new AzureStorageFileDirectoryClient(cloudFileShare);
        }

        public static async Task GetOrAddFolder(this IAzureStorageFileDirectoryClient client, string folderPath)
        {
            var cloudRootDirectory = client.CloudFileShare.GetRootDirectoryReference();
            var cloudFileDirectory = cloudRootDirectory.GetDirectoryReference(folderPath);
            await cloudFileDirectory.CreateIfNotExistsAsync().ConfigureAwait(false);
            client.CloudFileDirectory = cloudFileDirectory;
            return client;
        }

        public static IEnumerable ListFiles(this IAzureStorageFileDirectoryClient client)
        {
            return client.CloudFileDirectory.ListFilesAndDirectories().Where(f => !f.Uri.LocalPath.Contains(".folder"));
        }

        public static async Task AddFile(this IAzureStorageFileDirectoryClient client, string fileName, long length)
        {
            await client.CloudFileDirectory.CreateIfNotExistsAsync().ConfigureAwait(false);

            var cloudFile = client.CloudFileDirectory.GetFileReference(fileName);
            if (!await cloudFile.ExistsAsync().ConfigureAwait(false))
            {
                await cloudFile.CreateAsync(length).ConfigureAwait(false);
            }

            return new AzureStorageFileClient(cloudFile);
        }

        public static async Task GetFile(this IAzureStorageFileDirectoryClient client, string fileName)
        {
            var cloudFile = client.CloudFileDirectory.GetFileReference(fileName);
            if (await cloudFile.ExistsAsync().ConfigureAwait(false))
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    await cloudFile.DownloadToStreamAsync(ms).ConfigureAwait(false);
                    return ms.ToArray();
                }
            }

            return null;
        }

        public static async void UploadFile(this IAzureStorageFileClient client, byte[] data)
        {
            await client.CloudFile.UploadFromByteArrayAsync(data, 0, data.Length).ConfigureAwait(false);
        }
    }



Tuesday, October 17, 2017

EPiFind AndAny/OrAny Filter Expression Builder

Extending the EPiServer Find Functionality to support OrAny or AndAny Filter Expression

The OrAny and AndAny filter for EPiFind will allow the developer to combine list of contents as And or Or with the other query conditions.

We had custom property Tag and TagList class which we wanted EPiFind to index so we can query the contents based on those filters.

The Tag and TagList class looks like below

Tag Class

[JsonConverter(typeof(TagJsonConverter))]
    public class Tag
    {
        private string text;
        private string value;
        private string tag;
        private bool isLoaded;

        public string Text
        {
            get
            {
                if (!isLoaded)
                    Load();

                return text;
            }

            set
            {
                text = value;
            }
        }

        public string Value
        {
            get
            {
                if (!isLoaded)
                    Load();

                return value;
            }

            set
            {
                this.value = value;
            }
        }

        public Tag()
        { }

        public Tag(string tag)
        {
            this.tag = tag;
        }

        public Tag(string value, string text)
        {
            isLoaded = true;
            this.value = value;
            this.text = text;
        }

        public string GetId(string type) => Value?.Substring(type.Length);

        public override string ToString()
        {
            return $"{Value}{Constants.Tags.NameDelimiter}{Text}";
        }

        private void Load()
        {
            isLoaded = true;
            if (!string.IsNullOrWhiteSpace(tag))
            {
                var parts = tag.Split(new[] { Constants.Tags.NameDelimiter }, 2);
                if (parts.Length == 1)
                {
                    text = parts[0];
                    value = string.Empty;
                }
                else
                {
                    value = parts[0];
                    text = parts[1];
                }
            }
        }

        public static Tag Parse(string tag)
        {
            return string.IsNullOrWhiteSpace(tag) ? null : new Tag(tag);
        }
    }


TagList Class

[JsonConverter(typeof(TagListJsonConverter))]
    public class TagList : IEnumerable
    {
        private Tag[] tags;
        private string tag;
        private bool isLoaded;

        public TagList() { }

        public TagList(string tag)
        {
            tags = new Tag[0];
            this.tag = tag;
        }

        public TagList(IEnumerable tags)
        {
            isLoaded = true;
            this.tags = tags.ToArray();
        }

        public override string ToString() => string.Join(Constants.Tags.ValueDelimiter.ToString(), this.Select(s => s.ToString()));

        public IEnumerator GetEnumerator()
        {
            if (!isLoaded)
                Load();

            return ((IEnumerable)this.tags).GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        private void Load()
        {
            isLoaded = true;
            if (!string.IsNullOrWhiteSpace(tag))
                tags = tag.Split(new char[] { Constants.Tags.ValueDelimiter }, StringSplitOptions.RemoveEmptyEntries).Select(s => new Tag(s)).ToArray();
        }
    }

We use the Tag and TagList class like below just for reference so you can get a context

[Display(Order = 20, Description = "Start typing the topic name to see suggestions. Allows multiple.")]
[AutoSuggestTags(typeof(TopicSelectionQuery))]
[BackingType(typeof(PropertyTagList))]
public virtual TagList Topics { get; set; }

[BackingType(typeof(PropertyTag))]
[Display(Order = 30, Description = "Select competition to filter the contents of this section block")]
[LinkedAutoSelection(typeof(CompetitionTagSelectionQuery), null, null, new[] { "/season/topic", "/round/topic" })]
public virtual Tag Competition { get; set; }


The Problem

We wanted to filter the contents by applying filter on Tag and TagList class. Therefore, we build a filter expression using the below code for different property.

OrTopic Filter Builder

public static ITypeSearch TopicOrFilter(this ITypeSearch typeSearchQuery, IEnumerable values)
          where T : ContentPage
        {
            if (values.IsNullOrEmpty())
                return typeSearchQuery;

            FilterBuilder filterBuilder = new FilterBuilder(typeSearchQuery.Client);

            foreach (var value in values)
            {
                filterBuilder = filterBuilder.Or(t => t.Topic.Match(value));
            }

            return typeSearchQuery.Filter(filterBuilder);
        }


but we wanted this to be generic so we do not have to add separate function for each paramter.

Therefore we added below generic code

The AndAny Function

public static ITypeSearch AndAny(this ITypeSearch typeSearchQuery, IEnumerable values, Expression> filter)
            where T : ContentPage
        {
            if (values.IsNullOrEmpty())
                return typeSearchQuery;

            FilterBuilder filterBuilder = new FilterBuilder(typeSearchQuery.Client);

            /* Expression visitor to modify provided filter to epiFind filter expression */
            var resolver = new ParameterReplacerVisitor, Func>();

            /* Parameter to be replaced with constant */
            var parameter = filter.Parameters[1];
            foreach (var value in values)
            {
                resolver.Source = parameter;
                resolver.Value = value;

                filterBuilder = filterBuilder.Or(resolver.ReplaceValue(filter));
            }

            return typeSearchQuery.Filter(filterBuilder);
        }

The OrAny Function

public static ITypeSearch OrAny(this ITypeSearch typeSearchQuery, IEnumerable values, Expression> filter)
            where T : ContentPage
        {
            if (values.IsNullOrEmpty())
                return typeSearchQuery;

            FilterBuilder filterBuilder = new FilterBuilder(typeSearchQuery.Client);

            /* Expression visitor to modify provided filter to epiFind filter expression */
            var resolver = new ParameterReplacerVisitor, Func>();

            /* Parameter to be replaced with constant */
            var parameter = filter.Parameters[1];
            foreach (var value in values)
            {
                resolver.Source = parameter;
                resolver.Value = value;

                filterBuilder = filterBuilder.Or(resolver.ReplaceValue(filter));
            }

            return typeSearchQuery.OrFilter(filterBuilder);
        }

The Other Problem.

The EPiFind filter exprssion expects fitler expression with one parameter but our expression contains the two parameter. Therefore, we need to resolve parameter expression to align with the functions available for filter builder.

public class ParameterReplacerVisitor : System.Linq.Expressions.ExpressionVisitor
    {

        public ParameterReplacerVisitor()
        {
        }

        public ParameterExpression Source { get; set; }

        public object Value { get; set; }

        public Expression ReplaceValue(Expression exp)
        {
            var expNew = Visit(exp) as LambdaExpression;
            return Expression.Lambda(expNew.Body, expNew.Parameters);
        }

        /// 
        /// This functoin will replace the parameter expression with constant values if parameter name and type matches with provided value
        /// for e.g (num1, num2) => num1 + num2 will be replaced (num1) => num1 + 3. if we want to replace num2 with 3.
        /// 
        protected override Expression VisitParameter(ParameterExpression node)
        {
            // Replace the source with the target, visit other params as usual.
            if (node.Type == Source.Type && node.Name == Source.Name)
            {
                return Expression.Constant(Value);
            }

            return base.VisitParameter(node);
        }

        /// 
        /// This functoin will replace the member expression with constant values if parameter name and type matches with provided value
        /// for e.g (num1, classObj) => num1 + classObj.num2 will be replaced (num1) => num1 + 3. if we want to replace classObj.num2 with 3.
        /// 
        protected override Expression VisitMember(MemberExpression m)
        {
            if (m.Expression != null
                && m.Expression.NodeType == ExpressionType.Parameter
                && m.Expression.Type == Source.Type && ((ParameterExpression)m.Expression).Name == Source.Name)
            {
                object newVal;
                if (m.Member is FieldInfo)
                    newVal = ((FieldInfo)m.Member).GetValue(Value);
                else if (m.Member is PropertyInfo)
                    newVal = ((PropertyInfo)m.Member).GetValue(Value, null);
                else
                    newVal = null;
                return Expression.Constant(newVal);
            }

            return base.VisitMember(m);
        }

        /// 
        /// In this function we are reducing the number of parameters in lambda expression
        /// for e.g as we are replacing parameter with constants therefore, (num1, num2) => will become (num1) =>
        /// 
        protected override Expression VisitLambda(Expression node)
        {
            var parameters = node.Parameters.Where(p => p.Name != Source.Name || p.Type != Source.Type).ToList();
            return Expression.Lambda(Visit(node.Body), parameters);
        }
    }



Monday, October 9, 2017

Custom Property In EPiServer And EPiFInd Indexing

In one of our project we had to build the autosuggest property. However, the default type of autosuggest property is string but we wanted to store custom object so we can index through EPiFind with additional values.

Therefore, we implemented custom class to store additional values and also needed JsonConverter for 2 reasons

  1. Convert Class to String for Dojo Framework so existing autosuggest property works as expected
  2. Convert and index object for EPiFind differently
We will delimte the value of autosuggest property with '-' to break it into multiple property and save it and index it properly.

Define the property


[BackingType(typeof(PropertyTag))]
[Display(Order = 45, Description = "Select competition to filter the contents of the page")]
[AutoSuggestSelection(typeof(TagSelectionQuery), AllowCustomValues = false)]
public virtual Tag Competition { get; set; }


Define the PropertyTag Class



    [PropertyDefinitionTypePlugIn(Description = "A property to tag content", DisplayName = "Tag")]
    [JsonConverter(typeof(TagJsonConverter))]
    public class PropertyTag : PropertyLongString
    {
        public override PropertyDataType Type => PropertyDataType.LongString;

        public override Type PropertyValueType => typeof(Tag);

        public override object Value
        {
            get { return Tag.Parse(base.Value?.ToString()); }
            set { base.Value = value?.ToString(); }
        }

        public override object SaveData(PropertyDataCollection properties)
        {
            return this.LongString;
        }
    }


Define the JsonConverter



 internal class TagJsonConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return true;
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (reader.Value is Tag tag)
                return tag;

            return new Tag(reader.Value.ToString());
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            if (value == null)
            {
                writer.WriteNull();
                return;
            }

            if (writer is MaxDepthJsonWriter && value is Tag tag)
            {
                writer.WriteStartObject();
                writer.WritePropertyName($"Text{TypeSuffix.String}");
                writer.WriteValue(tag.Text);
                writer.WritePropertyName($"Value{TypeSuffix.String}");
                writer.WriteValue(tag.Value);
                writer.WriteEndObject();
                return;
            }

            if (value is Tag itag)
            {
                writer.WriteValue(itag.ToString());
            }
            else
            {
                writer.WriteValue(value.ToString());
            }
        }
    }


Define Tag Class




 [JsonConverter(typeof(TagJsonConverter))]
    public class Tag
    {
        private string text;
        private string value;
        private string tag;
        private bool isLoaded;

        public string Text
        {
            get
            {
                if (!isLoaded)
                    Load();

                return text;
            }

            set
            {
                text = value;
            }
        }

        public string Value
        {
            get
            {
                if (!isLoaded)
                    Load();

                return value;
            }

            set
            {
                this.value = value;
            }
        }

        public Tag()
        { }

        public Tag(string tag)
        {
            this.tag = tag;
        }

        public Tag(string value, string text)
        {
            isLoaded = true;
            this.value = value;
            this.text = text;
        }

        public string GetId(string type) => Value?.Substring(type.Length);

        public override string ToString()
        {
            return $"{Value}{ "-" }{Text}";
        }

        private void Load()
        {
            isLoaded = true;
            if (!string.IsNullOrWhiteSpace(tag))
            {
                var parts = tag.Split(new[] { "-" }, 2);
                if (parts.Length == 1)
                {
                    text = parts[0];
                    value = string.Empty;
                }
                else
                {
                    value = parts[0];
                    text = parts[1];
                }
            }
        }

        public static Tag Parse(string tag)
        {
            return string.IsNullOrWhiteSpace(tag) ? null : new Tag(tag);
        }
    }

Define TagSelectionQuery


    [ServiceConfiguration(typeof(ISelectionQuery))]
    public class TagSelectionQuery : ISelectionQuery
    {
        private const int Depth = 0;

        protected virtual string Prefix { get; } = Constants.Tags.Competition;

        public IEnumerable GetItems(string query)
        {
            /* Return List Of Select Items With Value Delimited by '-' For value and text of tag
        }

        ///         /// Will be called when initializing an editor with an existing value to get the corresponding text representation.
        ///         public ISelectItem GetItemByValue(string value) => /* Return SelectItem */;
    }


Pro Tip: Make Sure Admin For Property Looks Like below