MVC Multi tenant loads other tenant data - asp.net-mvc

I am building a multi tenant application using ASP.NET MVC. Now, noticed a bug but at completely random intervals, sometimes data from the wrong tenant is fetched for another tenant.
So for instance, Tenant1 logs in, but they see information from Tenant2. I am using same database from all the tenants but with TenantId.
I boot application from Global > Application_AcquireRequestState as given below:
namespace MultiTenantV1
{
public class Global : HttpApplication
{
public static OrganizationGlobal Tenant;
void Application_Start(object sender, EventArgs e)
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine());
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
UnityWebActivator.Start();
}
void Application_AcquireRequestState(object sender, EventArgs e)
{
// boot application
HttpApplication app = (HttpApplication)sender;
HttpContext context = app.Context;
// dependency runner
Tenant = new Tenant().Fetch(context.Request.Url);
// third party api calls
var session = HttpContext.Current.Session;
if (session != null)
{
if (string.IsNullOrEmpty(Session["CountryCode"] as string))
{
string isDevelopmentMode = ConfigurationManager.AppSettings["developmentmode"];
if (isDevelopmentMode == "false")
{
// api call to get country
}
else
{
// defaults
}
}
Tenant.CountryCode = Session["CountryCode"].ToString();
}
}
}
}
Now in the entire application I use 'Tenant' object as starting point and use this to query database for further data. I noticed, sometimes a tenant sees another tenant name (not sure if other other data is also visible same way).
I'm initializing 'Tenant' based on HttpContext.Request.Url. So there is no way to load other tenant data.
Can anyone see anything in the above code, or in my use of HttpContext.Request.Url that could result in the wrong tenant being extracted for any specific request?

Each request will override the static Tenant object, therefore on concurrent requests, the wrong tenant will be used.
The key is to store tenant per request, for example in the HttpContext.Current. I usually use a tenancy resolver, which contains code like this:
public string CurrentId {
get {
return (string)HttpContext.Current.Items["CurrentTenantId"];
}
set {
string val = value != null ? value.ToLower() : null;
if (!HttpContext.Current.Items.Contains("CurrentTenantId")) {
HttpContext.Current.Items.Add("CurrentTenantId", val);
} else {
HttpContext.Current.Items["CurrentTenantId"] = val;
}
}
}
In the Application_AcquireRequestState I set the CurrentId based on the url.
The tenancyresolver is then used in the classes that need to know the tenant, by getting the CurrentId.

Related

Dynamic internal redirect in ASP.NET MVC

I've searched a lot but didn't find any solution. So here is my case:
I have database model UrlRedirect
public class UrlRedirect : AuditInfo
{
[Key]
public int Id { get; set; }
public string OldUrl { get; set; }
public string NewUrl { get; set; }
}
As you may assume I am trying to save URL mapping between OldUrl and NewUrl.
I want to internally change the OldUrl path of the request to the NewUrl and then run all defined routes as they will run if the user is opening NewUrl directly.
The redirect should be server side URL rewrite and user should see the old URL in their browser
In Global.asax you have some events that are executed in the web application context. (for sample: Start_Application, End_Application, etc).
In your case, you could use the BeginRequest event, where every request made to your web application is executed. In this event you could check the URL and try to redirect it using the default of htpp protocols, such as 301 Moved Permanently status code. For sample, in the Global.asax file, add the code bellow:
protected void Application_BeginRequest(Object sender, EventArgs e)
{
// get the current url
string currentUrl = HttpContext.Current.Request.Url.ToString().ToLower();
// here, you could create a method to get the UrlRedirect object by the OldUrl.
var urlRedirect = GetUrlRedirect(currentUrl);
// check if the urlRedirect object was found in dataBase or any where you save it
if (urlRedirect != null)
{
// redirect to new URL.
Response.Status = "301 Moved Permanently";
Response.AddHeader("Location", urlRedirect.NewUrl);
Response.End();
}
}
In the sample, GetUrlRedirect(string) method should check it in a database, xml file, cache, or anywhere you save the UrlRedirect objects.
To understand more about how asp.net core applications life cycles works, read this article.
If you really want to redirect within the server you may use one of the following:
HttpServerUtility.TransferRequest
HttpServerUtility.Transfer
HttpServerUtility.Execute
HttpContext.RewritePath
You can read more about these options, where a similar problem is posed here.
Consider the impact of your choice on the request serving performance. IMHO you should try your best to utilize MVC infrastructure of Routing, or just fall back to simple redirections (even permanent for speed) as [user:316799] wrote when you compute new urls in business layer or map from db.
This is my final solution that works like a charm. Thanks to #WeTTTT for the idea.
public class MvcApplication : HttpApplication
{
protected void Application_Start()
{
// ...
}
protected void Application_BeginRequest(object sender, EventArgs e)
{
this.ApplyCustomUrlRedirects(new UowData(), HttpContext.Current);
}
private void ApplyCustomUrlRedirects(IUowData data, HttpContext context)
{
var currentUrl = context.Request.Path;
var url = data.UrlRedirects.All().FirstOrDefault(x => x.OldUrl == currentUrl);
if (url != null)
{
context.RewritePath(url.NewUrl);
}
}
}

