diff --git a/TickerQ.sln b/TickerQ.sln index c448310a..ef9bb45b 100644 --- a/TickerQ.sln +++ b/TickerQ.sln @@ -40,6 +40,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TickerQ.Caching.StackExchan EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TickerQ.Instrumentation.OpenTelemetry", "src\TickerQ.Instrumentation.OpenTelemetry\TickerQ.Instrumentation.OpenTelemetry.csproj", "{D87B9599-22C7-4E82-B300-1239E504E097}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TickerQ.Sample.WebApi", "samples\TickerQ.Sample.WebApi\TickerQ.Sample.WebApi.csproj", "{4DC9FE10-966F-4C8A-B7D2-1215DC319B9E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TickerQ.Sample.Console", "samples\TickerQ.Sample.Console\TickerQ.Sample.Console.csproj", "{723F26D9-C675-4883-8B62-AEBA370566D4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -146,6 +150,30 @@ Global {D87B9599-22C7-4E82-B300-1239E504E097}.Release|x64.Build.0 = Release|Any CPU {D87B9599-22C7-4E82-B300-1239E504E097}.Release|x86.ActiveCfg = Release|Any CPU {D87B9599-22C7-4E82-B300-1239E504E097}.Release|x86.Build.0 = Release|Any CPU + {4DC9FE10-966F-4C8A-B7D2-1215DC319B9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DC9FE10-966F-4C8A-B7D2-1215DC319B9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DC9FE10-966F-4C8A-B7D2-1215DC319B9E}.Debug|x64.ActiveCfg = Debug|Any CPU + {4DC9FE10-966F-4C8A-B7D2-1215DC319B9E}.Debug|x64.Build.0 = Debug|Any CPU + {4DC9FE10-966F-4C8A-B7D2-1215DC319B9E}.Debug|x86.ActiveCfg = Debug|Any CPU + {4DC9FE10-966F-4C8A-B7D2-1215DC319B9E}.Debug|x86.Build.0 = Debug|Any CPU + {4DC9FE10-966F-4C8A-B7D2-1215DC319B9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DC9FE10-966F-4C8A-B7D2-1215DC319B9E}.Release|Any CPU.Build.0 = Release|Any CPU + {4DC9FE10-966F-4C8A-B7D2-1215DC319B9E}.Release|x64.ActiveCfg = Release|Any CPU + {4DC9FE10-966F-4C8A-B7D2-1215DC319B9E}.Release|x64.Build.0 = Release|Any CPU + {4DC9FE10-966F-4C8A-B7D2-1215DC319B9E}.Release|x86.ActiveCfg = Release|Any CPU + {4DC9FE10-966F-4C8A-B7D2-1215DC319B9E}.Release|x86.Build.0 = Release|Any CPU + {723F26D9-C675-4883-8B62-AEBA370566D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {723F26D9-C675-4883-8B62-AEBA370566D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {723F26D9-C675-4883-8B62-AEBA370566D4}.Debug|x64.ActiveCfg = Debug|Any CPU + {723F26D9-C675-4883-8B62-AEBA370566D4}.Debug|x64.Build.0 = Debug|Any CPU + {723F26D9-C675-4883-8B62-AEBA370566D4}.Debug|x86.ActiveCfg = Debug|Any CPU + {723F26D9-C675-4883-8B62-AEBA370566D4}.Debug|x86.Build.0 = Debug|Any CPU + {723F26D9-C675-4883-8B62-AEBA370566D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {723F26D9-C675-4883-8B62-AEBA370566D4}.Release|Any CPU.Build.0 = Release|Any CPU + {723F26D9-C675-4883-8B62-AEBA370566D4}.Release|x64.ActiveCfg = Release|Any CPU + {723F26D9-C675-4883-8B62-AEBA370566D4}.Release|x64.Build.0 = Release|Any CPU + {723F26D9-C675-4883-8B62-AEBA370566D4}.Release|x86.ActiveCfg = Release|Any CPU + {723F26D9-C675-4883-8B62-AEBA370566D4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -160,5 +188,7 @@ Global {9E6EC713-AD6D-4909-8617-B909D76A6E3A} = {9A4EB4A4-FB92-477C-A6D8-0735579B7BAB} {B3049104-9C15-4933-A440-CA62CFBC5F70} = {BA086F2D-2778-4F58-A9AA-45F560CE3504} {D87B9599-22C7-4E82-B300-1239E504E097} = {BA086F2D-2778-4F58-A9AA-45F560CE3504} + {4DC9FE10-966F-4C8A-B7D2-1215DC319B9E} = {45D577FA-DB7A-4B96-BB3F-97DDA0A929D5} + {723F26D9-C675-4883-8B62-AEBA370566D4} = {45D577FA-DB7A-4B96-BB3F-97DDA0A929D5} EndGlobalSection EndGlobal diff --git a/samples/TickerQ.Sample.Console/Migrations/20251123154004_InitialTickerQOperationalStore.Designer.cs b/samples/TickerQ.Sample.Console/Migrations/20251123154004_InitialTickerQOperationalStore.Designer.cs new file mode 100644 index 00000000..09958c0f --- /dev/null +++ b/samples/TickerQ.Sample.Console/Migrations/20251123154004_InitialTickerQOperationalStore.Designer.cs @@ -0,0 +1,229 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickerQ.EntityFrameworkCore.DbContextFactory; + +#nullable disable + +namespace TickerQ.Sample.Console.Migrations +{ + [DbContext(typeof(TickerQDbContext))] + [Migration("20251123154004_InitialTickerQOperationalStore")] + partial class InitialTickerQOperationalStore + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Expression") + .HasColumnType("TEXT"); + + b.Property("Function") + .HasColumnType("TEXT"); + + b.Property("InitIdentifier") + .HasColumnType("TEXT"); + + b.Property("Request") + .HasColumnType("BLOB"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("RetryIntervals") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Expression") + .HasDatabaseName("IX_CronTickers_Expression"); + + b.HasIndex("Function", "Expression") + .HasDatabaseName("IX_Function_Expression"); + + b.ToTable("CronTickers", "ticker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerOccurrenceEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CronTickerId") + .HasColumnType("TEXT"); + + b.Property("ElapsedTime") + .HasColumnType("INTEGER"); + + b.Property("ExceptionMessage") + .HasColumnType("TEXT"); + + b.Property("ExecutedAt") + .HasColumnType("TEXT"); + + b.Property("ExecutionTime") + .HasColumnType("TEXT"); + + b.Property("LockHolder") + .HasColumnType("TEXT"); + + b.Property("LockedAt") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("SkippedReason") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CronTickerId") + .HasDatabaseName("IX_CronTickerOccurrence_CronTickerId"); + + b.HasIndex("ExecutionTime") + .HasDatabaseName("IX_CronTickerOccurrence_ExecutionTime"); + + b.HasIndex("CronTickerId", "ExecutionTime") + .IsUnique() + .HasDatabaseName("UQ_CronTickerId_ExecutionTime"); + + b.HasIndex("Status", "ExecutionTime") + .HasDatabaseName("IX_CronTickerOccurrence_Status_ExecutionTime"); + + b.ToTable("CronTickerOccurrences", "ticker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ElapsedTime") + .HasColumnType("INTEGER"); + + b.Property("ExceptionMessage") + .HasColumnType("TEXT"); + + b.Property("ExecutedAt") + .HasColumnType("TEXT"); + + b.Property("ExecutionTime") + .HasColumnType("TEXT"); + + b.Property("Function") + .HasColumnType("TEXT"); + + b.Property("InitIdentifier") + .HasColumnType("TEXT"); + + b.Property("LockHolder") + .HasColumnType("TEXT"); + + b.Property("LockedAt") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("Request") + .HasColumnType("BLOB"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("RetryIntervals") + .HasColumnType("TEXT"); + + b.Property("RunCondition") + .HasColumnType("INTEGER"); + + b.Property("SkippedReason") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ExecutionTime") + .HasDatabaseName("IX_TimeTicker_ExecutionTime"); + + b.HasIndex("ParentId"); + + b.HasIndex("Status", "ExecutionTime") + .HasDatabaseName("IX_TimeTicker_Status_ExecutionTime"); + + b.ToTable("TimeTickers", "ticker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerOccurrenceEntity", b => + { + b.HasOne("TickerQ.Utilities.Entities.CronTickerEntity", "CronTicker") + .WithMany() + .HasForeignKey("CronTickerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CronTicker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.HasOne("TickerQ.Utilities.Entities.TimeTickerEntity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.Navigation("Children"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/TickerQ.Sample.Console/Migrations/20251123154004_InitialTickerQOperationalStore.cs b/samples/TickerQ.Sample.Console/Migrations/20251123154004_InitialTickerQOperationalStore.cs new file mode 100644 index 00000000..9768f32d --- /dev/null +++ b/samples/TickerQ.Sample.Console/Migrations/20251123154004_InitialTickerQOperationalStore.cs @@ -0,0 +1,178 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickerQ.Sample.Console.Migrations +{ + /// + public partial class InitialTickerQOperationalStore : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "ticker"); + + migrationBuilder.CreateTable( + name: "CronTickers", + schema: "ticker", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Expression = table.Column(type: "TEXT", nullable: true), + Request = table.Column(type: "BLOB", nullable: true), + Retries = table.Column(type: "INTEGER", nullable: false), + RetryIntervals = table.Column(type: "TEXT", nullable: true), + Function = table.Column(type: "TEXT", nullable: true), + Description = table.Column(type: "TEXT", nullable: true), + InitIdentifier = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CronTickers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "TimeTickers", + schema: "ticker", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Function = table.Column(type: "TEXT", nullable: true), + Description = table.Column(type: "TEXT", nullable: true), + InitIdentifier = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + LockHolder = table.Column(type: "TEXT", nullable: true), + Request = table.Column(type: "BLOB", nullable: true), + ExecutionTime = table.Column(type: "TEXT", nullable: true), + LockedAt = table.Column(type: "TEXT", nullable: true), + ExecutedAt = table.Column(type: "TEXT", nullable: true), + ExceptionMessage = table.Column(type: "TEXT", nullable: true), + SkippedReason = table.Column(type: "TEXT", nullable: true), + ElapsedTime = table.Column(type: "INTEGER", nullable: false), + Retries = table.Column(type: "INTEGER", nullable: false), + RetryCount = table.Column(type: "INTEGER", nullable: false), + RetryIntervals = table.Column(type: "TEXT", nullable: true), + ParentId = table.Column(type: "TEXT", nullable: true), + RunCondition = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TimeTickers", x => x.Id); + table.ForeignKey( + name: "FK_TimeTickers_TimeTickers_ParentId", + column: x => x.ParentId, + principalSchema: "ticker", + principalTable: "TimeTickers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "CronTickerOccurrences", + schema: "ticker", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + LockHolder = table.Column(type: "TEXT", nullable: true), + ExecutionTime = table.Column(type: "TEXT", nullable: false), + CronTickerId = table.Column(type: "TEXT", nullable: false), + LockedAt = table.Column(type: "TEXT", nullable: true), + ExecutedAt = table.Column(type: "TEXT", nullable: true), + ExceptionMessage = table.Column(type: "TEXT", nullable: true), + SkippedReason = table.Column(type: "TEXT", nullable: true), + ElapsedTime = table.Column(type: "INTEGER", nullable: false), + RetryCount = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CronTickerOccurrences", x => x.Id); + table.ForeignKey( + name: "FK_CronTickerOccurrences_CronTickers_CronTickerId", + column: x => x.CronTickerId, + principalSchema: "ticker", + principalTable: "CronTickers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CronTickerOccurrence_CronTickerId", + schema: "ticker", + table: "CronTickerOccurrences", + column: "CronTickerId"); + + migrationBuilder.CreateIndex( + name: "IX_CronTickerOccurrence_ExecutionTime", + schema: "ticker", + table: "CronTickerOccurrences", + column: "ExecutionTime"); + + migrationBuilder.CreateIndex( + name: "IX_CronTickerOccurrence_Status_ExecutionTime", + schema: "ticker", + table: "CronTickerOccurrences", + columns: new[] { "Status", "ExecutionTime" }); + + migrationBuilder.CreateIndex( + name: "UQ_CronTickerId_ExecutionTime", + schema: "ticker", + table: "CronTickerOccurrences", + columns: new[] { "CronTickerId", "ExecutionTime" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CronTickers_Expression", + schema: "ticker", + table: "CronTickers", + column: "Expression"); + + migrationBuilder.CreateIndex( + name: "IX_Function_Expression", + schema: "ticker", + table: "CronTickers", + columns: new[] { "Function", "Expression" }); + + migrationBuilder.CreateIndex( + name: "IX_TimeTicker_ExecutionTime", + schema: "ticker", + table: "TimeTickers", + column: "ExecutionTime"); + + migrationBuilder.CreateIndex( + name: "IX_TimeTicker_Status_ExecutionTime", + schema: "ticker", + table: "TimeTickers", + columns: new[] { "Status", "ExecutionTime" }); + + migrationBuilder.CreateIndex( + name: "IX_TimeTickers_ParentId", + schema: "ticker", + table: "TimeTickers", + column: "ParentId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CronTickerOccurrences", + schema: "ticker"); + + migrationBuilder.DropTable( + name: "TimeTickers", + schema: "ticker"); + + migrationBuilder.DropTable( + name: "CronTickers", + schema: "ticker"); + } + } +} diff --git a/samples/TickerQ.Sample.Console/Migrations/TickerQDbContextModelSnapshot.cs b/samples/TickerQ.Sample.Console/Migrations/TickerQDbContextModelSnapshot.cs new file mode 100644 index 00000000..63d648cd --- /dev/null +++ b/samples/TickerQ.Sample.Console/Migrations/TickerQDbContextModelSnapshot.cs @@ -0,0 +1,226 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickerQ.EntityFrameworkCore.DbContextFactory; + +#nullable disable + +namespace TickerQ.Sample.Console.Migrations +{ + [DbContext(typeof(TickerQDbContext))] + partial class TickerQDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Expression") + .HasColumnType("TEXT"); + + b.Property("Function") + .HasColumnType("TEXT"); + + b.Property("InitIdentifier") + .HasColumnType("TEXT"); + + b.Property("Request") + .HasColumnType("BLOB"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("RetryIntervals") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Expression") + .HasDatabaseName("IX_CronTickers_Expression"); + + b.HasIndex("Function", "Expression") + .HasDatabaseName("IX_Function_Expression"); + + b.ToTable("CronTickers", "ticker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerOccurrenceEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CronTickerId") + .HasColumnType("TEXT"); + + b.Property("ElapsedTime") + .HasColumnType("INTEGER"); + + b.Property("ExceptionMessage") + .HasColumnType("TEXT"); + + b.Property("ExecutedAt") + .HasColumnType("TEXT"); + + b.Property("ExecutionTime") + .HasColumnType("TEXT"); + + b.Property("LockHolder") + .HasColumnType("TEXT"); + + b.Property("LockedAt") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("SkippedReason") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CronTickerId") + .HasDatabaseName("IX_CronTickerOccurrence_CronTickerId"); + + b.HasIndex("ExecutionTime") + .HasDatabaseName("IX_CronTickerOccurrence_ExecutionTime"); + + b.HasIndex("CronTickerId", "ExecutionTime") + .IsUnique() + .HasDatabaseName("UQ_CronTickerId_ExecutionTime"); + + b.HasIndex("Status", "ExecutionTime") + .HasDatabaseName("IX_CronTickerOccurrence_Status_ExecutionTime"); + + b.ToTable("CronTickerOccurrences", "ticker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ElapsedTime") + .HasColumnType("INTEGER"); + + b.Property("ExceptionMessage") + .HasColumnType("TEXT"); + + b.Property("ExecutedAt") + .HasColumnType("TEXT"); + + b.Property("ExecutionTime") + .HasColumnType("TEXT"); + + b.Property("Function") + .HasColumnType("TEXT"); + + b.Property("InitIdentifier") + .HasColumnType("TEXT"); + + b.Property("LockHolder") + .HasColumnType("TEXT"); + + b.Property("LockedAt") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("Request") + .HasColumnType("BLOB"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("RetryIntervals") + .HasColumnType("TEXT"); + + b.Property("RunCondition") + .HasColumnType("INTEGER"); + + b.Property("SkippedReason") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ExecutionTime") + .HasDatabaseName("IX_TimeTicker_ExecutionTime"); + + b.HasIndex("ParentId"); + + b.HasIndex("Status", "ExecutionTime") + .HasDatabaseName("IX_TimeTicker_Status_ExecutionTime"); + + b.ToTable("TimeTickers", "ticker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerOccurrenceEntity", b => + { + b.HasOne("TickerQ.Utilities.Entities.CronTickerEntity", "CronTicker") + .WithMany() + .HasForeignKey("CronTickerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CronTicker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.HasOne("TickerQ.Utilities.Entities.TimeTickerEntity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.Navigation("Children"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/TickerQ.Sample.Console/Program.cs b/samples/TickerQ.Sample.Console/Program.cs new file mode 100644 index 00000000..a04d5a9e --- /dev/null +++ b/samples/TickerQ.Sample.Console/Program.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using TickerQ.DependencyInjection; +using TickerQ.EntityFrameworkCore.DbContextFactory; +using TickerQ.EntityFrameworkCore.DependencyInjection; +using TickerQ.Utilities; +using TickerQ.Utilities.Base; +using TickerQ.Utilities.Entities; +using TickerQ.Utilities.Interfaces.Managers; + +var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + // Configure TickerQ with SQLite operational store (file-based) + services.AddTickerQ(options => + { + options.AddOperationalStore(efOptions => + { + efOptions.UseTickerQDbContext(dbOptions => + { + dbOptions.UseSqlite( + "Data Source=tickerq-console.db", + b => b.MigrationsAssembly("TickerQ.Sample.Console")); + }); + }); + }); + + services.AddHostedService(); + }) + .Build(); + +// Ensure TickerQ operational store schema is applied +using (var scope = host.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); +} + +// Build function metadata so TickerFunctionProvider.TickerFunctions is initialized +TickerFunctionProvider.Build(); + +await host.RunAsync(); + +// Simple sample job +public class ConsoleSampleJobs +{ + [TickerFunction("ConsoleSample_HelloWorld")] + public Task HelloWorldAsync(TickerFunctionContext context, CancellationToken cancellationToken) + { + Console.WriteLine($"[Console] Hello from TickerQ! Id={context.Id}, ScheduledFor={context.ScheduledFor:O}"); + return Task.CompletedTask; + } +} + +// Hosted service that schedules a single job on startup +public class SampleScheduler : IHostedService +{ + private readonly ITimeTickerManager _timeTickerManager; + + public SampleScheduler(ITimeTickerManager timeTickerManager) + { + _timeTickerManager = timeTickerManager; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var result = await _timeTickerManager.AddAsync(new TimeTickerEntity + { + Function = "ConsoleSample_HelloWorld", + ExecutionTime = DateTime.UtcNow.AddSeconds(5) + }, cancellationToken); + + if (!result.IsSucceeded) + { + Console.WriteLine($"Failed to schedule console sample job. Exception: {result.Exception}"); + return; + } + + Console.WriteLine($"Scheduled console sample job with Id={result.Result.Id}, ScheduledFor={result.Result.ExecutionTime:O}"); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/samples/TickerQ.Sample.Console/TickerQ.Sample.Console.csproj b/samples/TickerQ.Sample.Console/TickerQ.Sample.Console.csproj new file mode 100644 index 00000000..ed863b6f --- /dev/null +++ b/samples/TickerQ.Sample.Console/TickerQ.Sample.Console.csproj @@ -0,0 +1,23 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + + diff --git a/samples/TickerQ.Sample.WebApi/Migrations/20251123154016_InitialTickerQOperationalStore.Designer.cs b/samples/TickerQ.Sample.WebApi/Migrations/20251123154016_InitialTickerQOperationalStore.Designer.cs new file mode 100644 index 00000000..87405b90 --- /dev/null +++ b/samples/TickerQ.Sample.WebApi/Migrations/20251123154016_InitialTickerQOperationalStore.Designer.cs @@ -0,0 +1,229 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickerQ.EntityFrameworkCore.DbContextFactory; + +#nullable disable + +namespace TickerQ.Sample.WebApi.Migrations +{ + [DbContext(typeof(TickerQDbContext))] + [Migration("20251123154016_InitialTickerQOperationalStore")] + partial class InitialTickerQOperationalStore + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Expression") + .HasColumnType("TEXT"); + + b.Property("Function") + .HasColumnType("TEXT"); + + b.Property("InitIdentifier") + .HasColumnType("TEXT"); + + b.Property("Request") + .HasColumnType("BLOB"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("RetryIntervals") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Expression") + .HasDatabaseName("IX_CronTickers_Expression"); + + b.HasIndex("Function", "Expression") + .HasDatabaseName("IX_Function_Expression"); + + b.ToTable("CronTickers", "ticker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerOccurrenceEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CronTickerId") + .HasColumnType("TEXT"); + + b.Property("ElapsedTime") + .HasColumnType("INTEGER"); + + b.Property("ExceptionMessage") + .HasColumnType("TEXT"); + + b.Property("ExecutedAt") + .HasColumnType("TEXT"); + + b.Property("ExecutionTime") + .HasColumnType("TEXT"); + + b.Property("LockHolder") + .HasColumnType("TEXT"); + + b.Property("LockedAt") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("SkippedReason") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CronTickerId") + .HasDatabaseName("IX_CronTickerOccurrence_CronTickerId"); + + b.HasIndex("ExecutionTime") + .HasDatabaseName("IX_CronTickerOccurrence_ExecutionTime"); + + b.HasIndex("CronTickerId", "ExecutionTime") + .IsUnique() + .HasDatabaseName("UQ_CronTickerId_ExecutionTime"); + + b.HasIndex("Status", "ExecutionTime") + .HasDatabaseName("IX_CronTickerOccurrence_Status_ExecutionTime"); + + b.ToTable("CronTickerOccurrences", "ticker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ElapsedTime") + .HasColumnType("INTEGER"); + + b.Property("ExceptionMessage") + .HasColumnType("TEXT"); + + b.Property("ExecutedAt") + .HasColumnType("TEXT"); + + b.Property("ExecutionTime") + .HasColumnType("TEXT"); + + b.Property("Function") + .HasColumnType("TEXT"); + + b.Property("InitIdentifier") + .HasColumnType("TEXT"); + + b.Property("LockHolder") + .HasColumnType("TEXT"); + + b.Property("LockedAt") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("Request") + .HasColumnType("BLOB"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("RetryIntervals") + .HasColumnType("TEXT"); + + b.Property("RunCondition") + .HasColumnType("INTEGER"); + + b.Property("SkippedReason") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ExecutionTime") + .HasDatabaseName("IX_TimeTicker_ExecutionTime"); + + b.HasIndex("ParentId"); + + b.HasIndex("Status", "ExecutionTime") + .HasDatabaseName("IX_TimeTicker_Status_ExecutionTime"); + + b.ToTable("TimeTickers", "ticker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerOccurrenceEntity", b => + { + b.HasOne("TickerQ.Utilities.Entities.CronTickerEntity", "CronTicker") + .WithMany() + .HasForeignKey("CronTickerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CronTicker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.HasOne("TickerQ.Utilities.Entities.TimeTickerEntity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.Navigation("Children"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/TickerQ.Sample.WebApi/Migrations/20251123154016_InitialTickerQOperationalStore.cs b/samples/TickerQ.Sample.WebApi/Migrations/20251123154016_InitialTickerQOperationalStore.cs new file mode 100644 index 00000000..6c94dd30 --- /dev/null +++ b/samples/TickerQ.Sample.WebApi/Migrations/20251123154016_InitialTickerQOperationalStore.cs @@ -0,0 +1,178 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickerQ.Sample.WebApi.Migrations +{ + /// + public partial class InitialTickerQOperationalStore : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "ticker"); + + migrationBuilder.CreateTable( + name: "CronTickers", + schema: "ticker", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Expression = table.Column(type: "TEXT", nullable: true), + Request = table.Column(type: "BLOB", nullable: true), + Retries = table.Column(type: "INTEGER", nullable: false), + RetryIntervals = table.Column(type: "TEXT", nullable: true), + Function = table.Column(type: "TEXT", nullable: true), + Description = table.Column(type: "TEXT", nullable: true), + InitIdentifier = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CronTickers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "TimeTickers", + schema: "ticker", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Function = table.Column(type: "TEXT", nullable: true), + Description = table.Column(type: "TEXT", nullable: true), + InitIdentifier = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + LockHolder = table.Column(type: "TEXT", nullable: true), + Request = table.Column(type: "BLOB", nullable: true), + ExecutionTime = table.Column(type: "TEXT", nullable: true), + LockedAt = table.Column(type: "TEXT", nullable: true), + ExecutedAt = table.Column(type: "TEXT", nullable: true), + ExceptionMessage = table.Column(type: "TEXT", nullable: true), + SkippedReason = table.Column(type: "TEXT", nullable: true), + ElapsedTime = table.Column(type: "INTEGER", nullable: false), + Retries = table.Column(type: "INTEGER", nullable: false), + RetryCount = table.Column(type: "INTEGER", nullable: false), + RetryIntervals = table.Column(type: "TEXT", nullable: true), + ParentId = table.Column(type: "TEXT", nullable: true), + RunCondition = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TimeTickers", x => x.Id); + table.ForeignKey( + name: "FK_TimeTickers_TimeTickers_ParentId", + column: x => x.ParentId, + principalSchema: "ticker", + principalTable: "TimeTickers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "CronTickerOccurrences", + schema: "ticker", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + LockHolder = table.Column(type: "TEXT", nullable: true), + ExecutionTime = table.Column(type: "TEXT", nullable: false), + CronTickerId = table.Column(type: "TEXT", nullable: false), + LockedAt = table.Column(type: "TEXT", nullable: true), + ExecutedAt = table.Column(type: "TEXT", nullable: true), + ExceptionMessage = table.Column(type: "TEXT", nullable: true), + SkippedReason = table.Column(type: "TEXT", nullable: true), + ElapsedTime = table.Column(type: "INTEGER", nullable: false), + RetryCount = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CronTickerOccurrences", x => x.Id); + table.ForeignKey( + name: "FK_CronTickerOccurrences_CronTickers_CronTickerId", + column: x => x.CronTickerId, + principalSchema: "ticker", + principalTable: "CronTickers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CronTickerOccurrence_CronTickerId", + schema: "ticker", + table: "CronTickerOccurrences", + column: "CronTickerId"); + + migrationBuilder.CreateIndex( + name: "IX_CronTickerOccurrence_ExecutionTime", + schema: "ticker", + table: "CronTickerOccurrences", + column: "ExecutionTime"); + + migrationBuilder.CreateIndex( + name: "IX_CronTickerOccurrence_Status_ExecutionTime", + schema: "ticker", + table: "CronTickerOccurrences", + columns: new[] { "Status", "ExecutionTime" }); + + migrationBuilder.CreateIndex( + name: "UQ_CronTickerId_ExecutionTime", + schema: "ticker", + table: "CronTickerOccurrences", + columns: new[] { "CronTickerId", "ExecutionTime" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CronTickers_Expression", + schema: "ticker", + table: "CronTickers", + column: "Expression"); + + migrationBuilder.CreateIndex( + name: "IX_Function_Expression", + schema: "ticker", + table: "CronTickers", + columns: new[] { "Function", "Expression" }); + + migrationBuilder.CreateIndex( + name: "IX_TimeTicker_ExecutionTime", + schema: "ticker", + table: "TimeTickers", + column: "ExecutionTime"); + + migrationBuilder.CreateIndex( + name: "IX_TimeTicker_Status_ExecutionTime", + schema: "ticker", + table: "TimeTickers", + columns: new[] { "Status", "ExecutionTime" }); + + migrationBuilder.CreateIndex( + name: "IX_TimeTickers_ParentId", + schema: "ticker", + table: "TimeTickers", + column: "ParentId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CronTickerOccurrences", + schema: "ticker"); + + migrationBuilder.DropTable( + name: "TimeTickers", + schema: "ticker"); + + migrationBuilder.DropTable( + name: "CronTickers", + schema: "ticker"); + } + } +} diff --git a/samples/TickerQ.Sample.WebApi/Migrations/TickerQDbContextModelSnapshot.cs b/samples/TickerQ.Sample.WebApi/Migrations/TickerQDbContextModelSnapshot.cs new file mode 100644 index 00000000..81c3d049 --- /dev/null +++ b/samples/TickerQ.Sample.WebApi/Migrations/TickerQDbContextModelSnapshot.cs @@ -0,0 +1,226 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickerQ.EntityFrameworkCore.DbContextFactory; + +#nullable disable + +namespace TickerQ.Sample.WebApi.Migrations +{ + [DbContext(typeof(TickerQDbContext))] + partial class TickerQDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Expression") + .HasColumnType("TEXT"); + + b.Property("Function") + .HasColumnType("TEXT"); + + b.Property("InitIdentifier") + .HasColumnType("TEXT"); + + b.Property("Request") + .HasColumnType("BLOB"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("RetryIntervals") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Expression") + .HasDatabaseName("IX_CronTickers_Expression"); + + b.HasIndex("Function", "Expression") + .HasDatabaseName("IX_Function_Expression"); + + b.ToTable("CronTickers", "ticker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerOccurrenceEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CronTickerId") + .HasColumnType("TEXT"); + + b.Property("ElapsedTime") + .HasColumnType("INTEGER"); + + b.Property("ExceptionMessage") + .HasColumnType("TEXT"); + + b.Property("ExecutedAt") + .HasColumnType("TEXT"); + + b.Property("ExecutionTime") + .HasColumnType("TEXT"); + + b.Property("LockHolder") + .HasColumnType("TEXT"); + + b.Property("LockedAt") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("SkippedReason") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CronTickerId") + .HasDatabaseName("IX_CronTickerOccurrence_CronTickerId"); + + b.HasIndex("ExecutionTime") + .HasDatabaseName("IX_CronTickerOccurrence_ExecutionTime"); + + b.HasIndex("CronTickerId", "ExecutionTime") + .IsUnique() + .HasDatabaseName("UQ_CronTickerId_ExecutionTime"); + + b.HasIndex("Status", "ExecutionTime") + .HasDatabaseName("IX_CronTickerOccurrence_Status_ExecutionTime"); + + b.ToTable("CronTickerOccurrences", "ticker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ElapsedTime") + .HasColumnType("INTEGER"); + + b.Property("ExceptionMessage") + .HasColumnType("TEXT"); + + b.Property("ExecutedAt") + .HasColumnType("TEXT"); + + b.Property("ExecutionTime") + .HasColumnType("TEXT"); + + b.Property("Function") + .HasColumnType("TEXT"); + + b.Property("InitIdentifier") + .HasColumnType("TEXT"); + + b.Property("LockHolder") + .HasColumnType("TEXT"); + + b.Property("LockedAt") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("Request") + .HasColumnType("BLOB"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("RetryIntervals") + .HasColumnType("TEXT"); + + b.Property("RunCondition") + .HasColumnType("INTEGER"); + + b.Property("SkippedReason") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ExecutionTime") + .HasDatabaseName("IX_TimeTicker_ExecutionTime"); + + b.HasIndex("ParentId"); + + b.HasIndex("Status", "ExecutionTime") + .HasDatabaseName("IX_TimeTicker_Status_ExecutionTime"); + + b.ToTable("TimeTickers", "ticker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.CronTickerOccurrenceEntity", b => + { + b.HasOne("TickerQ.Utilities.Entities.CronTickerEntity", "CronTicker") + .WithMany() + .HasForeignKey("CronTickerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CronTicker"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.HasOne("TickerQ.Utilities.Entities.TimeTickerEntity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("TickerQ.Utilities.Entities.TimeTickerEntity", b => + { + b.Navigation("Children"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/TickerQ.Sample.WebApi/Program.cs b/samples/TickerQ.Sample.WebApi/Program.cs new file mode 100644 index 00000000..44c56e5f --- /dev/null +++ b/samples/TickerQ.Sample.WebApi/Program.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using TickerQ.DependencyInjection; +using TickerQ.EntityFrameworkCore.DbContextFactory; +using TickerQ.EntityFrameworkCore.DependencyInjection; +using TickerQ.Utilities.Base; +using TickerQ.Utilities.Entities; +using TickerQ.Utilities.Interfaces.Managers; + +var builder = WebApplication.CreateBuilder(args); + +// TickerQ setup with SQLite operational store (file-based) +builder.Services.AddTickerQ(options => +{ + options.AddOperationalStore(efOptions => + { + efOptions.UseTickerQDbContext(dbOptions => + { + dbOptions.UseSqlite( + "Data Source=tickerq-webapi.db", + b => b.MigrationsAssembly("TickerQ.Sample.WebApi")); + }); + }); +}); + +var app = builder.Build(); + +// Ensure TickerQ operational store schema is applied +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); +} + +// Activate TickerQ job processor (mirrors docs' minimal setup) +app.UseTickerQ(); + +// Minimal endpoint to schedule the sample job +app.MapPost("/schedule-sample", async (ITimeTickerManager manager) => +{ + var result = await manager.AddAsync(new TimeTickerEntity + { + Function = "WebApiSample_HelloWorld", + ExecutionTime = DateTime.UtcNow.AddSeconds(5) + }); + + return Results.Ok(new { result.Result.Id, ScheduledFor = result.Result.ExecutionTime }); +}); + +app.Run(); + +// Simple sample job +public class SampleJobs +{ + [TickerFunction("WebApiSample_HelloWorld")] + public Task HelloWorldAsync(TickerFunctionContext context, CancellationToken cancellationToken) + { + Console.WriteLine($"[WebApi] Hello from TickerQ! Id={context.Id}, ScheduledFor={context.ScheduledFor:O}"); + return Task.CompletedTask; + } +} diff --git a/samples/TickerQ.Sample.WebApi/TickerQ.Sample.WebApi.csproj b/samples/TickerQ.Sample.WebApi/TickerQ.Sample.WebApi.csproj new file mode 100644 index 00000000..fe4590fb --- /dev/null +++ b/samples/TickerQ.Sample.WebApi/TickerQ.Sample.WebApi.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/TickerQ.Dashboard/DependencyInjection/ServiceCollectionExtensions.cs b/src/TickerQ.Dashboard/DependencyInjection/ServiceCollectionExtensions.cs index c1c28724..81a78c5e 100644 --- a/src/TickerQ.Dashboard/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/TickerQ.Dashboard/DependencyInjection/ServiceCollectionExtensions.cs @@ -127,7 +127,7 @@ internal static void UseDashboardWithEndpoints(this IA var htmlContent = await reader.ReadToEndAsync(); // Inject the base tag and other replacements into the HTML - htmlContent = ReplaceBasePath(htmlContent, basePath, config); + htmlContent = ReplaceBasePath(htmlContent, context, basePath, config); context.Response.ContentType = "text/html"; context.Response.StatusCode = 200; @@ -149,11 +149,19 @@ private static string NormalizeBasePath(string basePath) return basePath.TrimEnd('/'); } - private static string ReplaceBasePath(string htmlContent, string basePath, DashboardOptionsBuilder config) + private static string ReplaceBasePath(string htmlContent, HttpContext httpContext, string basePath, DashboardOptionsBuilder config) { if (string.IsNullOrEmpty(htmlContent)) return htmlContent ?? string.Empty; + // Compute the frontend base path as PathBase + backend basePath. + // This ensures correct URLs when the host app uses UsePathBase. + var pathBase = httpContext.Request.PathBase.HasValue + ? httpContext.Request.PathBase.Value + : string.Empty; + + var frontendBasePath = CombinePathBase(pathBase, basePath); + // Build the config object var authInfo = new { @@ -164,7 +172,7 @@ private static string ReplaceBasePath(string htmlContent, string basePath, Dashb var envConfig = new { - basePath, + basePath = frontendBasePath, backendDomain = config.BackendDomain, auth = authInfo }; @@ -181,7 +189,7 @@ private static string ReplaceBasePath(string htmlContent, string basePath, Dashb json = SanitizeForInlineScript(json); // Add base tag for proper asset loading - var baseTag = $@""; + var baseTag = $@""; // Inline bootstrap: set TickerQConfig and derive __dynamic_base__ (vite-plugin-dynamic-base) var script = $@"