Answered Requests are permitted between...

raysefo

Well-known member
Joined
Feb 22, 2019
Messages
361
Programming Experience
10+
Hello,

I have an asp.net web API. The client wants me to add a valid time interval (09:00 - 21:30) in order to accept requests. Let's say if a request comes at 08:00, it will be denied with a message saying Requests are permitted between 9:00 AM and 21:30 PM. I am planning to implement this logic in the global.asax as follows, but I am not sure if there is a better way.

C#:
ublic class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            
            GlobalConfiguration.Configure(WebApiConfig.Register);
          
        }

        protected void Application_BeginRequest()
        {
            

            if (CheckTime9To2130())
            {
                //do something
            }
            else
            {
                Response.StatusCode = 429;
                Response.Write("Requests are permitted between 9:00 AM and 21:30 PM");
                Response.End();
            }
        }

        private bool CheckTime9To2130()
        {
            var h = DateTime.Now.TimeOfDay.Hours;
            var m = DateTime.Now.TimeOfDay.Minutes;

            if ((9 <= h && h <= 21) || (h == 21 && m <= 30))
            {
                return true;
            }

            return false;
        }
    }

What if the client wants to change the time interval? I need to change it in the code base and deploy it to the server. So I think I need to put those times into an appsettings of config. But it still has a cost, when I change it on the config, the IIS will be restarted right? So keeping those times in the database seems to be the only solution? What are your suggestions?
 
the IIS will be restarted right?
Close. IIS will actually stay running. The app pool that is running your code will be restarted.

But so what if it is restarted? The default setting of IIS is that app pools will restart every 29 hours. (You need to change the default setting if you don't want your app pool to restart in the middle of the day.) But still why does it matter? If a request comes in while the app pool is restarting, IIS will hold on to the request for a few minutes until the app pool starts.

Anyway your code for that time check could be improved dramatically. Take advantage of the DateTime.TimeOfDay property, and the fact that you can do greater than/less than comparison with TimeSpans. Couple that with the fact that you can use TimeSpan.Parse() to read a value from your web.config, and you'll have more readable maintainable code.
 
But still why does it matter? If a request comes in while the app pool is restarting, IIS will hold on to the request for a few minutes until the app pool starts.
On production, holding for a couple of minutes most probably will cause a big headache :)
 
Well, then you better make your app pool startup fast. :) Using lazy initialization help a lot. Anything that absolutely does not need to be done in the Global.asax and static class instances should be delayed.

That or use a load balancer. Take one machine out of the load balancer, change settings, let it warm up, then swap machines. Update the settings on the other machine and then bring that one back in as well.
 
You can also have multiple apps on a single server. The load balancer setting just becomes more complex, though.
 
Lazy initialization of simply the technique of delaying creating or initializing an object until it is first requested. .NET Framework 3.5(?) introduced the Lazy<T> class, to cut down on repetitive code that looks like this:
C#:
class Foo
{
    private Bar _bar;

    public Bar Bar
    {
        get
        {
            if (_bar == null)
                _bar = new Bar();
            return _bar;
        }
    }
}
 
Here's the reason why I advice getting the settings from the web.config instead reading a database. When you read the web.config, the .NET Framework is very smart about caching the values from the web.config because it knows that when the web.config changes, the app pool needs to restart. So reading the setting is very very very fast.

When you read from the database, you need to create a connection, perform a query, read the results. You need to do this for every web request. This time adds up quickly. Yes, you could add some caching so that you only actually hit the database every few minutes, but is the complexity of adding the caching worth it?
 
Where is the best suitable place to implement such a solution? Mine was in the global.asax but how about handlers, filters?
 
Personally, if I was already using ASP.NET WebAPI, I would take advantage of the rest of its infrastructure and use an IAuthorizationFilter. Yes, it maybe more work to have to tag each controller or method with that filter, but it also provides the flexibility of letting the web app continue running and accept web requests for other stuff that you do want to continue running in the blackout window. Perhaps if you decided it was worth really implementing the database storage of blackout times, then one of the "always on" web APIs could be a way to set the blackout time dynamically (and refresh the cache).
 
In my web API, I am using basic authentication. So I have already implemented Authorization Filter Attribute as follows:

C#:
public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
    {

        private const string Realm = "My Realm";
        readonly Func<IUserValidate> factory;

        public BasicAuthenticationAttribute(Func<IUserValidate> factory)
        {
            this.factory = factory;
        }


        public override void OnAuthorization(HttpActionContext actionContext)
        {
            //If the Authorization header is empty or null
            //then return Unauthorized
            if (actionContext.Request.Headers.Authorization == null)
            {
                actionContext.Response = actionContext.Request
                    .CreateResponse(HttpStatusCode.Unauthorized);
                // If the request was unauthorized, add the WWW-Authenticate header
                // to the response which indicates that it require basic authentication
                if (actionContext.Response.StatusCode == HttpStatusCode.Unauthorized)
                {
                    actionContext.Response.Headers.Add("WWW-Authenticate",
                        string.Format("Basic realm=\"{0}\"", Realm));
                }
            }
            else
            {
                //Get the authentication token from the request header
                string authenticationToken = actionContext.Request.Headers
                    .Authorization.Parameter;
                try
                {
                    //Decode the string
                    string decodedAuthenticationToken = Encoding.UTF8.GetString(
                        Convert.FromBase64String(authenticationToken));
                    //Convert the string into an string array
                    string[] usernamePasswordArray = decodedAuthenticationToken.Split(':');
                    //First element of the array is the username
                    string username = usernamePasswordArray[0];
                    //Second element of the array is the password
                    string password = usernamePasswordArray[1];
                    //call the login method to check the username and password
                    IUserValidate uv = factory();
                    if (uv.Login(username, password))
                    {
                        var identity = new GenericIdentity(username);
                        IPrincipal principal = new GenericPrincipal(identity, null);
                        Thread.CurrentPrincipal = principal;
                        if (HttpContext.Current != null)
                        {
                            HttpContext.Current.User = principal;
                        }
                    }
                    else
                    {
                        actionContext.Response = actionContext.Request
                            .CreateResponse(HttpStatusCode.Unauthorized);
                    }
                }
                catch
                {

                    actionContext.Response = actionContext.Request
                        .CreateResponse(HttpStatusCode.Unauthorized);

                }

            }
        }

    }

