Visibility of individual items in MvcSiteMapProvid

2019-07-20 08:00发布

I want to hide a certain page from menu, if the current session IP is in Israel. Here's what I've tried, but in fact the menu-item doesn't appear anywhere.
I tested the GeoIP provider and it seems to be working, what am I doing wrong?

Here's how I the menu is created and how I try to skip the items I don't want in the menu:

public class PagesDynamicNodeProvider
  : DynamicNodeProviderBase
{
  private static readonly Guid KeyGuid = Guid.NewGuid();
  private const string IsraelOnlyItemsPageKey = "publications-in-hebrew";

  public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode siteMapNode)
  {
    using (var context = new Context())
    { 
      var pages = context.Pages
                    .Include(p => p.Language)
                    .Where(p => p.IsPublished)
                    .OrderBy(p => p.SortOrder)
                    .ThenByDescending(p => p.PublishDate)
                    .ToArray();

      foreach (var page in pages)
      {


        //*********************************************************
        //Is it the right way to 'hide' the page in current session
        if (page.MenuKey == IsraelOnlyItemsPageKey && !Constants.IsIsraeliIp)
          continue;

        var node = new DynamicNode(
          key: page.MenuKey,
          parentKey: page.MenuParentKey,
          title: page.MenuTitle,
          description: page.Title,
          controller: "Home",
          action: "Page");          

        node.RouteValues.Add("id", page.PageId);
        node.RouteValues.Add("pagetitle", page.MenuKey);

        yield return node;
      }
    }
  }
}

Here's how I determine and cache whether the IP is from Israel:

private const string IsIsraeliIpCacheKey = "5522EDE1-0E22-4FDE-A664-7A5A594D3992";
private static bool? _IsIsraeliIp;
/// <summary>
/// Gets a value indicating wheather the current request IP is from Israel
/// </summary>
public static bool IsIsraeliIp
{
  get
  {
    if (!_IsIsraeliIp.HasValue)
    {
      var value = HttpContext.Current.Session[IsIsraeliIpCacheKey];
      if (value != null)
        _IsIsraeliIp = (bool)value;
      else
        HttpContext.Current.Session[IsIsraeliIpCacheKey] = _IsIsraeliIp = GetIsIsraelIpFromServer() == true;
    }
    return _IsIsraeliIp.Value;
  }
}

private static readonly Func<string, string> FormatIpWithGeoIpServerAddress = (ip) => @"http://www.telize.com/geoip/" + ip;
private static bool? GetIsIsraelIpFromServer()
{
  var ip = HttpContext.Current.Request.UserHostAddress;
  var address = FormatIpWithGeoIpServerAddress(ip);
  string jsonResult = null;
  using (var client = new WebClient())
  {
    try
    {
      jsonResult = client.DownloadString(address);
    }
    catch
    {
      return null;
    }
  }

  if (jsonResult != null)
  {
    var obj = JObject.Parse(jsonResult);
    var countryCode = obj["country_code"];

    if (countryCode != null)
      return string.Equals(countryCode.Value<string>(), "IL", StringComparison.OrdinalIgnoreCase);
  }
  return null;
}
  1. Is the DynamicNodeProvider cached? If yes, maybe this is what's causing the issue? How can I make it cache per session, so each sessions gets its specific menu?
  2. Is it right to cache the IP per session?
  3. Any other hints on tracking down the issue?

2条回答
祖国的老花朵
2楼-- · 2019-07-20 08:17

The reason why your link doesn't appear anywhere is because the SiteMap is cached and shared between all if its users. Whatever the state of the user request that builds the cache is what all of your users will see.

However without caching the performance of looking up the node hierarchy would be really expensive for each request. In general, the approach of using a session per SiteMap is supported (with external DI), but not recommended for performance and scalability reasons.

The recommended approach is to always load all of your anticipated nodes for every user into the SiteMap's cache (or to fake it by forcing a match). Then use one of the following approaches to show and/or hide the nodes as appropriate.

  1. Security Trimming
  2. Built-in or custom visibility providers
  3. Customized HTML helper templates (in the /Views/Shared/DisplayTemplates/ folder)
  4. A custom HTML helper

It is best to think of the SiteMap as a hierarchical database. You do little more than set up the data structure, and that data structure applies to every user of the application. Then you make per-request queries against that shared data (the SiteMap object) that can be filtered as desired.

Of course, if none of the above options cover your use case, please answer my open question as to why anyone would want to cache per user, as it pretty much defeats the purpose of making a site map.

Here is how you might set up a visibility provider to do your filtering in this case.

public class IsrealVisibilityProvider : SiteMapNodeVisibilityProviderBase
{
    public override bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
    {
        return Constants.IsIsraeliIp;
    }
}

Then remove the conditional logic from your DynamicNodeProvider and add the visibility provider to each node where it applies.

public class PagesDynamicNodeProvider
  : DynamicNodeProviderBase
{
    private const string IsraelOnlyItemsPageKey = "publications-in-hebrew";

    public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode siteMapNode)
    {
        using (var context = new Context())
        { 
            var pages = context.Pages
                        .Include(p => p.Language)
                        .Where(p => p.IsPublished)
                        .OrderBy(p => p.SortOrder)
                        .ThenByDescending(p => p.PublishDate)
                        .ToArray();

            foreach (var page in pages)
            {
                var node = new DynamicNode(
                  key: page.MenuKey,
                  parentKey: page.MenuParentKey,
                  title: page.MenuTitle,
                  description: page.Title,
                  controller: "Home",
                  action: "Page");          

                // Add the visibility provider to each node that has the condition you want to check
                if (page.MenuKey == IsraelOnlyItemsPageKey)
                {
                    node.VisibilityProvider = typeof(IsraelVisibilityProvider).AssemblyQualifiedName;
                }
                node.RouteValues.Add("id", page.PageId);
                node.RouteValues.Add("pagetitle", page.MenuKey);

                yield return node;
            }
        }
    }
}

For a more complex visibility scheme, you might want to make a parent visibility provider that calls child visibility providers based on your own custom logic and then set the parent visibility provider as the default in web.config.

<add key="MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider" value="MyNamespace.ParentVisibilityProvider, MyAssembly"/>

Or, using external DI, you would set the default value in the constructor of SiteMapNodeVisibilityProviderStrategy.

// Visibility Providers
this.For<ISiteMapNodeVisibilityProviderStrategy>().Use<SiteMapNodeVisibilityProviderStrategy>()
    .Ctor<string>("defaultProviderName").Is("MyNamespace.ParentVisibilityProvider, MyAssembly");
查看更多
兄弟一词,经得起流年.
3楼-- · 2019-07-20 08:25

I am not sure which version of MVCSiteMapProvider you are using, but the latest version is very extensible as it allows using internal/external DI(depenedency injection).

In your case it is easy to configure cache per session, by using sliding cache expiration set to session time out.

Link

// Setup cache
SmartInstance<CacheDetails> cacheDetails;

this.For<System.Runtime.Caching.ObjectCache>()
    .Use(s => System.Runtime.Caching.MemoryCache.Default);

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

var cacheDependency =
    this.For<ICacheDependency>().Use<RuntimeFileCacheDependency>()
        .Ctor<string>("fileName").Is(absoluteFileName);

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

If you are using Older Version, you can try to implement GetCacheDescription method in IDynamicNodeProvider

public interface IDynamicNodeProvider
{
  IEnumerable<DynamicNode> GetDynamicNodeCollection();
  CacheDescription GetCacheDescription();
}

Here are the details of CacheDescription structure. Link

查看更多
登录 后发表回答