Concurrent requests: session lost after removing session-id from one single request

In my ASP .NET MVC 2 - application, there are several controllers, that need the session state. However, one of my controllers in some cases runs very long and the client should be able to stop it.
Here is the long running controller:
[SessionExpireFilter]
[NoAsyncTimeout]
public void ComputeAsync(...) //needs the session
{
}
public ActionResult ComputeCompleted(...)
{
}
This is the controller to stop the request:
public ActionResult Stop()
{
...
}
Unfortunately, in ASP .NET MVC 2 concurrent requests are not possible for one and the same user, so my Stop-Request has to wait until the long running operation has completed. Therefore I have tried the trick described in this article and added the following handler to Global.asax.cs:
protected void Application_BeginRequest()
{
if (Request.Url.AbsoluteUri.Contains("Stop") && Request.Cookies["ASP.NET_SessionId"] != null)
{
var session_id = Request.Cookies["ASP.NET_SessionId"].Value;
Request.Cookies.Remove("ASP.NET_SessionId");
...
}
}
This simply removes the session-id from the Stop-Request. At the first glance this works well - the Stop-Request comes through and the operation is stopped. However, after that, it seems that the session of the user with the long running request has been killed.
I use my own SessionExpireFilter in order to recognize session timeouts:
public class SessionExpireFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpContext ctx = HttpContext.Current;
// check if session is supported
if (ctx.Session != null)
{
// check if a new session id was generated
if (ctx.Session.IsNewSession)
{
// If it says it is a new session, but an existing cookie exists, then it must
// have timed out
string sessionCookie = ctx.Request.Headers["Cookie"];
if ((null != sessionCookie) && (sessionCookie.IndexOf("ASP.NET_SessionId") >= 0))
{
filterContext.Result = new JsonResult() { Data = new { success = false, timeout = true }, JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}
}
}
base.OnActionExecuting(filterContext);
}
}
ctx.Session.IsNewSession is always true after the Stop-Request has been called, but I don't know why. Does anyone know why the session is lost? Is there any mistake in the implementation of the Stop-Controller?
The session is lost because you removed the session cookie. I'm not sure why that seems illogical. Each new page request supplies the cookie to asp.net, and if there is no cookie it generates a new one.
One option you could use to use cookieless sessions, which will add a token to the querystring. All you need to do is generate a new session for each login, or similar.
But this is one of the reasons why session variables are discouraged. Can you change the code to use an in-page variable, or store the variable in a database?

Multitenancy with Fluent nHibernate and Ninject. One Database per Tenant

