Aws DynamoDB CacheClient problem with Sessions

Hi, so I started using the AWS DynamoDB cache client and now starting to have real issues with sessions, when using the MemoryCacheClient all works fine, but with the AWS DynamoDB a new session is being Created everytime we do a IRequest.GetSession() call, was able to reproduce it using just the cache Client and calling the GetMethod passing an existing SessionId.

var sess = _cacheClient.Get<MyCustomSession>("urn:iauthsession:SPVDP6czSk2FuN3TRyLW");
var sess1 = _cacheClient.Get<AuthUserSession>("urn:iauthsession:SPVDP6czSk2FuN3TRyLW");
var sess2 = _cacheClient.Get<IMyCustomSession>("urn:iauthsession:SPVDP6czSk2FuN3TRyLW");
var sess3 = _cacheClient.Get<IAuthSession>("urn:iauthsession:SPVDP6czSk2FuN3TRyLW");

so making those calls where _cacheClient is a DynamoDbCacheClient sess and sess1 will be objects sess2 and sess3 will be null.
when looking into the code the GetSession method line 156 of ServiceExtensions.cs whe have the following.

        var session = (sessionKey != null ? HostContext.AppHost.OnSessionFilter(httpReq.GetCacheClient().Get<IAuthSession>(sessionKey), sessionId) : null)
            ?? SessionFeature.CreateNewSession(httpReq, sessionId);

so based on my test the cacheclient.Get(sessionKey) will return null so a new session is always created.
This is a major issue.

I’m not following, are you saying that a new session is created in DynamoDB each time you use IRequest.GetSession() when a session doesn’t exist? as that doesn’t seem possible. If you’re saying that it just returns an empty session, that’s by design.

Can you be clearer in what way is DynamoDB behavior is different to MemoryCacheClient?

Hi Demis,
What i’m saying is that everytime I call the IRequest.GetSession() method a new session gets returned, so it doesn’t load the session that’s on AWS DynamoDB.
I was able to pinpoint like I said on my previous post to the line 156 of the ServiceExtensions.cs, and based on that created my cacheclient test to prove it.
Like I said if I do a cacheclient.Get to a concrete class it returns the session object, but if I do it to an Interface like GetSession method does, it returns null, in Memory cache client this works fine.
Hope I explain myself better this time.

The cause/effect would be the other way around, i.e. an empty session would only be returned if it can’t find the session in DynamoDB first.

I’m assuming you have a User Session stored at urn:iauthsession:SPVDP6czSk2FuN3TRyLW? If so I’ll try re-create the session myself to repro the issue. Can you provide the serialized JSON that’s stored in DynamoDB as well as the class definition of MyCustomSession and any global JsConfig configuration you have defined (if any).

Hi Demis Sorry for the delay but had some issues to resolved, bellow is a unit test using nunit that shows the prb,
you will see that it will log stating that IMyCustomSession is null and also the IAuthSession, then the test will fail on the Assert.IsNotNull(sess2).
Hope this helps you finding the problem.

Also the Json that is stored under CacheEntry.Data is.

{“userId”:“df26d517-a739-481c-9185-8ecf6c7421ab”,“isLockedOut”:false,“loginAttempts”:0,“userAuthId”:“df26d517-a739-481c-9185-8ecf6c7421ab”,“userAuthName”:“TestUser”,“userName”:“Test User”,“timeZone”:“Eastern Standard Time”,“lastModified”:“2016-03-03T09:25:00.7253692”,“isAuthenticated”:true,“tag”:0,“providerOAuthAccess”:[]}

namespace Millennium.Aws.Tests
{
    public interface IMyCustomSession: IAuthSession
    {
        Guid UserId { get; set; }
        bool IsLockedOut { get; set; }
        int LoginAttempts { get; set; }
        DateTime? LastLoginAttempt { get; set; }
    }

    [DataContract]
    public class MyCustomSession: AuthUserSession, IMyCustomSession
    {
        [DataMember]
        public Guid UserId
        {
            get
            {

                return UserAuthId.To<Guid>();
            }
            set
            {
                UserAuthId = value.ToString("D");
            }
        }

        [DataMember]
        public bool IsLockedOut { get; set; }

        [DataMember]
        public int LoginAttempts { get; set; }

