0

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();
            }
        }
    }
}
Bmoe
  • 888
  • 1
  • 15
  • 37
  • As you’re not altering the body, you don’t need a second stream like that. Just read it and rewind it as [shown here](https://devblogs.microsoft.com/aspnet/re-reading-asp-net-core-request-bodies-with-enablebuffering/), being careful to ensure you pass `leaveOpen: true` when creating the `StreamReader`. – sellotape Apr 11 '21 at 16:16
  • @sellotape adding `leaveOpen: true` was enough to make it work so thanks. Can you show me what you mean by read and rewind? Couldn't make it out from the example. Are you saying all of the `injectedRequestStream` lines are currently serving no use? – Bmoe Apr 11 '21 at 18:36
  • They seem largely redundant, yes. You could drop them all and do the `Seek(0)` (or `context.Request.Body.Position = 0` as in the link) on the request body instead; that’s what I meant by "rewind". – sellotape Apr 11 '21 at 18:54

0 Answers0