I'm building a multi-tenant web application where for security concerns, we need to have one instance of the database per tenant. So I have a MainDB for authentication and many ClientDB for application data.
I am using Asp.net MVC with Ninject and Fluent nHibernate. I have already setup my SessionFactory/Session/Repositories using Ninject and Fluent nHibernate in a Ninject Module at the start of the application. My sessions are PerRequestScope, as are repositories.
My problem is now I need to instanciate a SessionFactory (SingletonScope) instance for each of my tenants whenever one of them connects to the application and create a new session and necessary repositories for each webrequest. I'm puzzled as to how to do this and would need a concrete example.
Here's the situation.
Application starts : The user of TenantX enters his login info. SessionFactory of MainDB gets created and opens a session to the MainDB to authenticate the user. Then the application creates the auth cookie.
Tenant accesses the application : The Tenant Name + ConnectionString are extracted from MainDB and Ninject must construct a tenant specific SessionFactory (SingletonScope) for that tenant. The rest of the web request, all controllers requiring a repository will be inject with a Tenant specific session/repository based on that tenant's SessionFactory.
How do I setup that dynamic with Ninject? I was originally using Named instance when I had multiple databases but now that the databases are tenant specific, I'm lost...
After further research I can give you a better answer.
Whilst it's possible to pass a connection string to ISession.OpenSession a better approach is to create a custom ConnectionProvider. The simplest approach is to derive from DriverConnectionProvider and override the ConnectionString property:
public class TenantConnectionProvider : DriverConnectionProvider
{
protected override string ConnectionString
{
get
{
// load the tenant connection string
return "";
}
}
public override void Configure(IDictionary<string, string> settings)
{
ConfigureDriver(settings);
}
}
Using FluentNHibernate you set the provider like so:
var config = Fluently.Configure()
.Database(
MsSqlConfiguration.MsSql2008
.Provider<TenantConnectionProvider>()
)
The ConnectionProvider is evaluated each time you open a session allowing you to connect to tenant specific databases in your application.
An issue with the above approach is that the SessionFactory is shared. This is not really a problem if you are only using the first level cache (since this is tied to the session) but is if you decide to enable the second level cache (tied to the SessionFactory).
The recommended approach therefore is to have a SessionFactory-per-tenant (this would apply to schema-per-tenant and database-per-tenant strategies).
Another issue often overlooked is that although the second level cache is tied to the SessionFactory, in some cases the cache space itself is shared (reference). This can be resolved by setting the "regionName" property of the provider.
Below is a working implementation of SessionFactory-per-tenant based on your requirements.
The Tenant class contains the information we need to set up NHibernate for the tenant:
public class Tenant : IEquatable<Tenant>
{
public string Name { get; set; }
public string ConnectionString { get; set; }
public bool Equals(Tenant other)
{
if (other == null)
return false;
return other.Name.Equals(Name) && other.ConnectionString.Equals(ConnectionString);
}
public override bool Equals(object obj)
{
return Equals(obj as Tenant);
}
public override int GetHashCode()
{
return string.Concat(Name, ConnectionString).GetHashCode();
}
}
Since we'll be storing a Dictionary<Tenant, ISessionFactory> we implement the IEquatable interface so we can evaluate the Tenant keys.
The process of getting the current tenant is abstracted like so:
public interface ITenantAccessor
{
Tenant GetCurrentTenant();
}
public class DefaultTenantAccessor : ITenantAccessor
{
public Tenant GetCurrentTenant()
{
// your implementation here
return null;
}
}
Finally the NHibernateSessionSource which manages the sessions:
public interface ISessionSource
{
ISession CreateSession();
}
public class NHibernateSessionSource : ISessionSource
{
private Dictionary<Tenant, ISessionFactory> sessionFactories =
new Dictionary<Tenant, ISessionFactory>();
private static readonly object factorySyncRoot = new object();
private string defaultConnectionString =
#"Server=(local)\sqlexpress;Database=NHibernateMultiTenancy;integrated security=true;";
private readonly ISessionFactory defaultSessionFactory;
private readonly ITenantAccessor tenantAccessor;
public NHibernateSessionSource(ITenantAccessor tenantAccessor)
{
if (tenantAccessor == null)
throw new ArgumentNullException("tenantAccessor");
this.tenantAccessor = tenantAccessor;
lock (factorySyncRoot)
{
if (defaultSessionFactory != null) return;
var configuration = AssembleConfiguration("default", defaultConnectionString);
defaultSessionFactory = configuration.BuildSessionFactory();
}
}
private Configuration AssembleConfiguration(string name, string connectionString)
{
return Fluently.Configure()
.Database(
MsSqlConfiguration.MsSql2008.ConnectionString(connectionString)
)
.Mappings(cfg =>
{
cfg.FluentMappings.AddFromAssemblyOf<NHibernateSessionSource>();
})
.Cache(c =>
c.UseSecondLevelCache()
.ProviderClass<HashtableCacheProvider>()
.RegionPrefix(name)
)
.ExposeConfiguration(
c => c.SetProperty(NHibernate.Cfg.Environment.SessionFactoryName, name)
)
.BuildConfiguration();
}
private ISessionFactory GetSessionFactory(Tenant currentTenant)
{
ISessionFactory tenantSessionFactory;
sessionFactories.TryGetValue(currentTenant, out tenantSessionFactory);
if (tenantSessionFactory == null)
{
var configuration = AssembleConfiguration(currentTenant.Name, currentTenant.ConnectionString);
tenantSessionFactory = configuration.BuildSessionFactory();
lock (factorySyncRoot)
{
sessionFactories.Add(currentTenant, tenantSessionFactory);
}
}
return tenantSessionFactory;
}
public ISession CreateSession()
{
var tenant = tenantAccessor.GetCurrentTenant();
if (tenant == null)
{
return defaultSessionFactory.OpenSession();
}
return GetSessionFactory(tenant).OpenSession();
}
}
When we create an instance of NHibernateSessionSource we set up a default SessionFactory to our "default" database.
When CreateSession() is called we get a ISessionFactory instance. This will either be the default session factory (if the current tenant is null) or a tenant specific session factory. The task of locating the tenant specific session factory is performed by the GetSessionFactory method.
Finally we call OpenSession on the ISessionFactory instance we have obtained.
Note that when we create a session factory we set the SessionFactory name (for debugging/profiling purposes) and cache region prefix (for the reasons mentioned above).
Our IoC tool (in my case StructureMap) wires everything up:
x.For<ISessionSource>().Singleton().Use<NHibernateSessionSource>();
x.For<ISession>().HttpContextScoped().Use(ctx =>
ctx.GetInstance<ISessionSource>().CreateSession());
x.For<ITenantAccessor>().Use<DefaultTenantAccessor>();
Here NHibernateSessionSource is scoped as a singleton and ISession per request.
Hope this helps.
If all the databases are on the same machine, maybe the schema property of class mappings could be used to set the database on a pre-tenant basis.

