Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

First round of pagination #41

Merged
merged 1 commit into from
Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ namespace WebScheduler.Api.ViewModels;
/// A paged collection of items.
/// </summary>
/// <typeparam name="T">The type of the items.</typeparam>
public class Connection<T>
public class PagedCollection<T>
{
public Connection() => this.Items = new List<T>();
public PagedCollection() => this.Items = new List<T>();

/// <summary>
/// Gets or sets the total count of items.
Expand Down
2 changes: 1 addition & 1 deletion Source/WebScheduler.Api/Commands/Car/GetCarPageCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public async Task<IActionResult> ExecuteAsync(PageOptions pageOptions, Cancellat
var carViewModels = this.carMapper.MapList(cars);

var httpContext = this.httpContextAccessor.HttpContext!;
var connection = new Connection<Car>()
var connection = new PagedCollection<Car>()
{
PageInfo = new PageInfo()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,9 @@ public async Task<IActionResult> ExecuteAsync(Guid scheduledTaskId, Cancellation
{
try
{
var scheduledTask = await this.scheduledTaskRepository.GetAsync(scheduledTaskId, cancellationToken).ConfigureAwait(false);
await this.scheduledTaskRepository.DeleteAsync(scheduledTaskId, cancellationToken).ConfigureAwait(false);

var result = await this.scheduledTaskRepository.DeleteAsync(scheduledTask, cancellationToken).ConfigureAwait(false);
var scheduledTaskViewModel = this.scheduledTaskMapper.Map(result);

return new ObjectResult(scheduledTaskViewModel)
{
StatusCode = StatusCodes.Status410Gone
};
return new NoContentResult();
}
catch (ScheduledTaskNotFoundException)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public async Task<IActionResult> ExecuteAsync(PageOptions pageOptions, Cancellat
var scheduledTaskViewModels = this.scheduledTaskMapper.MapList(scheduledTasks);

var httpContext = this.httpContextAccessor.HttpContext!;
var connection = new Connection<ScheduledTask>()
var connection = new PagedCollection<ScheduledTask>()
{
PageInfo = new PageInfo()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public PostScheduledTaskCommand(
public async Task<IActionResult> ExecuteAsync(SaveScheduledTask saveScheduledTask, CancellationToken cancellationToken)
{
var scheduledTask = this.saveScheduledTaskToScheduledTaskMapper.Map(saveScheduledTask);

if (scheduledTask.ScheduledTaskId == Guid.Empty)
{
scheduledTask.ScheduledTaskId = Guid.NewGuid();
}

scheduledTask = await this.scheduledTaskRepository.AddAsync(scheduledTask, cancellationToken).ConfigureAwait(false);
var scheduledTaskViewModel = this.scheduledTaskToScheduledTaskMapper.Map(scheduledTask);

Expand Down
2 changes: 1 addition & 1 deletion Source/WebScheduler.Api/Controllers/CarsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public Task<IActionResult> GetAsync(
[SwaggerResponse(
StatusCodes.Status200OK,
"A collection of cars for the specified page.",
typeof(Connection<Car>),
typeof(PagedCollection<Car>),
ContentType.RestfulJson,
ContentType.Json)]
[SwaggerResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public Task<IActionResult> GetAsync(
[SwaggerResponse(
StatusCodes.Status200OK,
"A collection of ScheduledTasks for the specified page.",
typeof(Connection<ScheduledTask>),
typeof(PagedCollection<ScheduledTask>),
ContentType.RestfulJson,
ContentType.Json)]
[SwaggerResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace WebScheduler.Api.Mappers;

using WebScheduler.Api.Constants;
using Boxed.Mapping;
using WebScheduler.Abstractions.Grains.Scheduler;

public class ScheduledTaskMetaDataToScheduledTaskMapper : IMapper<GuidIdWrapper<ScheduledTaskMetadata>, Models.ScheduledTask>, IMapper<Models.ScheduledTask, GuidIdWrapper<ScheduledTaskMetadata>>
{
private readonly IHttpContextAccessor httpContextAccessor;
private readonly LinkGenerator linkGenerator;

public ScheduledTaskMetaDataToScheduledTaskMapper(
IHttpContextAccessor httpContextAccessor,
LinkGenerator linkGenerator)
{
this.httpContextAccessor = httpContextAccessor;
this.linkGenerator = linkGenerator;
}

public void Map(GuidIdWrapper<ScheduledTaskMetadata> source, Models.ScheduledTask destination)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(destination);

destination.ScheduledTaskId = source.Id;
destination.IsEnabled = source.Value.IsEnabled;
destination.Description = source.Value.Description;
destination.Name = source.Value.Name;
destination.Url = new Uri(this.linkGenerator.GetUriByRouteValues(
this.httpContextAccessor.HttpContext!,
ScheduledTasksControllerRoute.GetScheduledTask,
new { source.Id })!);
}

public void Map(Models.ScheduledTask source, GuidIdWrapper<ScheduledTaskMetadata> destination)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(destination);
//var
//destination.Id = source.ScheduledTaskId;
//destination.IsEnabled = source.IsEnabled;
//destination.Description = source.Value.Description;
//destination.Name = source.Value.Name;
//destination.Url = new Uri(this.linkGenerator.GetUriByRouteValues(
// this.httpContextAccessor.HttpContext!,
// ScheduledTasksControllerRoute.GetScheduledTask,
// new { source.Id })!);
}
}

public record GuidIdWrapper<TValue>(Guid Id, TValue Value);
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace WebScheduler.Api;
using Orleans;
using WebScheduler.Api.Commands.Car;
using WebScheduler.Abstractions.Services;
using WebScheduler.Abstractions.Grains.Scheduler;

/// <summary>
/// <see cref="IServiceCollection"/> extension methods add project services.
Expand Down Expand Up @@ -41,7 +42,9 @@ public static IServiceCollection AddProjectMappers(this IServiceCollection servi
.AddSingleton<IMapper<SaveCar, Models.Car>, CarToSaveCarMapper>()
.AddSingleton<IMapper<Models.ScheduledTask, ScheduledTask>, ScheduledTaskToScheduledTaskMapper>()
.AddSingleton<IMapper<Models.ScheduledTask, SaveScheduledTask>, ScheduledTaskToSaveScheduledTaskMapper>()
.AddSingleton<IMapper<SaveScheduledTask, Models.ScheduledTask>, ScheduledTaskToSaveScheduledTaskMapper>();
.AddSingleton<IMapper<SaveScheduledTask, Models.ScheduledTask>, ScheduledTaskToSaveScheduledTaskMapper>()
.AddSingleton<IMapper<GuidIdWrapper<ScheduledTaskMetadata>, Models.ScheduledTask>, ScheduledTaskMetaDataToScheduledTaskMapper>()
.AddSingleton<IMapper<Models.ScheduledTask, GuidIdWrapper<ScheduledTaskMetadata>>, ScheduledTaskMetaDataToScheduledTaskMapper>();

public static IServiceCollection AddProjectRepositories(this IServiceCollection services) =>
services
Expand Down
10 changes: 5 additions & 5 deletions Source/WebScheduler.Api/Repositories/IScheduledTaskRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ public interface IScheduledTaskRepository
{
Task<ScheduledTask> AddAsync(ScheduledTask scheduledTask, CancellationToken cancellationToken);

Task<ScheduledTask> DeleteAsync(ScheduledTask scheduledTask, CancellationToken cancellationToken);
Task DeleteAsync(Guid scheduledTask, CancellationToken cancellationToken);

Task<ScheduledTask> GetAsync(Guid scheduledTaskId, CancellationToken cancellationToken);

Task<List<ScheduledTask>> GetScheduledTasksAsync(
int? first,
DateTimeOffset? createdAfter,
DateTimeOffset? createdBefore,
CancellationToken cancellationToken);
int? first,
DateTimeOffset? createdAfter,
DateTimeOffset? createdBefore,
CancellationToken cancellationToken);

Task<List<ScheduledTask>> GetScheduledTasksReverseAsync(
int? last,
Expand Down
162 changes: 120 additions & 42 deletions Source/WebScheduler.Api/Repositories/ScheduledTaskRepository.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
namespace WebScheduler.Api.Repositories;

using System.Runtime.CompilerServices;
using System.Text.Json;
using Boxed.Mapping;
using Dapper;
using MySqlConnector;
using Orleans;
using WebScheduler.Abstractions.Grains.Scheduler;
using WebScheduler.Api.Mappers;
using WebScheduler.Api.Models;
using WebScheduler.Server.Options;

public class ScheduledTaskRepository : IScheduledTaskRepository
{
private readonly IClusterClient clusterClient;
private readonly StorageOptions storageOptions;

public ScheduledTaskRepository(IClusterClient clusterClient) => this.clusterClient = clusterClient;
public ScheduledTaskRepository(IClusterClient clusterClient, StorageOptions storageOptions)
{
this.clusterClient = clusterClient;
this.storageOptions = storageOptions;
}

public async Task<ScheduledTask> AddAsync(ScheduledTask scheduledTask, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(scheduledTask);
if (scheduledTask.ScheduledTaskId == Guid.Empty)
{
scheduledTask.ScheduledTaskId = Guid.NewGuid();
}

var scheduledTaskGrain = this.clusterClient.GetGrain<IScheduledTaskGrain>(scheduledTask.ScheduledTaskId.ToString());

var result = await scheduledTaskGrain.CreateAsync(new ScheduledTaskMetadata()
Expand All @@ -28,70 +37,139 @@ public async Task<ScheduledTask> AddAsync(ScheduledTask scheduledTask, Cancellat
Modified = scheduledTask.Modified,
}).ConfigureAwait(false);

return new()
{
Description = result.Description,
IsEnabled = result.IsEnabled,
Name = result.Name,
Created = result.Created,
Modified = result.Modified,
ScheduledTaskId = scheduledTask.ScheduledTaskId,
};

return scheduledTask;
}

public async Task<ScheduledTask> DeleteAsync(ScheduledTask scheduledTask, CancellationToken cancellationToken)
public async Task DeleteAsync(Guid scheduledTask, CancellationToken cancellationToken)
{
var scheduledTaskGrain = this.clusterClient.GetGrain<IScheduledTaskGrain>(scheduledTask.ScheduledTaskId.ToString());
var result = await scheduledTaskGrain.DeleteAsync().ConfigureAwait(false);
return new()
{
Description = result.Description,
IsEnabled = result.IsEnabled,
Name = result.Name,
Created = result.Created,
Modified = result.Modified,
};
_ = await this.clusterClient.GetGrain<IScheduledTaskGrain>(scheduledTask.ToString()).DeleteAsync().ConfigureAwait(false);
}

public async Task<ScheduledTask> GetAsync(Guid scheduledTaskId, CancellationToken cancellationToken)
{
var scheduledTask = await this.clusterClient.GetGrain<IScheduledTaskGrain>(scheduledTaskId.ToString())
.GetAsync().ConfigureAwait(false);

return new ScheduledTask()
var result = await this.clusterClient.GetGrain<IScheduledTaskGrain>(scheduledTaskId.ToString()).GetAsync().ConfigureAwait(false);
return new()
{
Created = scheduledTask.Created,
Description = scheduledTask.Description,
IsEnabled = scheduledTask.IsEnabled,
Modified = scheduledTask.Modified,
Name = scheduledTask.Name,
Created = result.Created,
Modified = result.Modified,
Description = result.Description,
Name = result.Name,
IsEnabled = result.IsEnabled,
ScheduledTaskId = scheduledTaskId,
};
}

public Task<List<ScheduledTask>> GetScheduledTasksAsync(
public async Task<List<ScheduledTask>> GetScheduledTasksAsync(
int? first,
DateTimeOffset? createdAfter,
DateTimeOffset? createdBefore,
CancellationToken cancellationToken) => Task.FromResult(new List<ScheduledTask>());
CancellationToken cancellationToken)
{
// TODO: Figure out connection pooling
using var dbConnection = new MySqlConnection(this.storageOptions.ConnectionString);

var sql = @"SELECT m.GrainIdExtensionString, m.PayloadJson FROM OrleansStorage AS m JOIN
JSON_TABLE(
m.PayloadJson,
'$'
COLUMNS(
Created varchar(100) PATH '$.created' DEFAULT '0' ON EMPTY
)
) AS tt
ON m.GrainTypeString='WebScheduler.Grains.Scheduler.ScheduledTaskGrain,WebScheduler.Grains.ScheduledTaskMetadata'";
if (createdAfter != null || createdBefore != null)
{
sql += @"
AND ";
if (createdAfter != null)
{
sql += "tt.Created < @createdAfter";
}
if (createdAfter != null && createdBefore != null)
{
sql += " AND ";
}
if (createdAfter != null)
{
sql += "tt.Created > @createdbefore ";
}
}
if (first != null)
{
sql += $" ORDER BY tt.Created LIMIT {first}, 10";
}

object parameters = new CreatedBeforeAndAfterClause(createdAfter, createdBefore) switch
{
(CreatedAfter: null, CreatedBefore: null) => new { },
(CreatedAfter: not null, CreatedBefore: not null) => new { CreatedAfter = createdAfter, CreatedBefore = createdBefore },
(CreatedAfter: not null, CreatedBefore: null) => new { CreatedAfter = createdAfter },
(CreatedAfter: null, CreatedBefore: not null) => new { CreatedBefore = createdBefore },
};
using var reader = await dbConnection.ExecuteReaderAsync(new CommandDefinition(sql, cancellationToken: cancellationToken)).ConfigureAwait(false);

var buffer = new List<ScheduledTask>(10);
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
var result = JsonSerializer.Deserialize<ScheduledTaskMetadata>(reader.GetString(1), new JsonSerializerOptions(JsonSerializerDefaults.Web));
if (result == null)
{
// TODO: handle this
continue;
}
buffer.Add(new()
{
Created = result.Created,
Modified = result.Modified,
Description = result.Description,
Name = result.Name,
IsEnabled = result.IsEnabled,
ScheduledTaskId = reader.GetGuid(0),
});
}

return buffer;
}
private record struct CreatedBeforeAndAfterClause(DateTimeOffset? CreatedAfter, DateTimeOffset? CreatedBefore);

public Task<List<ScheduledTask>> GetScheduledTasksReverseAsync(
int? last,
DateTimeOffset? createdAfter,
DateTimeOffset? createdBefore,
CancellationToken cancellationToken) => Task.FromResult(new List<ScheduledTask>());
CancellationToken cancellationToken)
{

return Task.FromResult(new List<ScheduledTask>());
}

public Task<bool> GetHasNextPageAsync(
public async Task<bool> GetHasNextPageAsync(
int? first,
DateTimeOffset? createdAfter,
CancellationToken cancellationToken) => Task.FromResult(false);
CancellationToken cancellationToken) => (await this.GetTotalCountAsync(cancellationToken)) > (first ?? 0);

public Task<bool> GetHasPreviousPageAsync(
public async Task<bool> GetHasPreviousPageAsync(
int? last,
DateTimeOffset? createdBefore,
CancellationToken cancellationToken) => Task.FromResult(false);
CancellationToken cancellationToken) => (await this.GetTotalCountAsync(cancellationToken)) < (last ?? 0);

public Task<int> GetTotalCountAsync(CancellationToken cancellationToken) => Task.FromResult(0);
public async Task<int> GetTotalCountAsync(CancellationToken cancellationToken)
{
// TODO: Figure out connection pooling
using var dbConnection = new MySqlConnection(this.storageOptions.ConnectionString);

var sql = @"SELECT COUNT(*) from OrleansStorage
where GrainTypeString='WebScheduler.Grains.Scheduler.ScheduledTaskGrain,WebScheduler.Grains.ScheduledTaskMetadata'";

using var reader = await dbConnection.ExecuteReaderAsync(new CommandDefinition(sql, cancellationToken: cancellationToken)).ConfigureAwait(false);

var buffer = new List<ScheduledTask>(10);
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
return reader.GetInt32(0);
}
return 0;
}

public Task<ScheduledTask> UpdateAsync(ScheduledTask scheduledTask, CancellationToken cancellationToken)
{
Expand Down
2 changes: 0 additions & 2 deletions Source/WebScheduler.Api/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ public virtual void ConfigureServices(IServiceCollection services)
.AddCors()
.AddResponseCompression()
.AddRouting();
// Do we need this? I think so.
//JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

services
.AddAuthentication(option =>
Expand Down
Loading