Turn off Session State locking in Sitecore MVC pages

The default implementation of the ASP .NET Session State Module uses exclusive locking for each request from the same session. This means ASP .NET will only execute one request at a time from the same browser. Any other request will be locked by the Session State Module and will not be executed until the previous request is complete and it can obtain the exclusive lock. This can cause performance issues in many real-world scenarios.

Below screenshot from IIS shows 6 concurrent request to the homepage from the same browser. Sitecore is only executing the bottom request, which is in the ExecuteRequestHandler state. All other 5 requests are in the RequestAcquireState state and will only be fulfilled one at a time after the bottom request is complete. Each of the requests in RequestAcquireState state will check the session store every 0.5 seconds to see if it can obtain a lock.

This can cause pressure on the session state store in case many requests take some time to execute. Depending on the session store it is common to see messages like below in log:

Common errors with session state in Redis:

Exception type: TimeoutException
Exception message: Timeout performing EVAL, inst: ....
at StackExchange.Redis.ConnectionMultiplexer.ExecuteSyncImpl[T](Message message, ResultProcessor1 processor, ServerEndPoint server) at StackExchange.Redis.RedisBase.ExecuteSync[T](Message message, ResultProcessor1 processor, ServerEndPoint server)
at StackExchange.Redis.RedisDatabase.ScriptEvaluate(String script, RedisKey[] keys, RedisValue[] values, CommandFlags flags)
at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.<>c__DisplayClass7.b__6()
at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.RetryForScriptNotFound(Func1 redisOperation) at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.RetryLogic(Func1 redisOperation)
at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.Eval(String script, String[] keyArgs, Object[] valueArgs)
at Sitecore.SessionProvider.Redis.RedisConnectionWrapper.TryTakeWriteLockAndGetData(String sessionId, DateTime lockTime, Object& lockId, ISessionStateItemCollection& data, Int32& sessionTimeout)
at Sitecore.SessionProvider.Redis.RedisSessionStateProvider.GetItemFromSessionStore(Boolean isWriteLockRequired, HttpContext context, String id, Boolean& locked, TimeSpan& lockAge, Object& lockId, SessionStateActions& actions)
at Sitecore.SessionProvider.Redis.RedisSessionStateProvider.GetItemExclusive(HttpContext context, String id, Boolean& locked, TimeSpan& lockAge, Object& lockId, SessionStateActions& actions)
at System.Web.SessionState.SessionStateModule.GetSessionStateItem()

Common errors with session state in SQL:

Message: Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.
Source: System.Data
   at System.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
   ... 
   at System.Web.SessionState.SqlSessionStateStore.SqlStateConnection..ctor(SqlPartitionInfo sqlPartitionInfo, TimeSpan retryInterval)
Message: Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.
Source: System.Data
   at System.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
   ... 
   at System.Web.SessionState.SqlSessionStateStore.SqlStateConnection..ctor(SqlPartitionInfo sqlPartitionInfo, TimeSpan retryInterval)

Common errors with session state in Mongo:

ERROR Application error.
Exception: System.TimeoutException
Message: Timeout waiting for a MongoConnection.
Source: MongoDB.Driver
   at MongoDB.Driver.Internal.MongoConnectionPool.AcquireConnection(AcquireConnectionOptions options)
   ...
   at Sitecore.SessionProvider.MongoDB.MongoSessionStateProvider.GetItemExclusive(HttpContext context, String id, Boolean& locked, TimeSpan& lockAge, Object& lockId, SessionStateActions& actions)
   at System.Web.SessionState.SessionStateModule.GetSessionStateItem()
   at System.Web.SessionState.SessionStateModule.BeginAcquireState(Object source, EventArgs e, AsyncCallback cb, Object extraData)
   at System.Web.HttpApplication.AsyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Too many locked requests from a single session

ERROR Application error.
Exception: System.Web.HttpException
Message: The request queue limit of the session is exceeded.
Source: System.Web
   at System.Web.SessionState.SessionStateModule.QueueRef()
   at System.Web.SessionState.SessionStateModule.PollLockedSession()
   at System.Web.SessionState.SessionStateModule.GetSessionStateItem()
   at System.Web.SessionState.SessionStateModule.BeginAcquireState(Object source, EventArgs e, AsyncCallback cb, Object extraData)
   at System.Web.HttpApplication.AsyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step)
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Sitecore has a good KB article which describes this in more detail which can be found here. This article mentions to set session state to readonly and describes how to do this for 2 scenarios:

  • Custom MVC Routes: Set the session state to readonly on the controller. This can be done by decorating the controller with this attribute: [SessionState(SessionStateBehavior.ReadOnly)]
  • ASP.NET Web Forms pages: Set the EnableSessionState=”Readonly” on the pages directive

