diff --git a/AzureDataModel/AzureDataModel.csproj b/AzureDataModel/AzureDataModel.csproj
new file mode 100644
index 0000000..7a165ce
--- /dev/null
+++ b/AzureDataModel/AzureDataModel.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/AzureDataModel/AzureDbContext.cs b/AzureDataModel/AzureDbContext.cs
new file mode 100644
index 0000000..e44c194
--- /dev/null
+++ b/AzureDataModel/AzureDbContext.cs
@@ -0,0 +1,40 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using TaskScheduler = AzureDataModel.Entities.TaskScheduler;
+
+namespace AzureDataModel;
+
+public class AzureDbContext : DbContext
+{
+ public AzureDbContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ var configuration = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("appsettings.json")
+ .Build();
+
+ var connectionString = configuration.GetConnectionString("AzureConnection");
+ optionsBuilder.UseAzureSql(connectionString, options => options.CommandTimeout(300));
+ }
+
+ public DbSet TaskSchedulers { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasKey(e => e.Id);
+
+ modelBuilder.Entity()
+ .Property(e => e.RowPointer)
+ .HasDefaultValueSql("newid()");
+
+ modelBuilder.Entity()
+ .Property(e => e.CreateDate)
+ .HasDefaultValueSql("getdate()");
+ }
+}
\ No newline at end of file
diff --git a/AzureDataModel/Dtos/TaskScheduler.cs b/AzureDataModel/Dtos/TaskScheduler.cs
new file mode 100644
index 0000000..f7ec616
--- /dev/null
+++ b/AzureDataModel/Dtos/TaskScheduler.cs
@@ -0,0 +1,10 @@
+namespace AzureDataModel.Dtos;
+
+public class TaskSchedulerDto
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public string Path { get; set; }
+ public string CronOptions { get; set; }
+ public DateTime CreateDate { get; set; }
+}
\ No newline at end of file
diff --git a/AzureDataModel/Entities/TaskScheduler.cs b/AzureDataModel/Entities/TaskScheduler.cs
new file mode 100644
index 0000000..b1f14dd
--- /dev/null
+++ b/AzureDataModel/Entities/TaskScheduler.cs
@@ -0,0 +1,11 @@
+namespace AzureDataModel.Entities;
+
+public class TaskScheduler
+{
+ public int Id { get; set; }
+ public Guid RowPointer { get; set; }
+ public string Name { get; set; }
+ public string Path { get; set; }
+ public string CronOptions { get; set; }
+ public DateTime CreateDate { get; set; }
+}
\ No newline at end of file
diff --git a/AzureDataModel/MappingProfile.cs b/AzureDataModel/MappingProfile.cs
new file mode 100644
index 0000000..2c2918c
--- /dev/null
+++ b/AzureDataModel/MappingProfile.cs
@@ -0,0 +1,12 @@
+using AutoMapper;
+using AzureDataModel.Dtos;
+
+namespace AzureDataModel;
+
+public class MappingProfile : Profile
+{
+ public MappingProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+}
\ No newline at end of file
diff --git a/AzureDataModel/Services/ITaskSchedulerService.cs b/AzureDataModel/Services/ITaskSchedulerService.cs
new file mode 100644
index 0000000..5eb356f
--- /dev/null
+++ b/AzureDataModel/Services/ITaskSchedulerService.cs
@@ -0,0 +1,8 @@
+using AzureDataModel.Dtos;
+
+namespace AzureDataModel.Services;
+
+public interface ITaskSchedulerService
+{
+ Task> GetTaskSchedulers();
+}
\ No newline at end of file
diff --git a/AzureDataModel/Services/TaskSchedulerService.cs b/AzureDataModel/Services/TaskSchedulerService.cs
new file mode 100644
index 0000000..8af9b20
--- /dev/null
+++ b/AzureDataModel/Services/TaskSchedulerService.cs
@@ -0,0 +1,53 @@
+using AutoMapper;
+using AzureDataModel.Dtos;
+using Microsoft.EntityFrameworkCore;
+using TaskScheduler = AzureDataModel.Entities.TaskScheduler;
+
+namespace AzureDataModel.Services;
+
+public class TaskSchedulerService(AzureDbContext context, IMapper mapper) : ITaskSchedulerService
+{
+ public async Task> GetTaskSchedulers()
+ {
+ return await context.TaskSchedulers.Select(x => mapper.Map(x)).ToListAsync();
+ }
+
+ public async Task GetTaskSchedulerById(Guid id)
+ {
+ TaskSchedulerDto? taskScheduler = await context.TaskSchedulers.Where(x => x.RowPointer == id)
+ .Select(x => mapper.Map(x)).FirstOrDefaultAsync();
+ return taskScheduler;
+ }
+
+ public async Task GetTaskSchedulerByTaskName(string taskName)
+ {
+ TaskSchedulerDto? taskScheduler = await context.TaskSchedulers.Where(x => x.Name == taskName)
+ .Select(x => mapper.Map(x)).FirstOrDefaultAsync();
+ return taskScheduler;
+ }
+
+ public async Task AddTaskScheduler(TaskSchedulerDto taskSchedulerDto)
+ {
+ TaskScheduler taskScheduler = mapper.Map(taskSchedulerDto);
+ context.TaskSchedulers.Add(taskScheduler);
+ return await context.SaveChangesAsync();
+ }
+
+ public async Task UpdateTaskScheduler(TaskSchedulerDto taskSchedulerDto)
+ {
+ TaskScheduler taskScheduler = mapper.Map(taskSchedulerDto);
+ context.TaskSchedulers.Update(taskScheduler);
+ return await context.SaveChangesAsync();
+ }
+
+ public async Task DeleteTaskScheduler(Guid id)
+ {
+ TaskScheduler? taskScheduler =
+ await context.TaskSchedulers.Where(x => x.RowPointer == id).FirstOrDefaultAsync() ?? null;
+
+ if (taskScheduler == null) return 0;
+
+ context.TaskSchedulers.Remove(taskScheduler);
+ return await context.SaveChangesAsync();
+ }
+}
\ No newline at end of file
diff --git a/AzureDataModel/appsettings_Azure.json b/AzureDataModel/appsettings_Azure.json
new file mode 100644
index 0000000..c10bc55
--- /dev/null
+++ b/AzureDataModel/appsettings_Azure.json
@@ -0,0 +1,5 @@
+{
+ "ConnectionStrings": {
+ "AzureConnection": "Server=tcp:fakrosno-hangfire.database.windows.net,1433;Initial Catalog=FaKrosnoHangfire;Persist Security Info=False;User ID=fakrosno_hangfire@trentblaugranagmail.onmicrosoft.com;Password=juhrot-zigQuz-6jydpu;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Authentication=\"Active Directory Password\";"
+ }
+}
\ No newline at end of file
diff --git a/FA_WEB.sln b/FA_WEB.sln
index 8d46ffe..81ea7c8 100644
--- a/FA_WEB.sln
+++ b/FA_WEB.sln
@@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FaKrosnoEfDataModel", "FaKr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SytelineSaAppEfDataModel", "SytelineSaAppEfDataModel\SytelineSaAppEfDataModel.csproj", "{02B7F642-62C1-4BA2-87B0-0B9214319095}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HangfireApi", "HangfireApi\HangfireApi.csproj", "{52106FF2-E714-4FFB-B383-38B1E782B352}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureDataModel", "AzureDataModel\AzureDataModel.csproj", "{5971D564-5108-4777-8273-DDE90BF39879}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -35,6 +39,14 @@ Global
{02B7F642-62C1-4BA2-87B0-0B9214319095}.Debug|Any CPU.Build.0 = Debug|Any CPU
{02B7F642-62C1-4BA2-87B0-0B9214319095}.Release|Any CPU.ActiveCfg = Release|Any CPU
{02B7F642-62C1-4BA2-87B0-0B9214319095}.Release|Any CPU.Build.0 = Release|Any CPU
+ {52106FF2-E714-4FFB-B383-38B1E782B352}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {52106FF2-E714-4FFB-B383-38B1E782B352}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {52106FF2-E714-4FFB-B383-38B1E782B352}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {52106FF2-E714-4FFB-B383-38B1E782B352}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5971D564-5108-4777-8273-DDE90BF39879}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5971D564-5108-4777-8273-DDE90BF39879}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5971D564-5108-4777-8273-DDE90BF39879}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5971D564-5108-4777-8273-DDE90BF39879}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -42,5 +54,6 @@ Global
GlobalSection(NestedProjects) = preSolution
{4671E3B6-4D11-4521-A927-DC3B1072FC4A} = {6607ACC9-71B9-4095-9A81-B6FBB0AAAF17}
{02B7F642-62C1-4BA2-87B0-0B9214319095} = {6607ACC9-71B9-4095-9A81-B6FBB0AAAF17}
+ {5971D564-5108-4777-8273-DDE90BF39879} = {6607ACC9-71B9-4095-9A81-B6FBB0AAAF17}
EndGlobalSection
EndGlobal
diff --git a/HangfireApi/Controllers/HangfireJobsController.cs b/HangfireApi/Controllers/HangfireJobsController.cs
new file mode 100644
index 0000000..ca19e30
--- /dev/null
+++ b/HangfireApi/Controllers/HangfireJobsController.cs
@@ -0,0 +1,90 @@
+using System.Diagnostics;
+using AzureDataModel.Dtos;
+using AzureDataModel.Services;
+using Hangfire;
+using Hangfire.Storage;
+using HangfireApi.Dtos;
+using Microsoft.AspNetCore.Mvc;
+
+namespace HangfireApi.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class HangfireJobsController(JobStorage jobStorage, IRecurringJobManager recurringJobManager, ITaskSchedulerService service) : ControllerBase
+{
+ [HttpGet("GetJobsToRun")]
+ public async Task>> GetJobsToRun()
+ {
+ IList jobsToRun = new List();
+
+ using (IStorageConnection? connection = jobStorage.GetConnection())
+ {
+ IList? recurringJobs = connection.GetRecurringJobs();
+ IList? taskSchedulers = (await service.GetTaskSchedulers()).ToList();
+
+ foreach (var recurringJob in recurringJobs)
+ {
+ TaskSchedulerDto? taskScheduler = taskSchedulers?.FirstOrDefault(ts => ts.Name == recurringJob.Id);
+
+ if (taskScheduler != null)
+ {
+ jobsToRun.Add(new JobDto(recurringJob.Id, recurringJob.Cron, taskScheduler.Path,
+ recurringJob.LastExecution, recurringJob.NextExecution, recurringJob.Job));
+ }
+ }
+ }
+
+ return Ok(jobsToRun);
+ }
+
+ [HttpPost("RunJobs")]
+ public async Task RunJobs()
+ {
+ var jobsToRun = (await GetJobsToRun()).Value?.ToList();
+
+ if (jobsToRun == null || jobsToRun.Count == 0)
+ {
+ return BadRequest("Nie udało się pobrać zadań do uruchomienia.");
+ }
+
+ foreach (var job in jobsToRun)
+ {
+ if (!string.IsNullOrEmpty(job.Path))
+ {
+ recurringJobManager.AddOrUpdate(job.JobId, () => RunConsoleApplication(job.Path), job.Cron,
+ new RecurringJobOptions { TimeZone = TimeZoneInfo.Local });
+ }
+ }
+
+ return Ok("Zadania zostały zaplanowane do uruchamiania zgodnie z ich CRON.");
+ }
+
+ private void RunConsoleApplication(string pathToApp)
+ {
+ try
+ {
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = pathToApp,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ }
+ };
+ process.Start();
+ string output = process.StandardOutput.ReadToEnd();
+ string error = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+
+ Console.WriteLine($"Output: {output}");
+ Console.WriteLine($"Error: {error}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error executing console application: {ex.Message}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/HangfireApi/Dtos/JobDto.cs b/HangfireApi/Dtos/JobDto.cs
new file mode 100644
index 0000000..3e52d88
--- /dev/null
+++ b/HangfireApi/Dtos/JobDto.cs
@@ -0,0 +1,19 @@
+using Hangfire.Common;
+
+namespace HangfireApi.Dtos;
+
+public class JobDto(
+ string jobId,
+ string cron,
+ string path,
+ DateTime? lastExecution,
+ DateTime? nextExecution,
+ Job jobDetails)
+{
+ public string JobId { get; set; } = jobId;
+ public string Cron { get; set; } = cron;
+ public string Path { get; set; } = path;
+ public DateTime? LastExecution { get; set; } = lastExecution;
+ public DateTime? NextExecution { get; set; } = nextExecution;
+ public Job JobDetails { get; set; } = jobDetails;
+}
\ No newline at end of file
diff --git a/HangfireApi/HangfireApi.csproj b/HangfireApi/HangfireApi.csproj
new file mode 100644
index 0000000..867b55b
--- /dev/null
+++ b/HangfireApi/HangfireApi.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HangfireApi/HangfireApi.http b/HangfireApi/HangfireApi.http
new file mode 100644
index 0000000..ea1a2ba
--- /dev/null
+++ b/HangfireApi/HangfireApi.http
@@ -0,0 +1,6 @@
+@HangfireApi_HostAddress = http://localhost:5237
+
+GET {{HangfireApi_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/HangfireApi/Program.cs b/HangfireApi/Program.cs
new file mode 100644
index 0000000..0b7a1b6
--- /dev/null
+++ b/HangfireApi/Program.cs
@@ -0,0 +1,43 @@
+using Hangfire;
+using Hangfire.SqlServer;
+using HangfireApi.Controllers;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddHangfire(configuration => configuration
+ .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
+ .UseSimpleAssemblyNameTypeSerializer()
+ .UseRecommendedSerializerSettings()
+ .UseSqlServerStorage(builder.Configuration.GetConnectionString("HangfireConnection"), new SqlServerStorageOptions
+ {
+ CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
+ SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
+ QueuePollInterval = TimeSpan.Zero,
+ UseRecommendedIsolationLevel = true,
+ DisableGlobalLocks = true
+ }));
+
+builder.Services.AddControllers();
+builder.Services.AddHangfireServer();
+builder.Services.AddAuthorization();
+
+builder.Services.AddSingleton(provider =>
+ provider.GetRequiredService().GetService(typeof(JobStorage)) as JobStorage ??
+ throw new InvalidOperationException());
+
+var app = builder.Build();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseDeveloperExceptionPage();
+}
+
+app.UseRouting();
+app.UseAuthorization();
+
+app.MapControllers();
+app.MapHangfireDashboard();
+
+await ((IApplicationBuilder)app).ApplicationServices.GetRequiredService().RunJobs();
+
+await app.RunAsync();
\ No newline at end of file
diff --git a/HangfireApi/Properties/launchSettings.json b/HangfireApi/Properties/launchSettings.json
new file mode 100644
index 0000000..76fdf15
--- /dev/null
+++ b/HangfireApi/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:24492",
+ "sslPort": 44367
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5237",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7273;http://localhost:5237",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/HangfireApi/appsettings.json b/HangfireApi/appsettings.json
new file mode 100644
index 0000000..9d5551f
--- /dev/null
+++ b/HangfireApi/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "ConnectionStrings": {
+ "HangfireConnection": "Server=tcp:fakrosno-hangfire.database.windows.net,1433;Initial Catalog=FaKrosnoHangfire;Persist Security Info=False;User ID=fakrosno_hangfire@trentblaugranagmail.onmicrosoft.com;Password=juhrot-zigQuz-6jydpu;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Authentication=\"Active Directory Password\";"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}