2

I'm having an issue getting OutputCaching to work with HttpContext.RewritePath for a WCF 4.0 WebHttp service.

My service is localized. The idea is that you call a URL like so:

/languageCode/ServiceName/Method
e.g.
/en/MyService/GetItems

And it'll return the results localized to the correct language.

My scheme is based on this article. The idea is to create a derivative of RouteBase that creates a unique, "private" route to the real service. When the user makes a request, the language code is unpacked from the URL and set as the culture for the current thread, and then HttpContext.RewritePath is used to load the actual service.

For the life of me I can't figure out how to work OutputCaching into the mix. I've decorated my service method with AspNetCacheProfile and am seeing my own VaryByCustom override called. However despite receiving a duplicate result from VaryByCustom, .NET continues into my service method anyway.

Lots of code below, sorry for the dump but I suspect it's all relevant.


How I add a route in Global.asax.cs

RouteTable.Routes.Add(new CulturedServiceRoute(
    "devices", 
    new StructureMapServiceHostFactory(), 
    typeof(DeviceService)));

VaryByCustom override in Global.asax.cs:

public override string GetVaryByCustomString(
    HttpContext context, string custom)
{

    // This method gets called twice: Once for the initial request, then a 
    // second time for the rewritten URL. I only want it to be called once!

    if (custom == "XmlDataFreshness")
    {
        var outputString = String.Format("{0}|{1}|{2}", 
            XmlDataLoader.LastUpdatedTicks, 
            context.Request.RawUrl, 
            context.Request.HttpMethod);
        return outputString;
    }

    return base.GetVaryByCustomString(context, custom);
}

This is the dynamic service route class.

public class CulturedServiceRoute : RouteBase, IRouteHandler
{
    private readonly string _virtualPath = null;
    private readonly ServiceRoute _innerServiceRoute = null;
    private readonly Route _innerRoute = null;

    public CulturedServiceRoute(
        string pathPrefix, 
        ServiceHostFactoryBase serviceHostFactory, 
        Type serviceType)
    {
        if (pathPrefix.IndexOf("{") >= 0)
        {
            throw new ArgumentException(
                "Path prefix cannot include route parameters.", 
                "pathPrefix");
        }
        if (!pathPrefix.StartsWith("/")) pathPrefix = "/" + pathPrefix;
        pathPrefix = "{culture}" + pathPrefix;

        _virtualPath = String.Format("Cultured/{0}/", serviceType.FullName);
        _innerServiceRoute = new ServiceRoute(
            _virtualPath, serviceHostFactory, serviceType);
        _innerRoute = new Route(pathPrefix, this);
    }

    public override RouteData GetRouteData(
        HttpContextBase httpContext)
    {
        return _innerRoute.GetRouteData(httpContext);
    }

    public override VirtualPathData GetVirtualPath(
        RequestContext requestContext, RouteValueDictionary values)
    {
        return null;
    }

    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        // This method is called even if VaryByCustom 
        // returns a duplicate response!

        var culture = requestContext.RouteData.Values["culture"].ToString();
        var ci = new CultureInfo(culture);
        Thread.CurrentThread.CurrentUICulture = ci;
        Thread.CurrentThread.CurrentCulture = 
            CultureInfo.CreateSpecificCulture(ci.Name);

        requestContext.HttpContext.RewritePath("~/" + _virtualPath, true);
        return _innerServiceRoute.RouteHandler.GetHttpHandler(requestContext);
    }
}

Finally, the relevant portions of the service itself:

[ServiceContract]
[AspNetCompatibilityRequirements(
    RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class DeviceService
{
    [AspNetCacheProfile("MyCacheProfile")]
    [WebGet(UriTemplate = "")]
    public IEnumerable<DeviceListItemModel> GetDevices()
    {
        // This is called AFTER the first VaryByCustom override is called.
        // I'd expect it not to be called unless VaryByCustom changes!

        var devices =
            from d in _deviceRepository.GetAll()
            where d.ReleaseDate < DateTime.Now
            orderby d.Id descending
            select new DeviceListItemModel(d);

        return devices;
    }

UPDATE: My cache profile:

<caching>
  <outputCacheSettings>
    <outputCacheProfiles>
      <add name="MyCacheProfile" varyByCustom="XmlDataFreshness"
           varyByHeader="accept" varyByParam="*" location="Server"
           duration="3600" />
    </outputCacheProfiles>
  </outputCacheSettings>
</caching>
roufamatic
  • 18,187
  • 7
  • 57
  • 86

1 Answers1

0

Hmmm seems like a valid approach to me. Is the cache profile configured correctly? Is varyByCustom called multiple times and certain to return the same result when the cache does not need to be updated?

maartenba
  • 3,344
  • 18
  • 31
  • Cache profile seems OK to me, I added it to the bottom of the question just in case. VaryByCustom is definitely returning the same response each time. – roufamatic Jun 01 '11 at 08:22
  • Just as a test, can you strip out the varyByHeader="accept" varyByParam="*" settings? So only varyByCustom remains? – maartenba Jun 01 '11 at 12:16
  • I removed varyByHeader but was unable to remove varyByParam (throws an exception) so I simply left it blank. No change. Hrmmm. My situation is pretty simple compared to yours so I'm going to try a more naïve approach with BeginRequest and RewritePath. – roufamatic Jun 01 '11 at 16:09
  • Got it working as described in my prior comment. Still curious why this didn't work, but not as urgently so. :-) – roufamatic Jun 01 '11 at 17:27
  • Strange! I'm thinking the route's GetHttpHandler path rewrite may be coming at an inconvenient moment for caching to interfere. Should have a look at it using debugger symbols from System.Web :-) – maartenba Jun 03 '11 at 06:31