I have an Asp.Net MVC 5 web application which has an error handling mechanism for log error in DB and showing the errorId to the user who raised that error in a separate form which is called ErrorPage. When an error occurs in the web app I store that errorId in session and I read it from the session in ErrorPage to showing that errorId to the user who faced this error in order to be able for backup operations. The web server of this web application is processing requests with only one worker process currently, so all generated sessions are valid and accessible in the whole of the web app.
I am going to increase the number of worker processes from 1 to 4 for this web app but I have some problem with my web app. Also in IIS, I set the session state mode to In Process mode because of in the web app I used session in many cases and I can't set it to SQL Server mode because of it will increase performance overhead.
The problem is where a request goes in worker process A (for example) and a session will generate for this request in the worker process A and suppose this request encounters an error in web application, I will redirect the user to the ErrorPage and it is possible this new request (redirecting user to ErrorPage's action in ErrorController) goes in another worker process B (for example). But in the worker process B, I can't access that session which is generated for the first request because of that sessions is defined at the worker process level and they are valid only in that worker process.
So after a lot of search for this, I decided to save session info in DB instead of Ram and load it from DB when I need that info. But I have no idea about saving this info in DB with which key ID?
Imagine this scenario to find out my real problem easier:
let's have:
WorkerProcessId1 = W1;
WorkerProcessId2 = W2;
SessionId1 = S1;
SessionId2 = S2;
RequestId1 = R1;
RequestId2 = R2;
and the scenario:
R1 comes to web server
==> web server passes R1 to W1
==> W1 generates S1 for R1
==> R1 faces an error
==> for the user who sends R1 (it is possible the user has not logged in yet so I don't know the userId), I will save the error in DB using the combination of S1 and userId in a specific pattern as a unique identifier in Error table in DB
==> the user will redirect to ErrorPage with another request R2
==> web server passes R2 to W2
==> W2 generates S2 for R2
==> after the redirect is done, in the ErrorPage I need the errorId of that error which I save it to DB, for showing it to the user for backup operations
==> I don't know which error belongs to this user and which error should be load from DB????
If this is not possible to do that, is there any way to have a shared identifier across all worker processes of the web server?
Edit:
In this edit, I will explain where and how I used from the session in my ErrorHandling mechanism. At the end of the target line there is a commented phrase where it is written "Here I am using session":
namespace MyDomain.UI.Infrastructure.Attributes
{
public class CustomHandleErrorAttribute : HandleErrorAttribute
{
public CustomHandleErrorAttribute()
{
}
public override void OnException(ExceptionContext filterContext)
{
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
{
return;
}
if (!ExceptionType.IsInstanceOfType(filterContext.Exception))
{
return;
}
var errorid = 0;
try
{
errorid = SaveErrorToDatabase(filterContext);
}
catch (Exception e)
{
//Console.WriteLine(e);
//throw;
}
// if the request is AJAX return JSON else view.
if (filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
filterContext.Result = new JsonResult
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new
{
error = true,
message = "Error Message....",
errorid,
}
};
}
else
{
var controllerName = (string)filterContext.RouteData.Values["controller"];
var actionName = (string)filterContext.RouteData.Values["action"];
var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
filterContext.Controller.TempData.Clear();
filterContext.Controller.TempData.Add("ErrorCode", errorid);//Here I am using session
filterContext.Result = new ViewResult
{
ViewName = View,
MasterName = Master,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
}
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
private int SaveErrorToDatabase(ExceptionContext exception)
{
MyDomainDBContext dbContext = new MyDomainDBContext();
var browserType = exception.HttpContext.Request.Browser.Capabilities["type"];
var error = new Error
{
ErrorURL = exception.HttpContext.Request.Url.ToString(),
ExceptionType = exception.Exception.GetType().Name,
IsGlobalError = false,
Message = exception.Exception.Message,
StackTrace = exception.Exception.StackTrace,
ThrownTime = DateTime.Now,
UserIP = IPAddress.Parse(exception.HttpContext.Request.UserHostAddress).ToString(),
BrowserName = browserType.ToString() + "," +
GetUserPlatform(exception.HttpContext.Request)
};
AddRequestDetails(exception.Exception, exception.HttpContext.Request, error);
if (exception.Exception.InnerException != null)
{
error.Message += "\n Inner Excpetion : \n " + exception.Exception.InnerException.Message;
if (exception.Exception.InnerException.InnerException != null)
{
error.Message += "\n \t Inner Excpetion : \n " + exception.Exception.InnerException.InnerException.Message;
}
}
if (exception.HttpContext.User.Identity.IsAuthenticated)
{
error.UserID = exception.HttpContext.User.Identity.GetUserId<int>();
}
dbContext.Errors.Add(error);
dbContext.SaveChanges();
return error.ErrorID;
}
private void AddRequestDetails(Exception exception, HttpRequestBase request, Error err)
{
if (exception.GetType().Name == "HttpAntiForgeryException" && exception.Message == "The anti-forgery cookie token and form field token do not match.")
{
if (request.Form != null)
{
if (request.Cookies["__RequestVerificationToken"] != null)
{
err.RequestDetails = "Form : " + request.Form["__RequestVerificationToken"] +
" \n Cookie : " + request.Cookies["__RequestVerificationToken"].Value;
}
else
{
err.RequestDetails = "Does not have cookie for forgery";
}
}
}
}
private String GetUserPlatform(HttpRequestBase request)
{
var ua = request.UserAgent;
if (ua.Contains("Android"))
return $"Android";
if (ua.Contains("iPad"))
return $"iPad OS";
if (ua.Contains("iPhone"))
return $"iPhone OS";
if (ua.Contains("Linux") && ua.Contains("KFAPWI"))
return "Kindle Fire";
if (ua.Contains("RIM Tablet") || (ua.Contains("BB") && ua.Contains("Mobile")))
return "Black Berry";
if (ua.Contains("Windows Phone"))
return $"Windows Phone";
if (ua.Contains("Mac OS"))
return "Mac OS";
if (ua.Contains("Windows NT 5.1") || ua.Contains("Windows NT 5.2"))
return "Windows XP";
if (ua.Contains("Windows NT 6.0"))
return "Windows Vista";
if (ua.Contains("Windows NT 6.1"))
return "Windows 7";
if (ua.Contains("Windows NT 6.2"))
return "Windows 8";
if (ua.Contains("Windows NT 6.3"))
return "Windows 8.1";
if (ua.Contains("Windows NT 10"))
return "Windows 10";
//fallback to basic platform:
return request.Browser.Platform + (ua.Contains("Mobile") ? " Mobile " : "");
}
}
public class IgnoreErrorPropertiesResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
if (new[]{
"InputStream",
"Filter",
"Length",
"Position",
"ReadTimeout",
"WriteTimeout",
"LastActivityDate",
"LastUpdatedDate",
"Session"
}.Contains(property.PropertyName))
{
property.Ignored = true;
}
return property;
}
}
}
As you can see I filled TempData
which will be stored in the session for passing errorId by ErrorCode key to ErrorPage for showing to the user.