diff --git a/dotnet/src/dotnetcore/GxClasses.Web/Middleware/GXRouting.cs b/dotnet/src/dotnetcore/GxClasses.Web/Middleware/GXRouting.cs index aa66482bc..5584efba5 100644 --- a/dotnet/src/dotnetcore/GxClasses.Web/Middleware/GXRouting.cs +++ b/dotnet/src/dotnetcore/GxClasses.Web/Middleware/GXRouting.cs @@ -62,10 +62,10 @@ public GXRouting(string baseURL) } static public List GetRouteController(Dictionary apiPaths, - Dictionary> sValid, - Dictionary> sMap, - Dictionary, string>> sMapData, - string basePath, string verb, string path) + Dictionary> sValid, + Dictionary> sMap, + Dictionary, string>> sMapData, + string basePath, string verb, string path) { List result = new List(); string parms = string.Empty; @@ -78,10 +78,10 @@ static public List GetRouteController(Dictionary int questionMarkIdx = path.IndexOf(QUESTIONMARK); string controller; if (apiPaths.ContainsKey(basePath) - && sValid.ContainsKey(basePath) - && sMap.ContainsKey(basePath) - && sMapData.ContainsKey(basePath) - ) + && sValid.ContainsKey(basePath) + && sMap.ContainsKey(basePath) + && sMapData.ContainsKey(basePath) + ) { if (sValid[basePath].Contains(path.ToLower())) { @@ -174,12 +174,12 @@ internal async Task RouteHttpService(HttpContext context) HandlerFactory handlerFactory = new HandlerFactory(); await handlerFactory.Invoke(context); } - catch (Exception ex) + catch { - await Task.FromException(ex); + throw; } } - public Task ProcessRestRequest(HttpContext context) + public async Task ProcessRestRequest(HttpContext context) { try { @@ -188,7 +188,6 @@ public Task ProcessRestRequest(HttpContext context) IHttpContextAccessor contextAccessor = context.RequestServices.GetService(); context = new GxHttpContextAccesor(contextAccessor); } - Task result = Task.CompletedTask; string path = context.Request.Path.ToString(); string actualPath = string.Empty; bool isServiceInPath = ServiceInPath(path, out actualPath); @@ -214,43 +213,46 @@ public Task ProcessRestRequest(HttpContext context) } string controllerPath = path.ToLower().Split(actualPath).Last(); controllerWithParms = controllerPath.Split(QUESTIONMARK).First(); - + } } else { - if (path.Contains(oauthRoute) && (AzureDeploy.GAM == "true")) - return (RouteHttpService(context)); + if (path.Contains(oauthRoute) && (AzureDeploy.GAM == "true")) + { + await (RouteHttpService(context)); + return; + } controllerWithParms = GetGxRouteValue(path); GXLogging.Debug(log, $"Running Azure functions. ControllerWithParms :{controllerWithParms} path:{path}"); } - + List controllers = GetRouteController(servicesPathUrl, servicesValidPath, servicesMap, servicesMapData, actualPath, context.Request.Method, controllerWithParms); GxRestWrapper controller = null; ControllerInfo controllerInfo = controllers.FirstOrDefault(c => (controller = GetController(context, c)) != null); - + if (controller != null) { if (HttpMethods.IsGet(context.Request.Method) && (controllerInfo.Verb == null || HttpMethods.IsGet(controllerInfo.Verb))) { - result = controller.Get(controllerInfo.Parameters); + await controller.Get(controllerInfo.Parameters); } else if (HttpMethods.IsPost(context.Request.Method) && (controllerInfo.Verb == null || HttpMethods.IsPost(controllerInfo.Verb))) { - result = controller.Post(); + await controller.Post(); } else if (HttpMethods.IsDelete(context.Request.Method) && (controllerInfo.Verb == null || HttpMethods.IsDelete(controllerInfo.Verb))) { - result = controller.Delete(controllerInfo.Parameters); + await controller.Delete(controllerInfo.Parameters); } else if (HttpMethods.IsPut(context.Request.Method) && (controllerInfo.Verb == null || HttpMethods.IsPut(controllerInfo.Verb))) { - result = controller.Put(controllerInfo.Parameters); + await controller.Put(controllerInfo.Parameters); } else if (HttpMethods.IsPatch(context.Request.Method) && (controllerInfo.Verb == null || HttpMethods.IsPatch(controllerInfo.Verb))) { - result = controller.Patch(controllerInfo.Parameters); + await controller.Patch(controllerInfo.Parameters); } else if (HttpMethods.IsOptions(context.Request.Method)) { @@ -284,18 +286,20 @@ public Task ProcessRestRequest(HttpContext context) { GXLogging.Error(log, $"ProcessRestRequest controller not found path:{path} controllerWithParms:{controllerWithParms}"); context.Response.Headers.Clear(); - result = Task.FromException(new PageNotFoundException(path)); + throw new PageNotFoundException(path); } } - context.CommitSession(); - - return result; + } catch (Exception ex) { GXLogging.Error(log, "ProcessRestRequest", ex); HttpHelper.SetUnexpectedError(context, HttpStatusCode.InternalServerError, ex); - return Task.FromException(ex); + throw; + } + finally + { + await context.CommitSessionAsync(); } } @@ -374,20 +378,21 @@ public bool ServiceInPath(String path, out String actualPath) return true; } } - else { + else + { return true; - } + } } - private String FindPath(string innerPath, Dictionary servicesPathUrl, bool startTxt) + private String FindPath(string innerPath, Dictionary servicesPathUrl, bool startTxt) { string actualPath = String.Empty; - foreach (string subPath in from String subPath in servicesPathUrl.Keys - select subPath) + foreach (string subPath in from String subPath in servicesPathUrl.Keys + select subPath) { bool match = false; innerPath = innerPath.ToLower(); - match = (startTxt)? innerPath.StartsWith($"/{subPath.ToLower()}"): innerPath.Contains($"/{subPath.ToLower()}"); + match = (startTxt) ? innerPath.StartsWith($"/{subPath.ToLower()}") : innerPath.Contains($"/{subPath.ToLower()}"); if (match) { actualPath = subPath.ToLower(); @@ -431,7 +436,7 @@ public GxRestWrapper GetController(HttpContext context, ControllerInfo controlle bool privateDirExists = Directory.Exists(privateDir); GXLogging.Debug(log, $"PrivateDir:{privateDir} asssemblycontroller:{asssemblycontroller}"); - string svcFile=null; + string svcFile = null; if (privateDirExists && File.Exists(Path.Combine(privateDir, $"{asssemblycontroller.ToLower()}.grp.json"))) { controller = tmpController; @@ -498,7 +503,7 @@ string SvcFile(string controller) GXLogging.Warn(log, "Service file not found:" + controllerFullName); return null; } - + } public void ServicesGroupSetting() { @@ -524,7 +529,7 @@ public void ServicesGroupSetting() string mapPath = (m.BasePath.EndsWith("/")) ? m.BasePath : m.BasePath + "/"; string mapPathLower = mapPath.ToLower(); string mNameLower = m.Name.ToLower(); - servicesPathUrl[mapPathLower]= mNameLower; + servicesPathUrl[mapPathLower] = mNameLower; if (!RestAPIHelpers.ServiceAsController()) { GXLogging.Debug(log, $"addServicesPathUrl key:{mapPathLower} value:{mNameLower}"); @@ -575,7 +580,8 @@ public void ServicesGroupSetting() } } } - }catch (Exception ex) + } + catch (Exception ex) { GXLogging.Error(log, $"Error Loading Services Group Settings", ex); throw; @@ -676,7 +682,7 @@ public class Binding [DataContract()] public class MapGroup - { + { String _objectType; String _name; String _basePath; diff --git a/dotnet/src/dotnetcore/GxClasses.Web/Middleware/HandlerFactory.cs b/dotnet/src/dotnetcore/GxClasses.Web/Middleware/HandlerFactory.cs index d0f5865eb..39caddb34 100644 --- a/dotnet/src/dotnetcore/GxClasses.Web/Middleware/HandlerFactory.cs +++ b/dotnet/src/dotnetcore/GxClasses.Web/Middleware/HandlerFactory.cs @@ -89,13 +89,13 @@ public async Task Invoke(HttpContext context) } else { - await Task.FromException(new PageNotFoundException(url)); + throw new PageNotFoundException(url); } } catch (Exception ex) { GXLogging.Error(log, $"Handler Factory failed creating {url}", ex); - await Task.FromException(ex); + throw; } } private static string ObjectUrl(string requestPath, string basePath) diff --git a/dotnet/src/dotnetcore/GxClasses/Domain/HttpSessionState.cs b/dotnet/src/dotnetcore/GxClasses/Domain/HttpSessionState.cs index 979a838ff..08657a959 100644 --- a/dotnet/src/dotnetcore/GxClasses/Domain/HttpSessionState.cs +++ b/dotnet/src/dotnetcore/GxClasses/Domain/HttpSessionState.cs @@ -1,9 +1,11 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; using System.Reflection; +using System.Threading; using Microsoft.AspNetCore.Http; namespace GeneXus.Http @@ -32,34 +34,13 @@ public static CookieCollection GetCookies(this CookieContainer container) return allCookies; } } - internal class LockTracker : IDisposable + public static class LockTracker { - private static Dictionary _locks = new Dictionary(); - private int _activeUses = 0; - private readonly string _id; + private static readonly ConcurrentDictionary _locks = new(); - private LockTracker(string id) => _id = id; - - internal static LockTracker Get(string id) - { - lock (_locks) - { - if (!_locks.ContainsKey(id)) - _locks.Add(id, new LockTracker(id)); - var res = _locks[id]; - res._activeUses += 1; - return res; - } - } - - void IDisposable.Dispose() + public static SemaphoreSlim Get(string sessionId) { - lock (_locks) - { - _activeUses--; - if (_activeUses == 0) - _locks.Remove(_id); - } + return _locks.GetOrAdd(sessionId, _ => new SemaphoreSlim(1, 1)); } } internal class HttpSyncSessionState : HttpSessionState diff --git a/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs b/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs index 98e8eee23..2792f9fa6 100644 --- a/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs +++ b/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs @@ -69,7 +69,6 @@ public async Task Invoke(HttpContext context) if (!string.IsNullOrEmpty(subdomain)) context.Items[AppContext.TENANT_ID] = subdomain; } - await _next(context); } } diff --git a/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs b/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs index 7e11e9e7b..1e35401fb 100644 --- a/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs +++ b/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs @@ -258,7 +258,7 @@ public void ConfigureServices(IServiceCollection services) string sessionCookieName = GxWebSession.GetSessionCookieName(VirtualPath); if (!string.IsNullOrEmpty(sessionCookieName)) { - options.Cookie.Name = sessionCookieName; + options.Cookie.Name=sessionCookieName; GxWebSession.SessionCookieName = sessionCookieName; } string sameSite; @@ -283,20 +283,20 @@ public void ConfigureServices(IServiceCollection services) services.AddResponseCompression(options => { options.MimeTypes = new[] - { - // Default - "text/plain", - "text/css", - "application/javascript", - "text/html", - "application/xml", - "text/xml", - "application/json", - "text/json", - // Custom - "application/json", - "application/pdf" - }; + { + // Default + "text/plain", + "text/css", + "application/javascript", + "text/html", + "application/xml", + "text/xml", + "application/json", + "text/json", + // Custom + "application/json", + "application/pdf" + }; options.EnableForHttps = true; }); } @@ -305,7 +305,7 @@ public void ConfigureServices(IServiceCollection services) private void RegisterControllerAssemblies(IMvcBuilder mvcBuilder) { - + if (RestAPIHelpers.ServiceAsController()) { mvcBuilder.AddMvcOptions(options => options.ModelBinderProviders.Insert(0, new QueryStringModelBinderProvider())); @@ -374,9 +374,9 @@ private void RegisterRestServices(IMvcBuilder mvcBuilder) try { string[] controllerAssemblyQualifiedName = new string(File.ReadLines(svcFile).First().SkipWhile(c => c != '"') - .Skip(1) - .TakeWhile(c => c != '"') - .ToArray()).Trim().Split(','); + .Skip(1) + .TakeWhile(c => c != '"') + .ToArray()).Trim().Split(','); string controllerAssemblyName = controllerAssemblyQualifiedName.Last(); if (!serviceAssemblies.Contains(controllerAssemblyName)) { @@ -437,17 +437,17 @@ private void DefineCorsPolicy(IServiceCollection services) services.AddCors(options => { options.AddPolicy(name: CORS_POLICY_NAME, - policy => - { - policy.WithOrigins(origins); - if (!corsAllowedOrigins.Contains(CORS_ANY_ORIGIN)) - { - policy.AllowCredentials(); - } - policy.AllowAnyHeader(); - policy.AllowAnyMethod(); - policy.SetPreflightMaxAge(TimeSpan.FromSeconds(CORS_MAX_AGE_SECONDS)); - }); + policy => + { + policy.WithOrigins(origins); + if (!corsAllowedOrigins.Contains(CORS_ANY_ORIGIN)) + { + policy.AllowCredentials(); + } + policy.AllowAnyHeader(); + policy.AllowAnyMethod(); + policy.SetPreflightMaxAge(TimeSpan.FromSeconds(CORS_MAX_AGE_SECONDS)); + }); }); } } @@ -455,7 +455,7 @@ private void DefineCorsPolicy(IServiceCollection services) private void ConfigureSessionService(IServiceCollection services, ISessionService sessionService) { - + if (sessionService is GxRedisSession) { GxRedisSession gxRedisSession = (GxRedisSession)sessionService; @@ -551,7 +551,7 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos { app.UseMiddleware(); } - app.UseSession(); + app.UseAsyncSession(); app.UseStaticFiles(); ISessionService sessionService = GXSessionServiceFactory.GetProvider(); @@ -642,7 +642,7 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos }, ContentTypeProvider = provider }); - + app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandler = new CustomExceptionHandlerMiddleware().Invoke, @@ -685,7 +685,7 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos app.UseGXHandlerFactory(basePath); - app.Run(async context => + app.Run(async context => { await Task.FromException(new PageNotFoundException(context.Request.Path.Value)); }); @@ -711,13 +711,13 @@ private void ConfigureSwaggerUI(IApplicationBuilder app, string baseVirtualPath) app.UseSwaggerUI(options => { options.SwaggerEndpoint($"../../{finfo.Name}", finfo.Name); - options.RoutePrefix = $"{baseVirtualPathWithSep}{finfo.Name}/{SWAGGER_SUFFIX}"; + options.RoutePrefix =$"{baseVirtualPathWithSep}{finfo.Name}/{SWAGGER_SUFFIX}"; }); if (finfo.Name.Equals(SWAGGER_DEFAULT_YAML, StringComparison.OrdinalIgnoreCase) && File.Exists(Path.Combine(LocalPath, DEVELOPER_MENU))) app.UseSwaggerUI(options => { options.SwaggerEndpoint($"../../{SWAGGER_DEFAULT_YAML}", SWAGGER_DEFAULT_YAML); - options.RoutePrefix = $"{baseVirtualPathWithSep}{DEVELOPER_MENU}/{SWAGGER_SUFFIX}"; + options.RoutePrefix =$"{baseVirtualPathWithSep}{DEVELOPER_MENU}/{SWAGGER_SUFFIX}"; }); } @@ -751,10 +751,10 @@ public class CustomExceptionHandlerMiddleware static readonly IGXLogger log = GXLoggerFactory.GetLogger(); public async Task Invoke(HttpContext httpContext) { - string httpReasonPhrase = string.Empty; + string httpReasonPhrase=string.Empty; Exception ex = httpContext.Features.Get()?.Error; HttpStatusCode httpStatusCode = (HttpStatusCode)httpContext.Response.StatusCode; - if (ex != null) + if (ex!=null) { if (ex is PageNotFoundException) { @@ -772,7 +772,7 @@ public async Task Invoke(HttpContext httpContext) GXLogging.Error(log, $"Internal error", ex); } } - if (httpStatusCode != HttpStatusCode.OK) + if (httpStatusCode!= HttpStatusCode.OK) { string redirectPage = Config.MapCustomError(httpStatusCode.ToString(HttpHelper.INT_FORMAT)); if (!string.IsNullOrEmpty(redirectPage)) @@ -786,7 +786,7 @@ public async Task Invoke(HttpContext httpContext) if (!string.IsNullOrEmpty(httpReasonPhrase)) { IHttpResponseFeature responseReason = httpContext.Response.HttpContext.Features.Get(); - if (responseReason != null) + if (responseReason!=null) responseReason.ReasonPhrase = httpReasonPhrase; } } @@ -928,4 +928,36 @@ public void Apply(ApplicationModel application) } } } + public static class SesssionAsyncExtensions + { + /// + /// Ensures sessions load asynchronously by calling LoadAsync before accessing session data, + /// forcing the session provider to avoid synchronous operations. + /// + /// + /// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-5.0 + /// The default session provider in ASP.NET Core will only load the session record from the underlying IDistributedCache store asynchronously if the + /// ISession.LoadAsync method is explicitly called before calling the TryGetValue, Set or Remove methods. + /// Failure to call LoadAsync first will result in the underlying session record being loaded synchronously, + /// which could potentially impact the ability of an application to scale. + /// + /// See also: + /// https://github.com/aspnet/Session/blob/master/src/Microsoft.AspNetCore.Session/DistributedSession.cs + /// https://github.com/dotnet/AspNetCore.Docs/issues/1840#issuecomment-454182594 + /// + public static IApplicationBuilder UseAsyncSession(this IApplicationBuilder app) + { + app.UseSession(); + app.Use(async (context, next) => + { + if (context.Session != null) + { + await context.Session.LoadAsync(); + } + await next(); + }); + return app; + } + } + } diff --git a/dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs b/dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs index dbeabfce2..934730df2 100644 --- a/dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs +++ b/dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs @@ -30,6 +30,8 @@ using System.Linq; using GeneXus.Http.Client; using System.Net.Http.Headers; +using System.Threading.Tasks; +using System.Threading; namespace GeneXus.Http { @@ -944,38 +946,37 @@ public static void SetReasonPhrase(this HttpContext context, string statusDescri #endif } #if NETCORE - internal static void CommitSession(this HttpContext context) + internal static async Task CommitSessionAsync(this HttpContext context) { - Dictionary _contextSession; if (context.Items.TryGetValue(HttpSyncSessionState.CTX_SESSION, out object ctxSession)) { - _contextSession = ctxSession as Dictionary; + var _contextSession = ctxSession as Dictionary; if (_contextSession != null && _contextSession.Count > 0) { ISession _httpSession = context.Session; - var locker = LockTracker.Get(_httpSession.Id); - using (locker) + var semaphore = LockTracker.Get(_httpSession.Id); + await semaphore.WaitAsync(); + try { - lock (locker) + FieldInfo loaded = _httpSession.GetType().GetField("_loaded", BindingFlags.Instance | BindingFlags.NonPublic); + if (loaded != null) { - FieldInfo loaded = _httpSession.GetType().GetField("_loaded", BindingFlags.Instance | BindingFlags.NonPublic); - if (loaded != null) - { - loaded.SetValue(_httpSession, false); - _httpSession.LoadAsync().Wait(); - } - foreach (string s in _contextSession.Keys) - { - if (_contextSession[s] == null) - _httpSession.Remove(s); - else - { - _httpSession.SetString(s, _contextSession[s]); - } - } - context.Items.Remove(HttpSyncSessionState.CTX_SESSION); - _httpSession.CommitAsync().Wait(); + loaded.SetValue(_httpSession, false); + await _httpSession.LoadAsync(); + } + foreach (string s in _contextSession.Keys) + { + if (_contextSession[s] == null) + _httpSession.Remove(s); + else + _httpSession.SetString(s, _contextSession[s]); } + context.Items.Remove(HttpSyncSessionState.CTX_SESSION); + await _httpSession.CommitAsync(); + } + finally + { + semaphore.Release(); } } }