This article does not mention how to fix this for Sitecore MVC pages. The solution provided below describes how to address this for Sitecore MVC pages.

Solution

Sitecore sets this to the Default Session state behavior in the SitecoreControllerFactory for Sitecore MVC pages. This is a virtual method so this can be overridden to change the session state behavior:

using Sitecore.Diagnostics;
using Sitecore.Mvc.Controllers;
using Sitecore.Mvc.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;

namespace Foundation.Extensions.Factory
{
    public class ReadOnlySessionStateSitecoreControllerFactory : SitecoreControllerFactory
    {
        public ReadOnlySessionStateSitecoreControllerFactory(IControllerFactory innerFactory) : base(innerFactory)
        {
        }

        public override SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
        {
            Assert.ArgumentNotNull(requestContext, "requestContext");
            Assert.ArgumentNotNull(controllerName, "controllerName");

            if (controllerName.EqualsText(SitecoreControllerName))
            {
                return SessionStateBehavior.ReadOnly;
            }

            return InnerFactory.GetControllerSessionBehavior(requestContext, controllerName);
        }
    }
}

An initialize pipeline processor needs to be created to set our new controller factory:

using Foundation.SitecoreExtensions.Factory;
using Sitecore.Mvc.Controllers;
using Sitecore.Mvc.Pipelines.Loader;
using Sitecore.Pipelines;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Foundation.Extensions.Processors.Initialize
{
    public class InitializeReadOnlySessionStateSitecoreControllerFactory : InitializeControllerFactory
    {
        protected Func<System.Web.Mvc.ControllerBuilder> ControllerBuilder = () => System.Web.Mvc.ControllerBuilder.Current;

        protected override void SetControllerFactory(PipelineArgs args)
        {
            System.Web.Mvc.ControllerBuilder controllerBuilder = ControllerBuilder();
            var controllerFactory = new ReadOnlySessionStateSitecoreControllerFactory(controllerBuilder.GetControllerFactory());
            controllerBuilder.SetControllerFactory(controllerFactory);
        }
    }
}

Below XML file can be used to patch in this new pipeline processor

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="Foundation.Extensions.Processors.Initialize.InitializeReadOnlySessionStateSitecoreControllerFactory, Foundation.Extensions" patch:instead="*[@type='Sitecore.Mvc.Pipelines.Loader.InitializeControllerFactory, Sitecore.Mvc']"/>
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

Below screenshot shows the same scenario as in the beginning of this post, but now all 8 requests are getting executed at the same time.

Setting the session state to readonly for Sitecore MVC pages can cause significant performance improvements and will help reduce the load on the session store as described in Sitecore’s KB article. Before doing this it is important to understand below considerations:

  • Multiple requests from the same browser will execute at the same time. Your application should be able to handle this without causing any unintended issues by multiple threads modifying shared objects at the same time.
  • Custom objects cannot be stored in the session state anymore when it is set to ReadOnly, except when the session state is in process. Using a custom cache as already suggested in Sitecore’s article is a good solution.
  • This issue might not occur when a site is running smoothly, but can turn a small issue into an overall site stability issue. The session store can get under a lot of load for example if some pages in your site start being slow or in case of an app pool recycle. This can impact the overall stability of the site as it can overload SQL, Redis or Mongo.

Solve caching issues when rendering is on page multiple times

HTML caching is arguably the best way to improve Sitecore performance. Sometimes you can run into issues when you enable HTML cache on a rendering and the rendering has been added to the same page multiple times. This will only happen if the renderings do not have a datasource or share the same datasource, but will still render different content. This could happen for example when the renderings have different rendering parameters or have some custom logic which changes the content.

This can be fixed in a generic way by overriding the GenerateKey method of the GenerateCacheKey RenderRendering Processor. Below code will add the UniqueId of each rendering to the cachekey which will ensure the cached output is unique for each rendering.

using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using Sitecore.Mvc.Presentation;

namespace Foundation.Pipelines.RenderRendering
{
    public class GenerateCustomCacheKey : GenerateCacheKey
    {
        protected override string GenerateKey(Rendering rendering, RenderRenderingArgs args)
        {
            var cacheKey = base.GenerateKey(rendering, args);

            cacheKey += rendering.UniqueId;

            return cacheKey;
        }
    }
}

 

Sitecore 9 fix heartbeat.aspx

