A critical vulnerability was discovered in React Server Components (Next.js). Our systems remain protected but we advise to update packages to newest version. Learn More

Aniket
Aug 16, 2025
  29
(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: Learning Optimizely Just Got Easier: Introducing the Optimizely Learning Centre

On the back of my last post about the Opti Graph Learning Centre, I am now happy to announce a revamped interactive learning platform that makes...

Graham Carr | Jan 31, 2026

Scheduled job for deleting content types and all related content

In my previous blog post which was about getting an overview of your sites content https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/sche...

Per Nergård (MVP) | Jan 30, 2026

Working With Applications in Optimizely CMS 13

💡 Note:  The following content has been written based on Optimizely CMS 13 Preview 2 and may not accurately reflect the final release version. As...

Mark Stott | Jan 30, 2026

Experimentation at Speed Using Optimizely Opal and Web Experimentation

If you are working in experimentation, you will know that speed matters. The quicker you can go from idea to implementation, the faster you can...

Minesh Shah (Netcel) | Jan 30, 2026