Take the community feedback survey now.

Aniket
Aug 16, 2025
  10
(0 votes)

Getting around asynchronous limitations in Optimizely (Visitor Groups etc.)

Optmizely has a powerful personalization engine that allows creating custom Audiences/Visitor groups. It comes with one limitation though. It doesn't support ASYNC operations. The below solution can work for any scenario where you cannot run async operations in Optimizely. 

Here's a real world use case and how we got around it.

Personalizing user experiences often starts with knowing where your visitor is coming from. One lightweight approach is to translate the user's IP address into a zip code to personalize the experience for users depending on their location (state run promotion, etc.). In Optimizely CMS 12 (running on ASP.NET Core 8), on of the way to do this is by using middleware.


Why Middleware?

Visitor Groups has a key limitation: visitor group criteria must run synchronously. This means you can’t safely call an external API or await an async lookup when evaluating a visitor group without causing thread block calls to the external service, which rules out many IP-to-zip services. ASP.NET Core middleware, on the other hand, runs for every request (that can be filtered) and fully supports async operations. Using AsyncHelper.RunSync() as outlined here isn't a scalable solution and can cause thread pool starvation and deadlocks in your application. This makes it the ideal place for concerns like geolocation, logging, authentication, and request enrichment. By resolving the zip code once at the start of the pipeline, you avoid duplicate lookups in controllers, block controllers, or views at the same time avoid blocking calls to external services.


Step 1: Create a Request-Scoped Container

We’ll use a scoped class to store the resolved zip code so it’s easily accessible through DI.

public sealed class GeoContext
{
    public string? ZipCode { get; set; }
}
 

Step 2: Define an IP → Zip Resolver

This is your service that turns an IP address into a zip code. You can use a 3rd-party API, MaxMind, or your own data source.

 public interface IZipFromIpResolver
{
    Task<string?> GetZipFromIpAsync(string ip, CancellationToken ct = default);
}
 
 

Step 3: Implement Middleware

The middleware gets the client IP, calls the resolver, and stores the result in GeoContext

 public sealed class GeoZipMiddleware
{
    private readonly RequestDelegate _next;

    public GeoZipMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context, GeoContext geo, IZipFromIpResolver resolver)
    {
      // Filter out any paths to avoid calling the service on these requests
       var path = httpContext.Request.Path.Value ?? "";
       if (!path.StartsWith("/assets", StringComparison.OrdinalIgnoreCase) &&
       !path.StartsWith("/static", StringComparison.OrdinalIgnoreCase) &&
       !path.StartsWith("/api/custom", StringComparison.OrdinalIgnoreCase) &&
       !path.Contains("/globalassets", StringComparison.OrdinalIgnoreCase) &&
       !path.Contains("/.well-known", StringComparison.OrdinalIgnoreCase) &&
       !path.Contains("/siteassets", StringComparison.OrdinalIgnoreCase) &&
       !path.Contains("/Errors", StringComparison.OrdinalIgnoreCase) &&
       !path.Contains("/contentassets", StringComparison.OrdinalIgnoreCase)) // Optimizely blobs
       {
        var ip = context.Connection.RemoteIpAddress?.ToString();
        if (!string.IsNullOrWhiteSpace(ip))
        {
            try
            {
                // Call your GeoLocation Service
                geo.ZipCode = await resolver.GetZipFromIpAsync(ip);
                // You can also save it within the httpContext
                 httpContext.Items["ZipCode"] = geo.ZipCode;
            }
            catch
            {
                // log if needed; don’t block the request
            }
        }
       }
       await _next(context);
    }
}
 
 

Step 4: Register Services & Middleware

Update Program.cs

var builder = WebApplication.CreateBuilder(args);

// Add request-scoped container
builder.Services.AddScoped<GeoContext>();

// Add resolver implementation
// builder.Services.AddSingleton<IZipFromIpResolver, MyIpResolver>();

builder.Services.AddHttpContextAccessor();

var app = builder.Build();

// Use middleware early in the pipeline
app.UseMiddleware<GeoZipMiddleware>();

app.UseRouting();
app.UseAuthorization();

app.MapContent();

app.Run();

 


Step 5: Use in Controllers, Blocks, or Views

Since GeoContext is scoped, you can inject it anywhere. You can also use the httpContext.Items to retrieve the value (set in the middleware)

 public override bool IsMatch(IPrincipal principal, HttpContext httpContext)
 {
     var zipCode = httpContext.Items["ZipCode"];

     // do something with it
    return true;

 }
 

Notes & Best Practices

  • Caching: IP-to-zip lookups can be expensive. Use ISynchronizedObjectCache or a distributed cache to reduce API calls.

  • Privacy: Treat IP and location data as personal information (GDPR/CCPA). Avoid persisting unless you have a clear use case.

  • Performance: Consider skipping static asset and other URL requests inside your middleware for speed.


Wrapping Up

By leveraging ASP.NET Core middleware in Optimizely CMS 12, you can easily enrich each request with geolocation or any other data. This is powerful and can significantly improve the performance of the website by removing blocking calls from the application. 

Aug 16, 2025

Comments

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP - Opticon London 2025

This installment of a day in the life of an Optimizely OMVP gives an in-depth coverage of my trip down to London to attend Opticon London 2025 held...

Graham Carr | Oct 2, 2025

Optimizely Web Experimentation Using Real-Time Segments: A Step-by-Step Guide

  Introduction Personalization has become de facto standard for any digital channel to improve the user's engagement KPI’s.  Personalization uses...

Ratish | Oct 1, 2025 |

Trigger DXP Warmup Locally to Catch Bugs & Performance Issues Early

Here’s our documentation on warmup in DXP : 🔗 https://docs.developers.optimizely.com/digital-experience-platform/docs/warming-up-sites What I didn...

dada | Sep 29, 2025

Creating Opal Tools for Stott Robots Handler

This summer, the Netcel Development team and I took part in Optimizely’s Opal Hackathon. The challenge from Optimizely was to extend Opal’s abiliti...

Mark Stott | Sep 28, 2025

Integrating Commerce Search v3 (Vertex AI) with Optimizely Configured Commerce

Introduction This blog provides a technical guide for integrating Commerce Search v3, which leverages Google Cloud's Vertex AI Search, into an...

Vaibhav | Sep 27, 2025

A day in the life of an Optimizely MVP - Opti Graph Extensions add-on v1.0.0 released

I am pleased to announce that the official v1.0.0 of the Opti Graph Extensions add-on has now been released and is generally available. Refer to my...

Graham Carr | Sep 25, 2025