Aniket
Aug 16, 2025
visibility 1557
star star star star star
(1 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

Quan Mai
Quan Mai Aug 22, 2025 11:26 AM

Interesting approach and nice write up Aniket. Thanks for sharing!

error Please login to comment.
Latest blogs
Parallel Development in Optimizely CMS SaaS: Shifting to a Schema Migration Mindset

  Part 3 of the Parallel Development series. The branch-scoped push script from Part 1 works. Run it on a feature branch where you've touched a...

Vipin Banka | Jul 25, 2026

From AI Agents to AI Workflow with Opal

Introduction In the first article in this series , we talked about AI agents in Optimizely Opal and walked through the process of creating a...

Igor Safonov | Jul 23, 2026

Building Your First Optimizely Opal AI Agent: a Hands-On Walkthrough

Introduction There's a common assumption that building an AI agent is something only engineers or developers can do. It isn't. At the Optimizely AN...

Igor Safonov | Jul 23, 2026

Advanced Task Manager Gets a Big Update

One of the things I keep coming back to with Optimizely's content approval workflow is that it's solid at the individual level - you know a piece o...

Adnan Zameer | Jul 21, 2026 |