T. Grandemange
T. Grandemange

Reputation: 129

MVC.Net - Caching user object to provide data for app (ask summed up)

I would really appreciate some advices in order to help me with object caching. Here is my needs : I want to filters some actions of my controllers accordingly to user roles stored in SQL database.

What i'm doing now is : When first request of current user is done i generate an object with some properties initiliazed by SQL queries. I'm storing this object into session using an custom provider and i'm using custom role provider to handle authorization filters tags. If session expire user is regenarated.. It is something like that (simplified) :

User class

Public class User

   public property Login as string
   public property IsAdmin as boolean

   public sub Init(byval pLogin as string)
        Login = pLogin
        //Do some logic on database to provides roles....
        IsAdmin = dbReturnIsAdmin
   end sub

   public readonly property RolesList as string()
       Get
          Return New String() {If(IsAdmin, "UserIsAdmin", "")}
       End get
   end property 

End class

Session user provider

Public Class SessionProvider

   Private Const SESSION_USER As String = "SESSION_USER"

    Public Shared Sub ReloadUser()
        //'This instruction initiate user and load roles into an User class type object
        HttpContext.Current.Session(SESSION_USER) = StructureService.GetInitializedUser(My.User.Name, UowProvider.StructureUow)
    End Sub

    Public Shared ReadOnly Property User() As Application.User
        Get
            //'If user doesn't exist so we create an user

            If HttpContext.Current.Session(SESSION_USER) Is Nothing Then ReloadUser()

            //'Return user
            Return CType(HttpContext.Current.Session(SESSION_USER), Application.User)
        End Get
    End Property

End Class

Custom role provider

Public Class AuthentifiedRoleProvider
    Inherits RoleProvider
    //Implement base role provider....

    Public Overrides Function GetRolesForUser(username As String) As String()
        return SessionProvider.User.RolesList
    End Function

End Class

Implementation - WebConfig

    <system.web> .....
     <roleManager cacheRolesInCookie="false" defaultProvider="DefaultRoleProvider" enabled="true">
         <providers>
             <clear />
             <add name="DefaultRoleProvider" type="AuthentifiedRoleProvider" />
         </providers>
     </roleManager>     
   </system.web>

Implementation - Controller

    <Authorize(Roles:="UserIsAdmin")>
    Public Function List_Items() As ActionResult
        Return View()
    End Function

It is working...

But, i wonder if it's really a good way to achieve that. As i have a sitemap on my app that refers to controllers actions, sessionprovider user (and http session by the way) is requested 4 or 5 times each menu load.

So, my questions are :

Many thanks !

Upvotes: 1

Views: 1013

Answers (2)

T. Grandemange
T. Grandemange

Reputation: 129

Thanks to advices i've changed my session user provider to use caching. It must be extended but for now i can see the change !

Session user provider

   Public Class UserProvider


    Private Const USER_CACHE_PREFIX As String = "User|"

    Private Shared Sub AddUserToCache(ByVal pLogin As String, ByVal pUser As Application.User)
        Dim objCache As ObjectCache = MemoryCache.Default
        objCache.Add(USER_CACHE_PREFIX & pLogin, pUser, New CacheItemPolicy With {.SlidingExpiration = TimeSpan.FromSeconds(20)})
    End Sub

    Private Shared Function GetUserFromCache(ByVal pLogin As String) As Application.User
        Dim objCache As ObjectCache = MemoryCache.Default
        //Return cache if exists
        If objCache.Contains(USER_CACHE_PREFIX & pLogin) Then
            Return CType(objCache.GetCacheItem(USER_CACHE_PREFIX & pLogin).Value, Application.User)
        Else
            Return Nothing
        End If
    End Function

    Public Shared Function ReloadUser(ByVal pLogin As String) As Application.User
        Dim objCache As ObjectCache = MemoryCache.Default
        Dim tmpLogin As String = My.User.Name
        //Clear cache
        If objCache.Contains(USER_CACHE_PREFIX & tmpLogin) Then objCache.Remove(USER_CACHE_PREFIX & pLogin)
        Dim tmpUser As Application.User = StructureService.GetInitializedUser(pLogin, UowProvider.StructureUow) 
        AddUserToCache(tmpLogin, tmpUser)
        return tmpUser
    End Function

    Public Shared ReadOnly Property User() As Application.User
        Get
            Dim tmpLogin As String = My.User.Name
            //Try to get user from cache
            Dim tmpUser As Application.User = GetUserFromCache(tmpLogin)
            //If user is null then init and cache
            If tmpUser Is Nothing Then
                tmpUser = StructureService.GetInitializedUser(tmpLogin, UowProvider.StructureUow) 
                AddUserToCache(tmpLogin, tmpUser)

            End If

            //return  user
            Return tmpUser
        End Get
    End Property

End Class

Upvotes: 0

NightOwl888
NightOwl888

Reputation: 56849

Anyway, do you think this implementation is a good to achieve my goals ?

Probably not.

  1. For some reason, you are tying Session State to User Profile data. Session State has nothing to do with Authorization/Authentication and it is not recommended to use it for User Profile data, since using Session State (in any practical sense) means you will need 2 additional network requests per HTTP request to your application. Furthermore, these extra requests happen whether the user is logged in or not.
  2. MvcSiteMapProvider does not rely on Session State. It uses a shared cache to store the nodes in RAM, and it uses AuthorizeAttribute to determine which nodes to show/hide per request.

If you find you are requesting the same data multiple times per request, you should try to take advantage of the request cache (HttpContextBase.Items) by using a standard cache retrieval pattern similar to this:

Public Function GetSomeData() As ISomeData
    Dim key As String = "SomeDataKey"
    ' Me.cache refers to HttpContextBase.Items injected through 
    ' the constructor of this class and stored in a private field
    Dim someData As ISomeData = TryCast(Me.cache(key), ISomeData) 
    If someData Is Nothing Then
        ' The data is not cached, so look it up and populate the cache
        someData = GetDataFromExternalSource()
        Me.cache(key) = someData
    End If
    Return someData
End Function

Putting a method like this into a service that is shared between components means you don't have to worry about retrieving the same data multiple times in the request - the first time it will hit the external source, and each additional time it will use the cache.

Also, per MSDN:

There are two primary reasons for creating a custom role provider.

  1. You need to store role information in a data source that is not supported by the role providers included with the .NET Framework, such as a FoxPro database, an Oracle database, or other data sources.
  2. You need to manage role information using a database schema that is different from the database schema used by the providers that ship with the .NET Framework. A common example of this would be role data that already exists in a SQL Server database for a company or Web site.

So, if you are not doing either of these things, that might not be the right choice either. Most modern applications can use/extend ASP.NET Identity for user authentication and all MVC applications should use the AuthorizeAttribute (or a subclass of it) for authorization.

System.Runtime.Cache.ObjectCache (and MemoryCache) are good ways to cache data that is generally not shared between users. You can use it for user data if you design your cache key to include a unique identifier for the user along with a delimiter that makes the key unique...

Dim key As String = UserId & "|UserProfile"

That said, you should be aware that caching this way won't scale to multiple servers.

Anyway, I recommend you follow the advice in think twice about using Session State. MVC has freed us from having to use Session state in many cases - and we should not use it unless it is absolutely required.

Upvotes: 1

Related Questions