The heartbeat page is a useful page in Sitecore as it shows if Sitecore can connect to it’s databases. If so it will return a 200 status. It can be found at /sitecore/service/heartbeat.aspx and it can be a good practice to point the load balancer’s health check to this page. This will avoid that any traffic is send to a server which cannot connect to its backend database.

Sitecore 9 has introduced a number of new connectionstrings with xConnect and the heartbeat page will fail on these. This can be avoided by adding the new connectionstrings to the excluded connections so the heartbeat page will not return an error while Sitecore’s databases are online. Below is the value which can be used to get the heartbeat page to work in Sitecore 9.

<setting name=”Sitecore.Services.Heartbeat.ExcludeConnection” value=”LocalSqlServer| xconnect.collection| xconnect.collection.certificate| xdb.referencedata.client| xdb.referencedata.client.certificate| xdb.marketingautomation.reporting.client| xdb.marketingautomation.reporting.client.certificate| xdb.marketingautomation.operations.client| xdb.marketingautomation.operations.client.certificate|  EXM.CryptographicKey| EXM.AuthenticationKey| Session| sharedSession” />

How to fix “RedisConnectionException: No connection is available to service this operation” in Sitecore

Redis is a great choice for Sitecore’s shared session database. Sitecore has a good article which describes how to set this up, and links to this article to explain all options. I was running into issues when setting this up with my Azure Redis Cache which is using an access key. The “accessKey” attribute in the provider node was populated with the access key form the Azure portal. Initially i was seeing something like this in the log:

INFO  redisname.redis.cache.windows.net:6380,abortConnect=False
INFO
INFO  Connecting redisname.redis.cache.windows.net:6380/Interactive...
...
INFO  redisname.redis.cache.windows.net:6380 faulted: UnableToResolvePhysicalConnection on PING

Then the log would be full of errors like below:

ERROR GetItemFromSessionStore => StackExchange.Redis.RedisConnectionException: No connection is available to service this operation: EVAL
   at StackExchange.Redis.ConnectionMultiplexer.ExecuteSyncImpl[T](Message message, ResultProcessor`1 processor, ServerEndPoint server)
   at StackExchange.Redis.RedisBase.ExecuteSync[T](Message message, ResultProcessor`1 processor, ServerEndPoint server)
   at StackExchange.Redis.RedisDatabase.ScriptEvaluate(String script, RedisKey[] keys, RedisValue[] values, CommandFlags flags)
   at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.c__DisplayClass12_0.b__0()
   at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.RetryForScriptNotFound(Func`1 redisOperation)
   at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.RetryLogic(Func`1 redisOperation)
   at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.Eval(String script, String[] keyArgs, Object[] valueArgs)
   at Sitecore.SessionProvider.Redis.RedisConnectionWrapper.TryTakeWriteLockAndGetData(String sessionId, DateTime lockTime, Object& lockId, ISessionStateItemCollection& data, Int32& sessionTimeout)
   at Sitecore.SessionProvider.Redis.RedisSessionStateProvider.GetItemFromSessionStore(Boolean isWriteLockRequired, HttpContext context, String id, Boolean& locked, TimeSpan& lockAge, Object& lockId, SessionStateActions& actions)

The only way I was able to get this to work was by not putting the access key in the provider but instead specifying it in the connection string (in ConnectionStrings.config):

<add name="sharedSession" connectionString="redisname.redis.cache.windows.net:6380,password=rediskey,ssl=True,abortConnect=False" />

Sitecore is now able to connect to Redis and all errors are gone from the log. Below lines from log file show the successful connect:

INFO  redisname.redis.cache.windows.net:6380,password=rediskey,ssl=True,abortConnect=False
INFO  Connecting redisname.redis.cache.windows.net:6380/Interactive...
....
INFO  Connect complete: redisname.redis.cache.windows.net:6380

Sitecore namespace issues

Since I ran into this silly issue a couple of times I decided to dig into it a little bit and write a post about it. As developers we like to structure our solutions and projects by applying consistent naming conventions. Most Sitecore projects use a naming convention like this prefix.Sitecore.description. Usually the prefix is something like a company name or a brand name, and the description is something that describes the project’s role in the solution. There can also be multiple prefixes and descriptions separated by a period. Looking at Habitat an example of description could be Feature.Accounts.

A popular approach is to keep project names, assembly names, folder structure and namespaces all in sync for clarity. If you go with this approach you will end up with a namespace like MyBrand.Sitecore.Features.Accounts. This is where things can get a little messy because “Sitecore” is in the namespace and this can conflict with references to Sitecore DLL’s. Consider below piece of code:

