0

Trying to save search log into local file. I need async handler for it, but AddOnBeginRequestAsync needs a IAsyncResult returned from BeginRequest, EndRequest. How to to this without it? return null - not working.

P. S. This is IIS managed module.

public void Dispose()
{
}

public bool IsReusable
{ get { return false; } }

public void Init(HttpApplication app)
{
    app.AddOnBeginRequestAsync(BeginRequest, EndRequest);
}
        
private IAsyncResult BeginRequest(object sender, EventArgs e, AsyncCallback cb, object extraData)
{
    string reqPath = HttpContext.Current.Request.Url.PathAndQuery;
    bool correctString = reqPath.Contains("/?search=");

    if (HttpContext.Current.Request.HttpMethod == "POST" && correctString)
    {
        using (var reader = new StreamReader(HttpContext.Current.Request.InputStream))
        {
            string searchData = HttpUtility.UrlDecode(reader.ReadToEnd());
        }
        File.AppendAllText(workDir + "search_log.txt", searchData);
    }
}

private void EndRequest(IAsyncResult ar)
{
    return;
}

When return null added to BeginRequest, then error occurs "System.NullReferenceException".

Also tried:

public class NullAsyncResult : IAsyncResult
{
public object AsyncState
{
get { return null; }
}

public System.Threading.WaitHandle AsyncWaitHandle
{
get { return null; }
}

public bool CompletedSynchronously
{
get { return true; }
}

public bool IsCompleted
{
get { return true; }
}
}

Then:

private IAsyncResult BeginRequest(object sender, EventArgs e, AsyncCallback cb, object extraData)
{
    string reqPath = HttpContext.Current.Request.Url.PathAndQuery;
    bool correctString = reqPath.Contains("/?search=");

    if (HttpContext.Current.Request.HttpMethod == "POST" && correctString)
    {
        using (var reader = new StreamReader(HttpContext.Current.Request.InputStream))
        {
            string searchData = HttpUtility.UrlDecode(reader.ReadToEnd());
        }
        File.AppendAllText(workDir + "search_log.txt", searchData);
    }
return NullAsyncResult();
}

Got error:

CS1955 Non-callable member 'NullAsyncResult' cannot be used as a method.
ValB
  • 19
  • 2

1 Answers1

0

The purpose of using AddOnBeginRequestAsync is when you're doing something asynchronous. Right now, you're not, and you could do the same thing in the Application_BeginRequest.

However, I do think you could benefit from changing your code to write to the file asynchronously. That would speed up your application a bit, since it would allow your application to begin processing the request while the data is being written to the file. This async code can be simplified by using EventHandlerTaskAsyncHelper, which is briefly described in this answer.

Also, using File.AppendAllText() gets expensive when you're repeating it over and over. It has to open the file, write the data, close the file, only to do the same thing on the next request. So another enhancement you can make is to use a static variable for the file and hold it open for the lifetime of the application. Then you're not constantly opening and closing the file. You need to wrap the file stream with TextWriter.Synchronized to allow multiple threads to write to the file. However, this will only work if your app pool has only one worker process.

Also, if you wrap the InputStream in a StreamWriter and dispose it (which using will do), then it disposes the InputStream too, and ASP.NET can't use it anymore. So you will need to read the InputStream differently, and return the Position to 0 so that ASP.NET can reread the stream.

You also don't need to use HttpUtility.UrlDecode() because you're reading the body of the request, not the URL.

And to simplify your code, you can just use Request.InputStream rather than HttpContext.Current.Request.InputStream, since the HttpApplication class does expose a Request property.

I also don't think you even need a module to do it, since you're using AddOnBeginRequestAsync at the application level anyway. You can do it all in your Global.asax.cs file.

I've done some of this before, so I can share my code. Adapting it to your use case, it would look something like this:

private static TextWriter logFile;

public override void Init() {
    var wrapper = new EventHandlerTaskAsyncHelper(LogRequestData);
    AddOnBeginRequestAsync(wrapper.BeginEventHandler, wrapper.EndEventHandler);
}

protected void Application_Start() {
    // Whatever else you might have already had in your start event

    // We only use FileShare.ReadWrite to accommodate the short overlap from app pool recycling.
    // The app pool must have only one worker process.
    var fs = new FileStream(workDir + "search_log.txt", FileMode.Append, FileSystemRights.AppendData, FileShare.ReadWrite, 4096, FileOptions.None);
    logFile = TextWriter.Synchronized(new StreamWriter(fs) {
        AutoFlush = true
    });
}

protected void Application_End() {
    logFile.Dispose();
}

private Task LogRequestData(object sender, EventArgs e) {
    string reqPath = Request.Url.PathAndQuery;
    bool correctString = reqPath.Contains("/?search=");

    if (Request.HttpMethod == "POST" && correctString && Request.InputStream.Length > 0) {
        var bytes = new byte[Request.InputStream.Length];
        Request.InputStream.Read(bytes, 0, bytes.Length);
        var searchData = Request.ContentEncoding.GetString(bytes);
        Request.InputStream.Position = 0;

        return logFile.WriteLineAsync(searchData);
    }
    return Task.CompletedTask;
}

We initialize logFile in Application_Start because that is run only once in the lifetime of the application, whereas Init is run every time a new HttpApplication class is instantiated, which can happen many times in the lifetime of an application. Since we're setting a static variable, we want it to only be run once. You could also initialize logFile in the declaration, if you can make workDir a static value:

private static TextWriter logFile = TextWriter.Synchronized(
    new StreamWriter(new FileStream(workDir + "search_log.txt", FileMode.Append, FileSystemRights.AppendData, FileShare.ReadWrite, 4096, FileOptions.None)) {
        AutoFlush = true
    });

And you'll notice that if you don't need to log anything, it returns Task.CompletedTask, which is what you do in methods where you have to return a Task, but you haven't done anything asynchronous.

I also added a condition to the if so it only reads the data if there is actually data there (Request.InputStream.Length > 0).

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • No problem. I just realized (while testing this) that you were using a module, and I was doing this directly in `Global.asax.cs`. So I updated my answer a bit. I don't think you need a module to do it. – Gabriel Luci Mar 30 '23 at 14:48
  • This will also only work properly if your IIS app pool is configured to have only one worker process. I updated the file opening to use `FileShare.ReadWrite`, but only to accommodate app pool recycles, which will start up a new process before ending the previous one. If you try to run multiple worker processes, one process could write to the file in the middle of another process writing, and you'll end up with gibberish. – Gabriel Luci Mar 30 '23 at 15:52