Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Base controller class for Rest services. #992

Merged
merged 54 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
d5ef884
Add Base controller class for Rest services.
claudiamurialdo Apr 11, 2024
9d785a8
Temporary disable sending server name.CodeQL cs/user-controlled-bypass
claudiamurialdo Apr 11, 2024
9583d80
Move GxRestService from GxClasses to GxClasses.Web
claudiamurialdo Apr 15, 2024
0f43431
Return a StatusCode for WebException
claudiamurialdo Apr 23, 2024
493b619
Add method SendNotModified
claudiamurialdo Apr 23, 2024
b6de762
Register native services at startup when SERVICE_AS_CONTROLLER is ena…
claudiamurialdo May 23, 2024
ceb226e
Avoid setting headers when request has started.
claudiamurialdo Jun 6, 2024
5da2dc9
Fix build error.
claudiamurialdo Jun 6, 2024
5813b3c
Add Json annotations for Message SDT
claudiamurialdo Jun 21, 2024
dd46894
Fix registration of API services in modules.
claudiamurialdo Jun 21, 2024
1944954
Temporary cleanup of duplicated REST services: remove REST controller…
claudiamurialdo Jun 21, 2024
1e317ad
Set BasePath for rest services
claudiamurialdo Jun 28, 2024
5c28337
Add ApiException method to handle unexpected exceptions properly in d…
claudiamurialdo Jul 22, 2024
e412eac
Handle status code set by user code.
claudiamurialdo Jul 22, 2024
0573996
Handle GAMErrors property in declared rest services.
claudiamurialdo Jul 22, 2024
8f457c1
Add method ApiIntegratedSecurityLevel to base Rest class.
claudiamurialdo Jul 23, 2024
55f8e08
Improve error handling in rest controllers.
claudiamurialdo Jul 23, 2024
8710c67
Do not set statuscode if it is 0 (non initialized)
claudiamurialdo Jul 23, 2024
61541b5
Remove CustomControllerFeatureProvider as it is no longer needed.
claudiamurialdo Jul 25, 2024
47daaa6
Avoid processing .grp.json when services as controllers are enabled.
claudiamurialdo Jul 25, 2024
c423932
Handle BadRequest when json input is invalid.
claudiamurialdo Aug 8, 2024
6d4adf8
Add IsAnyDirty for SDTs
claudiamurialdo Aug 8, 2024
df30f81
Define IsNull method for rest interfaces
claudiamurialdo Aug 12, 2024
8c431ac
Add missing JsonIgnore flags.
claudiamurialdo Aug 12, 2024
84db7ef
Add method EmptyResponse to return a valid empty json instead of empt…
claudiamurialdo Aug 12, 2024
95ff891
Add AnyDirtySdt method.
Aug 12, 2024
00126b1
Add NullResult function.
Aug 13, 2024
826cfe8
Remove unneeded AnyDirtySdt property.
Aug 13, 2024
a0c45fc
Add EmptyObjectResult
Aug 13, 2024
32f1204
Property's name must use a case-insensitive comparison during json de…
Aug 26, 2024
495aa82
Grouped several lines of code into a new function, RegisterController…
Aug 27, 2024
7af05f6
Updated JsonSerializerOptions.PropertyNameCaseInsensitive to be contr…
Aug 27, 2024
0d9e026
Change key ServiceJsonSerializerCaseSensitive for a shorter one: Rest…
Aug 27, 2024
d2ee5a6
Remove unnecessary AddControllers() call as the current assembly has …
Sep 10, 2024
fced6c2
Ensure that controllers from the specified assembly are registered as…
Sep 10, 2024
6ee81e9
Support UploadImpl for controller rest services.
claudiamurialdo Dec 7, 2024
38d4bfa
SERVICE_AS_CONTROLLER is turned on by default now.
claudiamurialdo Dec 11, 2024
e982551
SERVICE_AS_CONTROLLER is now enabled by default. Tests relying on the…
claudiamurialdo Dec 11, 2024
aa04d8c
Merge branch 'master' into rest-service-as-controller
claudiamurialdo Dec 11, 2024
778dfed
HttpContext was null at rest services.
claudiamurialdo Dec 17, 2024
79b7e6f
Map NullReference Exception as BadRequest Error.
claudiamurialdo Dec 23, 2024
1f2c629
Try to fix failing test for DotNetCore.
claudiamurialdo Dec 23, 2024
51e987f
Generate rest controllers as HTTP Azure functions at deploy time for …
Dec 31, 2024
8604c32
Add BoolStringJsonConverter for Boolean properties in Stds.
claudiamurialdo Jan 7, 2025
5610d95
Update ErrorCheck to return the JSON response object.
claudiamurialdo Jan 7, 2025
7679bd2
Try to fix merge error.
claudiamurialdo Jan 7, 2025
171092f
Remove unnecessary SetError method and replace it with HandleError
claudiamurialdo Jan 8, 2025
08ab5ff
Create a GetErrorResponse for HandleError and for ErrorCheck
claudiamurialdo Jan 8, 2025
9ecd7c0
Add tracing to help identify the root cause of the failing test
claudiamurialdo Jan 8, 2025
9e041d3
Temporarily disable the test on Linux while investigating the issue
claudiamurialdo Jan 8, 2025
3d03c5c
Replace HttpHelper.SetError with HandleError to assign _errorDetail (…
claudiamurialdo Jan 8, 2025
3bdef87
Merge branch 'master' into rest-service-as-controller
claudiamurialdo Jan 9, 2025
36d2363
Add missing ApiIntegratedSecurityLevel method for .NETFramework.
claudiamurialdo Jan 9, 2025
c0d833f
Merge branch 'rest-service-as-controller' of https://github.com/genex…
claudiamurialdo Jan 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
484 changes: 484 additions & 0 deletions dotnet/src/dotnetcore/GxClasses.Web/Middleware/GXRestServices.cs

Large diffs are not rendered by default.

69 changes: 36 additions & 33 deletions dotnet/src/dotnetcore/GxClasses.Web/Middleware/GXRouting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ internal class GXRouting : IGXRouting
public static string UrlTemplateControllerWithParms;

//Azure Functions
public bool AzureRuntime;
public static bool AzureRuntime;
public AzureDeployFeature AzureDeploy = new AzureDeployFeature();
public static string AzureFunctionName;

Expand Down Expand Up @@ -382,7 +382,7 @@ public bool ServiceInPath(String path, out String actualPath)
private String FindPath(string innerPath, Dictionary<string,string > servicesPathUrl, bool startTxt)
{
string actualPath = String.Empty;
foreach (var subPath in from String subPath in servicesPathUrl.Keys
foreach (string subPath in from String subPath in servicesPathUrl.Keys
select subPath)
{
bool match = false;
Expand Down Expand Up @@ -525,48 +525,51 @@ public void ServicesGroupSetting()
string mapPathLower = mapPath.ToLower();
string mNameLower = m.Name.ToLower();
servicesPathUrl[mapPathLower]= mNameLower;
GXLogging.Debug(log, $"addServicesPathUrl key:{mapPathLower} value:{mNameLower}");
foreach (SingleMap sm in m.Mappings)
if (!RestAPIHelpers.ServiceAsController())
{
if (sm.Verb == null)
sm.Verb = "GET";
if (String.IsNullOrEmpty(sm.Path))
sm.Path = sm.Name;
else
GXLogging.Debug(log, $"addServicesPathUrl key:{mapPathLower} value:{mNameLower}");
foreach (SingleMap sm in m.Mappings)
{
sm.Path = Regex.Replace(sm.Path, "^/|/$", "");
}
if (sm.VariableAlias == null)
sm.VariableAlias = new Dictionary<string, string>();
else
{
Dictionary<string, string> vMap = new Dictionary<string, string>();
foreach (KeyValuePair<string, string> v in sm.VariableAlias)
if (sm.Verb == null)
sm.Verb = "GET";
if (String.IsNullOrEmpty(sm.Path))
sm.Path = sm.Name;
else
{
vMap.Add(v.Key.ToLower(), v.Value.ToLower());
sm.Path = Regex.Replace(sm.Path, "^/|/$", "");
}
sm.VariableAlias = vMap;
}
if (servicesMap.ContainsKey(mapPathLower))
{
if (!servicesMap[mapPathLower].ContainsKey(sm.Name.ToLower()))
if (sm.VariableAlias == null)
sm.VariableAlias = new Dictionary<string, string>();
else
{
Dictionary<string, string> vMap = new Dictionary<string, string>();
foreach (KeyValuePair<string, string> v in sm.VariableAlias)
{
vMap.Add(v.Key.ToLower(), v.Value.ToLower());
}
sm.VariableAlias = vMap;
}
if (servicesMap.ContainsKey(mapPathLower))
{
if (!servicesMap[mapPathLower].ContainsKey(sm.Name.ToLower()))
{
servicesValidPath[mapPathLower].Add(sm.Path.ToLower());

servicesMapData[mapPathLower].Add(Tuple.Create(sm.Path.ToLower(), sm.Verb.ToUpper()), sm.Name.ToLower());
servicesMap[mapPathLower].Add(sm.Name.ToLower(), sm);
}
}
else
{
servicesValidPath.Add(mapPathLower, new List<string>());
servicesValidPath[mapPathLower].Add(sm.Path.ToLower());

servicesMapData.Add(mapPathLower, new Dictionary<Tuple<string, string>, string>());
servicesMapData[mapPathLower].Add(Tuple.Create(sm.Path.ToLower(), sm.Verb.ToUpper()), sm.Name.ToLower());
servicesMap.Add(mapPathLower, new Dictionary<string, SingleMap>());
servicesMap[mapPathLower].Add(sm.Name.ToLower(), sm);
}
}
else
{
servicesValidPath.Add(mapPathLower, new List<string>());
servicesValidPath[mapPathLower].Add(sm.Path.ToLower());

servicesMapData.Add(mapPathLower, new Dictionary<Tuple<string, string>, string>());
servicesMapData[mapPathLower].Add(Tuple.Create(sm.Path.ToLower(), sm.Verb.ToUpper()), sm.Name.ToLower());
servicesMap.Add(mapPathLower, new Dictionary<string, SingleMap>());
servicesMap[mapPathLower].Add(sm.Name.ToLower(), sm);
}
}
}
}
Expand Down
202 changes: 168 additions & 34 deletions dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading.Tasks;
Expand All @@ -19,8 +20,9 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Server.Kestrel.Core;
Expand Down Expand Up @@ -120,7 +122,15 @@ public static IApplicationBuilder MapWebSocketManager(this IApplicationBuilder a
.Map($"{basePath}/gxwebsocket.svc", (_app) => _app.UseMiddleware<Notifications.WebSocket.WebSocketManagerMiddleware>()); //Compatibility reasons. Remove in the future.
}
}

public class CustomBadRequestObjectResult : ObjectResult
{
public CustomBadRequestObjectResult(ActionContext context)
: base(HttpHelper.GetJsonError(StatusCodes.Status400BadRequest.ToString(), HttpHelper.StatusCodeToTitle(HttpStatusCode.BadRequest)))
{
StatusCode = StatusCodes.Status400BadRequest;
}
}

public class Startup
{
static IGXLogger log;
Expand Down Expand Up @@ -163,24 +173,10 @@ public void ConfigureServices(IServiceCollection services)
{
OpenTelemetryService.Setup(services);

services.AddControllers();
string controllers = Path.Combine(Startup.LocalPath, "bin", GX_CONTROLLERS);
IMvcBuilder mvcBuilder = services.AddMvc(option => option.EnableEndpointRouting = false);
try
{
if (Directory.Exists(controllers))
{
foreach (string controller in Directory.GetFiles(controllers))
{
Console.WriteLine($"Loading controller {controller}");
mvcBuilder.AddApplicationPart(Assembly.LoadFrom(controller)).AddControllersAsServices();
}
}
}
catch (Exception ex)
{
Console.Error.WriteLine("Error loading gxcontrollers " + ex.Message);
}
IMvcBuilder builder = services.AddMvc(option => option.EnableEndpointRouting = false);

RegisterControllerAssemblies(builder);

services.Configure<KestrelServerOptions>(options =>
{
options.AllowSynchronousIO = true;
Expand Down Expand Up @@ -269,6 +265,113 @@ public void ConfigureServices(IServiceCollection services)
DefineCorsPolicy(services);
}

private void RegisterControllerAssemblies(IMvcBuilder mvcBuilder)
{

if (RestAPIHelpers.ServiceAsController() && !string.IsNullOrEmpty(VirtualPath))
{
mvcBuilder.AddMvcOptions(options => options.Conventions.Add(new SetRoutePrefix(new RouteAttribute(VirtualPath))));
}

if (RestAPIHelpers.JsonSerializerCaseSensitive())
{
mvcBuilder.AddJsonOptions(options => options.JsonSerializerOptions.PropertyNameCaseInsensitive = false);
}
mvcBuilder.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
return new CustomBadRequestObjectResult(context);
};
});

if (RestAPIHelpers.ServiceAsController())
{
RegisterRestServices(mvcBuilder);
RegisterApiServices(mvcBuilder, gxRouting);
}
RegisterNativeServices(mvcBuilder);

}

private void RegisterNativeServices(IMvcBuilder mvcBuilder)
{
try
{
string controllers = Path.Combine(Startup.LocalPath, "bin", GX_CONTROLLERS);

if (Directory.Exists(controllers))
{
foreach (string controller in Directory.GetFiles(controllers))
{
Console.WriteLine($"Loading controller {controller}");
mvcBuilder.AddApplicationPart(Assembly.LoadFrom(controller)).AddControllersAsServices();
}
}
}
catch (Exception ex)
{
Console.Error.WriteLine("Error loading gxcontrollers " + ex.Message);
}

}

private void RegisterRestServices(IMvcBuilder mvcBuilder)
{
HashSet<string> serviceAssemblies = new HashSet<string>();
foreach (string svcFile in gxRouting.svcFiles)
{
try
{
string[] controllerAssemblyQualifiedName = new string(File.ReadLines(svcFile).First().SkipWhile(c => c != '"')
.Skip(1)
.TakeWhile(c => c != '"')
.ToArray()).Trim().Split(',');
string controllerAssemblyName = controllerAssemblyQualifiedName.Last();
if (!serviceAssemblies.Contains(controllerAssemblyName))
{
serviceAssemblies.Add(controllerAssemblyName);
string controllerAssemblyFile = Path.Combine(Startup.LocalPath, "bin", $"{controllerAssemblyName}.dll");

if (File.Exists(controllerAssemblyFile))
{
GXLogging.Info(log, "Registering rest: " + controllerAssemblyName);
mvcBuilder.AddApplicationPart(Assembly.LoadFrom(controllerAssemblyFile)).AddControllersAsServices();
}
}
}
catch (Exception ex)
{
GXLogging.Error(log, "Error registering rest service", ex);
}
}
}
private void RegisterApiServices(IMvcBuilder mvcBuilder, GXRouting gxRouting)
{
HashSet<string> serviceAssemblies = new HashSet<string>();
foreach (string grp in gxRouting.servicesPathUrl.Values)
{
try
{
string assemblyName = grp.Replace('\\', '.');
if (!serviceAssemblies.Contains(assemblyName))
{
serviceAssemblies.Add(assemblyName);
string controllerAssemblyFile = Path.Combine(Startup.LocalPath, "bin", $"{assemblyName}.dll");
if (File.Exists(controllerAssemblyFile))
{
GXLogging.Info(log, "Registering api: " + grp);
mvcBuilder.AddApplicationPart(Assembly.LoadFrom(controllerAssemblyFile)).AddControllersAsServices();
}
}
}
catch (Exception ex)
{
GXLogging.Error(log, "Error registering api", ex);
}
}
}

private void DefineCorsPolicy(IServiceCollection services)
{
if (Preferences.CorsEnabled)
Expand Down Expand Up @@ -431,10 +534,6 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
ContentTypeProvider = provider
});

