-->

Make MVC Sitemap unique per session, not user

2019-03-04 04:55发布

问题:

Our MvcSitemap has some DynamicNodeProviders implemented.

We want these to be unique per session. But it appears they are unique per user.

So if a user logs into two different browsers, or computers, they currently share the same sitemap.

We do not want this.

But I can't seem to figure out how to get it to use the User/Session combination for uniqueness.

Is there a way to make this work?

回答1:

Option 1:

Implement your own ICacheProvider based on session state and inject it using DI.

using System;
using System.Collections.Generic;
using MvcSiteMapProvider.Web.Mvc;
using MvcSiteMapProvider.Caching;
using System.Web;

public class SessionStateCacheProvider<T>
    : ICacheProvider<T>
{
    public SessionStateCacheProvider(
        IMvcContextFactory mvcContextFactory
        )
    {
        if (mvcContextFactory == null)
            throw new ArgumentNullException("mvcContextFactory");
        this.mvcContextFactory = mvcContextFactory;
    }
    private readonly IMvcContextFactory mvcContextFactory;

    protected HttpContextBase Context
    {
        get
        {
            return this.mvcContextFactory.CreateHttpContext();
        }
    }

    #region ICacheProvider<ISiteMap> Members

    public bool Contains(string key)
    {
        return (Context.Session[key] != null);
    }

    public Caching.LazyLock Get(string key)
    {
        return (LazyLock)Context.Session[key];
    }

    public bool TryGetValue(string key, out Caching.LazyLock value)
    {
        value = this.Get(key);
        if (value != null)
        {
            return true;
        }
        return false;
    }

    public void Add(string key, LazyLock item, ICacheDetails cacheDetails)
    {
        // NOTE: cacheDetails is normally used to set the timeout - you might
        // need to roll your own method for doing that.
        Context.Session[key] = item;
    }

    public void Remove(string key)
    {
        Context.Session.Remove(key);
    }

    public event EventHandler<MicroCacheItemRemovedEventArgs<T>> ItemRemoved;

    #endregion

    // NOTE: Normally this is called by a callback from the cache when an item exprires.
    // It is required to ensure there is no memory leak because a sitemap has circular references
    // that need to be broken explicitly. You need to work out how to call this when the user's session
    // expires.
    protected virtual void OnCacheItemRemoved(MicroCacheItemRemovedEventArgs<T> e)
    {
        if (this.ItemRemoved != null)
        {
            ItemRemoved(this, e);
        }
    }

}

Then inject it like this (StructureMap example shown):

// Setup cache
SmartInstance<CacheDetails> cacheDetails;

this.For<ICacheProvider<ISiteMap>>().Use<SessionStateCacheProvider<ISiteMap>>();

var cacheDependency =
    this.For<ICacheDependency>().Use<NullCacheDependency>();

cacheDetails =
    this.For<ICacheDetails>().Use<CacheDetails>()
        .Ctor<TimeSpan>("absoluteCacheExpiration").Is(absoluteCacheExpiration)
        .Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.MinValue)
        .Ctor<ICacheDependency>().Is(cacheDependency);

Option 2:

Append the user name to the siteMapCacheKey in a custom ISiteMapCacheKeyGenerator, and inject it via DI:

public class SessionBasedSiteMapCacheKeyGenerator
    : ISiteMapCacheKeyGenerator
{
    public UserBasedSiteMapCacheKeyGenerator(
        IMvcContextFactory mvcContextFactory
        )
    {
        if (mvcContextFactory == null)
            throw new ArgumentNullException("mvcContextFactory");
        this.mvcContextFactory = mvcContextFactory;
    }

    protected readonly IMvcContextFactory mvcContextFactory;

    #region ISiteMapCacheKeyGenerator Members

    public virtual string GenerateKey()
    {
        var context = mvcContextFactory.CreateHttpContext();
        var builder = new StringBuilder();
        builder.Append("sitemap://");
        builder.Append(context.Request.Url.DnsSafeHost);
        builder.Append("/?sessionId=");
        builder.Append(context.Session.SessionID);
        return builder.ToString();
    }

    #endregion
}

Inject it like this (StructureMap example):

this.For<ISiteMapCacheKeyGenerator>().Use<SessionBasedSiteMapCacheKeyGenerator>();

Note that using an external DI container is required.

Please see my open question here and explain to me why you would want to do this on GitHub, as it renders most of the features useless: https://github.com/maartenba/MvcSiteMapProvider/issues/16#issuecomment-22229604