user3538411
user3538411

Reputation: 378

Is there a way to utilize sessionized storage in .NET core without the overhead of binary serializers?

Session stroage in .net core apps requires you to convert it to a byte array first before retrieving it. Compared to .net framework storage, the overhead of these serialization and deserialization operations is incurring a significant amount of overhead. Operations that take 5ms in our .Net framework apps take upwards of 850ms in .Net core. I need the ability to store and retrieve fairly large quantities of data from a server cache in a high performance way, similar to how we can utilizing sessionized storage in .Net framework.

Our apps consume a lot of somewhat large ADO.NET datatables. It is not uncommon for them to contain thousands of rows and dozens of columns. In the past we used .Net framework with session storage to quickly retrieve ADO.NET datatable objects to and from session.

DataTable dt = new DataTable();
HttpContext.Current.Session["data"] = dt; // store in session
dt = (DataTable)HttpContext.Current.Session["data"]; // retrieve from session

We also have our own custom classes that have datatables as members. These classes manipulate the data in different ways. We store and retrieve them from session also.

[Serializable]
 MyClass {
    public DataTable dt;
 }

On our .NET framework apps typical requests for filtering and paging data take around 5ms round trip. Session access is very fast, and the performance penalty for each get and set operation is negligible.

We have been trying to transition to .NET Core, which manages session a little bit differently. Rather than being able to store and retrieve any object in session, I first have to convert it to a byte array. I am using binary formatter with a little bit of logic to handle get and set operations. The code below is not as effecient as it could be, but by far the largest bottleneck is the deserialize operation in the retrieveData method.

Public class SessionManager : ISessionManager
    {
        private readonly IHttpContextAccessor _contextAccessor;
        public SessionManager(IHttpContextAccessor contextAccessor) {
            _contextAccessor = contextAccessor;
         }
        public T get<T>(string key)
        {
            object data = retrieveData(key);
            return data == null ? default(T) : (T)data;
        }

        public void set(string key, object data)
        {
            serializeBinary(key, data); 
        }

        public void remove(string key) {
            _contextAccessor.HttpContext.Session.Remove(key);
        }

        private void serializeBinary(string key, object data) {
            BinaryFormatter bf = new BinaryFormatter();
            using (var ms = new MemoryStream())
            {
                bf.Serialize(ms, data);
                var bytes = ms.ToArray();
                _contextAccessor.HttpContext.Session.Set(key, bytes);
            }
        }

        private object retrieveData(string key) {
            byte[] data = null;
            _contextAccessor.HttpContext.Session.TryGetValue(key, out data);
            if (data == null) return null;
            using (MemoryStream ms = new MemoryStream(data))
            {
                IFormatter br = new BinaryFormatter();
                return br.Deserialize(ms);
            }
        }
    }
}

Usage:

MyClass c;
c.dt = _myRepo.getLotsOfData();
_SessionManager.set("data", c);
c = _SessionManager.get<MyClass>("data");

The same paging and filtering operations we do on DataTables, utilizing the same classes, take between 850ms-950ms complete, compared to our .Net Framework apps which take 5ms. Profiling the performance in visual studio, the deserialization operation taked the lions share of that time, at 600ms for this operation alone.

I am aware of other libraries (like protobuf) that are much faster than binary formatter, and that is probably where I will go next. However, if I reduce the deserilization time to 300ms or even 200ms, I have still lost a lot of performance compared to.Net Framework.

It seems to me like a different kind of caching strategy is required. Is there a way to store and retrieve data in .Net core apps, that doesn't require the overhead of serializing and deserializing the data first?

Upvotes: 2

Views: 200

Answers (2)

user3538411
user3538411

Reputation: 378

I ended up using IMemoryCache as recommended by JohanP. IMemoryCache is a system store that doesn't require you to serialized your objects to store and retrieve them.

I kept my application configured for session, so that .NET will continue to provide a cookie for session ID. I then append the session ID to the user supplied key before storing or retrieving an item, which is more or less how session is managed on .NET framework.

public class SessionManager : ISessionManager
    {
        private readonly IHttpContextAccessor _contextAccessor;
        private readonly IMemoryCache _cache;
        public SessionManager(IHttpContextAccessor contextAccessor
                            , IMemoryCache cache
        ) {
            _contextAccessor = contextAccessor;
            _cache = cache;
         }
        public T get<T>(string key)
        {
           object data;
           _cache.TryGetValue(buildSessionKey(key), out data);
           return data == null ? default(T) : (T)data;
        }

        public void set(string key, object data, TimeSpan maxLifeTime = default(TimeSpan))
        {
            TimeSpan adjustLifeTime = maxLifeTime.TotalMinutes < 5 ? TimeSpan.FromMinutes(20) : maxLifeTime;
            if (adjustLifeTime.TotalMinutes > 90) adjustLifeTime = TimeSpan.FromMinutes(90);
            MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromMinutes(20))
            .SetAbsoluteExpiration(adjustLifeTime);
            _cache.Set(buildSessionKey(key), data);
        }

        public void remove(string key) {
            _cache.Remove(buildSessionKey(key));
        }

        private string buildSessionKey(string partialKey) {
            string sessionID = _contextAccessor.HttpContext.Session.Id;
            return sessionID + partialKey;
        } 
    }

Startup.cs

        services.AddMemoryCache(options => 
        {
            // options.SizeLimit = 4096;
            options.ExpirationScanFrequency = TimeSpan.FromMinutes(20);
        });
        services.AddSession(options =>
        {
            options.IdleTimeout = TimeSpan.FromMinutes(20);
            options.Cookie.HttpOnly = true;
            options.Cookie.IsEssential = true;
        });

Using this approach, performance is similar to our .NET framework apps.

Upvotes: 0

Marc Gravell
Marc Gravell

Reputation: 1063413

Our apps consume a lot of somewhat large ADO.NET datatables. It is not uncommon for them to contain thousands of rows and dozens of columns.

I think we've found the problem :)

Suggestions:

  1. don't try to serialize huge datasets in this kind of state; they are just too expensive
  2. enable dataSet.RemotingFormat = System.Data.SerializationFormat.Binary; before serializing a DataSet in BinaryFormatter
  3. stop using DataSet and stop using BinaryFormatter; I'm not saying this loosely - they are pretty expensive, so if the data is actually pretty fixed, consider using a POCO type with a different serializer - protobuf would be much more efficient in both CPU and bandwidth (data volume)

Upvotes: 2

Related Questions