Upgraded my app from .NET Core 2.2 to .NET Core 3.1. On one of my api endpoints that creates/updates a record, via HTTP POST or PUT, I am getting following error:
System.ObjectDisposedException: Cannot access a closed Stream.
at System.IO.MemoryStream.get_Position()
at Microsoft.AspNetCore.WebUtilities.FileBufferingReadStream.get_Position()
at Microsoft.AspNetCore.Mvc.Formatters.NewtonsoftJsonInputFormatter.ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
at Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BodyModelBinder.BindModelAsync(ModelBindingContext bindingContext)
at Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder.BindModelAsync(ActionContext actionContext, IModelBinder modelBinder, IValueProvider valueProvider, ParameterDescriptor parameter, ModelMetadata metadata, Object value)
at Microsoft.AspNetCore.Mvc.Controllers.ControllerBinderDelegateProvider.<>c__DisplayClass0_0.<<CreateBinderDelegate>g__Bind|0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
--- End of stack trace from previous location where exception was thrown ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
I'm testing my api via postman. I put a breakpoint in my controller in the beginning of the endpoint method. The breakpoint isn't even getting hit so this error is happening before I can even get inside the method. Judging from the stacktrace it looks like a config issue in Startup.cs
so I'll post that code.
namespace SomeTypeOf.Api
{
public class Startup
{
private bool clientSecurity = true;
// Startup constructor to set configuration
public Startup(IWebHostEnvironment env)
{
// Initial startup
// Use Environment-Instance based config files
var builder = new ConfigurationBuilder()
.SetBasePath(System.AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
//Set the local Configuration object with values from appsettings.json
Configuration = builder.Build();
clientSecurity = Boolean.Parse(Configuration["securitySettings:clientSecurity"]);
}
// Startup constructor to set configuration
public Startup(IConfiguration configuration)
{
//Set the local Configuration object with values from appsettings.json
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Initial Setup
services.AddMvc()
.AddNewtonsoftJson(options =>
options.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver());
services.AddApplicationInsightsTelemetry(Configuration);
services.AddScoped<SomeApiResourceFilter>();
services.AddSingleton<IConfiguration>(Configuration);
// Call this in case you need aspnet-user-authtype/aspnet-user-identity
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Register the Swagger generator, defining one or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc(Configuration["appSettings:appVersion"], new OpenApiInfo { Title = Configuration["appSettings:appName"], Version = Configuration["appSettings:appVersion"] });
});
services.AddDataProtection();
// if (clientSecurity) {
//Authentication Setup
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Jwt";
options.DefaultChallengeScheme = "Jwt";
}).AddJwtBearer("Jwt", options =>
{
//TODO: Improve token validation security
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
//ValidAudience = "the audience you want to validate",
ValidateIssuer = false,
//ValidIssuer = "the isser you want to validate",
ValidateIssuerSigningKey = true, // Validate the key
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("WOOOO")),
ValidateLifetime = true, //validate the expiration and not before values in the token
ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
};
options.SaveToken = true;
//TODO: Remove this event - its not required
options.Events = new JwtBearerEvents()
{
OnTokenValidated = context =>
{
// Add the access_token as a claim, as we may actually need it
var accessToken = context.SecurityToken as JwtSecurityToken;
if (accessToken != null)
{
ClaimsIdentity identity = context.Principal.Identity as ClaimsIdentity;
if (identity != null)
{
identity.AddClaim(new Claim("access_token", accessToken.RawData));
}
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization();
//}
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
//Initial Setup
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/" + Configuration["appSettings:appVersion"] + "/swagger.json", Configuration["appSettings:appName"]);
});
app.UseStaticFiles();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute("swagger", "swagger/");
});
//Set the default landing page to Swagger
app.UseWelcomePage("/swagger");
//add NLog to ASP.NET Core
loggerFactory.AddNLog();
//Set the connection string for the loggin database
LogManager.Configuration.Variables["connectionString"] = Configuration.GetConnectionString("LoggingDatabase");
//Set the instance name for logging (used in the nlog config)
LogManager.Configuration.Variables["appInstance"] = Configuration["appSettings:appInstance"];
XmlLoggingConfiguration xmlconfig = (XmlLoggingConfiguration)LogManager.Configuration;
//Allow AutoReload of NLog config
xmlconfig.AutoReload = Boolean.Parse(Configuration["Logging:AutoReload"]);
//Run NLog internal logging
bool isInternalLogging = Boolean.Parse(Configuration["Logging:InternalLogging"]);
if (isInternalLogging)
{
InternalLogger.LogFile = Configuration["Logging:LogFiles:Internal"];
InternalLogger.LogLevel = LogLevel.FromString(Configuration["Logging:LogLevel:Internal"]);
}
//Set the Path to locally generated log files for the API
LogManager.Configuration.Variables["apiFileLogPath"] = Configuration["Logging:LogFiles:ApiFileLogPath"];
var target = (FileTarget)LogManager.Configuration.FindTargetByName("apiFile-log");
target.FileName = Configuration["Logging:LogFiles:ApiFileLog"];
}
}
}
Not sure if it matters but here is the beginning snippet of my api endpoint just in case
[HttpPost("SomeApi/SomeRecords")]
[HttpPut("SomeApi/SomeRecords")]
[ServiceFilter(typeof(SomeApiResourceFilter))]
public SomeApiResponse PutSomeRecordBody([FromBody] SomeRecord indexRecord)
{
SomeApiResponse response = new SomeApiResponse();
SomeApiError sae;
var userClaims = HttpContext.User.Claims;
var clientSystem = userClaims.First(c => c.Type == ClaimTypes.Name).Value;
...
}
Here's the filter code
using System;
using Microsoft.AspNetCore.Mvc.Filters;
using NLog;
using System.Text;
using System.IO;
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
namespace SomeApi.Filters
{
public class SomeApiResourceFilter : Attribute, IResourceFilter
{
private readonly ILogger _logger;
public SomeApiResourceFilter()
{
_logger = LogManager.GetCurrentClassLogger();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
_logger.Debug(context.ActionDescriptor.DisplayName + "- OnResourceExecuted");
}
public void OnResourceExecuting(ResourceExecutingContext context)
{
_logger.Debug(context.ActionDescriptor.DisplayName + "- OnResourceExecuting");
//var body = context.HttpContext.Request.Body;
HttpRequestRewindExtensions.EnableBuffering(context.HttpContext.Request);
var injectedRequestStream = new MemoryStream();
try
{
string clientSystem = "Unknown";
var userClaims = context.HttpContext.User.Claims;
if (userClaims != null && userClaims.Count()>0)
{
clientSystem = userClaims.First(c => c.Type == ClaimTypes.Name).Value;
}
else
{
if (context.HttpContext.Connection.RemoteIpAddress != null &&
context.HttpContext.Connection.RemoteIpAddress.ToString().Length > 0)
{
clientSystem += " " + context.HttpContext.Connection.RemoteIpAddress.ToString();
}
}
var requestLog = clientSystem + " | " + context.HttpContext.Request.Method + " "
+ context.HttpContext.Request.Path + context.HttpContext.Request.QueryString.Value;
using (var bodyReader = new StreamReader(context.HttpContext.Request.Body))
{
var bodyAsText = bodyReader.ReadToEnd();
if (string.IsNullOrWhiteSpace(bodyAsText) == false)
{
requestLog += $" | {bodyAsText}";
}
var bytesToWrite = Encoding.UTF8.GetBytes(bodyAsText);
injectedRequestStream.Write(bytesToWrite, 0, bytesToWrite.Length);
injectedRequestStream.Seek(0, SeekOrigin.Begin);
context.HttpContext.Request.Body = injectedRequestStream;
}
_logger.Info(requestLog);
}
catch(Exception ex)
{
_logger.Error(ex, "Unable to generate token");
}
finally
{
//injectedRequestStream.Dispose();
}
}
}
}