        [DataMember]
        public DateTime? LastLoginAttempt { get; set; }
    }
    [TestFixture]
    public class DynamoDbSessionTests
    {
        private IAppSettings _settings;
        private ICacheClient _cacheClient ;
        private Funq.Container _container;
        [OneTimeSetUp]
        public void OneTimeSetUp()
        {
            var dic = new Dictionary<string, string>
                      {
                          {"Cache.Provider", "DynamoDb"},
                          {"Cache.Key", "AWSKey"},
                          {"Cache.Secret", "AWSSecret"}
                      };

            _settings = new DictionarySettings(dic);
            _container = new Funq.Container();
            _container.Register(_settings);
            var awsDb = new AmazonDynamoDBClient(_settings.GetString("Cache.Key"),
                                                 _settings.GetString("Cache.Secret"),
                                                 RegionEndpoint.USEast1);
           _cacheClient = new DynamoDbCacheClient(new PocoDynamo(awsDb));
            AuthenticateService.CurrentSessionFactory = ()=> new MeevoSession();
            _container.Register(_cacheClient);
            LogManager.LogFactory = new ConsoleLogFactory();

            JsConfig.EmitCamelCaseNames = true;
            JsConfig<Guid>.SerializeFn = (guid => guid.ToString("D"));
            JsConfig.ThrowOnDeserializationError = false;
            JsConfig.TypeWriter = (type) => type.Name;

            JsConfig.AppendUtcOffset = false;
            JsConfig.IncludeNullValues = false;
            JsConfig.TryToParseNumericType = true;
            JsConfig.TryToParsePrimitiveTypeValues = true;
            JsConfig.TreatEnumAsInteger = true;

            JsConfig<DateTime>.SerializeFn =
                (date => date.ChangeToUnspecifiedKind().ToString("O"));
            JsConfig<DateTime>.DeSerializeFn =
                (dateStr => dateStr.ToDateTime());
            JsConfig<DateTime?>.SerializeFn =
                (date =>
                 date?.ChangeToUnspecifiedKind().ToString("O") ?? "null");
            JsConfig<DateTime?>.DeSerializeFn =
                (dateStr =>
                 string.IsNullOrEmpty(dateStr) ||
                 string.Equals(dateStr, "null", StringComparison.OrdinalIgnoreCase)
                     ? null as DateTime?
                     : dateStr.ToDateTimeNullable());

            // TimeSpan serialization corresponds with Time of Day component of DateTime round-trip format ("O")
            JsConfig<TimeSpan>.SerializeFn =
                (time => (time.Ticks < 0 ? "-" : "") + time.ToString("hh':'mm':'ss'.'fffffff"));

            JsConfig<TimeSpan>.DeSerializeFn =
                (timeString =>
                {
                    bool negate = false;
                    if (timeString.IndexOf("-", StringComparison.InvariantCulture) == 0)
                    {
                        negate = true;
                        timeString = timeString.Substring(1);
                    }

                    TimeSpan time = TimeSpan.ParseExact(timeString, "hh':'mm':'ss'.'fffffff",
                                                            CultureInfo.InvariantCulture);

                    if (negate)
                    {
                        time = time.Negate();
                    }
                    return time;
                });
        }

        [OneTimeTearDown]
        public void OneTimeTearDown()
        {
            var client = _cacheClient as DynamoDbCacheClient;
            client?.Dispose();
        }

        [Test]

        public void SessionTests()
        {
            var logger = LogManager.GetLogger("SessionTests");
            var sessionKey = "urn:iauthsession:VVtJJbBJ0Po4xI6eU1zF";
            var session = new MyCustomSession
                          {
                              IsAuthenticated = true,
                              UserId = Guid.NewGuid(),
                              TimeZone = TimeZoneInfo.Local.Id,
                              UserAuthName = "TestUser",
                              UserName = "Test User",
                              LastModified = DateTime.UtcNow,
                          };
            _cacheClient.CacheSet(sessionKey, session, new TimeSpan(0, 14, 0));

            var sess = _cacheClient.Get<MyCustomSession>(sessionKey);
            var sess1 = _cacheClient.Get<AuthUserSession>(sessionKey);
            var sess2 = _cacheClient.Get<IMyCustomSession>(sessionKey);
            var sess3 = _cacheClient.Get<IAuthSession>(sessionKey);

            if (sess == null)
            {
                logger.Warn("Getting MyCustomSession from cache returned null.");
            }
            if (sess1 == null)
            {
                logger.Warn("Getting AuthUserSession from cache returned null.");
            }
            if (sess2 == null)
            {
                logger.Warn("Getting IMyCustomSession from cache returned null.");
            }
            if (sess3 == null)
            {
                logger.Warn("Getting IAuthSession from cache returned null.");
            }
            Assert.IsNotNull(sess);
            Assert.IsNotNull(sess1);
            Assert.IsNotNull(sess2);
            Assert.IsNotNull(sess3);
        }
    }
}

This test doesn’t reflect how sessions are saved in ServiceStack which is saved as an instance of IAuthSession.

This also requires the __type to be emitted with the payload because it needs to know which concrete type the session needs to be deserialized into - this functionality is broken when you’ve modified how the __type information is emitted:

JsConfig.TypeWriter = type => type.Name;

By commenting out the config above and changing the CacheSet to match how sessions are saved in ServiceStack, i.e:

_cacheClient.CacheSet(sessionKey, (IAuthSession)session, new TimeSpan(0, 14, 0));

Your tests will pass.