Skip to content

Commit 04be3b6

Browse files
claudiamurialdoClaudia Beatriz Murialdo Garronesjuarez
authored
Add Base controller class for Rest services. (#992)
* Add Base controller class for Rest services. * Temporary disable sending server name.CodeQL cs/user-controlled-bypass * Move GxRestService from GxClasses to GxClasses.Web * Return a StatusCode for WebException * Add method SendNotModified * Register native services at startup when SERVICE_AS_CONTROLLER is enabled. * Avoid setting headers when request has started. Add [JsonIgnore] to non-json serializable properties * Fix build error. * Add Json annotations for Message SDT * Fix registration of API services in modules. * Temporary cleanup of duplicated REST services: remove REST controllers loaded from API assemblies * Set BasePath for rest services * Add ApiException method to handle unexpected exceptions properly in declarative rest services. * Handle status code set by user code. * Handle GAMErrors property in declared rest services. * Add method ApiIntegratedSecurityLevel to base Rest class. * Improve error handling in rest controllers. * Do not set statuscode if it is 0 (non initialized) * Remove CustomControllerFeatureProvider as it is no longer needed. Rest service controllers now exist in a single assembly. * Avoid processing .grp.json when services as controllers are enabled. * Handle BadRequest when json input is invalid. Return custom error instead of: { "type": "....", "title": "One or more validation errors occurred.", "status": 400, "errors": { "$.VarCond": [ "The JSON value could not be converted to System.Int16. Path: $.VarCond | LineNumber: 0 | BytePositionInLine: 16." ] }, "traceId": ".... } * Add IsAnyDirty for SDTs * Define IsNull method for rest interfaces * Add missing JsonIgnore flags. * Add method EmptyResponse to return a valid empty json instead of empty body. * Add AnyDirtySdt method. * Add NullResult function. * Remove unneeded AnyDirtySdt property. SDT and collection properties on SDTS are marked as dirty on get method also. * Add EmptyObjectResult * Property's name must use a case-insensitive comparison during json deserialization * Grouped several lines of code into a new function, RegisterControllerAssemblies. * Updated JsonSerializerOptions.PropertyNameCaseInsensitive to be controlled by the ServiceJsonSerializerCaseSensitive configuration property * Change key ServiceJsonSerializerCaseSensitive for a shorter one: RestJsonCaseSensitive. * Remove unnecessary AddControllers() call as the current assembly has no controllers * Ensure that controllers from the specified assembly are registered as services and can have their dependencies injected. * Support UploadImpl for controller rest services. * SERVICE_AS_CONTROLLER is turned on by default now. * SERVICE_AS_CONTROLLER is now enabled by default. Tests relying on the previous default behavior must explicitly set it to off. * HttpContext was null at rest services. * Map NullReference Exception as BadRequest Error. P.e when executing Issue89123_API/Check with an invalid json body { "ClientId": 1, "ClientName": "CName", "ClientActive": false } the issue89123_api_check_RequestData entity is created but entity.Issue89123Client is null. * Try to fix failing test for DotNetCore. Failed UnitTesting.FileIOTests.GXDBFilePathTest System.UriFormatException : Invalid URI: The format of the URI could not be determined. * Generate rest controllers as HTTP Azure functions at deploy time for Azure Functions deployment * Add BoolStringJsonConverter for Boolean properties in Stds. * Update ErrorCheck to return the JSON response object. * Try to fix merge error. * Remove unnecessary SetError method and replace it with HandleError * Create a GetErrorResponse for HandleError and for ErrorCheck * Add tracing to help identify the root cause of the failing test * Temporarily disable the test on Linux while investigating the issue * Replace HttpHelper.SetError with HandleError to assign _errorDetail (returned by Unauthenticated) * Add missing ApiIntegratedSecurityLevel method for .NETFramework. --------- Co-authored-by: Claudia Beatriz Murialdo Garrone <c.murialdo@globant.com> Co-authored-by: sjuarez <sabrina.juarez@globant.com>
1 parent e865d56 commit 04be3b6

File tree

22 files changed

+1125
-681
lines changed

22 files changed

+1125
-681
lines changed

dotnet/src/dotnetcore/GxClasses.Web/Middleware/GXRestServices.cs

Lines changed: 484 additions & 0 deletions
Large diffs are not rendered by default.

dotnet/src/dotnetcore/GxClasses.Web/Middleware/GXRouting.cs

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ internal class GXRouting : IGXRouting
3838
public static string UrlTemplateControllerWithParms;
3939

4040
//Azure Functions
41-
public bool AzureRuntime;
41+
public static bool AzureRuntime;
4242
public AzureDeployFeature AzureDeploy = new AzureDeployFeature();
4343
public static string AzureFunctionName;
4444

@@ -382,7 +382,7 @@ public bool ServiceInPath(String path, out String actualPath)
382382
private String FindPath(string innerPath, Dictionary<string,string > servicesPathUrl, bool startTxt)
383383
{
384384
string actualPath = String.Empty;
385-
foreach (var subPath in from String subPath in servicesPathUrl.Keys
385+
foreach (string subPath in from String subPath in servicesPathUrl.Keys
386386
select subPath)
387387
{
388388
bool match = false;
@@ -525,48 +525,51 @@ public void ServicesGroupSetting()
525525
string mapPathLower = mapPath.ToLower();
526526
string mNameLower = m.Name.ToLower();
527527
servicesPathUrl[mapPathLower]= mNameLower;
528-
GXLogging.Debug(log, $"addServicesPathUrl key:{mapPathLower} value:{mNameLower}");
529-
foreach (SingleMap sm in m.Mappings)
528+
if (!RestAPIHelpers.ServiceAsController())
530529
{
531-
if (sm.Verb == null)
532-
sm.Verb = "GET";
533-
if (String.IsNullOrEmpty(sm.Path))
534-
sm.Path = sm.Name;
535-
else
530+
GXLogging.Debug(log, $"addServicesPathUrl key:{mapPathLower} value:{mNameLower}");
531+
foreach (SingleMap sm in m.Mappings)
536532
{
537-
sm.Path = Regex.Replace(sm.Path, "^/|/$", "");
538-
}
539-
if (sm.VariableAlias == null)
540-
sm.VariableAlias = new Dictionary<string, string>();
541-
else
542-
{
543-
Dictionary<string, string> vMap = new Dictionary<string, string>();
544-
foreach (KeyValuePair<string, string> v in sm.VariableAlias)
533+
if (sm.Verb == null)
534+
sm.Verb = "GET";
535+
if (String.IsNullOrEmpty(sm.Path))
536+
sm.Path = sm.Name;
537+
else
545538
{
546-
vMap.Add(v.Key.ToLower(), v.Value.ToLower());
539+
sm.Path = Regex.Replace(sm.Path, "^/|/$", "");
547540
}
548-
sm.VariableAlias = vMap;
549-
}
550-
if (servicesMap.ContainsKey(mapPathLower))
551-
{
552-
if (!servicesMap[mapPathLower].ContainsKey(sm.Name.ToLower()))
541+
if (sm.VariableAlias == null)
542+
sm.VariableAlias = new Dictionary<string, string>();
543+
else
544+
{
545+
Dictionary<string, string> vMap = new Dictionary<string, string>();
546+
foreach (KeyValuePair<string, string> v in sm.VariableAlias)
547+
{
548+
vMap.Add(v.Key.ToLower(), v.Value.ToLower());
549+
}
550+
sm.VariableAlias = vMap;
551+
}
552+
if (servicesMap.ContainsKey(mapPathLower))
553553
{
554+
if (!servicesMap[mapPathLower].ContainsKey(sm.Name.ToLower()))
555+
{
556+
servicesValidPath[mapPathLower].Add(sm.Path.ToLower());
557+
558+
servicesMapData[mapPathLower].Add(Tuple.Create(sm.Path.ToLower(), sm.Verb.ToUpper()), sm.Name.ToLower());
559+
servicesMap[mapPathLower].Add(sm.Name.ToLower(), sm);
560+
}
561+
}
562+
else
563+
{
564+
servicesValidPath.Add(mapPathLower, new List<string>());
554565
servicesValidPath[mapPathLower].Add(sm.Path.ToLower());
555566

567+
servicesMapData.Add(mapPathLower, new Dictionary<Tuple<string, string>, string>());
556568
servicesMapData[mapPathLower].Add(Tuple.Create(sm.Path.ToLower(), sm.Verb.ToUpper()), sm.Name.ToLower());
569+
servicesMap.Add(mapPathLower, new Dictionary<string, SingleMap>());
557570
servicesMap[mapPathLower].Add(sm.Name.ToLower(), sm);
558571
}
559572
}
560-
else
561-
{
562-
servicesValidPath.Add(mapPathLower, new List<string>());
563-
servicesValidPath[mapPathLower].Add(sm.Path.ToLower());
564-
565-
servicesMapData.Add(mapPathLower, new Dictionary<Tuple<string, string>, string>());
566-
servicesMapData[mapPathLower].Add(Tuple.Create(sm.Path.ToLower(), sm.Verb.ToUpper()), sm.Name.ToLower());
567-
servicesMap.Add(mapPathLower, new Dictionary<string, SingleMap>());
568-
servicesMap[mapPathLower].Add(sm.Name.ToLower(), sm);
569-
}
570573
}
571574
}
572575
}

dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs

Lines changed: 168 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.Linq;
45
using System.Net;
56
using System.Reflection;
67
using System.Threading.Tasks;
@@ -19,8 +20,9 @@
1920
using Microsoft.AspNetCore.Hosting;
2021
using Microsoft.AspNetCore.Http;
2122
using Microsoft.AspNetCore.Http.Features;
22-
using Microsoft.AspNetCore.HttpOverrides;
2323
using Microsoft.AspNetCore.Mvc;
24+
using Microsoft.AspNetCore.Mvc.ApplicationModels;
25+
using Microsoft.AspNetCore.Mvc.Routing;
2426
using Microsoft.AspNetCore.Rewrite;
2527
using Microsoft.AspNetCore.Routing;
2628
using Microsoft.AspNetCore.Server.Kestrel.Core;
@@ -120,7 +122,15 @@ public static IApplicationBuilder MapWebSocketManager(this IApplicationBuilder a
120122
.Map($"{basePath}/gxwebsocket.svc", (_app) => _app.UseMiddleware<Notifications.WebSocket.WebSocketManagerMiddleware>()); //Compatibility reasons. Remove in the future.
121123
}
122124
}
123-
125+
public class CustomBadRequestObjectResult : ObjectResult
126+
{
127+
public CustomBadRequestObjectResult(ActionContext context)
128+
: base(HttpHelper.GetJsonError(StatusCodes.Status400BadRequest.ToString(), HttpHelper.StatusCodeToTitle(HttpStatusCode.BadRequest)))
129+
{
130+
StatusCode = StatusCodes.Status400BadRequest;
131+
}
132+
}
133+
124134
public class Startup
125135
{
126136
static IGXLogger log;
@@ -163,24 +173,10 @@ public void ConfigureServices(IServiceCollection services)
163173
{
164174
OpenTelemetryService.Setup(services);
165175

166-
services.AddControllers();
167-
string controllers = Path.Combine(Startup.LocalPath, "bin", GX_CONTROLLERS);
168-
IMvcBuilder mvcBuilder = services.AddMvc(option => option.EnableEndpointRouting = false);
169-
try
170-
{
171-
if (Directory.Exists(controllers))
172-
{
173-
foreach (string controller in Directory.GetFiles(controllers))
174-
{
175-
Console.WriteLine($"Loading controller {controller}");
176-
mvcBuilder.AddApplicationPart(Assembly.LoadFrom(controller)).AddControllersAsServices();
177-
}
178-
}
179-
}
180-
catch (Exception ex)
181-
{
182-
Console.Error.WriteLine("Error loading gxcontrollers " + ex.Message);
183-
}
176+
IMvcBuilder builder = services.AddMvc(option => option.EnableEndpointRouting = false);
177+
178+
RegisterControllerAssemblies(builder);
179+
184180
services.Configure<KestrelServerOptions>(options =>
185181
{
186182
options.AllowSynchronousIO = true;
@@ -269,6 +265,113 @@ public void ConfigureServices(IServiceCollection services)
269265
DefineCorsPolicy(services);
270266
}
271267

268+
private void RegisterControllerAssemblies(IMvcBuilder mvcBuilder)
269+
{
270+
271+
if (RestAPIHelpers.ServiceAsController() && !string.IsNullOrEmpty(VirtualPath))
272+
{
273+
mvcBuilder.AddMvcOptions(options => options.Conventions.Add(new SetRoutePrefix(new RouteAttribute(VirtualPath))));
274+
}
275+
276+
if (RestAPIHelpers.JsonSerializerCaseSensitive())
277+
{
278+
mvcBuilder.AddJsonOptions(options => options.JsonSerializerOptions.PropertyNameCaseInsensitive = false);
279+
}
280+
mvcBuilder.ConfigureApiBehaviorOptions(options =>
281+
{
282+
options.InvalidModelStateResponseFactory = context =>
283+
{
284+
return new CustomBadRequestObjectResult(context);
285+
};
286+
});
287+
288+
if (RestAPIHelpers.ServiceAsController())
289+
{
290+
RegisterRestServices(mvcBuilder);
291+
RegisterApiServices(mvcBuilder, gxRouting);
292+
}
293+
RegisterNativeServices(mvcBuilder);
294+
295+
}
296+
297+
private void RegisterNativeServices(IMvcBuilder mvcBuilder)
298+
{
299+
try
300+
{
301+
string controllers = Path.Combine(Startup.LocalPath, "bin", GX_CONTROLLERS);
302+
303+
if (Directory.Exists(controllers))
304+
{
305+
foreach (string controller in Directory.GetFiles(controllers))
306+
{
307+
Console.WriteLine($"Loading controller {controller}");
308+
mvcBuilder.AddApplicationPart(Assembly.LoadFrom(controller)).AddControllersAsServices();
309+
}
310+
}
311+
}
312+
catch (Exception ex)
313+
{
314+
Console.Error.WriteLine("Error loading gxcontrollers " + ex.Message);
315+
}
316+
317+
}
318+
319+
private void RegisterRestServices(IMvcBuilder mvcBuilder)
320+
{
321+
HashSet<string> serviceAssemblies = new HashSet<string>();
322+
foreach (string svcFile in gxRouting.svcFiles)
323+
{
324+
try
325+
{
326+
string[] controllerAssemblyQualifiedName = new string(File.ReadLines(svcFile).First().SkipWhile(c => c != '"')
327+
.Skip(1)
328+
.TakeWhile(c => c != '"')
329+
.ToArray()).Trim().Split(',');
330+
string controllerAssemblyName = controllerAssemblyQualifiedName.Last();
331+
if (!serviceAssemblies.Contains(controllerAssemblyName))
332+
{
333+
serviceAssemblies.Add(controllerAssemblyName);
334+
string controllerAssemblyFile = Path.Combine(Startup.LocalPath, "bin", $"{controllerAssemblyName}.dll");
335+
336+
if (File.Exists(controllerAssemblyFile))
337+
{
338+
GXLogging.Info(log, "Registering rest: " + controllerAssemblyName);
339+
mvcBuilder.AddApplicationPart(Assembly.LoadFrom(controllerAssemblyFile)).AddControllersAsServices();
340+
}
341+
}
342+
}
343+
catch (Exception ex)
344+
{
345+
GXLogging.Error(log, "Error registering rest service", ex);
346+
}
347+
}
348+
}
349+
private void RegisterApiServices(IMvcBuilder mvcBuilder, GXRouting gxRouting)
350+
{
351+
HashSet<string> serviceAssemblies = new HashSet<string>();
352+
foreach (string grp in gxRouting.servicesPathUrl.Values)
353+
{
354+
try
355+
{
356+
string assemblyName = grp.Replace('\\', '.');
357+
if (!serviceAssemblies.Contains(assemblyName))
358+
{
359+
serviceAssemblies.Add(assemblyName);
360+
string controllerAssemblyFile = Path.Combine(Startup.LocalPath, "bin", $"{assemblyName}.dll");
361+
if (File.Exists(controllerAssemblyFile))
362+
{
363+
GXLogging.Info(log, "Registering api: " + grp);
364+
mvcBuilder.AddApplicationPart(Assembly.LoadFrom(controllerAssemblyFile)).AddControllersAsServices();
365+
}
366+
}
367+
}
368+
catch (Exception ex)
369+
{
370+
GXLogging.Error(log, "Error registering api", ex);
371+
}
372+
}
373+
}
374+
272375
private void DefineCorsPolicy(IServiceCollection services)
273376
{
274377
if (Preferences.CorsEnabled)
@@ -431,10 +534,6 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
431534
ContentTypeProvider = provider
432535
});
433536

434-
foreach( string p in gxRouting.servicesPathUrl.Keys)
435-
{
436-
servicesBase.Add( string.IsNullOrEmpty(VirtualPath) ? p : $"{VirtualPath}/{p}");
437-
}
438537
app.UseExceptionHandler(new ExceptionHandlerOptions
439538
{
440539
ExceptionHandler = new CustomExceptionHandlerMiddleware().Invoke,
@@ -449,21 +548,31 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
449548
antiforgery = app.ApplicationServices.GetRequiredService<IAntiforgery>();
450549
app.UseAntiforgeryTokens(apiBasePath);
451550
}
452-
app.UseMvc(routes =>
551+
if (!RestAPIHelpers.ServiceAsController())
453552
{
454-
foreach (string serviceBasePath in servicesBase)
455-
{
456-
string tmpPath = string.IsNullOrEmpty(apiBasePath) ? serviceBasePath : serviceBasePath.Replace(apiBasePath, string.Empty);
457-
foreach (string sPath in gxRouting.servicesValidPath[tmpPath])
553+
foreach (string p in gxRouting.servicesPathUrl.Keys)
554+
{
555+
servicesBase.Add(string.IsNullOrEmpty(VirtualPath) ? p : $"{VirtualPath}/{p}");
556+
}
557+
app.UseMvc(routes =>
558+
{
559+
foreach (string serviceBasePath in servicesBase)
458560
{
459-
string s = serviceBasePath + sPath;
460-
routes.MapRoute($"{s}", new RequestDelegate(gxRouting.ProcessRestRequest));
561+
string tmpPath = string.IsNullOrEmpty(apiBasePath) ? serviceBasePath : serviceBasePath.Replace(apiBasePath, string.Empty);
562+
foreach (string sPath in gxRouting.servicesValidPath[tmpPath])
563+
{
564+
string s = serviceBasePath + sPath;
565+
routes.MapRoute($"{s}", new RequestDelegate(gxRouting.ProcessRestRequest));
566+
}
461567
}
462-
}
463-
routes.MapRoute($"{restBasePath}{{*{UrlTemplateControllerWithParms}}}", new RequestDelegate(gxRouting.ProcessRestRequest));
568+
routes.MapRoute($"{restBasePath}{{*{UrlTemplateControllerWithParms}}}", new RequestDelegate(gxRouting.ProcessRestRequest));
569+
});
570+
}
571+
app.UseMvc(routes =>
572+
{
464573
routes.MapRoute("Default", VirtualPath, new { controller = "Home", action = "Index" });
465574
});
466-
575+
467576
app.UseWebSockets();
468577
string basePath = string.IsNullOrEmpty(VirtualPath) ? string.Empty : $"/{VirtualPath}";
469578
Config.ScriptPath = string.IsNullOrEmpty(basePath) ? "/" : basePath;
@@ -618,4 +727,29 @@ public IActionResult Index()
618727
return Redirect(defaultFiles[0]);
619728
}
620729
}
730+
internal class SetRoutePrefix : IApplicationModelConvention
731+
{
732+
private readonly AttributeRouteModel _routePrefix ;
733+
public SetRoutePrefix(IRouteTemplateProvider route)
734+
{
735+
_routePrefix = new AttributeRouteModel(route);
736+
}
737+
public void Apply(ApplicationModel application)
738+
{
739+
foreach (var controller in application.Controllers)
740+
{
741+
foreach (var selector in controller.Selectors)
742+
{
743+
if (selector.AttributeRouteModel != null)
744+
{
745+
selector.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_routePrefix, selector.AttributeRouteModel);
746+
}
747+
else
748+
{
749+
selector.AttributeRouteModel = _routePrefix;
750+
}
751+
}
752+
}
753+
}
754+
}
621755
}

0 commit comments

Comments
 (0)