namespace MyBrand.Sitecore.Features.Accounts
{
    using Sitecore;

    public class Helper
    {
        public string ReturnCurrentItemName()
        {
            var item = Sitecore.Context.Item;

            return item.Name;
        }
    }
}

This code will create below compile error:

Error CS0234 The type or namespace name ‘Context’ does not exist in the namespace ‘MyBrand.Sitecore’ (are you missing an assembly reference?)

Problem

The problem here is how namespace resolution works for nested namespaces, more details can be found in the C# language specification here. As the compile error suggests it is looking for the Context object in our class instead of in Sitecore in the root scope. The good news is that there are at least 3 different ways to solve this:

1. Avoid using anything before “Sitecore” in namespace

The same code will compile just fine when “Sitecore” is the first part of the namespace, see line 1. This is the easiest solution if changing the namespace is not a problem.

namespace Sitecore.Features.Accounts
{
    using Sitecore;

    public class Helper
    {
        public string ReturnCurrentItemName()
        {
            var item = Sitecore.Context.Item;

            return item.Name;
        }
    }
}

2. Use namespace alias

This is my least favorite way to solve this but it also seems to be the most common solution. The Sitecore namespace from the Sitecore DLL can be declared as an alias and then used to reference:

using RealSitecore = Sitecore;

namespace MyBrand.Sitecore.Features.Accounts
{
    public class Helper
    {
        public string ReturnCurrentItemName()
        {
            var item = RealSitecore.Context.Item;

            return item.Name;
        }
    }
}

3. Use global namespace alias

Instead of making up a random name for Sitecore as in the last solution, it feels a lot cleaner to use the global namespace alias to tell the compiler to look in the global namespace instead:

namespace MyBrand.Sitecore.Features.Accounts
{
    using Sitecore;

    public class Helper
    {
        public string ReturnCurrentItemName()
        {
            var item = global::Sitecore.Context.Item;

            return item.Name;
        }
    }
}

Summary

As long as there is no problem changing the namespace to start with “Sitecore” then option 1 is the best choice. If for whatever reason that is not an option then go with using the global namespace alias as described in option 3.

Angular 2 and Sitecore Best Practices

Angular 2 is out of beta and I have been using this for a while now to write single page applications in Sitecore. There are a couple of good blog posts around that describes how to get Angular 2 and Sitecore working. This post will focus on some touch points between Sitecore and Angular 2. The official Angular 2 site has a good article which describes setting up Angular 2 in Visual Studio 2015.

Retrieving data from Sitecore

Angular 2 has a HTTP library which makes it easy to work with JSON data returned from the server. Sitecore makes it easy to return JSON data especially from a controller action when using MVC.

However it seems like Sitecore.Services.Client is often overlooked when a restful API is required. Sitecore.Services.Client allows you to fetch Sitecore items in JSON format without writing any code, this link shows several ways to retrieve data this way. The Sitecore.Services.Client is also extensible just like almost everything else in Sitecore. The developer guide can be found here

PushState Routing

Angular 2 uses pushState routing by default, which means that the URL of the Angular route gets appended to the regular URL using a slash (/) as a separator just like Sitecore does. This is the preferred style and is good for SEO as well. An example of this would be an Angular 2 application that is hosted on mysite.com home page that has a route at mysite.com/details which will be displayed by Angular’s routing from the app on home page.

There will be an issue when someone tries to deep link into a mysite.com/details as Sitecore will try to resolve this item but cannot since this is only an Angular route not a Sitecore item. Perhaps the easiest way to solve this is by creating rewrite rules in IIS to resolve these links to the homepage. However I do not like this approach as it is not very maintainable. Creating the same app on a different page would require deploying additional rewrites. Instead I’m using below pipeline to detect this scenario and assign the correct item. Now the content author can add additional instances of the Angular 2 application without involvement from the development team:

using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Links;
using Sitecore.Pipelines.HttpRequest;
using System;

namespace Jeroen.Sitecore
{
    public class Angular2PushStateItemResolver : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (Context.Item == null && Context.Database != null && Context.Database.Name != "core")
            {
                //find all items that have the Angular app
                var query = string.Format("{0}//*[@@templateid='{1}']", args.StartPath, "<Guid of template with Angular 2 app>");
                var items = Context.Database.SelectItems(query);