NInject, nHibernate, and auditing in ASP.NET MVC

I am working on an inherited application which makes use of NInject and nHibernate as part of an ASP.NET MVC (C#) application. Currently, I'm looking at a problem with the auditing of modifications. Each entity has ChangedOn/ChangedBy and CreatedOn/CreatedBy fields, which are mapped to database columns. However, these either get filled with the wrong username or no username at all. I think this is because it has been configured in the wrong way, but I don't know enough about nHibernate and NInject to solve the issue, so I hope someone can help. Below some code snippets to hopefully provide sufficient insight in the application.
Creating the session factory and session:
public class NHibernateModule : NinjectModule
{
public override void Load()
{
Bind<ISessionFactory>().ToProvider(new SessionFactoryProvider()).InSingletonScope();
Bind<ISession>().ToProvider(new SessionProvider()).InRequestScope();
Bind<INHibernateUnitOfWork>().To<NHibernateUnitOfWork>().InRequestScope();
Bind<User>().ToProvider(new UserProvider()).InRequestScope();
Bind<IStamper>().ToProvider(new StamperProvider()).InRequestScope();
}
}
public class SessionProvider : Provider<ISession>
{
protected override ISession CreateInstance(IContext context)
{
// Create session
var sessionFactory = context.Kernel.Get<ISessionFactory>();
var session = sessionFactory.OpenSession();
session.FlushMode = FlushMode.Commit;
return session;
}
}
public class SessionFactoryProvider : Provider<ISessionFactory>
{
protected override ISessionFactory CreateInstance(IContext context)
{
var connectionString = ConfigurationManager.ConnectionStrings["DefaultConnectionString"].ToString();
var stamper = context.Kernel.Get<IStamper>();
return NHibernateHelper.CreateSessionFactory(connectionString, stamper);
}
}
public class StamperProvider : Provider<IStamper>
{
protected override IStamper CreateInstance(IContext context)
{
System.Security.Principal.IPrincipal user = HttpContext.Current.User;
System.Security.Principal.IIdentity identity = user == null ? null : user.Identity;
string name = identity == null ? "Unknown" : identity.Name;
return new Stamper(name);
}
}
public class UserProvider : Provider<User>
{
protected override UserCreateInstance(IContext context)
{
var userRepos = context.Kernel.Get<IUserRepository>();
System.Security.Principal.IPrincipal user = HttpContext.Current.User;
System.Security.Principal.IIdentity identity = user == null ? null : user.Identity;
string name = identity == null ? "" : identity.Name;
var user = userRepos.GetByName(name);
return user;
}
}
Configuring the session factory:
public static ISessionFactory CreateSessionFactory(string connectionString, IStamper stamper)
{
// Info: http://wiki.fluentnhibernate.org/Fluent_configuration
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008
.ConnectionString(connectionString))
.Mappings(m =>
{
m.FluentMappings
.Conventions.Add(PrimaryKey.Name.Is(x => "Id"))
.AddFromAssemblyOf<NHibernateHelper>();
m.HbmMappings.AddFromAssemblyOf<NHibernateHelper>();
})
// Register
.ExposeConfiguration(c => {
c.EventListeners.PreInsertEventListeners =
new IPreInsertEventListener[] { new EventListener(stamper) };
c.EventListeners.PreUpdateEventListeners =
new IPreUpdateEventListener[] { new EventListener(stamper) };
})
.BuildSessionFactory();
}
Snippet from the eventlistener:
public bool OnPreInsert(PreInsertEvent e)
{
_stamper.Insert(e.Entity as IStampedEntity, e.State, e.Persister);
return false;
}
As you can see the session factory is in a singleton scope. Therefore the eventlistener and stamper also get instantiated in this scope (I think). And this means that when the user is not yet logged in, the username in the stamper is set to an empty string or "Unknown".
I tried to compensate for this problem, by modifying the Stamper. It checks if the username is null or empty. If this is true, it tries to find the active user, and fill the username-property with that user's name:
private string GetUserName()
{
if (string.IsNullOrWhiteSpace(_userName))
{
var user = ServiceLocator.Resolve<User>();
if (user != null)
{
_userName = user.UserName;
}
}
return _userName;
}
But this results in a completely different user's name, which is also logged in to the application, being logged in the database. My guess this is because it resolves the wrong active user, being the last user logged in, instead of the user that started the transaction.
The offending parts are here:
Bind<ISessionFactory>().
.ToProvider(new SessionFactoryProvider())
.InSingletonScope();
Bind<IStamper>()
.ToProvider(new StamperProvider())
.InRequestScope();
And later on:
public class SessionFactoryProvider : Provider<ISessionFactory>
{
protected override ISessionFactory CreateInstance(IContext context)
{
// Unimportant lines omitted
var stamper = context.Kernel.Get<IStamper>();
return NHibernateHelper.CreateSessionFactory(connectionString, stamper);
}
}
public class StamperProvider : Provider<IStamper>
{
protected override IStamper CreateInstance(IContext context)
{
// Unimportant lines omitted
string name = /* whatever */
return new Stamper(name);
}
}
Let's analyze what's going on with the code:
The ISessionFactory is bound as single-instance. There will only ever be one throughout the lifetime of the process. This is fairly typical.
The ISessionFactory is initialized with SessionFactoryProvider which immediately goes out to get an instance of IStamper, and passes this as a constant argument to initialize the session factory.
The IStamper in turn is initialized by the StamperProvider which initializes a Stamper class with a constant name set to the current user principal/identity.
The net result of this is that as long as the process is alive, every single "stamp" will be assigned the name of whichever user was first to log in. This might even be the anonymous user, which explains why you're seeing so many blank entries.
Whoever wrote this only got half the equation right. The IStamper is bound to the request scope, but it's being supplied to a singleton, which means that only one IStamper will ever be created. You're lucky that the Stamper doesn't hold any resources or have any finalizers, otherwise you'd probably end up with a lot of ObjectDisposedException and other weird errors.
There are three possible solutions to this:
(Recommended) - Rewrite the Stamper class to look up the current user on each call, instead of being initialized with static user info. Afterward, the Stamper class would no longer take any constructor arguments. You can the bind the IStamper InSingletonScope instead of InRequestScope.
Create an abstract IStamperFactory with a GetStamper method, and a concrete StamperFactory which implements it by wrapping the IKernel instance. Bind these together InSingletonScope. Have your concrete factory return kernel.Get<IStamper>(). Modify the session factory to accept and hold an IStamperFactory instead of an IStamper. Each time it needs to stamp, use the factory to get a new IStamper instance.
Change the ISessionFactory to be InRequestScope. Not recommended because it will hurt performance and potentially mess up ID generators if you don't use DB-generated identities, but it will solve your auditing problem.
Aaronaught, you're analysis describes exactly what I suspected. However, I found there is a fourth solution which is easier and more straightforward IMHO.
I modified the sessionprovider, such that the call to OpenSession takes an instance of IInterceptor as argument. As it turns out, the event listeners aren't actually supposed to be used for auditing (a bit of a rant, but other than that he is right, according to Fabio as well).
The AuditInterceptor implements OnFlushDirty (for auditing existing entities) and OnSave (for auditing newly created entities). The SessionProvider looks as below:
public class SessionProvider : Provider<ISession>
{
protected override ISession CreateInstance(IContext context)
{
// Create session
System.Security.Principal.IPrincipal user = HttpContext.Current.User;
System.Security.Principal.IIdentity identity = user == null ? null : user.Identity;
string name = identity == null ? "" : identity.Name;
var sessionFactory = context.Kernel.Get<ISessionFactory>();
var session = sessionFactory.OpenSession(new AuditInterceptor(name));
session.FlushMode = FlushMode.Commit;
return session;
}
}

