2

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?
Shimmy Weitzhandler
  • 101,809
  • 122
  • 424
  • 632

2 Answers2

1

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

pjobs
  • 1,247
  • 12
  • 14
  • Thanks for your reply, but this is an old website and doesn't have DI enabled. Is there a simpler way to achieve this? I don't want to mess with this website too much so I want to keep my changes as minimal as possible. Is there a way to cache the site map per session? – Shimmy Weitzhandler Apr 20 '15 at 17:02
  • It's from Nuget. Current version is 4.6.3. The interface `IDynamicNodeProvider` doesn't seem to have a method `GetCacheDescription`. – Shimmy Weitzhandler Apr 20 '15 at 17:20
  • I've found [this](http://stackoverflow.com/a/18163187/75500) post which sounds what I'm after, but I want to avoid enabling DI in that project. – Shimmy Weitzhandler Apr 20 '15 at 17:44
  • With 4.6.3 Version, your best bet with least amount of code changes would be using Internal DI and following the First Option. – pjobs Apr 20 '15 at 17:54
  • 1) What does the generic type T refer to in the referred post? 2) The project has CommonServiceLocator package installed, is that helpful? Where is the place where the injection code should be placed? – Shimmy Weitzhandler Apr 20 '15 at 18:48
1

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");
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • Hi, and thanks for your comprehensive reply. The visibility provider sounds like the best solution. Not only that but whether the IP is Israeli is already cached per session which makes it even better. I now also learned that I can invalidate the cache programmatically, so I'm gonna increase the cache duration and reset it manually upon menu-change. Anyway **the above code doesn't work**, it's unable to find the visibility provider. – Shimmy Weitzhandler Apr 20 '15 at 19:26
  • Here's the error I get: "The visibility provider instance named 'MyNamespace.IsraelVisibilityProvider, MyAssembly' was not found. Check your DI configuration to ensure a visibility provider instance with this name exists and is configured correctly." – Shimmy Weitzhandler Apr 20 '15 at 19:27
  • 1
    You could instead use `node.VisibilityProvider = typeof(IsraelVisibilityProvider).AssemblyQualifiedName` to ensure the string is correct. Alternatively, if you import the namespace `MvcSiteMapProvider.Reflection` you can use the extension method `typeof(IsraelVisibilityProvider).ShortAssemblyQualifiedName()`. – NightOwl888 Apr 20 '15 at 19:27
  • Thanks for your effort, I used that ex. method and it still doesn't work. – Shimmy Weitzhandler Apr 20 '15 at 19:31
  • Ahh, looks like you might have stumbled across a bug. Workaround: add the string for the type of the visibility provider to the `visibilityProvider` attribute of a node in the Mvc.sitemap file. ``. Make sure you use an assembly qualified type name using the namespace and assembly name of your type. See [this answer](http://stackoverflow.com/questions/2404061/need-fully-qualified-type-name). – NightOwl888 Apr 20 '15 at 19:47
  • Or alternatively add it as the default visibility provider in `web.config` as shown at the bottom of my answer. The problem seems to be that the type is not instantiated at startup as it should be because it is not taking into consideration visibility providers that are declared within dynamic node providers. – NightOwl888 Apr 20 '15 at 19:49
  • I don't use an Mvc.sitemap file, anyway I set it in the home page index action's `MvcSiteMapNodeAttribute`. It now works great. You wrawkkz! Anyway I now lose the capability of setting the visibility provider on only one page and will have to check for the node in the visibility provider itself... – Shimmy Weitzhandler Apr 20 '15 at 19:53
  • 1
    I added an issue [here](https://github.com/maartenba/MvcSiteMapProvider/issues/389). You can track it by subscribing to the issue. – NightOwl888 Apr 20 '15 at 20:07