1

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.

Milad Rashidi
  • 1,296
  • 4
  • 22
  • 40
  • See [this post for guidance](https://stackoverflow.com/q/2147578/304683). In the end you're going to do out of process - so SQL, StateServer, etc. – EdSF Feb 28 '19 at 23:39
  • Additionally, unsure how you're identifying "a user", nor how sensitive the error data is, but you could explore client side persistence and/or standard logging solutions that don't involve Session. – EdSF Feb 28 '19 at 23:43
  • Thank you @EdSF for that link you posted in your comment. Before I ask this question I read that post which you linked in your comment, and some similar posts like that also, but those posts didn't solve my problem. For identifying the user I am using identity framework and for logging errors and exceptions which occur in the web app, I used standard solutions for saving error in DB. But my problem is where I want to know the errorId of that saved error for showing it to the user in another page. – Milad Rashidi Mar 01 '19 at 10:04
  • So what are `Sessions` for then (if you are using OWIN/Identity)? I think you'll need to clarify because my 2nd comment _seems_ to apply (no need for Session) – EdSF Mar 01 '19 at 21:25
  • OK @EdSF, I will update my question in order to increase clarity of my question. – Milad Rashidi Mar 02 '19 at 07:26

1 Answers1

0

I found a temporary solution for passing errorId to ErrorPage by creating a new class which is inherited from HandleErrorInfo with below structure, and using this errorId in ErrorPage:

public class HandleErrorInfoExtension : HandleErrorInfo
{
    public HandleErrorInfoExtension(Exception exception, string controllerName, string actionName, int errorId) : base(exception, controllerName, actionName)
    {
        ErrorId = errorId;
    }

    public int ErrorId { get; set; }
}

But I don't accept my own answer because of that still I am looking for a real solution to resolve the main problem of this question which is being able to share a data (or data structure) between all worker processes of the application. You should know I used session in some other places of my application that some of these places are vital (like a payment module) so I don't find the main solution for removing the using of the session (except using DB data storing because of performance overhead) yet. So I ask the community of developers of the StackOverflow.com to help me solve this problem.

Thanks to all of you dear colleagues.

Milad Rashidi
  • 1,296
  • 4
  • 22
  • 40