                foreach (var item in items)
                {
                    var options = LinkManager.GetDefaultUrlOptions();
                    options.LanguageEmbedding = LanguageEmbedding.Never;

                    //check if this is an Angular component inside the current item and if so assign context item
                    if (args.LocalPath.StartsWith(LinkManager.GetItemUrl(item, options), StringComparison.OrdinalIgnoreCase))
                    {
                        Context.Item = item;
                        break;
                    }
                }
            }
        }
    }
}

Here is the code to include the pipeline:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor patch:after="*[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" type="Jeroen.Sitecore.Angular2PushStateItemResolver, Jeroen.Sitecore" />
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

What about SEO, Social and performance?

Angular2 is rendering the page client side which can adversely affect a number of key areas:

  • Page performance as the browser needs to download and run a good amount of javascript to run the application. This can get especially troublesome in mobile and larger more complicated apps
  • SEO as search engine bots are not able to crawl the page
  • Social as Twitter, Facebook and other social media are not able to correctly display a preview

Angular Universal will address these issues by sending the rendered output to the client. This is currently only supported in node.js and ASP .NET core but should hopefully be available for ‘regular’ ASP .NET soon. Therefore I recommend keeping the best practices from the universal team in mind to ensure the application will work in Angular universal when this is ready. Maybe Angular Universal can even get the experience editor working?!

Sitecore vary output cache by context item and datasource

Sitecore’s output cache is an excellent solution to improve the performance of a Sitecore solution. The output cache can be configured to be different for a variety of criteria as explained by John West in this post.

The most common option is the VaryByData option. I don’t think this option is documented very well, but it will vary the cache based on the datasource if it is set. The context item will be used instead if no datasource is set.

VaryByData will vary the cache based on Datasource if set, otherwise it will use the context item.

This will work well in most cases, but there are valid scenarios where the cache should vary based on the datasource and the context item. This can be avoided by adding below pipeline. This will use Sitecore’s logic to determine the cachekey, but will add the item path. The if statement can be adjusted depending on the need. Examples include setting it to the ID of the rendering that requires it or checking if rendering’s Datasource property is set if this is required for all renderings that have a datasource.

public class GenerateCacheKeyCustomized : GenerateCacheKey
{
    protected override string GenerateKey(Rendering rendering, RenderRenderingArgs args)
    {
        var cacheKey = base.GenerateKey(rendering, args);

        if (rendering.RenderingItemPath == "<your ID>")
        {
            cacheKey += string.Format("_{0}", Sitecore.Context.Item.Paths.Path);
        }

        return cacheKey;
    }
}

Sitecore WFFM 8.1 incorrectly detects forged request

Sitecore Web Forms For Marketers 8.1 rev.151217 (Update-1) has recently been released, see release notes here As mentioned in the release notes this includes a new feature to prevent forging request.

For some reason I’m seeing valid request (the initial form was served by the Sitecore server and is submitted back to the same server) incorrectly being detected as forged request which causes the form the error. I’m currently working with Sitecore to try to understand why this is happening and understand what the root cause is. I will let you know if i figure out this issue.

Please let me know if anyone else is running into the same issue hopefully it can help troubleshoot this. Also Sitecore can provide a custom DLL which avoids the page crashing in this scenario.

Filter items without language version in Sitecore Item Web API

The Sitecore Item Web API is a great tool to query Sitecore client side and retrieve JSON data which can be shown through a JavaScript library like Angular or Knockout. The Item Web API will return all items that match a query even if there is no version of an item in the current language. This can lead to unnecessarily large HTTP responses which will slow down the page, especially in multilingual sites with many languages with different content per language.

One nice feature of Item Web API is the ability to easily customize it. Below simple pipeline can be added to filter out any item that matches a query but does not have a version in the current language:

using Sitecore.Diagnostics;
using Sitecore.ItemWebApi.Pipelines.Read;
using Sitecore.Web;
using System.Linq;

namespace Jeroen.Pipelines.WebAPI
{
    public class RemoveItemsWithoutLanguageVersion : ReadProcessor
    {
        public override void Process(ReadArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");

            if (WebUtil.GetQueryString("language-filter", null) == "1")
            {
                arguments.Scope = arguments.Scope.Where(i => i.Versions.Count > 0).ToArray();
            }
        }
    }
}

I’m only doing this filtering when a query string of language-filter=1 is passed. I add this to every query that needs it. This avoids any conflicts with existing functionality which uses Item Web API like the Sitecore Desktop. Above pipeline is enabled in Sitecore.ItemWebApi.config as follows:

<itemWebApiRead>
   <processor type="Jeroen.Pipelines.WebAPI.RemoveItemsWithoutLanguageVersion, Jeroen.Pipelines" />
 </itemWebApiRead>