From 22577fe3fbe96ede5589cc7f49e7501146178ced Mon Sep 17 00:00:00 2001 From: shaamilahmed Date: Tue, 5 May 2026 18:47:11 +0500 Subject: [PATCH] Removed Immediate.Apis, Added AOT Testing Scripts. --- .gitignore | 9 +- Htmx.ApiDemo/AppJsonSerializerContext.cs | 8 -- Htmx.ApiDemo/Data/AuthService.cs | 2 +- Htmx.ApiDemo/Helpers/HtmxExtensions.cs | 23 ++++ Htmx.ApiDemo/Helpers/RouteMap.cs | 24 ++++ Htmx.ApiDemo/Htmx.ApiDemo.csproj | 15 +-- Htmx.ApiDemo/Htmx.ApiDemo.http | 11 -- Htmx.ApiDemo/Htmx.ApiDemo.sln | 24 ++++ Htmx.ApiDemo/HtmxPageExtensions.cs | 40 ------ Htmx.ApiDemo/HtmxResult.cs | 31 ----- Htmx.ApiDemo/Program.cs | 72 +---------- Htmx.ApiDemo/Templates/Greeting.htmx.cs | 28 ---- .../Templates/Greeting.htmx.routing.cs | 23 ++++ Htmx.ApiDemo/Templates/Login.htmx.cs | 59 +-------- Htmx.ApiDemo/Templates/Login.htmx.routing.cs | 65 ++++++++++ Htmx.ApiDemo/Templates/Logout.cs | 27 ---- Htmx.ApiDemo/Templates/MainLayout.htmx.cs | 26 +--- Htmx.ApiDemo/Templates/Register.htmx.cs | 69 +--------- .../Templates/Register.htmx.routing.cs | 72 +++++++++++ Htmx.ApiDemo/Templates/UiDemo.htmx.cs | 24 +--- Htmx.ApiDemo/Templates/UiDemo.htmx.routing.cs | 29 +++++ Htmx.ApiDemo/appsettings.Development.json | 6 +- Htmx.ApiDemo/rd.xml | 17 --- Htmx.SourceGenerator/HtmxGenerator.cs | 8 +- Testing/AOT/README.md | 97 ++++++++++++++ Testing/AOT/build-aot.ps1 | 122 ++++++++++++++++++ Testing/AOT/build-aot.sh | 114 ++++++++++++++++ 27 files changed, 623 insertions(+), 422 deletions(-) create mode 100644 Htmx.ApiDemo/Helpers/HtmxExtensions.cs create mode 100644 Htmx.ApiDemo/Helpers/RouteMap.cs delete mode 100644 Htmx.ApiDemo/Htmx.ApiDemo.http create mode 100644 Htmx.ApiDemo/Htmx.ApiDemo.sln delete mode 100644 Htmx.ApiDemo/HtmxPageExtensions.cs delete mode 100644 Htmx.ApiDemo/HtmxResult.cs create mode 100644 Htmx.ApiDemo/Templates/Greeting.htmx.routing.cs create mode 100644 Htmx.ApiDemo/Templates/Login.htmx.routing.cs delete mode 100644 Htmx.ApiDemo/Templates/Logout.cs create mode 100644 Htmx.ApiDemo/Templates/Register.htmx.routing.cs create mode 100644 Htmx.ApiDemo/Templates/UiDemo.htmx.routing.cs delete mode 100644 Htmx.ApiDemo/rd.xml create mode 100644 Testing/AOT/README.md create mode 100644 Testing/AOT/build-aot.ps1 create mode 100644 Testing/AOT/build-aot.sh diff --git a/.gitignore b/.gitignore index 7ef65da..7f9c29e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -bin -obj -node_modules -.env \ No newline at end of file +**/bin/ +**/obj/ +**/node_modules/ +.env +**/Publish/ \ No newline at end of file diff --git a/Htmx.ApiDemo/AppJsonSerializerContext.cs b/Htmx.ApiDemo/AppJsonSerializerContext.cs index 33f2f8d..b29a22f 100644 --- a/Htmx.ApiDemo/AppJsonSerializerContext.cs +++ b/Htmx.ApiDemo/AppJsonSerializerContext.cs @@ -5,14 +5,6 @@ using Microsoft.AspNetCore.Http; namespace Htmx.ApiDemo; [JsonSerializable(typeof(string))] -[JsonSerializable(typeof(Task), GenerationMode = JsonSourceGenerationMode.Metadata)] -[JsonSerializable(typeof(ValueTask), GenerationMode = JsonSourceGenerationMode.Metadata)] -[JsonSerializable(typeof(IResult), GenerationMode = JsonSourceGenerationMode.Metadata)] -[JsonSerializable(typeof(Task), GenerationMode = JsonSourceGenerationMode.Metadata)] -[JsonSerializable(typeof(ValueTask), GenerationMode = JsonSourceGenerationMode.Metadata)] -[JsonSerializable(typeof(PostLoginHandler.Command), TypeInfoPropertyName = "LoginCommand")] -[JsonSerializable(typeof(PostRegisterHandler.Command), TypeInfoPropertyName = "RegisterCommand")] -[JsonSerializable(typeof(PostLogoutHandler.Command), TypeInfoPropertyName = "LogoutCommand")] internal partial class AppJsonSerializerContext : JsonSerializerContext { } \ No newline at end of file diff --git a/Htmx.ApiDemo/Data/AuthService.cs b/Htmx.ApiDemo/Data/AuthService.cs index 1a1aeaa..c4ca64a 100644 --- a/Htmx.ApiDemo/Data/AuthService.cs +++ b/Htmx.ApiDemo/Data/AuthService.cs @@ -10,7 +10,7 @@ namespace Htmx.ApiDemo.Data; /// No EF Core, no LINQ-to-SQL, no RelationalModel fully NativeAOT safe. /// IPasswordHasher is pure PBKDF2 crypto with no dynamic IL. /// -public sealed class AuthService( +public sealed class AppAuthService( MongoDbService mongo, IPasswordHasher passwordHasher, IHttpContextAccessor httpContextAccessor) diff --git a/Htmx.ApiDemo/Helpers/HtmxExtensions.cs b/Htmx.ApiDemo/Helpers/HtmxExtensions.cs new file mode 100644 index 0000000..c7e6bf2 --- /dev/null +++ b/Htmx.ApiDemo/Helpers/HtmxExtensions.cs @@ -0,0 +1,23 @@ +namespace Htmx.ApiDemo; + +public static class HtmxExtensions +{ + public static void HtmxAwareWriteToBody( + this IHtmxComponent component, HttpContext context, string title, string appName, string pageTitle) + { + // If not a HX-Request, render the component inside main layout + if (!context.Request.Headers.ContainsKey("HX-Request")) + { + var layout = new MainLayout(component, title: title, appName: appName, pageTitle: pageTitle, + userName: context.User.Identity?.IsAuthenticated == true ? context.User.Identity.Name : null); + + context.Response.ContentType = "text/html; charset=utf-8"; + var renderContext = new HtmxRenderContext(context.Response.BodyWriter); + layout.Render(renderContext); + return; + } + + //Else only render the component + component.WriteToResponseBody(context); + } +} \ No newline at end of file diff --git a/Htmx.ApiDemo/Helpers/RouteMap.cs b/Htmx.ApiDemo/Helpers/RouteMap.cs new file mode 100644 index 0000000..08a6ebc --- /dev/null +++ b/Htmx.ApiDemo/Helpers/RouteMap.cs @@ -0,0 +1,24 @@ +using Htmx.ApiDemo.Data; + +namespace Htmx.ApiDemo; + +public static partial class RouteMap +{ + public static void MapHtmxRoutes(this WebApplication app) + { + MapGetIndex(app); + GetRegister(app); + PostRegister(app); + PostLogout(app); + GetLogin(app); + PostLogin(app); + GetUiDemo(app); + } + + private static void PostLogout(WebApplication app) + => app.MapPost("/logout", async (HttpContext context, AppAuthService authService) => + { + await authService.SignOutAsync(); + return Results.Redirect("/login"); + }); +} \ No newline at end of file diff --git a/Htmx.ApiDemo/Htmx.ApiDemo.csproj b/Htmx.ApiDemo/Htmx.ApiDemo.csproj index f445c4d..5df97ac 100644 --- a/Htmx.ApiDemo/Htmx.ApiDemo.csproj +++ b/Htmx.ApiDemo/Htmx.ApiDemo.csproj @@ -10,20 +10,13 @@ - true true - $(InterceptorsPreviewNamespaces);Immediate.Apis.Generators - - $(NoWarn);IL2026 - + + + @@ -38,8 +31,6 @@ - - diff --git a/Htmx.ApiDemo/Htmx.ApiDemo.http b/Htmx.ApiDemo/Htmx.ApiDemo.http deleted file mode 100644 index f64fdf5..0000000 --- a/Htmx.ApiDemo/Htmx.ApiDemo.http +++ /dev/null @@ -1,11 +0,0 @@ -@Htmx.ApiDemo_HostAddress = http://localhost:5120 - -GET {{Htmx.ApiDemo_HostAddress}}/todos/ -Accept: application/json - -### - -GET {{Htmx.ApiDemo_HostAddress}}/todos/1 -Accept: application/json - -### diff --git a/Htmx.ApiDemo/Htmx.ApiDemo.sln b/Htmx.ApiDemo/Htmx.ApiDemo.sln new file mode 100644 index 0000000..354dbd8 --- /dev/null +++ b/Htmx.ApiDemo/Htmx.ApiDemo.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Htmx.ApiDemo", "Htmx.ApiDemo.csproj", "{38A5EDEF-D21B-8D4E-D1F2-DDFE0335BD22}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {38A5EDEF-D21B-8D4E-D1F2-DDFE0335BD22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38A5EDEF-D21B-8D4E-D1F2-DDFE0335BD22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38A5EDEF-D21B-8D4E-D1F2-DDFE0335BD22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38A5EDEF-D21B-8D4E-D1F2-DDFE0335BD22}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {61C4ACCC-FDDE-4F92-B2A9-A5744496122B} + EndGlobalSection +EndGlobal diff --git a/Htmx.ApiDemo/HtmxPageExtensions.cs b/Htmx.ApiDemo/HtmxPageExtensions.cs deleted file mode 100644 index 3616b21..0000000 --- a/Htmx.ApiDemo/HtmxPageExtensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.AspNetCore.Antiforgery; - -namespace Htmx.ApiDemo; - -/// -/// Renders a full page or just the body component depending on whether -/// the request was made by HTMX (HX-Request header present). -/// -/// Full request → wraps body in MainLayout (complete HTML page) -/// HTMX request → renders body only + sets HX-Title so the browser -/// tab title still updates -/// -public static class HtmxPageExtensions -{ - public static HtmxResult WriteHtmxPage( - this HttpContext ctx, - IHtmxComponent body, - string title = "App", - string appName = "HtmxApp", - string pageTitle = "") - { - if (ctx.Request.Headers.ContainsKey("HX-Request")) - { - ctx.Response.Headers["HX-Title"] = title; - return ctx.WriteHtmxBody(body); - } - else - { - string? userName = ctx.User.Identity?.IsAuthenticated == true - ? (ctx.User.FindFirst("DisplayName")?.Value - ?? ctx.User.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value) - : null; - - var antiforgery = ctx.RequestServices.GetRequiredService(); - var afTokens = antiforgery.GetAndStoreTokens(ctx); - - return ctx.WriteHtmxBody(new Templates.MainLayout(body, title, appName, pageTitle, userName, afTokens.RequestToken)); - } - } -} diff --git a/Htmx.ApiDemo/HtmxResult.cs b/Htmx.ApiDemo/HtmxResult.cs deleted file mode 100644 index a8fff59..0000000 --- a/Htmx.ApiDemo/HtmxResult.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace Htmx.ApiDemo; - -/// -/// IResult implementation for rendering Htmx components as HTML or issuing redirects. -/// Defined as user code (not source-generated) so that RequestDelegateGenerator can -/// see it and emit NativeAOT-safe endpoint interceptors for lambdas returning this type. -/// -public readonly struct HtmxResult : IResult -{ - private readonly IHtmxComponent? _component; - private readonly string? _redirectUrl; - - public HtmxResult(IHtmxComponent component) { _component = component; _redirectUrl = null; } - public HtmxResult(string redirectUrl) { _component = null; _redirectUrl = redirectUrl; } - - public Task ExecuteAsync(HttpContext context) - { - if (_redirectUrl is not null) - { - context.Response.Redirect(_redirectUrl); - return Task.CompletedTask; - } - - context.Response.ContentType = "text/html; charset=utf-8"; - var writerContext = new HtmxRenderContext(context.Response.BodyWriter); - _component!.Render(writerContext); - return Task.CompletedTask; - } -} diff --git a/Htmx.ApiDemo/Program.cs b/Htmx.ApiDemo/Program.cs index 63d9d09..e8fa0bc 100644 --- a/Htmx.ApiDemo/Program.cs +++ b/Htmx.ApiDemo/Program.cs @@ -1,9 +1,11 @@ using Htmx.ApiDemo; using Htmx.ApiDemo.Data; -using Immediate.Handlers; +using Htmx.ApiDemo.Templates; +using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; @@ -59,13 +61,10 @@ builder.Services }); builder.Services.AddScoped, PasswordHasher>(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); // ── App services ────────────────────────────────────────────────────────── builder.Services.AddHttpContextAccessor(); -builder.Services - .AddHtmxApiDemoBehaviors() - .AddHtmxApiDemoHandlers(); builder.Services.AddOpenApi(); builder.Services.AddAuthorization(); @@ -83,67 +82,6 @@ app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); -// ── Guard: redirect unauthenticated users to /login ─────────────────────── -app.Use(async (context, next) => -{ - var path = context.Request.Path.Value ?? ""; - bool isPublic = path.StartsWith("/login", StringComparison.OrdinalIgnoreCase) - || path.StartsWith("/register", StringComparison.OrdinalIgnoreCase) - || path.StartsWith("/logout", StringComparison.OrdinalIgnoreCase) - || path.StartsWith("/css/", StringComparison.OrdinalIgnoreCase) - || path.StartsWith("/js/", StringComparison.OrdinalIgnoreCase); - - if (!isPublic && context.User.Identity?.IsAuthenticated != true) - { - context.Response.Redirect("/login"); - return; - } - - await next(); -}); - -// Explicit MapGet/MapPost with explicit lambda return type ValueTask. -// The explicit return type annotation lets RequestDelegateGenerator see IResult -// directly (without needing to resolve the generated Handler.HandleAsync type), -// so it emits `await result.ExecuteAsync(httpContext)` instead of JSON serialization. -app.MapGet("/", static ValueTask ( - [AsParameters] Htmx.ApiDemo.Templates.GetIndexHandler.Command cmd, - Htmx.ApiDemo.Templates.GetIndexHandler.Handler handler, - CancellationToken token) => handler.HandleAsync(cmd, token)); - -app.MapGet("/greet/{username}/{count?}/{id?}", static ValueTask ( - [AsParameters] Htmx.ApiDemo.Templates.GetGreetingHandler.Query query, - Htmx.ApiDemo.Templates.GetGreetingHandler.Handler handler, - CancellationToken token) => handler.HandleAsync(query, token)); - -app.MapGet("/login", static ValueTask ( - [AsParameters] Htmx.ApiDemo.Templates.GetLoginHandler.Query query, - Htmx.ApiDemo.Templates.GetLoginHandler.Handler handler, - CancellationToken token) => handler.HandleAsync(query, token)); - -app.MapPost("/login", static ValueTask ( - [AsParameters] Htmx.ApiDemo.Templates.PostLoginHandler.Command cmd, - Htmx.ApiDemo.Templates.PostLoginHandler.Handler handler, - CancellationToken token) => handler.HandleAsync(cmd, token)); - -app.MapGet("/register", static ValueTask ( - [AsParameters] Htmx.ApiDemo.Templates.GetRegisterHandler.Query query, - Htmx.ApiDemo.Templates.GetRegisterHandler.Handler handler, - CancellationToken token) => handler.HandleAsync(query, token)); - -app.MapPost("/register", static ValueTask ( - [AsParameters] Htmx.ApiDemo.Templates.PostRegisterHandler.Command cmd, - Htmx.ApiDemo.Templates.PostRegisterHandler.Handler handler, - CancellationToken token) => handler.HandleAsync(cmd, token)); - -app.MapPost("/logout", static ValueTask ( - [AsParameters] Htmx.ApiDemo.Templates.PostLogoutHandler.Command cmd, - Htmx.ApiDemo.Templates.PostLogoutHandler.Handler handler, - CancellationToken token) => handler.HandleAsync(cmd, token)); - -app.MapGet("/ui-demo", static ValueTask ( - [AsParameters] Htmx.ApiDemo.Templates.GetUiDemoHandler.Query query, - Htmx.ApiDemo.Templates.GetUiDemoHandler.Handler handler, - CancellationToken token) => handler.HandleAsync(query, token)); +app.MapHtmxRoutes(); app.Run(); \ No newline at end of file diff --git a/Htmx.ApiDemo/Templates/Greeting.htmx.cs b/Htmx.ApiDemo/Templates/Greeting.htmx.cs index 63a7ced..18449d5 100644 --- a/Htmx.ApiDemo/Templates/Greeting.htmx.cs +++ b/Htmx.ApiDemo/Templates/Greeting.htmx.cs @@ -1,5 +1,3 @@ -using Immediate.Apis.Shared; -using Immediate.Handlers.Shared; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -18,29 +16,3 @@ public sealed class Greeting : GreetingBase protected override void RenderGreetingId(HtmxRenderContext context) => context.Writer.WriteUtf8(_greetingIdData); protected override void RenderUser(HtmxRenderContext context) => context.Writer.WriteUtf8(_userData); } - -[Handler] -[MapGet("/greet/{username}/{count?}/{id?}")] -public static partial class GetGreetingHandler -{ - public class Query - { - [FromRoute] public string Username { get; set; } = default!; - [FromRoute] public string? Count { get; set; } - [FromRoute] public string? Id { get; set; } - } - - private static ValueTask HandleAsync( - Query query, - IHttpContextAccessor httpContextAccessor, - CancellationToken token) - { - var context = httpContextAccessor.HttpContext - ?? throw new InvalidOperationException("HttpContext is not available."); - - var count = int.TryParse(query.Count, out var parsedCount) ? parsedCount + 1 : 0; - var greetingId = Guid.TryParse(query.Id, out var parsedId) ? parsedId : Guid.NewGuid(); - var template = new Greeting { Username = query.Username, Count = count, GreetingId = greetingId }; - return ValueTask.FromResult(context.WriteHtmxBody(template)); - } -} \ No newline at end of file diff --git a/Htmx.ApiDemo/Templates/Greeting.htmx.routing.cs b/Htmx.ApiDemo/Templates/Greeting.htmx.routing.cs new file mode 100644 index 0000000..2972338 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Greeting.htmx.routing.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Builder; +using Htmx.ApiDemo.Templates; + +namespace Htmx.ApiDemo; + +public static partial class RouteMap +{ + public static void MapGetIndex(WebApplication app) + => app.MapGet("/", (IHttpContextAccessor contextAccessor) => + { + var context = contextAccessor.HttpContext + ?? throw new InvalidOperationException("HttpContext is not available."); + + var greet = new Greeting { Username = "Enciphered", Count = 0, GreetingId = Guid.NewGuid() }; + greet.HtmxAwareWriteToBody( + context: context, + title: "Home", + appName: "HtmxApp", + pageTitle: "Home" + ); + }); +} \ No newline at end of file diff --git a/Htmx.ApiDemo/Templates/Login.htmx.cs b/Htmx.ApiDemo/Templates/Login.htmx.cs index aa6de34..cde18e9 100644 --- a/Htmx.ApiDemo/Templates/Login.htmx.cs +++ b/Htmx.ApiDemo/Templates/Login.htmx.cs @@ -1,6 +1,4 @@ using Htmx.ApiDemo.Data; -using Immediate.Apis.Shared; -using Immediate.Handlers.Shared; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -25,59 +23,4 @@ public sealed class Login : LoginBase protected override void RenderErrorMessage(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_errorData); protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afTokenData); -} - - -[Handler] -[MapGet("/login")] -public static partial class GetLoginHandler -{ - public class Query; - - private static ValueTask HandleAsync( - Query _, - IHttpContextAccessor httpContextAccessor, - IAntiforgery antiforgery, - CancellationToken token) - { - var ctx = httpContextAccessor.HttpContext - ?? throw new InvalidOperationException("HttpContext is not available."); - - if (ctx.User.Identity?.IsAuthenticated == true) - return ValueTask.FromResult(new HtmxResult("/")); - - var afTokens = antiforgery.GetAndStoreTokens(ctx); - return ValueTask.FromResult(ctx.WriteHtmxPage(new Login(afToken: afTokens.RequestToken), title: "Sign in", appName: "HtmxApp", pageTitle: "Sign in")); - } -} - - -[Handler] -[MapPost("/login")] -public static partial class PostLoginHandler -{ - public class Command - { - [FromForm] public string Email { get; set; } = default!; - [FromForm] public string Password { get; set; } = default!; - } - - private static async ValueTask HandleAsync( - [AsParameters] Command command, - IHttpContextAccessor httpContextAccessor, - IAntiforgery antiforgery, - AuthService authService, - CancellationToken token) - { - var ctx = httpContextAccessor.HttpContext - ?? throw new InvalidOperationException("HttpContext is not available."); - - var (success, error) = await authService.LoginAsync(command.Email, command.Password); - - if (success) - return new HtmxResult("/"); - - var afTokens = antiforgery.GetAndStoreTokens(ctx); - return ctx.WriteHtmxPage(new Login(error, afToken: afTokens.RequestToken), title: "Sign in", appName: "HtmxApp", pageTitle: "Sign in"); - } -} +} \ No newline at end of file diff --git a/Htmx.ApiDemo/Templates/Login.htmx.routing.cs b/Htmx.ApiDemo/Templates/Login.htmx.routing.cs new file mode 100644 index 0000000..0b92dec --- /dev/null +++ b/Htmx.ApiDemo/Templates/Login.htmx.routing.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Antiforgery; +using Htmx.ApiDemo.Templates; +using Htmx.ApiDemo.Data; +using Microsoft.AspNetCore.Mvc; + +namespace Htmx.ApiDemo; + +public static partial class RouteMap +{ + private static void GetLogin(WebApplication app) + => app.MapGet("/login", (IHttpContextAccessor contextAccessor, IAntiforgery antiforgery) => + { + var context = contextAccessor.HttpContext + ?? throw new InvalidOperationException("HttpContext is not available."); + + if (context.User.Identity?.IsAuthenticated == true) + { + context.Response.Redirect("/"); + return; + } + + var afToken = antiforgery.GetAndStoreTokens(context).RequestToken; + var loginComponent = new Login(afToken: afToken); + loginComponent.HtmxAwareWriteToBody( + context: context, + title: "Login", + appName: "HtmxApp", + pageTitle: "Welcome back" + ); + }); + + private static void PostLogin(WebApplication app) + => app.MapPost("/login", async ValueTask + ( + [FromForm] string email, + [FromForm] string password, + [FromServices] IHttpContextAccessor httpContextAccessor, + [FromServices] IAntiforgery antiforgery, + [FromServices] AppAuthService authService + ) => + { + var context = httpContextAccessor.HttpContext + ?? throw new InvalidOperationException("HttpContext is not available."); + + var afToken = antiforgery.GetAndStoreTokens(context).RequestToken; + + var (success, error) = await authService.LoginAsync(email, password); + + if (success) + { + context.Response.Redirect("/"); + return; + } + + var loginComponent = new Login(error, afToken: afToken); + loginComponent.HtmxAwareWriteToBody( + context: context, + title: "Login", + appName: "HtmxApp", + pageTitle: "Welcome back" + ); + }); +} \ No newline at end of file diff --git a/Htmx.ApiDemo/Templates/Logout.cs b/Htmx.ApiDemo/Templates/Logout.cs deleted file mode 100644 index 0b91947..0000000 --- a/Htmx.ApiDemo/Templates/Logout.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Htmx.ApiDemo; -using Htmx.ApiDemo.Data; -using Immediate.Apis.Shared; -using Immediate.Handlers.Shared; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Htmx.ApiDemo.Templates; - -[Handler] -[MapPost("/logout")] -public static partial class PostLogoutHandler -{ - // Empty command — [AsParameters] ensures form content-type is accepted - // and antiforgery token in the form is validated by the middleware. - public class Command; - - private static async ValueTask HandleAsync( - [AsParameters] Command _, - AuthService authService, - IHttpContextAccessor httpContextAccessor, - CancellationToken token) - { - await authService.SignOutAsync(); - return new HtmxResult("/login"); - } -} diff --git a/Htmx.ApiDemo/Templates/MainLayout.htmx.cs b/Htmx.ApiDemo/Templates/MainLayout.htmx.cs index cddd7ac..2277a17 100644 --- a/Htmx.ApiDemo/Templates/MainLayout.htmx.cs +++ b/Htmx.ApiDemo/Templates/MainLayout.htmx.cs @@ -1,12 +1,10 @@ using Htmx.ApiDemo; using Htmx.ApiDemo.Templates.Components; -using Immediate.Apis.Shared; -using Immediate.Handlers.Shared; using Microsoft.AspNetCore.Http; -namespace Htmx.ApiDemo.Templates; +namespace Htmx.ApiDemo; -public sealed class MainLayout : MainLayoutBase +public sealed class MainLayout : Templates.MainLayoutBase { private byte[] _titleData = []; private byte[] _appNameData = []; @@ -67,24 +65,4 @@ public sealed class MainLayout : MainLayoutBase protected override void RenderAppName(HtmxRenderContext context) => context.Writer.WriteUtf8(_appNameData); protected override void RenderPageTitle(HtmxRenderContext context) => context.Writer.WriteUtf8(_pageTitleData); protected override void RenderUserSection(HtmxRenderContext context) => context.Writer.WriteUtf8(_userSectionData); -} - - -[Handler] -[MapGet("/")] -public static partial class GetIndexHandler -{ - public class Command; - - private static ValueTask HandleAsync( - Command command, - IHttpContextAccessor httpContextAccessor, - CancellationToken token) - { - var context = httpContextAccessor.HttpContext - ?? throw new InvalidOperationException("HttpContext is not available."); - - var greet = new Greeting { Username = "Enciphered", Count = 0, GreetingId = Guid.NewGuid() }; - return ValueTask.FromResult(context.WriteHtmxPage(greet, title: "Home", appName: "HtmxApp", pageTitle: "Home")); - } } \ No newline at end of file diff --git a/Htmx.ApiDemo/Templates/Register.htmx.cs b/Htmx.ApiDemo/Templates/Register.htmx.cs index 8119220..895dc30 100644 --- a/Htmx.ApiDemo/Templates/Register.htmx.cs +++ b/Htmx.ApiDemo/Templates/Register.htmx.cs @@ -1,6 +1,4 @@ using Htmx.ApiDemo.Data; -using Immediate.Apis.Shared; -using Immediate.Handlers.Shared; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -25,69 +23,4 @@ public sealed class Register : RegisterBase protected override void RenderErrorMessage(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_errorData); protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afTokenData); -} - - -[Handler] -[MapGet("/register")] -public static partial class GetRegisterHandler -{ - public class Query; - - private static ValueTask HandleAsync( - Query _, - IHttpContextAccessor httpContextAccessor, - IAntiforgery antiforgery, - CancellationToken token) - { - var ctx = httpContextAccessor.HttpContext - ?? throw new InvalidOperationException("HttpContext is not available."); - - if (ctx.User.Identity?.IsAuthenticated == true) - return ValueTask.FromResult(new HtmxResult("/")); - - var afTokens = antiforgery.GetAndStoreTokens(ctx); - return ValueTask.FromResult(ctx.WriteHtmxPage(new Register(afToken: afTokens.RequestToken), title: "Register", appName: "HtmxApp", pageTitle: "Create account")); - } -} - - -[Handler] -[MapPost("/register")] -public static partial class PostRegisterHandler -{ - public class Command - { - [FromForm] public string Email { get; set; } = default!; - [FromForm] public string Password { get; set; } = default!; - [FromForm] public string ConfirmPassword { get; set; } = default!; - [FromForm] public string? DisplayName { get; set; } - } - - private static async ValueTask HandleAsync( - [AsParameters] Command command, - IHttpContextAccessor httpContextAccessor, - IAntiforgery antiforgery, - AuthService authService, - CancellationToken token) - { - var ctx = httpContextAccessor.HttpContext - ?? throw new InvalidOperationException("HttpContext is not available."); - - if (command.Password != command.ConfirmPassword) - { - var afTokens1 = antiforgery.GetAndStoreTokens(ctx); - return ctx.WriteHtmxPage(new Register("Passwords do not match.", afToken: afTokens1.RequestToken), - title: "Register", appName: "HtmxApp", pageTitle: "Create account"); - } - - var (success, error) = await authService.RegisterAsync(command.Email, command.Password, command.DisplayName); - - if (success) - return new HtmxResult("/"); - - var afTokens2 = antiforgery.GetAndStoreTokens(ctx); - return ctx.WriteHtmxPage(new Register(error, afToken: afTokens2.RequestToken), - title: "Register", appName: "HtmxApp", pageTitle: "Create account"); - } -} +} \ No newline at end of file diff --git a/Htmx.ApiDemo/Templates/Register.htmx.routing.cs b/Htmx.ApiDemo/Templates/Register.htmx.routing.cs new file mode 100644 index 0000000..dd84ab4 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Register.htmx.routing.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Builder; +using Htmx.ApiDemo.Templates; +using Microsoft.AspNetCore.Antiforgery; +using Htmx.ApiDemo.Data; +using Microsoft.AspNetCore.Mvc; + +namespace Htmx.ApiDemo; + +public static partial class RouteMap +{ + private static void GetRegister(WebApplication app) + => app.MapGet("/register", (IHttpContextAccessor contextAccessor, IAntiforgery antiforgery) => + { + var context = contextAccessor.HttpContext + ?? throw new InvalidOperationException("HttpContext is not available."); + + if (context.User.Identity?.IsAuthenticated == true) + { + context.Response.Redirect("/"); + return; + } + + var afTokens = antiforgery.GetAndStoreTokens(context); + var registerComponent = new Register(afToken: afTokens.RequestToken); + registerComponent.HtmxAwareWriteToBody( + context: context, + title: "Register", + appName: "HtmxApp", + pageTitle: "Create account" + ); + }); + + private static void PostRegister(WebApplication app) + => app.MapPost("/register", async ValueTask + ( + [FromForm] string email, + [FromForm] string password, + [FromForm] string confirmPassword, + [FromForm] string? displayName, + [FromServices] IHttpContextAccessor httpContextAccessor, + [FromServices] IAntiforgery antiforgery, + [FromServices] AppAuthService authService + ) => + { + var context = httpContextAccessor.HttpContext + ?? throw new InvalidOperationException("HttpContext is not available."); + + var afToken = antiforgery.GetAndStoreTokens(context).RequestToken; + + if (password != confirmPassword) + { + var errorComponent = new Register("Passwords do not match.", afToken: afToken); + errorComponent.HtmxAwareWriteToBody( + context: context, + title: "Register", + appName: "HtmxApp", + pageTitle: "Create account" + ); + } + + var (success, error) = await authService.RegisterAsync(email, password, displayName); + + if (success) + { + context.Response.Redirect("/"); + return; + } + + var registerComponent = new Register(error, afToken: afToken); + }); +} \ No newline at end of file diff --git a/Htmx.ApiDemo/Templates/UiDemo.htmx.cs b/Htmx.ApiDemo/Templates/UiDemo.htmx.cs index d819c81..1adaa57 100644 --- a/Htmx.ApiDemo/Templates/UiDemo.htmx.cs +++ b/Htmx.ApiDemo/Templates/UiDemo.htmx.cs @@ -1,6 +1,4 @@ using Htmx.ApiDemo.Templates.Components; -using Immediate.Apis.Shared; -using Immediate.Handlers.Shared; using Microsoft.AspNetCore.Http; namespace Htmx.ApiDemo.Templates; @@ -364,24 +362,4 @@ public sealed class UiDemo : UiDemoBase protected override void RenderDropdownDemo(HtmxRenderContext ctx) => DropdownDemo.Render(ctx); protected override void RenderToastViewportDemo(HtmxRenderContext ctx) => ToastViewportDemo.Render(ctx); -} - - -[Handler] -[MapGet("/ui-demo")] -public static partial class GetUiDemoHandler -{ - public class Query; - - private static ValueTask HandleAsync( - Query query, - IHttpContextAccessor httpContextAccessor, - CancellationToken token) - { - var context = httpContextAccessor.HttpContext - ?? throw new InvalidOperationException("HttpContext is not available."); - - var page = new UiDemo(); - return ValueTask.FromResult(context.WriteHtmxPage(page, title: "UI Demo", appName: "HtmxApp", pageTitle: "UI Components")); - } -} +} \ No newline at end of file diff --git a/Htmx.ApiDemo/Templates/UiDemo.htmx.routing.cs b/Htmx.ApiDemo/Templates/UiDemo.htmx.routing.cs new file mode 100644 index 0000000..50f1d23 --- /dev/null +++ b/Htmx.ApiDemo/Templates/UiDemo.htmx.routing.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Builder; +using Htmx.ApiDemo.Templates; + +namespace Htmx.ApiDemo; + +public static partial class RouteMap +{ + private static void GetUiDemo(WebApplication app) + => app.MapGet("/ui-demo", (IHttpContextAccessor contextAccessor) => + { + var context = contextAccessor.HttpContext + ?? throw new InvalidOperationException("HttpContext is not available."); + + if (context.User.Identity?.IsAuthenticated == false) + { + context.Response.Redirect("/login"); + return; + } + + var uiDemoComponent = new UiDemo(); + uiDemoComponent.HtmxAwareWriteToBody( + context: context, + title: "UI Demo", + appName: "HtmxApp", + pageTitle: "Htmx UI Demo" + ); + }); +} \ No newline at end of file diff --git a/Htmx.ApiDemo/appsettings.Development.json b/Htmx.ApiDemo/appsettings.Development.json index ff66ba6..0f0f0ff 100644 --- a/Htmx.ApiDemo/appsettings.Development.json +++ b/Htmx.ApiDemo/appsettings.Development.json @@ -4,5 +4,9 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } - } + }, + "ConnectionStrings": { + "DefaultConnection": "mongodb://localhost:27017/" + }, + "MongoDbName": "HtmxDemoDb" } diff --git a/Htmx.ApiDemo/rd.xml b/Htmx.ApiDemo/rd.xml deleted file mode 100644 index 17ab541..0000000 --- a/Htmx.ApiDemo/rd.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - diff --git a/Htmx.SourceGenerator/HtmxGenerator.cs b/Htmx.SourceGenerator/HtmxGenerator.cs index 983e757..910ac2d 100644 --- a/Htmx.SourceGenerator/HtmxGenerator.cs +++ b/Htmx.SourceGenerator/HtmxGenerator.cs @@ -69,8 +69,12 @@ public static class HtmxGeneratedExtensions writer.Advance(data.Length); }} - public static HtmxResult WriteHtmxBody(this HttpContext context, IHtmxComponent component) - => new HtmxResult(component); + public static void WriteToResponseBody(this IHtmxComponent component, HttpContext context) + {{ + context.Response.ContentType = ""text/html; charset=utf-8""; + var renderContext = new HtmxRenderContext(context.Response.BodyWriter); + component.Render(renderContext); + }} }}"; spc.AddSource("HtmxInfrastructure.g.cs", SourceText.From(infrastructureSource, Encoding.UTF8)); }); diff --git a/Testing/AOT/README.md b/Testing/AOT/README.md new file mode 100644 index 0000000..0cac44b --- /dev/null +++ b/Testing/AOT/README.md @@ -0,0 +1,97 @@ +# AOT Testing Guide + +This directory contains scripts for building and testing the Htmx.ApiDemo application with Ahead-of-Time (AOT) compilation enabled. AOT compilation helps identify potential trimming issues that may occur at runtime. + +## Directory Structure + +``` +Testing/ +└── AOT/ + ├── build-aot.ps1 # Windows PowerShell script + ├── build-aot.sh # Linux/POP_OS bash script + ├── Publish/ # Output directory (created during build) + └── README.md # This file +``` + +## Prerequisites + +- .NET SDK (version 10.0 or later) +- For Windows: PowerShell 5.1 or later +- For Linux/POP_OS: Bash shell + +## Usage + +### Windows (PowerShell) + +**Build and Run:** +```powershell +.\build-aot.ps1 +``` + +**Build Only:** +```powershell +.\build-aot.ps1 -BuildOnly +``` + +**Run Only (if already built):** +```powershell +.\build-aot.ps1 -RunOnly +``` + +### Linux/POP_OS (Bash) + +Make the script executable first: +```bash +chmod +x build-aot.sh +``` + +**Build and Run:** +```bash +./build-aot.sh +``` + +**Build Only:** +```bash +./build-aot.sh --build-only +``` + +**Run Only (if already built):** +```bash +./build-aot.sh --run-only +``` + +## What These Scripts Do + +1. **Clean**: Removes any previous publish directory +2. **Build**: Publishes the application with the following AOT settings: + - `PublishAot=true` - Enables AOT compilation + - `TrimMode=link` - Uses link-time trimming + - `PublishTrimmed=true` - Enables trimming + - `SelfContained=true` - Creates a self-contained executable + - Debug symbols are disabled for optimized output + +3. **Run**: Launches the compiled application to test for any trimming-related issues + +## Output + +The compiled application and all dependencies are published to the `Testing/AOT/Publish` directory. + +## Troubleshooting + +### Build Failures + +If the AOT build fails, check the error messages for: +- **Trimming issues**: Indicates code that cannot be safely trimmed +- **Reflection warnings**: APIs that use reflection may not work properly +- **Missing dependencies**: Required libraries that aren't properly configured + +### Common Issues + +1. **"Executable not found"**: The build may have failed silently. Check the build output for errors. +2. **Runtime crashes**: Trimming may have removed necessary code. Consider adding trimming configuration in your project file or using `[DynamicallyAccessedMembers]` attributes. + +## Related Documentation + +- [AOT Deployment](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) +- [Trimming .NET Applications](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-self-contained) +- [Reflections and Trimming](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-self-contained#reflections) diff --git a/Testing/AOT/build-aot.ps1 b/Testing/AOT/build-aot.ps1 new file mode 100644 index 0000000..6369666 --- /dev/null +++ b/Testing/AOT/build-aot.ps1 @@ -0,0 +1,122 @@ +# AOT Build and Test Script for Windows PowerShell +# This script builds the Htmx.ApiDemo application with AOT compilation enabled +# and runs the application to check for trimming issues + +param( + [switch]$RunOnly, + [switch]$BuildOnly +) + +# Set error handling +$ErrorActionPreference = "Stop" + +# Define paths +$ProjectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +$ApiDemoProject = Join-Path $ProjectRoot "Htmx.ApiDemo" +$PublishPath = Join-Path $PSScriptRoot "Publish" + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "AOT Build and Test Script" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" + +if (-not $RunOnly) { + Write-Host "Starting AOT Build Process..." -ForegroundColor Yellow + Write-Host "Project Path: $ApiDemoProject" -ForegroundColor Gray + Write-Host "Publish Path: $PublishPath" -ForegroundColor Gray + Write-Host "" + + # Kill any running instance of the app before cleaning (it locks the publish folder) + $AppName = "Htmx.ApiDemo" + $Running = Get-Process -Name $AppName -ErrorAction SilentlyContinue + if ($Running) { + Write-Host "Stopping running instance of $AppName..." -ForegroundColor Yellow + $Running | Stop-Process -Force + Start-Sleep -Milliseconds 500 + } + + # Clean previous publish folder if it exists + if (Test-Path $PublishPath) { + Write-Host "Cleaning previous publish folder..." -ForegroundColor Yellow + Remove-Item -Path $PublishPath -Recurse -Force + } + + # Create publish folder + New-Item -Path $PublishPath -ItemType Directory -Force | Out-Null + + try { + # Run dotnet publish with AOT enabled + # PublishAot=true is already set in the .csproj + # A Runtime Identifier (RID) is required for AOT compilation + Write-Host "Publishing with AOT compilation enabled (win-x64)..." -ForegroundColor Yellow + Push-Location $ApiDemoProject + + dotnet publish -c Release -r win-x64 -o $PublishPath ` + -p:DebugSymbols=false ` + -p:DebugType=none + + if ($LASTEXITCODE -ne 0) { + Write-Host "Build failed!" -ForegroundColor Red + Pop-Location + exit 1 + } + + Pop-Location + Write-Host "AOT build completed successfully!" -ForegroundColor Green + Write-Host "" + + # Copy appsettings.Development.json over appsettings.json in the publish folder + # so the AOT executable has the correct connection strings for local testing + $DevSettings = Join-Path $ApiDemoProject "appsettings.Development.json" + $PublishedSettings = Join-Path $PublishPath "appsettings.json" + if (Test-Path $DevSettings) { + Write-Host "Copying appsettings.Development.json -> appsettings.json in publish folder..." -ForegroundColor Yellow + Copy-Item -Path $DevSettings -Destination $PublishedSettings -Force + Write-Host "Done." -ForegroundColor Green + Write-Host "" + } else { + Write-Host "Warning: appsettings.Development.json not found, skipping copy." -ForegroundColor DarkYellow + } + } + catch { + Write-Host "Error during build: $_" -ForegroundColor Red + Pop-Location + exit 1 + } +} + +if (-not $BuildOnly) { + Write-Host "Starting Application..." -ForegroundColor Yellow + Write-Host "" + + # Find the executable + $Executable = Get-ChildItem -Path $PublishPath -Filter "*.exe" -Recurse | Select-Object -First 1 + + if (-not $Executable) { + Write-Host "Executable not found in publish directory!" -ForegroundColor Red + exit 1 + } + + Write-Host "Running: $($Executable.FullName)" -ForegroundColor Yellow + Write-Host "Press Ctrl+C to stop the application" -ForegroundColor Gray + Write-Host "" + + try { + # Use Start-Process with WorkingDirectory so appsettings.json is found, + # and the shell CWD is never changed (safe against Ctrl+C) + $proc = Start-Process -FilePath $Executable.FullName ` + -WorkingDirectory $PublishPath ` + -NoNewWindow ` + -PassThru + $proc.WaitForExit() + } + catch { + Write-Host "Error running application: $_" -ForegroundColor Red + exit 1 + } +} + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "AOT Test Complete" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan diff --git a/Testing/AOT/build-aot.sh b/Testing/AOT/build-aot.sh new file mode 100644 index 0000000..f0a3909 --- /dev/null +++ b/Testing/AOT/build-aot.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# AOT Build and Test Script for POP_OS/Linux +# This script builds the Htmx.ApiDemo application with AOT compilation enabled +# and runs the application to check for trimming issues + +# Set error handling +set -e + +# Initialize flags +RUN_ONLY=false +BUILD_ONLY=false + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --run-only) + RUN_ONLY=true + shift + ;; + --build-only) + BUILD_ONLY=true + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--run-only] [--build-only]" + exit 1 + ;; + esac +done + +# Define paths +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +API_DEMO_PROJECT="$PROJECT_ROOT/Htmx.ApiDemo" +PUBLISH_PATH="$SCRIPT_DIR/Publish" + +echo "========================================" +echo "AOT Build and Test Script" +echo "========================================" +echo "" + +if [ "$RUN_ONLY" = false ]; then + echo "Starting AOT Build Process..." + echo "Project Path: $API_DEMO_PROJECT" + echo "Publish Path: $PUBLISH_PATH" + echo "" + + # Clean previous publish folder if it exists + if [ -d "$PUBLISH_PATH" ]; then + echo "Cleaning previous publish folder..." + rm -rf "$PUBLISH_PATH" + fi + + # Create publish folder + mkdir -p "$PUBLISH_PATH" + + # Navigate to project directory + cd "$API_DEMO_PROJECT" || exit 1 + + # Run dotnet publish with AOT enabled + # PublishAot=true is already set in the .csproj + # A Runtime Identifier (RID) is required for AOT compilation + echo "Publishing with AOT compilation enabled (linux-x64)..." + + if ! dotnet publish -c Release -r linux-x64 -o "$PUBLISH_PATH" \ + -p:DebugSymbols=false \ + -p:DebugType=none; then + echo "Build failed!" + exit 1 + fi + + echo "AOT build completed successfully!" + echo "" + + # Copy appsettings.Development.json over appsettings.json in the publish folder + # so the AOT executable has the correct connection strings for local testing + DEV_SETTINGS="$API_DEMO_PROJECT/appsettings.Development.json" + PUBLISHED_SETTINGS="$PUBLISH_PATH/appsettings.json" + if [ -f "$DEV_SETTINGS" ]; then + echo "Copying appsettings.Development.json -> appsettings.json in publish folder..." + cp -f "$DEV_SETTINGS" "$PUBLISHED_SETTINGS" + echo "Done." + echo "" + else + echo "Warning: appsettings.Development.json not found, skipping copy." + fi +fi + +if [ "$BUILD_ONLY" = false ]; then + echo "Starting Application..." + echo "" + + # Find the executable + EXECUTABLE=$(find "$PUBLISH_PATH" -type f -perm /u+x ! -name "*.so" ! -name "*.a" ! -name "*.o" | head -n 1) + + if [ -z "$EXECUTABLE" ]; then + echo "Executable not found in publish directory!" + exit 1 + fi + + echo "Running: $EXECUTABLE" + echo "Press Ctrl+C to stop the application" + echo "" + + # cd into publish dir so appsettings.json is found relative to the executable + cd "$PUBLISH_PATH" || exit 1 + "$EXECUTABLE" +fi + +echo "" +echo "========================================" +echo "AOT Test Complete" +echo "========================================"