foreach( string p in gxRouting.servicesPathUrl.Keys)
{
servicesBase.Add( string.IsNullOrEmpty(VirtualPath) ? p : $"{VirtualPath}/{p}");
}
app.UseExceptionHandler(new ExceptionHandlerOptions
{
ExceptionHandler = new CustomExceptionHandlerMiddleware().Invoke,
Expand All @@ -449,21 +548,31 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
antiforgery = app.ApplicationServices.GetRequiredService<IAntiforgery>();
app.UseAntiforgeryTokens(apiBasePath);
}
app.UseMvc(routes =>
if (!RestAPIHelpers.ServiceAsController())
{
foreach (string serviceBasePath in servicesBase)
{
string tmpPath = string.IsNullOrEmpty(apiBasePath) ? serviceBasePath : serviceBasePath.Replace(apiBasePath, string.Empty);
foreach (string sPath in gxRouting.servicesValidPath[tmpPath])
foreach (string p in gxRouting.servicesPathUrl.Keys)
{
servicesBase.Add(string.IsNullOrEmpty(VirtualPath) ? p : $"{VirtualPath}/{p}");
}
app.UseMvc(routes =>
{
foreach (string serviceBasePath in servicesBase)
{
string s = serviceBasePath + sPath;
routes.MapRoute($"{s}", new RequestDelegate(gxRouting.ProcessRestRequest));
string tmpPath = string.IsNullOrEmpty(apiBasePath) ? serviceBasePath : serviceBasePath.Replace(apiBasePath, string.Empty);
foreach (string sPath in gxRouting.servicesValidPath[tmpPath])
{
string s = serviceBasePath + sPath;
routes.MapRoute($"{s}", new RequestDelegate(gxRouting.ProcessRestRequest));
}
}
}
routes.MapRoute($"{restBasePath}{{*{UrlTemplateControllerWithParms}}}", new RequestDelegate(gxRouting.ProcessRestRequest));
routes.MapRoute($"{restBasePath}{{*{UrlTemplateControllerWithParms}}}", new RequestDelegate(gxRouting.ProcessRestRequest));
});
}
app.UseMvc(routes =>
{
routes.MapRoute("Default", VirtualPath, new { controller = "Home", action = "Index" });
});

app.UseWebSockets();
string basePath = string.IsNullOrEmpty(VirtualPath) ? string.Empty : $"/{VirtualPath}";
Config.ScriptPath = string.IsNullOrEmpty(basePath) ? "/" : basePath;
Expand Down Expand Up @@ -618,4 +727,29 @@ public IActionResult Index()
return Redirect(defaultFiles[0]);
}
}
internal class SetRoutePrefix : IApplicationModelConvention
{
private readonly AttributeRouteModel _routePrefix ;
public SetRoutePrefix(IRouteTemplateProvider route)
{
_routePrefix = new AttributeRouteModel(route);
}
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
foreach (var selector in controller.Selectors)
{
if (selector.AttributeRouteModel != null)
{
selector.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_routePrefix, selector.AttributeRouteModel);
}
else
{
selector.AttributeRouteModel = _routePrefix;
}
}
}
}
}
}
Loading
Loading