From 23586c169b29640f05828323db1822f644fa1d6e Mon Sep 17 00:00:00 2001 From: ninjapiratica Date: Sat, 2 Nov 2024 01:35:36 -0700 Subject: [PATCH] Startup enhancements (#11) * WithDataService * WithDefaultNameRoleProvisioningService * WithDefaultDeepLinkingService * WithDefaultPlatformService * WithDefaultTokenService * WithDefaultAssignmentGradeService * Simplify Routing Configuring * Rename public service interfaces to include Lti13 * Rename services to include lti13, default, and config * Update Default Services to use Configuration settings --- .../Populators/ServiceEndpointsPopulator.cs | 2 +- .../README.md | 54 +++++++---- ...=> DefaultAssignmentGradeConfigService.cs} | 2 +- ... => ILti13AssignmentGradeConfigService.cs} | 2 +- ...cs => ILti13AssignmentGradeDataService.cs} | 2 +- .../Startup.cs | 35 +++++--- .../Configs/Lti13PlatformCoreConfig.cs | 2 +- .../LtiServicesAuthHandler.cs | 2 +- .../Populators/CustomPopulator.cs | 2 +- .../Populators/PlatformPopulator.cs | 2 +- .../Populators/RolesPopulator.cs | 2 +- NP.Lti13Platform.Core/README.md | 90 +++++++++++++------ ...rmService.cs => DefaultPlatformService.cs} | 2 +- ...ervice.cs => DefaultTokenConfigService.cs} | 2 +- ...ataService.cs => ILti13CoreDataService.cs} | 2 +- ...ormService.cs => ILti13PlatformService.cs} | 2 +- ...Service.cs => ILti13TokenConfigService.cs} | 2 +- .../Services/UrlServiceHelper.cs | 3 +- NP.Lti13Platform.Core/Startup.cs | 38 +++++--- .../Populators/DeepLinkingPopulator.cs | 2 +- NP.Lti13Platform.DeepLinking/README.md | 73 ++++++++++----- ....cs => DefaultDeepLinkingConfigService.cs} | 4 +- .../Services/DefaultDeepLinkingHandler.cs | 17 ++++ .../ILti13DeepLinkingConfigService.cs | 10 +++ ...ice.cs => ILti13DeepLinkingDataService.cs} | 9 +- ...Service.cs => ILti13DeepLinkingHandler.cs} | 5 +- NP.Lti13Platform.DeepLinking/Startup.cs | 32 +++++-- .../Populators/CustomPopulator.cs | 2 +- .../Populators/ServiceEndpointsPopulator.cs | 2 +- .../README.md | 52 ++++++++--- ...faultNameRoleProvisioningConfigService.cs} | 2 +- ...Lti13NameRoleProvisioningConfigService.cs} | 2 +- ... ILti13NameRoleProvisioningDataService.cs} | 2 +- .../Startup.cs | 22 +++-- .../Controllers/HomeController.cs | 2 +- NP.Lti13Platform.WebExample/Program.cs | 54 ++++++----- .../appsettings.Development.json | 5 ++ .../{IDataService.cs => ILti13DataService.cs} | 2 +- NP.Lti13Platform/README.md | 37 ++++---- NP.Lti13Platform/Startup.cs | 83 ++++------------- 40 files changed, 396 insertions(+), 271 deletions(-) rename NP.Lti13Platform.AssignmentGradeServices/Services/{AssignmentGradeService.cs => DefaultAssignmentGradeConfigService.cs} (81%) rename NP.Lti13Platform.AssignmentGradeServices/Services/{IAssignmentGradeService.cs => ILti13AssignmentGradeConfigService.cs} (81%) rename NP.Lti13Platform.AssignmentGradeServices/Services/{IAssignmentGradeDataService.cs => ILti13AssignmentGradeDataService.cs} (92%) rename NP.Lti13Platform.Core/Services/{PlatformService.cs => DefaultPlatformService.cs} (78%) rename NP.Lti13Platform.Core/Services/{TokenService.cs => DefaultTokenConfigService.cs} (72%) rename NP.Lti13Platform.Core/Services/{ICoreDataService.cs => ILti13CoreDataService.cs} (97%) rename NP.Lti13Platform.Core/Services/{IPlatformService.cs => ILti13PlatformService.cs} (82%) rename NP.Lti13Platform.Core/Services/{ITokenService.cs => ILti13TokenConfigService.cs} (82%) rename NP.Lti13Platform.DeepLinking/Services/{DeepLinkingService.cs => DefaultDeepLinkingConfigService.cs} (67%) create mode 100644 NP.Lti13Platform.DeepLinking/Services/DefaultDeepLinkingHandler.cs create mode 100644 NP.Lti13Platform.DeepLinking/Services/ILti13DeepLinkingConfigService.cs rename NP.Lti13Platform.DeepLinking/Services/{IDeepLinkingDataService.cs => ILti13DeepLinkingDataService.cs} (57%) rename NP.Lti13Platform.DeepLinking/Services/{IDeepLinkingService.cs => ILti13DeepLinkingHandler.cs} (58%) rename NP.Lti13Platform.NameRoleProvisioningServices/Services/{NameRoleProvisioningService.cs => DefaultNameRoleProvisioningConfigService.cs} (80%) rename NP.Lti13Platform.NameRoleProvisioningServices/Services/{INameRoleProvisioningService.cs => ILti13NameRoleProvisioningConfigService.cs} (80%) rename NP.Lti13Platform.NameRoleProvisioningServices/Services/{INameRoleProvisioningDataService.cs => ILti13NameRoleProvisioningDataService.cs} (89%) rename NP.Lti13Platform/{IDataService.cs => ILti13DataService.cs} (58%) diff --git a/NP.Lti13Platform.AssignmentGradeServices/Populators/ServiceEndpointsPopulator.cs b/NP.Lti13Platform.AssignmentGradeServices/Populators/ServiceEndpointsPopulator.cs index 9abe5e7..f7b207c 100644 --- a/NP.Lti13Platform.AssignmentGradeServices/Populators/ServiceEndpointsPopulator.cs +++ b/NP.Lti13Platform.AssignmentGradeServices/Populators/ServiceEndpointsPopulator.cs @@ -25,7 +25,7 @@ public class LineItemServiceEndpoints } } - public class ServiceEndpointsPopulator(LinkGenerator linkGenerator, ICoreDataService dataService, IAssignmentGradeService assignmentGradeService) : Populator + public class ServiceEndpointsPopulator(LinkGenerator linkGenerator, ILti13CoreDataService dataService, ILti13AssignmentGradeConfigService assignmentGradeService) : Populator { public override async Task PopulateAsync(IServiceEndpoints obj, MessageScope scope, CancellationToken cancellationToken = default) { diff --git a/NP.Lti13Platform.AssignmentGradeServices/README.md b/NP.Lti13Platform.AssignmentGradeServices/README.md index 9c0a54d..e812ae2 100644 --- a/NP.Lti13Platform.AssignmentGradeServices/README.md +++ b/NP.Lti13Platform.AssignmentGradeServices/README.md @@ -11,10 +11,10 @@ The IMS [Assignment and Grade Services](https://www.imsglobal.org/spec/lti-ags/v 1. Add the nuget package to your project: -2. Add an implementation of the `IAssignmentGradeDataService` interface: +2. Add an implementation of the `ILti13AssignmentGradeDataService` interface: ```csharp -public class DataService: IAssignmentGradeDataService +public class DataService: ILti13AssignmentGradeDataService { ... } @@ -26,9 +26,7 @@ public class DataService: IAssignmentGradeDataService builder.Services .AddLti13PlatformCore() .AddLti13PlatformAssignmentGradeServices() - .AddDefaultAssignmentGradeService(); - -builder.Services.AddTransient(); + .WithLti13AssignmentGradeDataService(); ``` 4. Setup the routing for the LTI 1.3 platform endpoints: @@ -37,11 +35,11 @@ builder.Services.AddTransient(); app.UseLti13PlatformAssignmentGradeServices(); ``` -## IAssignmentGradeDataService +## ILti13AssignmentGradeDataService -There is no default `IAssignmentGradeDataService` implementation to allow each project to store the data how they see fit. +There is no default `ILti13AssignmentGradeDataService` implementation to allow each project to store the data how they see fit. -The `IAssignmentGradeDataService` interface is used to manage the persistance of line items and grades. +The `ILti13AssignmentGradeDataService` interface is used to manage the persistance of line items and grades. All of the internal services are transient and therefore the data service may be added at any scope (Transient, Scoped, Singleton). @@ -55,18 +53,44 @@ Default routes are provided for all endpoints. Routes can be configured when cal app.UseLti13PlatformAssignmentGradeServices(config => { config.LineItemsUrl = "/lti13/{deploymentId}/{contextId}/lineItems"; // {deploymentId} and {contextId} are required config.LineItemUrl = "/lti13/{deploymentId}/{contextId}/lineItems/{lineItemId}"; // {deploymentId}, {contextId}, and {lineItemId} are required + return config; }); ``` -### IAssignmentGradeService +### ILti13AssignmentGradeConfigService + +The `ILti13AssignmentGradeConfigService` interface is used to get the config for the assignment and grade service. The config is used to tell the tools how to request the members of a context. -The `IAssignmentGradeService` interface is used to get the config for the assignment and grade service. The config is used to tell the tools how to request the members of a context. +There is a default implementation of the `ILti13AssignmentGradeConfigService` interface that uses a configuration set up on app start. +It will be configured using the [`IOptions`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration) pattern and configuration. +The configuration path for the service is `Lti13Platform:AssignmentGradeServices`. -There is a default implementation of the `IAssignmentGradeService` interface that uses a configuration set up on app start. When calling the `AddDefaultAssignmentGradeService` method, the configuration can be setup at that time. A fallback to the current request scheme and host will be used if no ServiceEndpoint is configured. The Default implementation can be overridden by adding a new implementation of the `INameRoleProvisioningService` interface and not including the Default. This may be useful if the service URL is dynamic or needs to be determined at runtime. +Examples: +```json +{ + "Lti13Platform": { + "AssignmentGradeServices": { + "ServiceAddress": "https://" + } + } +} +``` ```csharp -builder.Services - .AddLti13PlatformCore() +builder.Services.Configure(x => { }); +``` + +The Default implementation can be overridden by adding a new implementation of the `ILti13AssignmentGradeConfigService` interface. +This may be useful if the service URL is dynamic or needs to be determined at runtime. + +```csharp +builder.AddLti13PlatformCore() .AddLti13PlatformAssignmentGradeServices() - .AddDefaultAssignmentGradeService(x => { x.ServiceAddress = new Uri("https://") }); -``` \ No newline at end of file + .WithLti13AssignmentGradeConfigService(); +``` + +## Configuration + +`ServiceAddress` + +The base url used to tell tools where the service is located. \ No newline at end of file diff --git a/NP.Lti13Platform.AssignmentGradeServices/Services/AssignmentGradeService.cs b/NP.Lti13Platform.AssignmentGradeServices/Services/DefaultAssignmentGradeConfigService.cs similarity index 81% rename from NP.Lti13Platform.AssignmentGradeServices/Services/AssignmentGradeService.cs rename to NP.Lti13Platform.AssignmentGradeServices/Services/DefaultAssignmentGradeConfigService.cs index 8e3c156..24752ee 100644 --- a/NP.Lti13Platform.AssignmentGradeServices/Services/AssignmentGradeService.cs +++ b/NP.Lti13Platform.AssignmentGradeServices/Services/DefaultAssignmentGradeConfigService.cs @@ -4,7 +4,7 @@ namespace NP.Lti13Platform.AssignmentGradeServices.Services { - internal class AssignmentGradeService(IOptionsMonitor config, IHttpContextAccessor httpContextAccessor) : IAssignmentGradeService + internal class DefaultAssignmentGradeConfigService(IOptionsMonitor config, IHttpContextAccessor httpContextAccessor) : ILti13AssignmentGradeConfigService { public async Task GetConfigAsync(string clientId, CancellationToken cancellationToken = default) { diff --git a/NP.Lti13Platform.AssignmentGradeServices/Services/IAssignmentGradeService.cs b/NP.Lti13Platform.AssignmentGradeServices/Services/ILti13AssignmentGradeConfigService.cs similarity index 81% rename from NP.Lti13Platform.AssignmentGradeServices/Services/IAssignmentGradeService.cs rename to NP.Lti13Platform.AssignmentGradeServices/Services/ILti13AssignmentGradeConfigService.cs index 124bdee..886d3fe 100644 --- a/NP.Lti13Platform.AssignmentGradeServices/Services/IAssignmentGradeService.cs +++ b/NP.Lti13Platform.AssignmentGradeServices/Services/ILti13AssignmentGradeConfigService.cs @@ -2,7 +2,7 @@ namespace NP.Lti13Platform.AssignmentGradeServices.Services { - public interface IAssignmentGradeService + public interface ILti13AssignmentGradeConfigService { Task GetConfigAsync(string clientId, CancellationToken cancellationToken = default); } diff --git a/NP.Lti13Platform.AssignmentGradeServices/Services/IAssignmentGradeDataService.cs b/NP.Lti13Platform.AssignmentGradeServices/Services/ILti13AssignmentGradeDataService.cs similarity index 92% rename from NP.Lti13Platform.AssignmentGradeServices/Services/IAssignmentGradeDataService.cs rename to NP.Lti13Platform.AssignmentGradeServices/Services/ILti13AssignmentGradeDataService.cs index e71aa80..cbf9271 100644 --- a/NP.Lti13Platform.AssignmentGradeServices/Services/IAssignmentGradeDataService.cs +++ b/NP.Lti13Platform.AssignmentGradeServices/Services/ILti13AssignmentGradeDataService.cs @@ -2,7 +2,7 @@ namespace NP.Lti13Platform.AssignmentGradeServices.Services { - public interface IAssignmentGradeDataService + public interface ILti13AssignmentGradeDataService { Task GetLineItemAsync(string lineItemId, CancellationToken cancellationToken = default); Task DeleteLineItemAsync(string lineItemId, CancellationToken cancellationToken = default); diff --git a/NP.Lti13Platform.AssignmentGradeServices/Startup.cs b/NP.Lti13Platform.AssignmentGradeServices/Startup.cs index 7596f27..668b71a 100644 --- a/NP.Lti13Platform.AssignmentGradeServices/Startup.cs +++ b/NP.Lti13Platform.AssignmentGradeServices/Startup.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.JsonWebTokens; using NP.Lti13Platform.AssignmentGradeServices.Configs; @@ -23,25 +24,31 @@ public static Lti13PlatformBuilder AddLti13PlatformAssignmentGradeServices(this { builder.ExtendLti13Message(); + builder.Services.AddOptions().BindConfiguration("Lti13Platform:AssignmentGradeServices"); + builder.Services.TryAddSingleton(); + return builder; } - public static Lti13PlatformBuilder AddDefaultAssignmentGradeService(this Lti13PlatformBuilder builder, Action? configure = null) + public static Lti13PlatformBuilder WithLti13AssignmentGradeDataService(this Lti13PlatformBuilder builder, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) where T : ILti13AssignmentGradeDataService { - configure ??= (x) => { }; + builder.Services.Add(new ServiceDescriptor(typeof(ILti13AssignmentGradeDataService), typeof(T), serviceLifetime)); + return builder; + } - builder.Services.Configure(configure); - builder.Services.AddTransient(); + public static Lti13PlatformBuilder WithLti13AssignmentGradeConfigService(this Lti13PlatformBuilder builder, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) where T : ILti13AssignmentGradeConfigService + { + builder.Services.Add(new ServiceDescriptor(typeof(ILti13AssignmentGradeConfigService), typeof(T), serviceLifetime)); return builder; } - public static IEndpointRouteBuilder UseLti13PlatformAssignmentGradeServices(this IEndpointRouteBuilder app, Action? configure = null) + public static IEndpointRouteBuilder UseLti13PlatformAssignmentGradeServices(this IEndpointRouteBuilder app, Func? configure = null) { - var config = new ServiceEndpointsConfig(); - configure?.Invoke(config); + ServiceEndpointsConfig config = new(); + config = configure?.Invoke(config) ?? config; app.MapGet(config.LineItemsUrl, - async (IHttpContextAccessor httpContextAccessor, ICoreDataService coreDataService, LinkGenerator linkGenerator, string deploymentId, string contextId, string? resource_id, string? resource_link_id, string? tag, int? limit, int pageIndex = 0, CancellationToken cancellationToken = default) => + async (IHttpContextAccessor httpContextAccessor, ILti13CoreDataService coreDataService, LinkGenerator linkGenerator, string deploymentId, string contextId, string? resource_id, string? resource_link_id, string? tag, int? limit, int pageIndex = 0, CancellationToken cancellationToken = default) => { var httpContext = httpContextAccessor.HttpContext!; var clientId = httpContext.User.FindFirstValue(JwtRegisteredClaimNames.Sub)!; @@ -106,7 +113,7 @@ public static IEndpointRouteBuilder UseLti13PlatformAssignmentGradeServices(this }); app.MapPost(config.LineItemsUrl, - async (IHttpContextAccessor httpContextAccessor, ICoreDataService coreDataService, IAssignmentGradeDataService assignmentGradeDataService, LinkGenerator linkGenerator, string deploymentId, string contextId, LineItemRequest request, CancellationToken cancellationToken) => + async (IHttpContextAccessor httpContextAccessor, ILti13CoreDataService coreDataService, ILti13AssignmentGradeDataService assignmentGradeDataService, LinkGenerator linkGenerator, string deploymentId, string contextId, LineItemRequest request, CancellationToken cancellationToken) => { const string INVALID_CONTENT_TYPE = "Invalid Content-Type"; const string CONTENT_TYPE_REQUIRED = "Content-Type must be 'application/vnd.ims.lis.v2.lineitem+json'"; @@ -200,7 +207,7 @@ public static IEndpointRouteBuilder UseLti13PlatformAssignmentGradeServices(this .DisableAntiforgery(); app.MapGet(config.LineItemUrl, - async (IHttpContextAccessor httpContextAccessor, ICoreDataService coreDataService, IAssignmentGradeDataService assignmentGradeDataService, LinkGenerator linkGenerator, string deploymentId, string contextId, string lineItemId, CancellationToken cancellationToken) => + async (IHttpContextAccessor httpContextAccessor, ILti13CoreDataService coreDataService, ILti13AssignmentGradeDataService assignmentGradeDataService, LinkGenerator linkGenerator, string deploymentId, string contextId, string lineItemId, CancellationToken cancellationToken) => { var httpContext = httpContextAccessor.HttpContext!; var clientId = httpContext.User.FindFirstValue(JwtRegisteredClaimNames.Sub)!; @@ -249,7 +256,7 @@ public static IEndpointRouteBuilder UseLti13PlatformAssignmentGradeServices(this }); app.MapPut(config.LineItemUrl, - async (IHttpContextAccessor httpContextAccessor, ICoreDataService coreDataService, IAssignmentGradeDataService assignmentGradeDataService, LinkGenerator linkGenerator, string deploymentId, string contextId, string lineItemId, LineItemRequest request, CancellationToken cancellationToken) => + async (IHttpContextAccessor httpContextAccessor, ILti13CoreDataService coreDataService, ILti13AssignmentGradeDataService assignmentGradeDataService, LinkGenerator linkGenerator, string deploymentId, string contextId, string lineItemId, LineItemRequest request, CancellationToken cancellationToken) => { const string INVALID_CONTENT_TYPE = "Invalid Content-Type"; const string CONTENT_TYPE_REQUIRED = "Content-Type must be 'application/vnd.ims.lis.v2.lineitem+json'"; @@ -343,7 +350,7 @@ public static IEndpointRouteBuilder UseLti13PlatformAssignmentGradeServices(this .DisableAntiforgery(); app.MapDelete(config.LineItemUrl, - async (IHttpContextAccessor httpContextAccessor, ICoreDataService coreDataService, IAssignmentGradeDataService assignmentGradeDataService, string deploymentId, string contextId, string lineItemId, CancellationToken cancellationToken) => + async (IHttpContextAccessor httpContextAccessor, ILti13CoreDataService coreDataService, ILti13AssignmentGradeDataService assignmentGradeDataService, string deploymentId, string contextId, string lineItemId, CancellationToken cancellationToken) => { var httpContext = httpContextAccessor.HttpContext!; var clientId = httpContext.User.FindFirstValue(JwtRegisteredClaimNames.Sub)!; @@ -384,7 +391,7 @@ public static IEndpointRouteBuilder UseLti13PlatformAssignmentGradeServices(this .DisableAntiforgery(); app.MapGet($"{config.LineItemUrl}/results", - async (IHttpContextAccessor httpContextAccessor, ICoreDataService coreDataService, IAssignmentGradeDataService assignmentGradeDataService, LinkGenerator linkGenerator, string deploymentId, string contextId, string lineItemId, string? user_id, int? limit, int pageIndex = 0, CancellationToken cancellationToken = default) => + async (IHttpContextAccessor httpContextAccessor, ILti13CoreDataService coreDataService, ILti13AssignmentGradeDataService assignmentGradeDataService, LinkGenerator linkGenerator, string deploymentId, string contextId, string lineItemId, string? user_id, int? limit, int pageIndex = 0, CancellationToken cancellationToken = default) => { var httpContext = httpContextAccessor.HttpContext!; var clientId = httpContext.User.FindFirstValue(JwtRegisteredClaimNames.Sub)!; @@ -454,7 +461,7 @@ public static IEndpointRouteBuilder UseLti13PlatformAssignmentGradeServices(this }); app.MapPost($"{config.LineItemUrl}/scores", - async (IHttpContextAccessor httpContextAccessor, ICoreDataService coreDataService, IAssignmentGradeDataService assignmentGradeDataService, string deploymentId, string contextId, string lineItemId, ScoreRequest request, CancellationToken cancellationToken) => + async (IHttpContextAccessor httpContextAccessor, ILti13CoreDataService coreDataService, ILti13AssignmentGradeDataService assignmentGradeDataService, string deploymentId, string contextId, string lineItemId, ScoreRequest request, CancellationToken cancellationToken) => { const string RESULT_TOO_EARLY = "startDateTime"; const string RESULT_TOO_EARLY_DESCRIPTION = "lineItem startDateTime is in the future"; diff --git a/NP.Lti13Platform.Core/Configs/Lti13PlatformCoreConfig.cs b/NP.Lti13Platform.Core/Configs/Lti13PlatformCoreConfig.cs index 3e79a97..8b5eb5d 100644 --- a/NP.Lti13Platform.Core/Configs/Lti13PlatformCoreConfig.cs +++ b/NP.Lti13Platform.Core/Configs/Lti13PlatformCoreConfig.cs @@ -8,7 +8,7 @@ public class Lti13PlatformTokenConfig /// /// A case-sensitive URL using the HTTPS scheme that contains: scheme, host; and, optionally, port number, and path components; and, no query or fragment components. The issuer identifies the platform to the tools. /// - public string Issuer + public required string Issuer { get => _issuer; set diff --git a/NP.Lti13Platform.Core/LtiServicesAuthHandler.cs b/NP.Lti13Platform.Core/LtiServicesAuthHandler.cs index 95f5939..35f7ccc 100644 --- a/NP.Lti13Platform.Core/LtiServicesAuthHandler.cs +++ b/NP.Lti13Platform.Core/LtiServicesAuthHandler.cs @@ -9,7 +9,7 @@ namespace NP.Lti13Platform.Core { - public class LtiServicesAuthHandler(ICoreDataService dataService, ITokenService tokenService, IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) + public class LtiServicesAuthHandler(ILti13CoreDataService dataService, ILti13TokenConfigService tokenService, IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : AuthenticationHandler(options, logger, encoder) { public const string SchemeName = "NP.Lti13Platform.Services"; diff --git a/NP.Lti13Platform.Core/Populators/CustomPopulator.cs b/NP.Lti13Platform.Core/Populators/CustomPopulator.cs index c564c92..42688b4 100644 --- a/NP.Lti13Platform.Core/Populators/CustomPopulator.cs +++ b/NP.Lti13Platform.Core/Populators/CustomPopulator.cs @@ -12,7 +12,7 @@ public interface ICustomMessage public IDictionary? Custom { get; set; } } - public class CustomPopulator(IPlatformService platformService, ICoreDataService dataService) : Populator + public class CustomPopulator(ILti13PlatformService platformService, ILti13CoreDataService dataService) : Populator { private static readonly IEnumerable LineItemAttemptGradeVariables = [ Lti13ResourceLinkVariables.AvailableUserStartDateTime, diff --git a/NP.Lti13Platform.Core/Populators/PlatformPopulator.cs b/NP.Lti13Platform.Core/Populators/PlatformPopulator.cs index 9fa86e4..6e60d36 100644 --- a/NP.Lti13Platform.Core/Populators/PlatformPopulator.cs +++ b/NP.Lti13Platform.Core/Populators/PlatformPopulator.cs @@ -33,7 +33,7 @@ public class ToolPlatform } } - public class PlatformPopulator(IPlatformService platformService) : Populator + public class PlatformPopulator(ILti13PlatformService platformService) : Populator { public override async Task PopulateAsync(IPlatformMessage obj, MessageScope scope, CancellationToken cancellationToken = default) { diff --git a/NP.Lti13Platform.Core/Populators/RolesPopulator.cs b/NP.Lti13Platform.Core/Populators/RolesPopulator.cs index 27ffbbc..0f879cf 100644 --- a/NP.Lti13Platform.Core/Populators/RolesPopulator.cs +++ b/NP.Lti13Platform.Core/Populators/RolesPopulator.cs @@ -13,7 +13,7 @@ public interface IRolesMessage public IEnumerable? RoleScopeMentor { get; set; } } - public class RolesPopulator(ICoreDataService dataService) : Populator + public class RolesPopulator(ILti13CoreDataService dataService) : Populator { public override async Task PopulateAsync(IRolesMessage obj, MessageScope scope, CancellationToken cancellationToken = default) { diff --git a/NP.Lti13Platform.Core/README.md b/NP.Lti13Platform.Core/README.md index fb2564e..b4c7c41 100644 --- a/NP.Lti13Platform.Core/README.md +++ b/NP.Lti13Platform.Core/README.md @@ -12,10 +12,10 @@ The IMS [Lti Core](https://www.imsglobal.org/spec/lti/v1p3/) spec defines a way 1. Add the nuget package to your project: -2. Add an implementation of the `ICoreDataService` interface: +2. Add an implementation of the `ILti13CoreDataService` interface: ```csharp -public class DataService: ICoreDataService +public class DataService: ILti13CoreDataService { ... } @@ -26,13 +26,7 @@ public class DataService: ICoreDataService ```csharp builder.Services .AddLti13PlatformCore() - .AddDefaultPlatformService() - .AddDefaultTokenService(x => - { - x.Issuer = "https://"; - }); - -builder.Services.AddTransient(); + .WithLti13CoreDataService(); ``` 4. Setup the routing for the LTI 1.3 platform endpoints: @@ -41,11 +35,11 @@ builder.Services.AddTransient(); app.UseLti13PlatformCore(); ``` -## ICoreDataService +## ILti13CoreDataService -There is no default `ICoreDataService` implementation to allow each project to store the data how they see fit. +There is no default `ILti13CoreDataService` implementation to allow each project to store the data how they see fit. -The `ICoreDataService` interface is used to manage the persistance of most of the data involved in LTI communication. +The `ILti13CoreDataService` interface is used to manage the persistance of most of the data involved in LTI communication. All of the internal services are transient and therefore the data service may be added at any scope (Transient, Scoped, Singleton). @@ -60,36 +54,76 @@ app.UseLti13PlatformCore(config => { config.AuthorizationUrl = "/lti13/authorization"; config.JwksUrl = "/lti13/jwks"; config.TokenUrl = "/lti13/token"; + return config; }); ``` -### IPlatformService +### ILti13PlatformService + +The `ILti13PlatformService` interface is used to get the platform details to give to the tools. + +There is a default implementation of the `ILti13PlatformService` interface that uses a configuration set up on app start. +It will be configured using the [`IOptions`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration) pattern and configuration. +The configuration path for the service is `Lti13Platform:Platform`. + +Examples: + +```json +{ + "Lti13Platform": { + "Platform": { + "Guid": "server-id", + ... + } + } +} +``` -The `IPlatformService` interface is used to get the platform details to give to the tools. +```csharp +builder.Services.Configure(x => { x.Guid = "server-id"; ... }); +``` -There is a default implementation of the `IPlatformService` interface that uses a configuration set up on app start. When calling the `AddDefaultPlatformService` method, the configuration can be setup at that time. The Default implementation can be overridden by adding a new implementation of the `IPlatformService` interface and not including the Default. +The Default implementation can be overridden by adding a new implementation of the `ILti13PlatformService` interface. ```csharp -builder.Services - .AddLti13PlatformCore() - .AddDefaultPlatformService(x => { /* Set platform data */ }); +builder.AddLti13PlatformCore() + .WithLti13PlatformService(); ``` -### ITokenService +### ILti13TokenConfigService -The `ITokenService` interface is used to get the token details for the tools. +The `ILti13TokenConfigService` interface is used to get the token details for the tools. -There is a default implementation of the `ITokenService` interface that uses a configuration set up on app start. When calling the `AddDefaultTokenService` method, the configuration can be setup at that time. The Default implementation can be overridden by adding a new implementation of the `ITokenService` interface and not including the Default. +There is a default implementation of the `ILti13TokenConfigService` interface that uses a configuration set up on app start. +It will be configured using the [`IOptions`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration) pattern and configuration. +The configuration path for the service is `Lti13Platform:Token`. + +Examples + +```json +{ + "Lti13Platform": { + "Token": { + "Issuer": "https://", + ... + } + } +} +``` ```csharp -builder.Services - .AddLti13PlatformCore() - .AddDefaultTokenService(x => - { - x.Issuer = "https://"; // This is required to be set when using the default token service. - }); +builder.Services.Configure(x => { x.Issuer = "https://"; ... }); ``` +The Default implementation can be overridden by adding a new implementation of the `ILti13TokenConfigService` interface. + +```csharp +builder.AddLti13PlatformCore() + .WithLti13TokenConfigService(); +``` + +***Important***: The `Issuer` is required for the default token service to load. + ## Configuration ### Platform @@ -146,7 +180,7 @@ The configuration for handling of tokens between the platform and the tools. `Issuer` -A case-sensitive URL using the HTTPS scheme that contains: scheme, host; and, optionally, port number, and path components; and, no query or fragment components. The issuer identifies the platform to the tools. +A case-sensitive URL using the HTTPS scheme that contains: scheme, host; and, optionally, port number, and path components; and, no query or fragment components. The issuer identifies the platform to the tools. An issuer is required. *** diff --git a/NP.Lti13Platform.Core/Services/PlatformService.cs b/NP.Lti13Platform.Core/Services/DefaultPlatformService.cs similarity index 78% rename from NP.Lti13Platform.Core/Services/PlatformService.cs rename to NP.Lti13Platform.Core/Services/DefaultPlatformService.cs index c553f50..56812a7 100644 --- a/NP.Lti13Platform.Core/Services/PlatformService.cs +++ b/NP.Lti13Platform.Core/Services/DefaultPlatformService.cs @@ -3,7 +3,7 @@ namespace NP.Lti13Platform.Core.Services { - internal class PlatformService(IOptionsMonitor config) : IPlatformService + internal class DefaultPlatformService(IOptionsMonitor config) : ILti13PlatformService { public async Task GetPlatformAsync(string clientId, CancellationToken cancellationToken = default) => await Task.FromResult(!string.IsNullOrWhiteSpace(config.CurrentValue.Guid) ? config.CurrentValue : null); } diff --git a/NP.Lti13Platform.Core/Services/TokenService.cs b/NP.Lti13Platform.Core/Services/DefaultTokenConfigService.cs similarity index 72% rename from NP.Lti13Platform.Core/Services/TokenService.cs rename to NP.Lti13Platform.Core/Services/DefaultTokenConfigService.cs index 746ec39..6f3a8ed 100644 --- a/NP.Lti13Platform.Core/Services/TokenService.cs +++ b/NP.Lti13Platform.Core/Services/DefaultTokenConfigService.cs @@ -3,7 +3,7 @@ namespace NP.Lti13Platform.Core.Services { - internal class TokenService(IOptionsMonitor config) : ITokenService + internal class DefaultTokenConfigService(IOptionsMonitor config) : ILti13TokenConfigService { public async Task GetTokenConfigAsync(string clientId, CancellationToken cancellationToken = default) => await Task.FromResult(config.CurrentValue); } diff --git a/NP.Lti13Platform.Core/Services/ICoreDataService.cs b/NP.Lti13Platform.Core/Services/ILti13CoreDataService.cs similarity index 97% rename from NP.Lti13Platform.Core/Services/ICoreDataService.cs rename to NP.Lti13Platform.Core/Services/ILti13CoreDataService.cs index 6f61545..6a5a316 100644 --- a/NP.Lti13Platform.Core/Services/ICoreDataService.cs +++ b/NP.Lti13Platform.Core/Services/ILti13CoreDataService.cs @@ -3,7 +3,7 @@ namespace NP.Lti13Platform.Core.Services { - public interface ICoreDataService + public interface ILti13CoreDataService { Task GetToolAsync(string clientId, CancellationToken cancellationToken = default); Task GetDeploymentAsync(string deploymentId, CancellationToken cancellationToken = default); diff --git a/NP.Lti13Platform.Core/Services/IPlatformService.cs b/NP.Lti13Platform.Core/Services/ILti13PlatformService.cs similarity index 82% rename from NP.Lti13Platform.Core/Services/IPlatformService.cs rename to NP.Lti13Platform.Core/Services/ILti13PlatformService.cs index b04c932..18c90ea 100644 --- a/NP.Lti13Platform.Core/Services/IPlatformService.cs +++ b/NP.Lti13Platform.Core/Services/ILti13PlatformService.cs @@ -2,7 +2,7 @@ namespace NP.Lti13Platform.Core.Services { - public interface IPlatformService + public interface ILti13PlatformService { Task GetPlatformAsync(string clientId, CancellationToken cancellationToken = default); } diff --git a/NP.Lti13Platform.Core/Services/ITokenService.cs b/NP.Lti13Platform.Core/Services/ILti13TokenConfigService.cs similarity index 82% rename from NP.Lti13Platform.Core/Services/ITokenService.cs rename to NP.Lti13Platform.Core/Services/ILti13TokenConfigService.cs index 7103ede..49941c1 100644 --- a/NP.Lti13Platform.Core/Services/ITokenService.cs +++ b/NP.Lti13Platform.Core/Services/ILti13TokenConfigService.cs @@ -2,7 +2,7 @@ namespace NP.Lti13Platform.Core.Services { - public interface ITokenService + public interface ILti13TokenConfigService { Task GetTokenConfigAsync(string clientId, CancellationToken cancellationToken = default); } diff --git a/NP.Lti13Platform.Core/Services/UrlServiceHelper.cs b/NP.Lti13Platform.Core/Services/UrlServiceHelper.cs index 1d4cf67..40b0832 100644 --- a/NP.Lti13Platform.Core/Services/UrlServiceHelper.cs +++ b/NP.Lti13Platform.Core/Services/UrlServiceHelper.cs @@ -18,7 +18,7 @@ public interface IUrlServiceHelper Task<(string MessageType, string DeploymentId, string? ContextId, string? ResourceLinkId, string? MessageHint)> ParseLtiMessageHintAsync(string messageHint, CancellationToken cancellationToken = default); } - public class UrlServiceHelper(ITokenService tokenService) : IUrlServiceHelper + public class UrlServiceHelper(ILti13TokenConfigService tokenService) : IUrlServiceHelper { public async Task GetResourceLinkInitiationUrlAsync(Tool tool, string deploymentId, string contextId, ResourceLink resourceLink, string userId, bool isAnonymous, string? actualUserId = null, LaunchPresentationOverride? launchPresentation = null, CancellationToken cancellationToken = default) => await GetUrlAsync( @@ -60,6 +60,7 @@ public async Task GetUrlAsync( return builder.Uri; } + public async Task GetLoginHintAsync(string userId, string? actualUserId, bool isAnonymous, CancellationToken cancellationToken = default) => await Task.FromResult($"{userId}|{(isAnonymous ? "1" : string.Empty)}|{actualUserId}"); diff --git a/NP.Lti13Platform.Core/Startup.cs b/NP.Lti13Platform.Core/Startup.cs index c814fd8..3f0a6e3 100644 --- a/NP.Lti13Platform.Core/Startup.cs +++ b/NP.Lti13Platform.Core/Startup.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using NP.Lti13Platform.Core.Configs; @@ -62,31 +62,41 @@ public static Lti13PlatformBuilder AddLti13PlatformCore(this IServiceCollection builder.Services.AddHttpContextAccessor(); + builder.Services.AddOptions().BindConfiguration("Lti13Platform:Platform"); + builder.Services.TryAddSingleton(); + + builder.Services.AddOptions() + .BindConfiguration("Lti13Platform:Token") + .Validate(x => !string.IsNullOrWhiteSpace(x.Issuer), "Lti13Platform:Token:Issuer is required when using default ILti13TokenConfigService."); + builder.Services.TryAddSingleton(); + return builder; } - public static Lti13PlatformBuilder AddDefaultPlatformService(this Lti13PlatformBuilder builder, Action? configure = null) + public static Lti13PlatformBuilder WithLti13CoreDataService(this Lti13PlatformBuilder builder, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) where T : ILti13CoreDataService { - configure ??= x => { }; + builder.Services.Add(new ServiceDescriptor(typeof(ILti13CoreDataService), typeof(T), serviceLifetime)); + return builder; + } - builder.Services.Configure(configure); - builder.Services.AddTransient(); + public static Lti13PlatformBuilder WithLti13PlatformService(this Lti13PlatformBuilder builder, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) where T : ILti13PlatformService + { + builder.Services.Add(new ServiceDescriptor(typeof(ILti13PlatformService), typeof(T), serviceLifetime)); return builder; } - public static Lti13PlatformBuilder AddDefaultTokenService(this Lti13PlatformBuilder builder, Action configure) + public static Lti13PlatformBuilder WithLti13TokenConfigService(this Lti13PlatformBuilder builder, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) where T : ILti13TokenConfigService { - builder.Services.Configure(configure); - builder.Services.AddTransient(); + builder.Services.Add(new ServiceDescriptor(typeof(ILti13TokenConfigService), typeof(T), serviceLifetime)); return builder; } - public static IEndpointRouteBuilder UseLti13PlatformCore(this IEndpointRouteBuilder routeBuilder, Action? configure = null) + public static IEndpointRouteBuilder UseLti13PlatformCore(this IEndpointRouteBuilder routeBuilder, Func? configure = null) { Lti13PlatformBuilder.CreateTypes(); - var config = new Lti13PlatformCoreEndpointsConfig(); - configure?.Invoke(config); + Lti13PlatformCoreEndpointsConfig config = new(); + config = configure?.Invoke(config) ?? config; if (routeBuilder is IApplicationBuilder appBuilder) { @@ -102,7 +112,7 @@ public static IEndpointRouteBuilder UseLti13PlatformCore(this IEndpointRouteBuil } routeBuilder.MapGet(config.JwksUrl, - async (ICoreDataService dataService, CancellationToken cancellationToken) => + async (ILti13CoreDataService dataService, CancellationToken cancellationToken) => { var keys = await dataService.GetPublicKeysAsync(cancellationToken); var keySet = new JsonWebKeySet(); @@ -119,7 +129,7 @@ public static IEndpointRouteBuilder UseLti13PlatformCore(this IEndpointRouteBuil }); routeBuilder.Map(config.AuthorizationUrl, - async ([AsParameters] AuthenticationRequest queryString, [FromForm] AuthenticationRequest form, IServiceProvider serviceProvider, ITokenService tokenService, ICoreDataService dataService, IUrlServiceHelper urlServiceHelper, CancellationToken cancellationToken) => + async ([AsParameters] AuthenticationRequest queryString, [FromForm] AuthenticationRequest form, IServiceProvider serviceProvider, ILti13TokenConfigService tokenService, ILti13CoreDataService dataService, IUrlServiceHelper urlServiceHelper, CancellationToken cancellationToken) => { const string OPENID = "openid"; const string ID_TOKEN = "id_token"; @@ -321,7 +331,7 @@ public static IEndpointRouteBuilder UseLti13PlatformCore(this IEndpointRouteBuil .DisableAntiforgery(); routeBuilder.MapPost(config.TokenUrl, - async ([FromForm] TokenRequest request, LinkGenerator linkGenerator, IHttpContextAccessor httpContextAccessor, ICoreDataService dataService, ITokenService tokenService, CancellationToken cancellationToken) => + async ([FromForm] TokenRequest request, LinkGenerator linkGenerator, IHttpContextAccessor httpContextAccessor, ILti13CoreDataService dataService, ILti13TokenConfigService tokenService, CancellationToken cancellationToken) => { const string AUTH_SPEC_URI = "https://www.imsglobal.org/spec/security/v1p0/#using-json-web-tokens-with-oauth-2-0-client-credentials-grant"; const string SCOPE_SPEC_URI = "https://www.imsglobal.org/spec/lti-ags/v2p0"; diff --git a/NP.Lti13Platform.DeepLinking/Populators/DeepLinkingPopulator.cs b/NP.Lti13Platform.DeepLinking/Populators/DeepLinkingPopulator.cs index a287e6f..c417f1c 100644 --- a/NP.Lti13Platform.DeepLinking/Populators/DeepLinkingPopulator.cs +++ b/NP.Lti13Platform.DeepLinking/Populators/DeepLinkingPopulator.cs @@ -57,7 +57,7 @@ public class DeepLinkSettingsMessage } } - public class DeepLinkingPopulator(LinkGenerator linkGenerator, IDeepLinkingService deepLinkingService) : Populator + public class DeepLinkingPopulator(LinkGenerator linkGenerator, ILti13DeepLinkingConfigService deepLinkingService) : Populator { public override async Task PopulateAsync(IDeepLinkingMessage obj, MessageScope scope, CancellationToken cancellationToken = default) { diff --git a/NP.Lti13Platform.DeepLinking/README.md b/NP.Lti13Platform.DeepLinking/README.md index b7a1f9c..c68cdae 100644 --- a/NP.Lti13Platform.DeepLinking/README.md +++ b/NP.Lti13Platform.DeepLinking/README.md @@ -11,10 +11,10 @@ The IMS [Deep Linking](https://www.imsglobal.org/spec/lti-dl/v2p0) spec defines 1. Add the nuget package to your project: -2. Add an implementation of the `IDeepLinkingDataService` interface: +2. Add an implementation of the `ILti13DeepLinkingDataService` interface: ```csharp -public class DataService: IDeepLinkingDataService +public class DataService: ILti13DeepLinkingDataService { ... } @@ -26,9 +26,7 @@ public class DataService: IDeepLinkingDataService builder.Services .AddLti13PlatformCore() .AddLti13PlatformDeepLinking() - .AddDefaultDeepLinkingService(); - -builder.Services.AddTransient(); + .WithLti13DeepLinkingDataService(); ``` 4. Setup the routing for the LTI 1.3 platform endpoints: @@ -37,11 +35,11 @@ builder.Services.AddTransient(); app.UseLti13PlatformDeepLinking(); ``` -## IDeepLinkingDataService +## ILti13DeepLinkingDataService -There is no default `IDeepLinkingDataService` implementation to allow each project to store the data how they see fit. +There is no default `ILti13DeepLinkingDataService` implementation to allow each project to store the data how they see fit. -The `IDeepLinkingDataService` interface is used to manage the persistance of resource links and other content items. +The `ILti13DeepLinkingDataService` interface is used to manage the persistance of resource links and other content items. All of the internal services are transient and therefore the data service may be added at any scope (Transient, Scoped, Singleton). @@ -54,25 +52,58 @@ Default routes are provided for all endpoints. Routes can be configured when cal ```csharp app.UseLti13PlatformDeepLinking(config => { config.DeepLinkingResponseUrl = "/lti13/deeplinking/{contextId?}"; // {contextId?} is required + return config; }); ``` -### IDeepLinkingService +### ILti13DeepLinkingHandler -The `IDeepLinkingService` interface is used to get the config for the deep linking service as well as handle the response from the tool. The config is used to control how deep link requests are made and how the response will be handled. +The `ILti13DeepLinkingHandler` interface is used to handle the response from the tool. -There is a default implementation of the `IDeepLinkingService` interface that uses a configuration set up on app start. When calling the `AddDefaultDeepLinkingService` method, the configuration can be setup at that time. A fallback to the current request scheme and host will be used if no ServiceAddress is configured. The Default implementation can be overridden by adding a new implementation of the `IDeepLinkingService` interface and not including the Default. +***Recommended***: +The default handling of the response is to return a placeholder page. It is strongly recommended to use the Default for development only. ```csharp builder.Services .AddLti13PlatformCore() .AddLti13PlatformDeepLinking() - .AddDefaultDeepLinkingService(x => { /* Update config as needed */ }); + .WithLti13DeepLinkingHandler(); ``` -***Recommended***: +### ILti13DeepLinkingConfigService + +The `ILti13DeepLinkingConfigService` interface is used to get the config for the deep linking service. The config is used to control how deep link requests are made and how the response will be handled. + +There is a default implementation of the `ILti13DeepLinkingConfigService` interface that uses a configuration set up on app start. +It will be configured using the [`IOptions`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration) pattern and configuration. +The configuration path for the service is `Lti13Platform:DeepLinking`. +A fallback to the current request scheme and host will be used if no ServiceAddress is configured. + +Examples + +```json +{ + "Lti13Platform": { + "DeepLinking": { + "ServiceAddress": "https://", + ... + } + } +} +``` + +```csharp +builder.Services.Configure(x => { }); +``` -Then default handling of the response is to return it as an 200 OK response with the response body being a JSON representation of the content items returned from the tool. It is strongly recommended to use the Default for development only. +The Default implementation can be overridden by adding a new implementation of the `ILti13DeepLinkingConfigService` interface. + +```csharp +builder.Services + .AddLti13PlatformCore() + .AddLti13PlatformDeepLinking() + .WithLti13DeepLinkingDataService(); +``` ## Configuration @@ -127,11 +158,9 @@ The web address where the deep linking responses will be handled. If not set, th A dictionary of type configurations to be used when deserialzing the content items. If not set, the content items will be deserialized as `Dictionary`{:csharp} objects. A convenience method to add the known content items to this dictionary is provided. ```csharp -builder.Services - .AddLti13PlatformCore() - .AddLti13PlatformDeepLinking() - .AddDefaultDeepLinkingService(x => - { - x.AddDefaultContentItemMapping(); - }); -``` \ No newline at end of file +builder.Services.Configure(x => +{ + x.AddDefaultContentItemMapping(); +}); +``` + diff --git a/NP.Lti13Platform.DeepLinking/Services/DeepLinkingService.cs b/NP.Lti13Platform.DeepLinking/Services/DefaultDeepLinkingConfigService.cs similarity index 67% rename from NP.Lti13Platform.DeepLinking/Services/DeepLinkingService.cs rename to NP.Lti13Platform.DeepLinking/Services/DefaultDeepLinkingConfigService.cs index 409b0ae..6a2a7ab 100644 --- a/NP.Lti13Platform.DeepLinking/Services/DeepLinkingService.cs +++ b/NP.Lti13Platform.DeepLinking/Services/DefaultDeepLinkingConfigService.cs @@ -4,10 +4,8 @@ namespace NP.Lti13Platform.DeepLinking.Services { - internal class DeepLinkingService(IOptionsMonitor config, IHttpContextAccessor httpContextAccessor) : IDeepLinkingService + internal class DefaultDeepLinkingConfigService(IOptionsMonitor config, IHttpContextAccessor httpContextAccessor) : ILti13DeepLinkingConfigService { - public Task HandleResponseAsync(string clientId, string deploymentId, string? contextId, DeepLinkResponse response, CancellationToken cancellationToken = default) => Task.FromResult(Results.Ok(response)); - public async Task GetConfigAsync(string clientId, CancellationToken cancellationToken = default) { var deepLinkingConfig = config.CurrentValue; diff --git a/NP.Lti13Platform.DeepLinking/Services/DefaultDeepLinkingHandler.cs b/NP.Lti13Platform.DeepLinking/Services/DefaultDeepLinkingHandler.cs new file mode 100644 index 0000000..54c81e2 --- /dev/null +++ b/NP.Lti13Platform.DeepLinking/Services/DefaultDeepLinkingHandler.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Http; +using System.Net.Mime; + +namespace NP.Lti13Platform.DeepLinking.Services +{ + internal class DefaultDeepLinkingHandler() : ILti13DeepLinkingHandler + { + public Task HandleResponseAsync(string clientId, string deploymentId, string? contextId, DeepLinkResponse response, CancellationToken cancellationToken = default) => + Task.FromResult(Results.Content(@$" + + +

This is the end of the Deep Linking flow. Please override the {nameof(ILti13DeepLinkingHandler)} for a better experience.

+ + ", + MediaTypeNames.Text.Html)); + } +} diff --git a/NP.Lti13Platform.DeepLinking/Services/ILti13DeepLinkingConfigService.cs b/NP.Lti13Platform.DeepLinking/Services/ILti13DeepLinkingConfigService.cs new file mode 100644 index 0000000..87e4e37 --- /dev/null +++ b/NP.Lti13Platform.DeepLinking/Services/ILti13DeepLinkingConfigService.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Http; +using NP.Lti13Platform.DeepLinking.Configs; + +namespace NP.Lti13Platform.DeepLinking.Services +{ + public interface ILti13DeepLinkingConfigService + { + Task GetConfigAsync(string clientId, CancellationToken cancellationToken = default); + } +} diff --git a/NP.Lti13Platform.DeepLinking/Services/IDeepLinkingDataService.cs b/NP.Lti13Platform.DeepLinking/Services/ILti13DeepLinkingDataService.cs similarity index 57% rename from NP.Lti13Platform.DeepLinking/Services/IDeepLinkingDataService.cs rename to NP.Lti13Platform.DeepLinking/Services/ILti13DeepLinkingDataService.cs index 39848c9..833abf7 100644 --- a/NP.Lti13Platform.DeepLinking/Services/IDeepLinkingDataService.cs +++ b/NP.Lti13Platform.DeepLinking/Services/ILti13DeepLinkingDataService.cs @@ -3,15 +3,8 @@ namespace NP.Lti13Platform.DeepLinking.Services { - public interface IDeepLinkingDataService + public interface ILti13DeepLinkingDataService { - /// - /// - /// - /// - /// - /// - /// The id of the content item. Task SaveContentItemAsync(string deploymentId, string? contextId, ContentItem contentItem, CancellationToken cancellationToken = default); Task SaveLineItemAsync(LineItem lineItem, CancellationToken cancellationToken = default); diff --git a/NP.Lti13Platform.DeepLinking/Services/IDeepLinkingService.cs b/NP.Lti13Platform.DeepLinking/Services/ILti13DeepLinkingHandler.cs similarity index 58% rename from NP.Lti13Platform.DeepLinking/Services/IDeepLinkingService.cs rename to NP.Lti13Platform.DeepLinking/Services/ILti13DeepLinkingHandler.cs index 96170d6..802aada 100644 --- a/NP.Lti13Platform.DeepLinking/Services/IDeepLinkingService.cs +++ b/NP.Lti13Platform.DeepLinking/Services/ILti13DeepLinkingHandler.cs @@ -1,12 +1,9 @@ using Microsoft.AspNetCore.Http; -using NP.Lti13Platform.DeepLinking.Configs; namespace NP.Lti13Platform.DeepLinking.Services { - public interface IDeepLinkingService + public interface ILti13DeepLinkingHandler { Task HandleResponseAsync(string clientId, string deploymentId, string? contextId, DeepLinkResponse response, CancellationToken cancellationToken = default); - - Task GetConfigAsync(string clientId, CancellationToken cancellationToken = default); } } diff --git a/NP.Lti13Platform.DeepLinking/Startup.cs b/NP.Lti13Platform.DeepLinking/Startup.cs index 58e06c6..05d5486 100644 --- a/NP.Lti13Platform.DeepLinking/Startup.cs +++ b/NP.Lti13Platform.DeepLinking/Startup.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; @@ -29,25 +30,38 @@ public static Lti13PlatformBuilder AddLti13PlatformDeepLinking(this Lti13Platfor .ExtendLti13Message(Lti13MessageType.LtiDeepLinkingRequest) .ExtendLti13Message(Lti13MessageType.LtiDeepLinkingRequest); + builder.Services.AddOptions().BindConfiguration("Lti13Platform:DeepLinking"); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + + return builder; + } + + public static Lti13PlatformBuilder WithLti13DeepLinkingDataService(this Lti13PlatformBuilder builder, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) where T : ILti13DeepLinkingDataService + { + builder.Services.Add(new ServiceDescriptor(typeof(ILti13DeepLinkingDataService), typeof(T), serviceLifetime)); return builder; } - public static Lti13PlatformBuilder AddDefaultDeepLinkingService(this Lti13PlatformBuilder builder, Action? configure = null) + public static Lti13PlatformBuilder WithLti13DeepLinkingConfigService(this Lti13PlatformBuilder builder, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) where T : ILti13DeepLinkingConfigService { - configure ??= x => { }; + builder.Services.Add(new ServiceDescriptor(typeof(ILti13DeepLinkingConfigService), typeof(T), serviceLifetime)); + return builder; + } - builder.Services.Configure(configure); - builder.Services.AddTransient(); + public static Lti13PlatformBuilder WithLti13DeepLinkingHandler(this Lti13PlatformBuilder builder, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) where T : ILti13DeepLinkingHandler + { + builder.Services.Add(new ServiceDescriptor(typeof(ILti13DeepLinkingHandler), typeof(T), serviceLifetime)); return builder; } - public static IEndpointRouteBuilder UseLti13PlatformDeepLinking(this IEndpointRouteBuilder app, Action? configure = null) + public static IEndpointRouteBuilder UseLti13PlatformDeepLinking(this IEndpointRouteBuilder app, Func? configure = null) { - var config = new DeepLinkingEndpointsConfig(); - configure?.Invoke(config); + DeepLinkingEndpointsConfig config = new(); + config = configure?.Invoke(config) ?? config; _ = app.MapPost(config.DeepLinkingResponseUrl, - async ([FromForm] DeepLinkResponseRequest request, string? contextId, ILogger logger, ITokenService tokenService, ICoreDataService coreDataService, IDeepLinkingDataService deepLinkingDataService, IDeepLinkingService deepLinkingService, CancellationToken cancellationToken) => + async ([FromForm] DeepLinkResponseRequest request, string? contextId, ILogger logger, ILti13TokenConfigService tokenService, ILti13CoreDataService coreDataService, ILti13DeepLinkingDataService deepLinkingDataService, ILti13DeepLinkingConfigService deepLinkingService, ILti13DeepLinkingHandler deepLinkingHandler, CancellationToken cancellationToken) => { const string DEEP_LINKING_SPEC = "https://www.imsglobal.org/spec/lti-dl/v2p0/#deep-linking-response-message"; const string INVALID_CLIENT = "invalid_client"; @@ -173,7 +187,7 @@ await deepLinkingDataService.SaveLineItemAsync(new LineItem await Task.WhenAll(saveTasks); } - return await deepLinkingService.HandleResponseAsync(tool.ClientId, deployment.Id, contextId, response, cancellationToken); + return await deepLinkingHandler.HandleResponseAsync(tool.ClientId, deployment.Id, contextId, response, cancellationToken); }) .WithName(RouteNames.DEEP_LINKING_RESPONSE) .DisableAntiforgery(); diff --git a/NP.Lti13Platform.NameRoleProvisioningServices/Populators/CustomPopulator.cs b/NP.Lti13Platform.NameRoleProvisioningServices/Populators/CustomPopulator.cs index a59faab..4f25750 100644 --- a/NP.Lti13Platform.NameRoleProvisioningServices/Populators/CustomPopulator.cs +++ b/NP.Lti13Platform.NameRoleProvisioningServices/Populators/CustomPopulator.cs @@ -13,7 +13,7 @@ public interface ICustomMessage public IDictionary? Custom { get; set; } } - public class CustomPopulator(ICoreDataService dataService) : Populator + public class CustomPopulator(ILti13CoreDataService dataService) : Populator { private static readonly IEnumerable LineItemAttemptGradeVariables = [ Lti13ResourceLinkVariables.AvailableUserStartDateTime, diff --git a/NP.Lti13Platform.NameRoleProvisioningServices/Populators/ServiceEndpointsPopulator.cs b/NP.Lti13Platform.NameRoleProvisioningServices/Populators/ServiceEndpointsPopulator.cs index 9b77121..b5da5f3 100644 --- a/NP.Lti13Platform.NameRoleProvisioningServices/Populators/ServiceEndpointsPopulator.cs +++ b/NP.Lti13Platform.NameRoleProvisioningServices/Populators/ServiceEndpointsPopulator.cs @@ -21,7 +21,7 @@ public class ServiceEndpoints } } - public class ServiceEndpointsPopulator(LinkGenerator linkGenerator, INameRoleProvisioningService nameRoleProvisioningService) : Populator + public class ServiceEndpointsPopulator(LinkGenerator linkGenerator, ILti13NameRoleProvisioningConfigService nameRoleProvisioningService) : Populator { public override async Task PopulateAsync(IServiceEndpoints obj, MessageScope scope, CancellationToken cancellationToken = default) { diff --git a/NP.Lti13Platform.NameRoleProvisioningServices/README.md b/NP.Lti13Platform.NameRoleProvisioningServices/README.md index 0ba3709..671e608 100644 --- a/NP.Lti13Platform.NameRoleProvisioningServices/README.md +++ b/NP.Lti13Platform.NameRoleProvisioningServices/README.md @@ -10,10 +10,10 @@ The IMS [Name and Role Provisioning Services](https://www.imsglobal.org/spec/lti 1. Add the nuget package to your project: -2. Add an implementation of the `INameRoleProvisioningDataService` interface: +2. Add an implementation of the `ILti13NameRoleProvisioningDataService` interface: ```csharp -public class DataService: INameRoleProvisioningDataService +public class DataService: ILti13NameRoleProvisioningDataService { ... } @@ -25,9 +25,7 @@ public class DataService: INameRoleProvisioningDataService builder.Services .AddLti13PlatformCore() .AddLti13PlatformNameRoleProvisioningServices() - .AddDefaultNameRoleProvisioningService(); - -builder.Services.AddTransient(); + .WithLti13NameRoleProvisioningDataService(); ``` 4. Setup the routing for the LTI 1.3 platform endpoints: @@ -36,11 +34,11 @@ builder.Services.AddTransient(); app.UseLti13PlatformNameRoleProvisioningServices(); ``` -## INameRoleProvisioningDataService +## ILti13NameRoleProvisioningDataService -There is no default `INameRoleProvisioningDataService` implementation to allow each project to store the data how they see fit. +There is no default `ILti13NameRoleProvisioningDataService` implementation to allow each project to store the data how they see fit. -The `INameRoleProvisioningDataService` interface is used to get the persisted members of a context filtered by multiple parameters. +The `ILti13NameRoleProvisioningDataService` interface is used to get the persisted members of a context filtered by multiple parameters. All of the internal services are transient and therefore the data service may be added at any scope (Transient, Scoped, Singleton). @@ -53,22 +51,50 @@ Default routes are provided for all endpoints. Routes can be configured when cal ```csharp app.UseLti13PlatformNameRoleProvisioningServices(config => { config.NamesAndRoleProvisioningServicesUrl = "/lti13/{deploymentId}/{contextId}/memberships"; // {deploymentId} and {contextId} are required + return config; }); ``` -### INameRoleProvisioningService +### ILti13NameRoleProvisioningConfigService + +The `ILti13NameRoleProvisioningService` interface is used to get the config for the name and role provisioning service. The config is used to tell the tools how to request the members of a context. -The `INameRoleProvisioningService` interface is used to get the config for the name and role provisioning service. The config is used to tell the tools how to request the members of a context. +There is a default implementation of the `ILti13NameRoleProvisioningConfigService` interface that uses a configuration set up on app start. +It will be configured using the [`IOptions`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration) pattern and configuration. +The configuration path for the service is `Lti13Platform:NameRoleProvisioningServices`. -There is a default implementation of the `INameRoleProvisioningService` interface that uses a configuration set up on app start. When calling the `AddDefaultNameRoleProvisioningService` method, the configuration can be setup at that time. A fallback to the current request scheme and host will be used if no ServiceEndpoint is configured. The Default implementation can be overridden by adding a new implementation of the `INameRoleProvisioningService` interface and not including the Default. This may be useful if the service URL is dynamic or needs to be determined at runtime. +Examples: + +```json +{ + "Lti13Platform": { + "NameRoleProvisioningServices": { + "ServiceAddress": "https://" + } + } +} +``` + +```csharp +builder.Services.Configure(x => { }); +``` + +The Default implementation can be overridden by adding a new implementation of the `ILti13NameRoleProvisioningConfigService` interface. +This may be useful if the service URL is dynamic or needs to be determined at runtime. ```csharp builder.Services .AddLti13PlatformCore() .AddLti13PlatformNameRoleProvisioningServices() - .AddDefaultNameRoleProvisioningService(x => { x.ServiceAddress = new Uri("https://") }); + .WithLti13NameRoleProvisioningConfigService(); ``` +## Configuration + +`ServiceAddress` + +The base url used to tell tools where the service is located. + ## Member Message The IMS [Name and Role Provisioning Services](https://www.imsglobal.org/spec/lti-nrps/v2p0#message-section) spec defines a way to give tools access to the parts of LTI messages that are specific to members. This project includes the specifics for the core message and known properties defined within the spec. Additional message can be added by calling `ExtendNameRoleProvisioningMessage` on startup. This follows the same pattern as [Populators](../NP.Lti13Platform.Core/README.md#populators) from the core spec. These messages should only contain the user specific message properties of the given message. Multiple populators may be added for the same interface and multiple interfaces may be added for the same . @@ -77,6 +103,6 @@ The IMS [Name and Role Provisioning Services](https://www.imsglobal.org/spec/lti builder.Services .AddLti13PlatformCore() .AddLti13PlatformNameRoleProvisioningServices() - .AddDefaultNameRoleProvisioningService() + .WithDefaultNameRoleProvisioningService() .ExtendNameRoleProvisioningMessage(""); ``` \ No newline at end of file diff --git a/NP.Lti13Platform.NameRoleProvisioningServices/Services/NameRoleProvisioningService.cs b/NP.Lti13Platform.NameRoleProvisioningServices/Services/DefaultNameRoleProvisioningConfigService.cs similarity index 80% rename from NP.Lti13Platform.NameRoleProvisioningServices/Services/NameRoleProvisioningService.cs rename to NP.Lti13Platform.NameRoleProvisioningServices/Services/DefaultNameRoleProvisioningConfigService.cs index 263624c..703485d 100644 --- a/NP.Lti13Platform.NameRoleProvisioningServices/Services/NameRoleProvisioningService.cs +++ b/NP.Lti13Platform.NameRoleProvisioningServices/Services/DefaultNameRoleProvisioningConfigService.cs @@ -4,7 +4,7 @@ namespace NP.Lti13Platform.NameRoleProvisioningServices.Services { - internal class NameRoleProvisioningService(IOptionsMonitor config, IHttpContextAccessor httpContextAccessor) : INameRoleProvisioningService + internal class DefaultNameRoleProvisioningConfigService(IOptionsMonitor config, IHttpContextAccessor httpContextAccessor) : ILti13NameRoleProvisioningConfigService { public async Task GetConfigAsync(string clientId, CancellationToken cancellationToken = default) { diff --git a/NP.Lti13Platform.NameRoleProvisioningServices/Services/INameRoleProvisioningService.cs b/NP.Lti13Platform.NameRoleProvisioningServices/Services/ILti13NameRoleProvisioningConfigService.cs similarity index 80% rename from NP.Lti13Platform.NameRoleProvisioningServices/Services/INameRoleProvisioningService.cs rename to NP.Lti13Platform.NameRoleProvisioningServices/Services/ILti13NameRoleProvisioningConfigService.cs index fe533f2..88a2eff 100644 --- a/NP.Lti13Platform.NameRoleProvisioningServices/Services/INameRoleProvisioningService.cs +++ b/NP.Lti13Platform.NameRoleProvisioningServices/Services/ILti13NameRoleProvisioningConfigService.cs @@ -2,7 +2,7 @@ namespace NP.Lti13Platform.NameRoleProvisioningServices.Services { - public interface INameRoleProvisioningService + public interface ILti13NameRoleProvisioningConfigService { Task GetConfigAsync(string clientId, CancellationToken cancellationToken = default); } diff --git a/NP.Lti13Platform.NameRoleProvisioningServices/Services/INameRoleProvisioningDataService.cs b/NP.Lti13Platform.NameRoleProvisioningServices/Services/ILti13NameRoleProvisioningDataService.cs similarity index 89% rename from NP.Lti13Platform.NameRoleProvisioningServices/Services/INameRoleProvisioningDataService.cs rename to NP.Lti13Platform.NameRoleProvisioningServices/Services/ILti13NameRoleProvisioningDataService.cs index 883f177..116f632 100644 --- a/NP.Lti13Platform.NameRoleProvisioningServices/Services/INameRoleProvisioningDataService.cs +++ b/NP.Lti13Platform.NameRoleProvisioningServices/Services/ILti13NameRoleProvisioningDataService.cs @@ -2,7 +2,7 @@ namespace NP.Lti13Platform.NameRoleProvisioningServices.Services { - public interface INameRoleProvisioningDataService + public interface ILti13NameRoleProvisioningDataService { Task> GetMembershipsAsync(string deploymnetId, string contextId, int pageIndex, int limit, string? role, string? resourceLinkId, DateTime? asOfDate = null, CancellationToken cancellationToken = default); Task> GetUsersAsync(IEnumerable userIds, DateTime? asOfDate = null, CancellationToken cancellationToken = default); diff --git a/NP.Lti13Platform.NameRoleProvisioningServices/Startup.cs b/NP.Lti13Platform.NameRoleProvisioningServices/Startup.cs index 5abe885..8ca8d70 100644 --- a/NP.Lti13Platform.NameRoleProvisioningServices/Startup.cs +++ b/NP.Lti13Platform.NameRoleProvisioningServices/Startup.cs @@ -34,15 +34,21 @@ public static Lti13PlatformBuilder AddLti13PlatformNameRoleProvisioningServices( builder.ExtendLti13Message(); + builder.Services.AddOptions().BindConfiguration("Lti13Platform:NameRoleProvisioningServices"); + builder.Services.TryAddSingleton(); + return builder; } - public static Lti13PlatformBuilder AddDefaultNameRoleProvisioningService(this Lti13PlatformBuilder builder, Action? configure = null) + public static Lti13PlatformBuilder WithLti13NameRoleProvisioningDataService(this Lti13PlatformBuilder builder, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) where T : ILti13NameRoleProvisioningDataService { - configure ??= (x) => { }; + builder.Services.Add(new ServiceDescriptor(typeof(ILti13NameRoleProvisioningDataService), typeof(T), serviceLifetime)); + return builder; + } - builder.Services.Configure(configure); - builder.Services.AddTransient(); + public static Lti13PlatformBuilder WithLti13NameRoleProvisioningConfigService(this Lti13PlatformBuilder builder, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) where T : ILti13NameRoleProvisioningConfigService + { + builder.Services.Add(new ServiceDescriptor(typeof(ILti13NameRoleProvisioningConfigService), typeof(T), serviceLifetime)); return builder; } @@ -96,15 +102,15 @@ private static void CreateTypes() } } - public static IEndpointRouteBuilder UseLti13PlatformNameRoleProvisioningServices(this IEndpointRouteBuilder routeBuilder, Action? configure = null) + public static IEndpointRouteBuilder UseLti13PlatformNameRoleProvisioningServices(this IEndpointRouteBuilder routeBuilder, Func? configure = null) { CreateTypes(); - var config = new EndpointsConfig(); - configure?.Invoke(config); + EndpointsConfig config = new(); + config = configure?.Invoke(config) ?? config; routeBuilder.MapGet(config.NamesAndRoleProvisioningServicesUrl, - async (IServiceProvider serviceProvider, IHttpContextAccessor httpContextAccessor, ICoreDataService coreDataService, INameRoleProvisioningDataService nrpsDataService, LinkGenerator linkGenerator, string deploymentId, string contextId, string? role, string? rlid, int? limit, int pageIndex = 0, long? since = null, CancellationToken cancellationToken = default) => + async (IServiceProvider serviceProvider, IHttpContextAccessor httpContextAccessor, ILti13CoreDataService coreDataService, ILti13NameRoleProvisioningDataService nrpsDataService, LinkGenerator linkGenerator, string deploymentId, string contextId, string? role, string? rlid, int? limit, int pageIndex = 0, long? since = null, CancellationToken cancellationToken = default) => { const string RESOURCE_LINK_UNAVAILABLE = "resource link unavailable"; const string RESOURCE_LINK_UNAVAILABLE_DESCRIPTION = "resource link does not exist in the context"; diff --git a/NP.Lti13Platform.WebExample/Controllers/HomeController.cs b/NP.Lti13Platform.WebExample/Controllers/HomeController.cs index 244ace1..58c8195 100644 --- a/NP.Lti13Platform.WebExample/Controllers/HomeController.cs +++ b/NP.Lti13Platform.WebExample/Controllers/HomeController.cs @@ -6,7 +6,7 @@ namespace NP.Lti13Platform.WebExample.Controllers { - public class HomeController(ILogger logger, IUrlServiceHelper service, ICoreDataService dataService) : Controller + public class HomeController(ILogger logger, IUrlServiceHelper service, ILti13CoreDataService dataService) : Controller { public async Task Index(CancellationToken cancellationToken) { diff --git a/NP.Lti13Platform.WebExample/Program.cs b/NP.Lti13Platform.WebExample/Program.cs index db2c3a1..bc58a61 100644 --- a/NP.Lti13Platform.WebExample/Program.cs +++ b/NP.Lti13Platform.WebExample/Program.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using NP.Lti13Platform; +using NP.Lti13Platform.DeepLinking.Configs; using NP.Lti13Platform.WebExample; var builder = WebApplication.CreateBuilder(args); @@ -8,12 +9,17 @@ builder.Services.AddControllersWithViews(); builder.Services - .AddLti13PlatformWithDefaults(x => { x.Issuer = "https://mytest.com"; }) - .AddDataService(); + .AddLti13Platform() + .WithLti13DataService(); builder.Services.RemoveAll(); builder.Services.AddSingleton(); +builder.Services.Configure(x => +{ + x.AddDefaultContentItemMapping(); +}); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -51,7 +57,7 @@ namespace NP.Lti13Platform.WebExample using NP.Lti13Platform.NameRoleProvisioningServices.Services; using System.Security.Cryptography; - public class DataService : IDataService + public class DataService : ILti13DataService { private static readonly CryptoProviderFactory CRYPTO_PROVIDER_FACTORY = new() { CacheSignatureProviders = false }; @@ -116,42 +122,42 @@ static DataService() }); } - Task ICoreDataService.GetToolAsync(string clientId, CancellationToken cancellationToken) + Task ILti13CoreDataService.GetToolAsync(string clientId, CancellationToken cancellationToken) { return Task.FromResult(Tools.SingleOrDefault(t => t.ClientId == clientId)); } - Task ICoreDataService.GetDeploymentAsync(string deploymentId, CancellationToken cancellationToken) + Task ILti13CoreDataService.GetDeploymentAsync(string deploymentId, CancellationToken cancellationToken) { return Task.FromResult(Deployments.SingleOrDefault(d => d.Id == deploymentId)); } - Task ICoreDataService.GetContextAsync(string contextId, CancellationToken cancellationToken) + Task ILti13CoreDataService.GetContextAsync(string contextId, CancellationToken cancellationToken) { return Task.FromResult(Contexts.SingleOrDefault(c => c.Id == contextId)); } - Task ICoreDataService.GetUserAsync(string userId, CancellationToken cancellationToken) + Task ILti13CoreDataService.GetUserAsync(string userId, CancellationToken cancellationToken) { return Task.FromResult(Users.SingleOrDefault(u => u.Id == userId)); } - Task ICoreDataService.GetMembershipAsync(string contextId, string userId, CancellationToken cancellationToken) + Task ILti13CoreDataService.GetMembershipAsync(string contextId, string userId, CancellationToken cancellationToken) { return Task.FromResult(Memberships.SingleOrDefault(m => m.ContextId == contextId && m.UserId == userId)); } - Task> ICoreDataService.GetMentoredUserIdsAsync(string contextId, string userId, CancellationToken cancellationToken) + Task> ILti13CoreDataService.GetMentoredUserIdsAsync(string contextId, string userId, CancellationToken cancellationToken) { return Task.FromResult>([]); } - Task ICoreDataService.GetResourceLinkAsync(string resourceLinkId, CancellationToken cancellationToken) + Task ILti13CoreDataService.GetResourceLinkAsync(string resourceLinkId, CancellationToken cancellationToken) { return Task.FromResult(ResourceLinks.SingleOrDefault(r => r.Id == resourceLinkId)); } - Task> ICoreDataService.GetLineItemsAsync(string deploymentId, string contextId, int pageIndex, int limit, string? resourceId, string? resourceLinkId, string? tag, CancellationToken cancellationToken) + Task> ILti13CoreDataService.GetLineItemsAsync(string deploymentId, string contextId, int pageIndex, int limit, string? resourceId, string? resourceLinkId, string? tag, CancellationToken cancellationToken) { var lineItems = LineItems.Where(li => li.DeploymentId == deploymentId && li.ContextId == contextId && (resourceId == null || li.ResourceId == resourceId) && (resourceLinkId == null || li.ResourceLinkId == resourceLinkId) && (tag == null || li.Tag == tag)).ToList(); @@ -179,12 +185,12 @@ public Task SaveLineItemAsync(LineItem lineItem, CancellationToken cance } } - async Task ICoreDataService.GetAttemptAsync(string resourceLinkId, string userId, CancellationToken cancellationToken) + async Task ILti13CoreDataService.GetAttemptAsync(string resourceLinkId, string userId, CancellationToken cancellationToken) { return await Task.FromResult(Attempts.SingleOrDefault(a => a.ResourceLinkId == resourceLinkId && a.UserId == userId)); } - Task> IAssignmentGradeDataService.GetGradesAsync(string lineItemId, int pageIndex, int limit, string? userId, CancellationToken cancellationToken) + Task> ILti13AssignmentGradeDataService.GetGradesAsync(string lineItemId, int pageIndex, int limit, string? userId, CancellationToken cancellationToken) { var grades = Grades.Where(x => x.LineItemId == lineItemId && (userId == null || x.UserId == userId)).ToList(); @@ -195,12 +201,12 @@ Task> IAssignmentGradeDataService.GetGradesAsync(string lineI }); } - Task ICoreDataService.GetGradeAsync(string lineItemId, string userId, CancellationToken cancellationToken) + Task ILti13CoreDataService.GetGradeAsync(string lineItemId, string userId, CancellationToken cancellationToken) { return Task.FromResult(Grades.SingleOrDefault(g => g.LineItemId == lineItemId && g.UserId == userId)); } - Task IAssignmentGradeDataService.SaveGradeAsync(Grade grade, CancellationToken cancellationToken) + Task ILti13AssignmentGradeDataService.SaveGradeAsync(Grade grade, CancellationToken cancellationToken) { var existingGrade = Grades.SingleOrDefault(x => x.LineItemId == grade.LineItemId && x.UserId == grade.UserId); if (existingGrade != null) @@ -215,12 +221,12 @@ Task IAssignmentGradeDataService.SaveGradeAsync(Grade grade, CancellationToken c return Task.CompletedTask; } - Task ICoreDataService.GetServiceTokenRequestAsync(string toolId, string serviceTokenId, CancellationToken cancellationToken) + Task ILti13CoreDataService.GetServiceTokenRequestAsync(string toolId, string serviceTokenId, CancellationToken cancellationToken) { return Task.FromResult(ServiceTokens.FirstOrDefault(x => x.ToolId == toolId && x.Id == serviceTokenId)); } - Task ICoreDataService.SaveServiceTokenRequestAsync(ServiceToken serviceToken, CancellationToken cancellationToken) + Task ILti13CoreDataService.SaveServiceTokenRequestAsync(ServiceToken serviceToken, CancellationToken cancellationToken) { var existing = ServiceTokens.SingleOrDefault(x => x.ToolId == serviceToken.ToolId && x.Id == serviceToken.Id); if (existing != null) @@ -235,7 +241,7 @@ Task ICoreDataService.SaveServiceTokenRequestAsync(ServiceToken serviceToken, Ca return Task.CompletedTask; } - Task> ICoreDataService.GetPublicKeysAsync(CancellationToken cancellationToken) + Task> ILti13CoreDataService.GetPublicKeysAsync(CancellationToken cancellationToken) { var rsaProvider = RSA.Create(); var key = "-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6S7asUuzq5Q/3U9rbs+P\r\nkDVIdjgmtgWreG5qWPsC9xXZKiMV1AiV9LXyqQsAYpCqEDM3XbfmZqGb48yLhb/X\r\nqZaKgSYaC/h2DjM7lgrIQAp9902Rr8fUmLN2ivr5tnLxUUOnMOc2SQtr9dgzTONY\r\nW5Zu3PwyvAWk5D6ueIUhLtYzpcB+etoNdL3Ir2746KIy/VUsDwAM7dhrqSK8U2xF\r\nCGlau4ikOTtvzDownAMHMrfE7q1B6WZQDAQlBmxRQsyKln5DIsKv6xauNsHRgBAK\r\nctUxZG8M4QJIx3S6Aughd3RZC4Ca5Ae9fd8L8mlNYBCrQhOZ7dS0f4at4arlLcaj\r\ntwIDAQAB\r\n-----END PUBLIC KEY-----"; @@ -249,7 +255,7 @@ Task> ICoreDataService.GetPublicKeysAsync(CancellationT return Task.FromResult>([securityKey]); } - Task ICoreDataService.GetPrivateKeyAsync(CancellationToken cancellationToken) + Task ILti13CoreDataService.GetPrivateKeyAsync(CancellationToken cancellationToken) { var rsaProvider = RSA.Create(); var key = "-----BEGIN PRIVATE KEY-----\r\nMIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDpLtqxS7OrlD/d\r\nT2tuz4+QNUh2OCa2Bat4bmpY+wL3FdkqIxXUCJX0tfKpCwBikKoQMzddt+ZmoZvj\r\nzIuFv9eploqBJhoL+HYOMzuWCshACn33TZGvx9SYs3aK+vm2cvFRQ6cw5zZJC2v1\r\n2DNM41hblm7c/DK8BaTkPq54hSEu1jOlwH562g10vcivbvjoojL9VSwPAAzt2Gup\r\nIrxTbEUIaVq7iKQ5O2/MOjCcAwcyt8TurUHpZlAMBCUGbFFCzIqWfkMiwq/rFq42\r\nwdGAEApy1TFkbwzhAkjHdLoC6CF3dFkLgJrkB7193wvyaU1gEKtCE5nt1LR/hq3h\r\nquUtxqO3AgMBAAECggEBANX6C+7EA/TADrbcCT7fMuNnMb5iGovPuiDCWc6bUIZC\r\nQ0yac45l7o1nZWzfzpOkIprJFNZoSgIF7NJmQeYTPCjAHwsSVraDYnn3Y4d1D3tM\r\n5XjJcpX2bs1NactxMTLOWUl0JnkGwtbWp1Qq+DBnMw6ghc09lKTbHQvhxSKNL/0U\r\nC+YmCYT5ODmxzLBwkzN5RhxQZNqol/4LYVdji9bS7N/UITw5E6LGDOo/hZHWqJsE\r\nfgrJTPsuCyrYlwrNkgmV2KpRrGz5MpcRM7XHgnqVym+HyD/r9E7MEFdTLEaiiHcm\r\nIsh1usJDEJMFIWkF+rnEoJkQHbqiKlQBcoqSbCmoMWECgYEA/4379mMPF0JJ/EER\r\n4VH7/ZYxjdyphenx2VYCWY/uzT0KbCWQF8KXckuoFrHAIP3EuFn6JNoIbja0NbhI\r\nHGrU29BZkATG8h/xjFy/zPBauxTQmM+yS2T37XtMoXNZNS/ubz2lJXMOapQQiXVR\r\nl/tzzpyWaCe9j0NT7DAU0ZFmDbECgYEA6ZbjkcOs2jwHsOwwfamFm4VpUFxYtED7\r\n9vKzq5d7+Ii1kPKHj5fDnYkZd+mNwNZ02O6OGxh40EDML+i6nOABPg/FmXeVCya9\r\nVump2Yqr2fAK3xm6QY5KxAjWWq2kVqmdRmICSL2Z9rBzpXmD5o06y9viOwd2bhBo\r\n0wB02416GecCgYEA+S/ZoEa3UFazDeXlKXBn5r2tVEb2hj24NdRINkzC7h23K/z0\r\npDZ6tlhPbtGkJodMavZRk92GmvF8h2VJ62vAYxamPmhqFW5Qei12WL+FuSZywI7F\r\nq/6oQkkYT9XKBrLWLGJPxlSKmiIGfgKHrUrjgXPutWEK1ccw7f10T2UXvgECgYEA\r\nnXqLa58G7o4gBUgGnQFnwOSdjn7jkoppFCClvp4/BtxrxA+uEsGXMKLYV75OQd6T\r\nIhkaFuxVrtiwj/APt2lRjRym9ALpqX3xkiGvz6ismR46xhQbPM0IXMc0dCeyrnZl\r\nQKkcrxucK/Lj1IBqy0kVhZB1IaSzVBqeAPrCza3AzqsCgYEAvSiEjDvGLIlqoSvK\r\nMHEVe8PBGOZYLcAdq4YiOIBgddoYyRsq5bzHtTQFgYQVK99Cnxo+PQAvzGb+dpjN\r\n/LIEAS2LuuWHGtOrZlwef8ZpCQgrtmp/phXfVi6llcZx4mMm7zYmGhh2AsA9yEQc\r\nacgc4kgDThAjD7VlXad9UHpNMO8=\r\n-----END PRIVATE KEY-----"; @@ -265,7 +271,7 @@ Task ICoreDataService.GetPrivateKeyAsync(CancellationToken cancella - Task> INameRoleProvisioningDataService.GetMembershipsAsync(string deploymentId, string contextId, int pageIndex, int limit, string? role, string? resourceLinkId, DateTime? asOfDate, CancellationToken cancellationToken) + Task> ILti13NameRoleProvisioningDataService.GetMembershipsAsync(string deploymentId, string contextId, int pageIndex, int limit, string? role, string? resourceLinkId, DateTime? asOfDate, CancellationToken cancellationToken) { if (ResourceLinks.Any(x => x.ContextId == contextId && x.DeploymentId == deploymentId && (resourceLinkId == null || resourceLinkId == x.Id))) { @@ -281,14 +287,14 @@ Task> INameRoleProvisioningDataService.GetMembershipsAsy return Task.FromResult(PartialList.Empty); } - Task> INameRoleProvisioningDataService.GetUsersAsync(IEnumerable userIds, DateTime? asOfDate, CancellationToken cancellationToken) + Task> ILti13NameRoleProvisioningDataService.GetUsersAsync(IEnumerable userIds, DateTime? asOfDate, CancellationToken cancellationToken) { return Task.FromResult(Users.Where(x => userIds.Contains(x.Id))); } - Task IDeepLinkingDataService.SaveContentItemAsync(string deploymentId, string? contextId, ContentItem contentItem, CancellationToken cancellationToken) + Task ILti13DeepLinkingDataService.SaveContentItemAsync(string deploymentId, string? contextId, ContentItem contentItem, CancellationToken cancellationToken) { var id = Guid.NewGuid().ToString(); @@ -316,12 +322,12 @@ Task IDeepLinkingDataService.SaveContentItemAsync(string deploymentId, s - Task IAssignmentGradeDataService.GetLineItemAsync(string lineItemId, CancellationToken cancellationToken) + Task ILti13AssignmentGradeDataService.GetLineItemAsync(string lineItemId, CancellationToken cancellationToken) { return Task.FromResult(LineItems.SingleOrDefault(x => x.Id == lineItemId)); } - Task IAssignmentGradeDataService.DeleteLineItemAsync(string lineItemId, CancellationToken cancellationToken) + Task ILti13AssignmentGradeDataService.DeleteLineItemAsync(string lineItemId, CancellationToken cancellationToken) { LineItems.RemoveAll(i => i.Id == lineItemId); diff --git a/NP.Lti13Platform.WebExample/appsettings.Development.json b/NP.Lti13Platform.WebExample/appsettings.Development.json index 0c208ae..936756a 100644 --- a/NP.Lti13Platform.WebExample/appsettings.Development.json +++ b/NP.Lti13Platform.WebExample/appsettings.Development.json @@ -4,5 +4,10 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "Lti13Platform": { + "Token": { + "Issuer": "https://mytest.com" + } } } diff --git a/NP.Lti13Platform/IDataService.cs b/NP.Lti13Platform/ILti13DataService.cs similarity index 58% rename from NP.Lti13Platform/IDataService.cs rename to NP.Lti13Platform/ILti13DataService.cs index 0dd865a..b7ae68a 100644 --- a/NP.Lti13Platform/IDataService.cs +++ b/NP.Lti13Platform/ILti13DataService.cs @@ -5,5 +5,5 @@ namespace NP.Lti13Platform { - public interface IDataService : ICoreDataService, IDeepLinkingDataService, INameRoleProvisioningDataService, IAssignmentGradeDataService { } + public interface ILti13DataService : ILti13CoreDataService, ILti13DeepLinkingDataService, ILti13NameRoleProvisioningDataService, ILti13AssignmentGradeDataService { } } \ No newline at end of file diff --git a/NP.Lti13Platform/README.md b/NP.Lti13Platform/README.md index d7fae4e..0584951 100644 --- a/NP.Lti13Platform/README.md +++ b/NP.Lti13Platform/README.md @@ -27,8 +27,8 @@ public class DataService: IDataService ```csharp builder.Services - .AddLti13PlatformWithDefaults(x => { x.Issuer = "https://.com"; }) - .AddDataService(); + .AddLti13Platform() + .WithLti13DataService(); ``` 4. Setup the routing for the LTI 1.3 platform endpoints: @@ -45,31 +45,28 @@ The `IDataService` interface is a combination of all data services required for ```diff builder.Services -+ .AddLti13PlatformWithDefaults(x => { x.Issuer = "https://.com"; }); -- .AddLti13PlatformWithDefaults(x => { x.Issuer = "https://.com"; }) -- .AddDataService(); - -+ builder.Services.AddTransient(); -+ builder.Services.AddTransient(); -+ builder.Services.AddTransient(); -+ builder.Services.AddTransient(); + .AddLti13Platform() +- .WithLti13DataService(); ++ .WithLti13CoreDataService() ++ .WithLti13DeepLinkingDataService() ++ .WithLti13AssignmentGradeDataService() ++ .WithLti13NameRoleProvisioningDataService(); ``` All of the internal services are transient and therefore the data services may be added at any scope (Transient, Scoped, Singleton). ## Defaults -Many of the specs have default implementations that use a static configuration on startup. The defaults are set in the `AddLti13PlatformWithDefaults` method. If you can't configure the services at startup you can use the non-default extension method and add your own implementation of the services. +Many of the specs have default implementations that use a static configuration on startup. If you can't configure the services at startup you can add your own implementation of the services. ```diff builder.Services -- .AddLti13PlatformWithDefaults(x => { x.Issuer = "https://.com"; }) -+ .AddLti13Platform() - .AddDataService(); - -+ builder.Services.AddTransient(); -+ builder.Services.AddTransient(); -+ builder.Services.AddTransient(); -+ builder.Services.AddTransient(); -+ builder.Services.AddTransient(); + .AddLti13Platform() + .WithLti13DataService() ++ .WithLti13TokenConfigService() ++ .WithLti13PlatformService() ++ .WithLti13DeepLinkingConfigService() ++ .WithLti13DeepLinkingHandler() ++ .WithLti13AssignmentGradeConfigService() ++ .WithLti13NameRoleProvisioningConfigService(); ``` \ No newline at end of file diff --git a/NP.Lti13Platform/Startup.cs b/NP.Lti13Platform/Startup.cs index 63739a9..21c8db4 100644 --- a/NP.Lti13Platform/Startup.cs +++ b/NP.Lti13Platform/Startup.cs @@ -1,19 +1,13 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using NP.Lti13Platform.AssignmentGradeServices; using NP.Lti13Platform.AssignmentGradeServices.Configs; -using NP.Lti13Platform.AssignmentGradeServices.Services; using NP.Lti13Platform.Core; using NP.Lti13Platform.Core.Configs; -using NP.Lti13Platform.Core.Models; -using NP.Lti13Platform.Core.Services; using NP.Lti13Platform.DeepLinking; using NP.Lti13Platform.DeepLinking.Configs; -using NP.Lti13Platform.DeepLinking.Services; using NP.Lti13Platform.NameRoleProvisioningServices; using NP.Lti13Platform.NameRoleProvisioningServices.Configs; -using NP.Lti13Platform.NameRoleProvisioningServices.Services; namespace NP.Lti13Platform { @@ -28,78 +22,35 @@ public static Lti13PlatformBuilder AddLti13Platform(this IServiceCollection serv .AddLti13PlatformAssignmentGradeServices(); } - public static Lti13PlatformBuilder AddLti13PlatformWithDefaults( - this IServiceCollection services, - Action configureToken, - Action? configurePlatform = null, - Action? configureDeepLinking = null, - Action? configureAssignmentGradeService = null, - Action? configureNameRoleProvisioningService = null) + public static Lti13PlatformBuilder WithLti13DataService(this Lti13PlatformBuilder builder, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + where T : ILti13DataService { - return services.AddLti13Platform() - .AddDefaultTokenService(configureToken) - .AddDefaultPlatformService(configurePlatform) - .AddDefaultDeepLinkingService(configureDeepLinking) - .AddDefaultAssignmentGradeService(configureAssignmentGradeService) - .AddDefaultNameRoleProvisioningService(configureNameRoleProvisioningService); - } - - public static Lti13PlatformBuilder AddDataService(this Lti13PlatformBuilder builder, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) - where T : IDataService - { - builder.Services.TryAdd(new ServiceDescriptor(typeof(ICoreDataService), typeof(T), serviceLifetime)); - builder.Services.TryAdd(new ServiceDescriptor(typeof(IDeepLinkingDataService), typeof(T), serviceLifetime)); - builder.Services.TryAdd(new ServiceDescriptor(typeof(INameRoleProvisioningDataService), typeof(T), serviceLifetime)); - builder.Services.TryAdd(new ServiceDescriptor(typeof(IAssignmentGradeDataService), typeof(T), serviceLifetime)); + builder.WithLti13CoreDataService(serviceLifetime) + .WithLti13DeepLinkingDataService(serviceLifetime) + .WithLti13NameRoleProvisioningDataService(serviceLifetime) + .WithLti13AssignmentGradeDataService(serviceLifetime); return builder; } - public static IEndpointRouteBuilder UseLti13Platform(this IEndpointRouteBuilder app, Action? configure = null) + public static IEndpointRouteBuilder UseLti13Platform(this IEndpointRouteBuilder app, Func? configure = null) { - var endpointsConfig = new Lti13PlatformEndpointsConfig(); - configure?.Invoke(endpointsConfig); + Lti13PlatformEndpointsConfig config = new(); + config = configure?.Invoke(config) ?? config; return app - .UseLti13PlatformCore(x => - { - if (endpointsConfig.Core != null) - { - x.JwksUrl = endpointsConfig.Core.JwksUrl; - x.AuthorizationUrl = endpointsConfig.Core.AuthorizationUrl; - x.TokenUrl = endpointsConfig.Core.TokenUrl; - } - }) - .UseLti13PlatformDeepLinking(x => - { - if (endpointsConfig.DeepLinking != null) - { - x.DeepLinkingResponseUrl = endpointsConfig.DeepLinking.DeepLinkingResponseUrl; - } - }) - .UseLti13PlatformNameRoleProvisioningServices(x => - { - if (endpointsConfig.NameRoleProvisioningServices != null) - { - x.NamesAndRoleProvisioningServicesUrl = endpointsConfig.NameRoleProvisioningServices.NamesAndRoleProvisioningServicesUrl; - } - }) - .UseLti13PlatformAssignmentGradeServices(x => - { - if (endpointsConfig.AssignmentGradeServices != null) - { - x.LineItemUrl = endpointsConfig.AssignmentGradeServices.LineItemUrl; - x.LineItemsUrl = endpointsConfig.AssignmentGradeServices.LineItemsUrl; - } - }); + .UseLti13PlatformCore(x => config.Core) + .UseLti13PlatformDeepLinking(x => config.DeepLinking) + .UseLti13PlatformNameRoleProvisioningServices(x => config.NameRoleProvisioningServices) + .UseLti13PlatformAssignmentGradeServices(x => config.AssignmentGradeServices); } } public class Lti13PlatformEndpointsConfig { - public Lti13PlatformCoreEndpointsConfig? Core { get; set; } - public DeepLinkingEndpointsConfig? DeepLinking { get; set; } - public EndpointsConfig? NameRoleProvisioningServices { get; set; } - public ServiceEndpointsConfig? AssignmentGradeServices { get; set; } + public Lti13PlatformCoreEndpointsConfig Core { get; set; } = new(); + public DeepLinkingEndpointsConfig DeepLinking { get; set; } = new(); + public EndpointsConfig NameRoleProvisioningServices { get; set; } = new(); + public ServiceEndpointsConfig AssignmentGradeServices { get; set; } = new(); } } \ No newline at end of file