I think I should add the checking date time logic in the else portion and something like this right?
C#:
if (!CheckRequestTime())
            {
                Response.StatusCode = 429;
                Response.Write("Requests are permitted between " + WebConfigurationManager.AppSettings["TimeStart"].ToString() + " AM and " + WebConfigurationManager.AppSettings["TimeEnd"].ToString()+ " PM.");
                Response.End();
            }
        }

        private bool CheckRequestTime()
        {
          
            var t = DateTime.Now.TimeOfDay;
            var s = TimeSpan.Parse(WebConfigurationManager.AppSettings["TimeStart"]);
            var e = TimeSpan.Parse(WebConfigurationManager.AppSettings["TimeEnd"]);

            if ((s <= t) && (t <= e))
            {
                return true;
            }


            return false;
        }
 
Or do the time check even before doing the actual check for identity...

And for the sake of code hygiene implement it in a different class which is also an authorization filter. As I recall, ASP.NET MVC lets you put multiple filters. If it does not, then you'll also have to implement the chain of responsibility pattern to get all your authorization filters to be called in the correct sequence.
 
It is working, how does it look @Skydiver , any suggestions?

Authorization filter for valid time interval:
C#:
public class ValidTimeFilter : AuthorizationFilterAttribute
    {
        public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
        {
            //Check Request Start-End Period
            if (!CheckRequestTime())
            {
                var response = new HttpResponseMessage
                {
                    StatusCode = (HttpStatusCode)429,
                    ReasonPhrase = "Invalid Request Time",
                    Content = new StringContent("Requests are permitted between " + WebConfigurationManager.AppSettings["TimeStart"].ToString() + " AM and " + WebConfigurationManager.AppSettings["TimeEnd"].ToString() + " PM.")

                };

                actionContext.Response = response;
            }
        }

        private bool CheckRequestTime()
        {

            var t = DateTime.Now.TimeOfDay;
            var s = TimeSpan.Parse(WebConfigurationManager.AppSettings["TimeStart"]);
            var e = TimeSpan.Parse(WebConfigurationManager.AppSettings["TimeEnd"]);

            if ((s <= t) && (t <= e))
            {
                return true;
            }

            return false;
        }
    }

Authorization filter for total daily sales:
C#:
public class ValidTotalDailySale : AuthorizationFilterAttribute
    {
        public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
        {
            double totalSale = Double.Parse(WebConfigurationManager.AppSettings["TotalDailySale"].ToString());
            
            var terminal_number = HttpContext.Current.Request.Params["shopNo"];
            using (GameContext ctx = new GameContext())
            {


                
                var totalDailySales = ctx.Database.SqlQuery<Double>(
                    "WITH [CTE] AS ( SELECT [CR].[totalPrice] , [C].[ReferenceID] , ROW_NUMBER() OVER (PARTITION BY [GR].[ShopNo], [C].[ReferenceID] ORDER BY [C].[ConfirmCancelDateTime]) AS [RN] FROM [GameRequests] AS [GR] JOIN [GameConfirmResponses] AS [CR] ON [CR].referenceId = [GR].referenceId JOIN [ConfirmCancels] AS [C] ON [GR].[referenceId] = [C].[referenceId] WHERE [C].[Status] = 1 AND [C].[ShopNo] = @shopNo ) SELECT SUM([CTE].[totalPrice]) AS [TotalPrice] FROM [CTE] WHERE [CTE].[RN] = 1;",
                    new SqlParameter("shopNo", terminal_number)).ToList();

                if (totalDailySales.Count > 0 && totalDailySales[0]>totalSale)
                {
                    var response = new HttpResponseMessage
                    {
                        StatusCode = (HttpStatusCode)429,
                        ReasonPhrase = "Too Many Requests",
                        Content = new StringContent("Daily total sales limit " + totalSale + ", is exceeded for "+ terminal_number)

                    };

                    actionContext.Response = response;
                }
            
            }
        }
    }

Here is how I registered:

C#:
//Registering ValidTime Filter
            config.Filters.Add(new ValidTimeFilter());
            //Registering ValidRequestInterval Filter
            config.Filters.Add(new ValidIntervalFilter());
            //Registering ValidTotalDailySales Filter
            config.Filters.Add(new ValidTotalDailySale());
 
This is my personal style preference. You may choose to follow suit, or not. When I have a situation like:
C#:
bool SomeMethod()
{
    :
    if (condition)
        return true;

    return false;
}

I write:
C#:
bool SomeMethod()
{
    :
    return condition;
}
 
Back
Top Bottom