I have a simple .NET6 OData API with the default configuration with Batch enabled. The API is configured with IdentityServer from the default VS template.
Program.cs
var builder = WebApplication.CreateBuilder(args);
if (builder.Configuration.GetSection("ConnectionStrings:Active").Value == "Postgres")
{
var connectionString = builder.Configuration.GetConnectionString("Postgres");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString));
}
else
{
var connectionString = builder.Configuration.GetConnectionString("SQLServer");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
}
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Password.RequiredLength = 8;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddIdentityServer(options =>
{
options.UserInteraction.LoginUrl = "/login";
options.UserInteraction.LogoutUrl = "/logout";
})
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
builder.Services.AddAuthentication()
.AddIdentityServerJwt();
builder.Services.AddLocalization();
builder.Services.AddControllersWithViews(options =>
{
options.ModelBinderProviders.Insert(0, new CustomModelBinderProvider());
})
.AddOData(opt =>
{
var batchHandler = new DefaultODataBatchHandler();
batchHandler.MessageQuotas.MaxNestingDepth = 2;
batchHandler.MessageQuotas.MaxReceivedMessageSize = 100;
batchHandler.MessageQuotas.MaxOperationsPerChangeset = 10;
opt.AddRouteComponents("oapi",
new OdataModelBuilder().GetEDM(),
services => services.AddSingleton<ISearchBinder, ODataSearch>());
opt.AddRouteComponents("oapi_b",
new OdataModelBuilder().GetEDM(),
batchHandler);
opt.EnableQueryFeatures();
})
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) =>
factory.Create(typeof(SharedResources));
})
.AddRazorRuntimeCompilation();
builder.Services.AddRazorPages();
builder.Services.AddAutoMapper(System.Reflection.Assembly.GetExecutingAssembly());
builder.Services.AddHttpContextAccessor();
if (builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<DummySeedService>();
}
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
app.UseWebAssemblyDebugging();
app.UseODataRouteDebug();
using (var scope = app.Services.CreateScope())
{
var seedService = scope.ServiceProvider.GetRequiredService<DummySeedService>();
await seedService.Seed();
}
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseODataBatching();
app.UseRouting();
app.UseMiddleware<ODataTestMiddleware>();
var supportedCultures = new[] { "en-US", "ar-SY" };
var localizationOptions = new RequestLocalizationOptions().SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
app.UseRequestLocalization(localizationOptions);
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");
app.Run();
The Problem
After making a $batch
request, when execution falls out of the UseODataBatching
middleware, the HttpContext
property in the IHttpContextAccessor
becomes null, and this triggers a NullReferenceException
in the IdentityServer
middleware thus returning 500
for all requests
Requests
(from Postman):
{
"requests": [
{
"id": "{{$guid}}",
"url": "Patients(1)",
"method": "GET",
"headers": {
"content-type": "application/json"
}
},
{
"id": "{{$guid}}",
"url": "Patients(2)",
"method": "GET",
"headers": {
"content-type": "application/json"
}
}
]
}
Responses
:
{
"responses": [
{
"id": "8a4dac81-2662-472b-bef9-273401a53cfb",
"status": 500,
"headers": {}
},
{
"id": "933c6bbe-db67-4526-8199-2eedc176dc7b",
"status": 500,
"headers": {}
}
]
}
When removing the IdentityServer
middleware the batch request gets through and returns 200
for both requests with no problems.
And as a test i wrote a test middleware ODataTestMiddleware
public class ODataTestMiddleware
{
private readonly RequestDelegate requestDelegate;
public ODataTestMiddleware(RequestDelegate requestDelegate)
{
this.requestDelegate = requestDelegate;
}
public async Task InvokeAsync(HttpContext httpContext, IHttpContextAccessor contextAccessor)
{
await requestDelegate(httpContext);
}
}
And IHttpContextAccessor.HttpContext
is also null in here.
I saw this issue on the OData repo IHttpContextAccessor.HttpContext returns null when executing within an odata batch call, but i am using the ASP.NET Core version so i don't know the difference between the two in terms of implementation.
Is there a workaround for this that i can try? Thank you for your time.
CodePudding user response:
I ended up adding a middleware that repopulates the IHttpContextAccessor
manually.
public class ODataHttpContextFixer
{
private readonly RequestDelegate requestDelegate;
public ODataHttpContextFixer(RequestDelegate requestDelegate)
{
this.requestDelegate = requestDelegate;
}
public async Task InvokeAsync(HttpContext httpContext, IHttpContextAccessor contextAccessor)
{
contextAccessor.HttpContext ??= httpContext;
await requestDelegate(httpContext);
}
}
The middleware should be placed after the ODataBatching
middleware.