ASP.NET MVC - Alternative to Using Session

I have an ASP.NET MVC view that uses jquery.Uploadify to post files to one of my controllers for use in my application and one of the side effects I noticed with Uploadify is that when the Flash file Uploadify uses to submit files to the server posts to my controller it gets its own SessionID from ASP.NET. This would be fine, of course, if my Upload controller didn't use the Session to store a list of files that have been uploaded by the current user for future manipulation by my application... So given this issue, after I upload files with my View, the current user's session does not contain any of the files that were just posted.
Any suggestions on how to achieve what I want without relying on Sessions (and, preferably, without a database)?
Since Uploadify is purely a front end script, I don't understand why it would be getting a session from ASP.NET. I also don't fully understand what your particular problem is.
If your problem is that once the files are uploaded, the user can't see them on the screen, then I would suggest figuring out a method displaying the list of files that is independent of Uploadify. If it can, have it send an ID token along with the files and then grab the data needed to show the list from a database.
Maybe a static hashmap which key is the user:ip of the client?
The value will be whatever object you want to stored across the different Sessions.
One thing to cross check--did you make the session "live" by adding some data to it before going to uploadify? ASP.NET regenerates sessions until it has data in the session.
This is the solution I came up with. I haven't done much testing, but it seems to be an acceptable alternative to Session in my current scenario. I will use the Global.asax's Session_End/Session_Start to ensure rows are created and removed as needed.
public class UserTable : Dictionary<string, Dictionary<string, object>>
{
public new object this[string key]
{
get
{
object value = null;
if (HttpContext.Current != null)
{
var sessionId = HttpContext.Current.Session.SessionID;
if (ContainsKey(sessionId) && base[sessionId].ContainsKey(key))
value = base[sessionId][key];
}
else
throw new Exception("No HttpContext present.");
return value;
}
set
{
if (HttpContext.Current != null)
{
var sessionId = HttpContext.Current.Session.SessionID;
if (!ContainsKey(sessionId))
Add(sessionId, new Dictionary<string, object>());
if (!base[sessionId].ContainsKey(key))
base[sessionId].Add(key, value);
else
base[sessionId][key] = value;
}
else
throw new Exception("No HttpContext present.");
}
}
public object this[string sessionId, string key]
{
get
{
object value = null;
if (ContainsKey(sessionId) && base[sessionId].ContainsKey(key))
value = base[sessionId][key];
return value;
}
set
{
if (!ContainsKey(sessionId))
Add(sessionId, new Dictionary<string, object>());
if (!base[sessionId].ContainsKey(key))
base[sessionId].Add(key, value);
else
base[sessionId][key] = value;
}
}
public void Add(string sessionId)
{
Add(sessionId, new Dictionary<string, object>());
}
public void Add()
{
if (HttpContext.Current != null)
Add(HttpContext.Current.Session.SessionID);
else
throw new Exception("No HttpContext present.");
}
public new void Remove(string sessionId)
{
base.Remove(sessionId);
}
public void Remove()
{
if (HttpContext.Current != null)
Remove(HttpContext.Current.Session.SessionID);
else
throw new Exception("No HttpContext present.");
}
}

Resources