Provides an almost plug and play web interface with a few different utility modules that can enabled as needed and access to each module can be restricted.
Available modules:
- Tests module that allows given backend methods to be executed in a UI to check the status of integrations, run utility methods and other things.
- Messages module where latest sent messages from the system can be viewed, optionally along with any error message. Can be used for e.g. outgoing mail and sms.
- Endpoint Control module to set request limits for decorated endpoints, as well as viewing some request statistics.
- IP Whitelist module to handle blocking everything except for configurable whitelisted ips & sections of the site.
- Overview module where registed events that can be shown in a status interface, e.g. showing the stability of integrations.
- Audit module where actions from other modules are logged.
- Data repeater module that can store and retry sending/recieving data with modifications.
- Data flow module that can show filtered custom data. For e.g. previewing the latest imported/exported data.
- Data exporter module that can filter and export data.
- Content permutations module to help find permutations of site content.
- Comparison module where content can be compared in a bit more simplified interface.
- GoTo module where content can be located in a bit more simplified interface.
- Mapped Data module to show how models are mapped.
- Event notifications module for notifying through custom implementations when custom events occur.
- Settings module where custom settings can be changed at runtime.
- IDE where C# scripts can be stored and executed in the context of the web application.
- Access token module where tokens with limited access and lifespan can be created to access other modules.
- Downloads module where files can be made available for download, optionally protected by password, expiration date and download count limit.
- Metrics module that outputs some simple metrics you can track manually.
- Request log module that lists controllers and actions with their latest requests and errors.
- Release notes module that can show release notes.
- [Not styled yet in 4.x+] Documentation module that shows generated sequence diagrams from code decorated with attributes.
- [Deprecated in 4.x+] Log searcher module for searching through logfiles on disk.
-
Install the QoDL.Toolkit.WebUI nuget package.
-
Create a custom flags enum with any desired access roles, e.g:
[Flags] public enum AccessRoles { None = 0, Guest = 1, WebAdmins = 2, SystemAdmins = 4 }
-
Create a controller and inherit
ToolkitControllerBase<AccessRoles>, where AccessRoles is your enum from the step above. -
Invoke
UseModule(..)to enable any desired modules. -
For .NET Core configure HttpContextAccessor and your instance resolver:
services.AddHttpContextAccessor()TKIoCSetup.ConfigureForServiceProvider(app.Services);- or
TKGlobalConfig.DefaultInstanceResolver = (type, scopeContainer) => app.ApplicationServices.GetService(type); - or
TKGlobalConfig.DefaultInstanceResolver = (type, scopeContainer) => app.Services.GetService(type);
Example controller
public class MyController : ToolkitControllerBase<AccessRoles>
{
// Enable any modules by invoking the UseModule(..) method.
public MyController()
{
// UseModule(<module>, <optionally override name>)
UseModule(new TKTestsModule(new TKTestsModuleOptions() {
AssembliesContainingTests = new[] { typeof(MyController).Assembly }
}));
}
// Set any options that will be passed to the front-end here,
// including the path to this controller.
protected override TKFrontEndOptions GetFrontEndOptions()
=> new TKFrontEndOptions("/Toolkit")
{
ApplicationTitle = "Toolkit",
// See own section below on how to avoid using cdn for js assets if needed.
EditorConfig = new TKFrontEndOptions.EditorWorkerConfig
{
EditorWorkerUrl = "/scripts/editor.worker.js",
JsonWorkerUrl = "/scripts/json.worker.js"
}
//...
};
// Set any options for the view here.
protected override TKPageOptions GetPageOptions()
=> new TKPageOptions()
{
PageTitle = "Toolkit",
// See own section below on how to avoid using cdn for js assets if needed.
JavaScriptUrls = new List<string> {
"/scripts/toolkit.js",
"/scripts/toolkit.vendor.js"
},
//...
};
// Return the user id/name and any roles the the current request have here.
protected override RequestInformation<AccessRoles> GetRequestInformation(HttpRequestBase request)
{
var roles = AccessRoles.Guest;
if (request.IsWebAdmin())
{
roles |= AccessRoles.WebAdmins;
}
if (request.IsSysAdmin())
{
roles |= AccessRoles.SystemAdmins;
}
// The user id/name provided are used for the audit module, "changed by" texts etc.
return new RequestInformation<AccessRoles>(
roles, request.UserId(), request.UserName());
}
// Access options and other configs here.
protected override void ConfigureAccess(HttpRequestBase request, AccessConfig<AccessRoles> config)
{
// There's 3 methods available to grant the request access to modules:
// #1: Give a given role access to a given module,
// without setting any module access options:
config.GiveRolesAccessToModule<TKTestsModule>(AccessRoles.SystemAdmins);
// #2: Give a given role access to a given module,
// with the given access options:
config.GiveRolesAccessToModule(AccessRoles.SystemAdmins, TKTestsModule.AccessOption.ViewInvalidTests);
// Optionally limit access to the given categories
config.GiveRolesAccessToModule(AccessRoles.SystemAdmins, TKTestsModule.AccessOption.ViewInvalidTests, new[] { "CategoryX" });
// #3: Give a given role full access to a given module,
// including all module access options:
config.GiveRolesAccessToModuleWithFullAccess<TKTestsModule>(AccessRoles.WebAdmins);
// Other access options are available on the config object:
config.ShowFailedModuleLoadStackTrace = new Maybe<AccessRole>(AccessRoles.WebAdmins);
config.PingAccess = new Maybe<AccessRole>(AccessRoles.WebAdmins);
config.RedirectTargetOnNoAccess = "/no-access";
// To redirect after login and persist state something like this can be used:
config.RedirectTargetOnNoAccessUsingRequest = (r, q) => $"/login?returnUrl={HttpUtility.UrlEncode($"/toolkit?{q}")}";
//..
// Properties CurrentRequestAccessRoles and CurrentRequestInformation
// are available to use here as well if needed.
}
}(Optional) How to bundle frontend instead of using CDN
By default frontend scripts with versions matching the nuget package version are fetched from unpkg.com. Alternatively use one of the following methods to bundle the frontend with the project:
The fastest and easiest way is to add the QoDL.Toolkit.WebUI.Assets nuget package. The package contains all frontend assets, will load them into memory and configure the ui to use them. Requires a few extra mb of memory but makes it easy to update. Does not include the summary scripts for metrics and release notes (see below).
Optionally manually download the frontend files from https://www.npmjs.com/package/christianw-toolkit and include in project. Then configure JavaScriptUrls to include healthecheck.js, and EditorWorkerUrl + JsonWorkerUrl to include their scripts in the frontend and page option models. Requires the files to be manually updated when updating to a new version of the nuget package.
If metrics or release notes summary is to be bundled with the project, they will have to be configured manually. See example below.
// Example using QoDL.Toolkit.WebUI.Assets nuget package that enables the GetAsset endpoint.
var tkController = "/url_to_your_tk_controller";
var assemblyVersion = "your_version";
TKAssetGlobalConfig.DefaultMetricsSummaryJavascriptUrl = $"{tkController}/GetAsset?n=metrics.js&v={assemblyVersion}";
TKAssetGlobalConfig.DefaultReleaseNotesSummaryJavascriptUrl = $"{tkController}/GetAsset?n=releaseNotesSummary.js&v={assemblyVersion}";Allows given backend methods to be executed in a UI to check the status of integrations, run utility methods and other things. Any exception thrown from a test will be included in full detail in the UI for easy debugging.
Hold ctrl-shift to view any test categories and show links to open tests in single-mode.
By default test definitions are cached statically, if this is not desired call TestDiscoveryService.UseCache = false on project startup.
UseModule(new TKTestsModule(new TKTestsModuleOptions() {
AssembliesContainingTests = new[] { typeof(MyController).Assembly },
// Optionally support custom reference parameter types
// ReferenceParameterFactories = ...
}))
// Optionally configure group order
.ConfigureGroups((options) => options
.ConfigureGroup(MyTKConstants.Group.StatusChecks, uiOrder: 100)
.ConfigureGroup(...)
);;For a method to be discovered it needs to..
- ..be public.
- ..be in a class with a
[RuntimeTestClass]attribute. - ..be decorated with a
[RuntimeTest]attribute. - ..return a
TestResultor be async and return aTask<TestResult>.
[RuntimeTestClass]
public class MyClass
{
[RuntimeTest]
public TestResult MyMethod()
=> TestResult.CreateSuccess("Executed successfully");
}Another method example
[RuntimeTest("Get data from somewhere", "Retrieves data from service X and shows the response data.")]
[RuntimeTestParameter(target: "id", name: "Data id", description: "Id of the thing to get")]
[RuntimeTestParameter(target: "orgName", name: "Organization name", description: "Name of the organization the data belongs to", uIHints: TKUIHint.NotNull)]
public async Task<TestResult> GetDataFromServiceX(int id = 42, string orgName = "Test Organization")
{
var data = await dataService.GetData(id, orgName);
return TestResult.CreateSuccess("Recieved data successfully")
.AddSerializedData(data, data.Name);
}Executable methods can have parameter with or without default values. Default values will be included in the generated interface.
Supported parameter types:
stringint,int?long,long?float/single,float/single?double,double?decimal,decimal?bool,bool?DateTime,DateTime?,DateTimeOffset,DateTimeOffset?DateTime[],DateTime?[],DateTimeOffset[],DateTimeOffset?[](-> date range selection)TimeSpan,TimeSpan?Enum,Enum?(-> select)Enumwith[Flags](-> multiselect)Guid,Guid?(combine with TKUIHint.AllowRandom to allow new guid generation)byte[],HttpPostedFileBase(.NET Framework),IFormFile(.NET Core) (-> file upload)List<T>where<T>is any of the above types (w/ option for readable list for setting order only)CancellationTokento make the method cancellable, see below.- Search and filter for any custom type when custom factory methods are implemented, see below.
- Any other serializable type can be inputted as json.
If the first parameter is of the type CancellationToken a cancel button will be shown in the UI while the method is running, and only one instance of the method will be able to execute at a time.
Custom parameter types for [RuntimeTest]-methods can be used by providing parameter factories to ReferenceParameterFactories in TKTestsModuleOptions.
Example
// The first factory with a matching parameter type will be used if any.
private List<RuntimeTestReferenceParameterFactory> CreateReferenceParameterFactories()
{
return new List<RuntimeTestReferenceParameterFactory>()
{
new RuntimeTestReferenceParameterFactory(
parameterType: typeof(CustomReferenceType),
// `choicesFactory` has to return all the available options for the user to pick
choicesFactory: (filter) => GetUserChoices()
.Where(x => string.IsNullOrWhiteSpace(filter) || x.Title.Contains(filter) || x.Id.ToString().Contains(filter))
.Select(x => new RuntimeTestReferenceParameterChoice(x.Id.ToString(), x.Title)),
// `getInstanceByIdFactory` has to return one selected instance by id.
getInstanceByIdFactory: (id) => GetUserChoices().FirstOrDefault(x => x.Id.ToString() == id)
)
// Optionally use overload that takes derived types: (type, filter) => ...
// Can be used to easily support base types in e.g. a cms.
};
}The global parameter factory provided in the options can be overridden per test through the ReferenceParameterFactoryProviderMethodName attribute option if needed: RuntimeTest(ReferenceParameterFactoryProviderMethodName = nameof(GetReferenceFactories)).
To automatically create tests for all public methods of another class you can use the [ProxyRuntimeTests] instead of [RuntimeTest].
The method has to be static, take zero parameters and return a ProxyRuntimeTestConfig where you define what type to create tests from.
Example
[ProxyRuntimeTests]
public static ProxyRuntimeTestConfig SomeServiceProxyTest()
{
// This will result in one test per public method on the SomeService class.
return new ProxyRuntimeTestConfig(typeof(SomeService));
}Example with custom result action
[ProxyRuntimeTests]
public static ProxyRuntimeTestConfig SomeServiceProxyTest()
{
return new ProxyRuntimeTestConfig(typeof(SomeService))
// After test is executed this callback is invoked where you can e.g. add any extra data to results
.SetCustomResultAction((result) => result.AddTextData(result.ProxyTestResultObject?.GetType()?.Name, "Result type");
}Example with context and custom result action
[ProxyRuntimeTests]
public static ProxyRuntimeTestConfig SomeServiceProxyTest()
{
return new ProxyRuntimeTestConfig(typeof(SomeService))
// Optionally add a custom context for more flexibility
.SetCustomContext(
// Create any object as a context object that will be used in the resultAction below
// Using logger auto-creation logic from the QoDL.Toolkit.Utility.Reflection nuget package here.
contextFactory: () => new { MemoryLogger = TKLogTypeBuilder.CreateMemoryLoggerFor<ISomeLogger>() },
// Optionally override service activation to inject e.g. a memory logger and dump the log along with the test result.
// instanceFactory: (context) => new SomeService(context.MemoryLogger),
// After test is executed this callback is invoked where you can e.g. add any extra data to results
resultAction: (result, context) =>
{
result
// For proxy tests, the raw return value from the executed method will be placed in result.ProxyTestResultObject
.AddTextData(result.ProxyTestResultObject?.GetType()?.Name, "Result type")
// Shortcut for executing the given action if the method result is of the given type.
.ForProxyResult<OrderLinks>((value) => result.AddUrlsData(value.Select(x => x.Url)))
// E.g. include data logged during execution
.AddCodeData(context.MemoryLogger.ToString());
}
);
}The TestResult class has a few static factory methods for quick creation of a result object, and can contain extra data in various formats.
| Data methods | |
|---|---|
| AddImageUrlsData | Will be shown as a image gallery |
| AddUrlsData | Will be shown as a list of links |
| AddJsonData | Will be formatted as Json |
| AddXmlData | Will be formatted as XML |
| AddCodeData | Text shown in a monaco-editor |
| AddDiff | Show a diff of two strings or objects to be serialized in a monaco diff-editor. |
| AddTextData | Just plain text |
| AddData | Adds string data and optionally define the type yourself. |
| AddSerializedData | Two variants of this method exists. Use the extension method variant unless you want to provide your own serializer implementation. The method simply serializes the given object to json and includes it. |
| AddHtmlData | Two variants of this method exists. Use the extension method variant for html presets using new HtmlPresetBuilder() or the non-extension method for raw html. |
| AddDataTable | Creates a sortable, filterable datatable from the given list of objects. Top-level properties will be used. |
| AddTimingData | Creates timing metric display. |
| AddTimelineData | Creates a timeline from the given steps. Each step can show a dialog with more info/links. |
| AddFileDownload | Creates a download button that can download e.g. larger files by id. Requires TKTestsModuleOptions.FileDownloadHandler to be implemented, see further below. |
| AddExceptionData | Creates a summary of a given exception to display. |
The following methods can be called on the testresult instance to tweak the output look.
| Method | Effect |
|---|---|
SetCleanMode() |
Removes expansion panel and copy/fullscreeen/download buttons. Always shows any dump data. |
DisallowDataExpansion() |
Always shows any dump data. |
SetDataExpandedByDefault() |
Expands any dump data by default. |
If you want to display validation errors on input fields, you can use the following methods on the testresult instance.
| Method | Effect |
|---|---|
SetParameterFeedback(..) |
Sets parameter feedback for a single parameter. |
SetParametersFeedback(..) |
Sets parameter feedback conditionally for all parameters. |
Example:
UseModule(new TKTestsModule(new TKTestsModuleOptions()
{
AssembliesContainingTests = assemblies,
FileDownloadHandler = (type, id) =>
{
if (type == "blob") return ToolkitFileDownloadResult.CreateFromStream("myfile.pdf", CreateFileDownloadBlobStream(id));
else return null;
}
...When an exception is thrown during a test, the final result can be modified through the exception if it implements ITKExceptionWithTestResultData.
[Serializable]
public class MyCustomException : Exception, ITKExceptionWithTestResultData
{
public Action<TestResult> ResultModifier { get; } = x => x.AddHtmlData("<b>Success!</b>");
...Methods are configured through the RuntimeTestClass, RuntimeTest and RuntimeTestParameter attributes.
Must be applied to the class that contains methods to include. Constructor parameter injection is supported for test classes.
| Property Name | Function |
|---|---|
| Name | Name of the test set that is shown in the UI. |
| Description | Description of the test set that is shown in the UI. Can include html. |
| DefaultAllowParallelExecution | Default value for AllowParallelExecution for all methods within this class. |
| DefaultAllowManualExecution | Default value for AllowManualExecution for all methods within this class. |
| DefaultRolesWithAccess | Default value for RolesWithAccess for all methods within this class. Defaults to controller access options value. |
| DefaultCategory/DefaultCategories | Default value for Category/Categories for all methods within this class. Categories can be viewed in the UI by holding ctrl+shift |
| GroupName | Optional group name in the UI. |
| UIOrder | Order of the set in the UI, higher value = higher up. |
Must be applied to the method that should be executed.
| Property Name | Function |
|---|---|
| Name | Name of the test that is shown in the UI. Defaults to prettified method name. |
| Description | Description of the test that is hown in the UI. Can include HTML. |
| Category/Categories | Optional categories that can be filtered upon. |
| RolesWithAccess | Roles allowed to view/execute this method. Uses roles from the parent RuntimeTestClass by default. |
| RunButtonText/RunningButtonText | Optional custom texts for the button that executes the method. |
| AllowManualExecution | True by default, can be set to false to hide the method from the interface. |
| AllowParallelExecution | True by default, can be overridden for single methods. Does not have any effect when running methods from the UI, only when executing multiple methods via code. |
| ReferenceParameterFactoryProviderMethodName | Optional name of a static method that provides factory methods for reference parameters. See example above. |
Can be applied to either the method itself using the Target property or the parameters directly.
| Property Name | Function |
|---|---|
| Target | If the attribute is placed on a method this needs to be the name of the target property. |
| Name | Name of the property. Defaults to a prettified name. |
| Description | Description of the property. Shown as a help text and can contain html. |
| UIHint | Options for parameter display can be set here. Read only lists, prevent null-values, text areas etc. |
| NullName | Override "null"-placeholder values for nullable types if desired. |
| TextPattern | Can be used on text inputs to require the input to match the given regex pattern. Input is validated on blur. |
| DefaultValueFactoryMethod | For property types that cannot have default values (e.g. lists), use this to specify the name of a public static method in the same class as the method. The method should have the same return type as this parameter, and have zero parameters or one string parameter. If the method has one string parameter, the name of the parameter will be its value. |
Can be used to automatically create tests from all public methods on a type. See own section above.
| Property Name | Function |
|---|---|
| RolesWithAccess | Roles allowed to view/execute the generated methods. Uses roles from the parent RuntimeTestClass by default. |
There is no built in scheduler but the TestRunnerService can be used to easily execute a subset of the methods from e.g. a scheduled job and report the results to the given site ISiteEventService.
TestDiscoveryService testDiscovererService = ..;
ISiteEventService siteEventService = ..;
var runner = new TestRunnerService();
var results = await runner.ExecuteTests(testDiscovererService,
// Only include methods belonging to the custom "Scheduled Checks"-category
(m) => m.Categories.Contains("Scheduled Checks"),
// Provide an event service to automatically report to it
siteEventService);Inject a memory logger into the instances being tested and include the output in the result.
// Optionally include the nuget package QoDL.Toolkit.Utility.Reflection to create a memory logger for any interface at runtime e.g:
var memoryLogger = TKLogTypeBuilder.CreateMemoryLoggerFor<ILogger>();
// GetInstance<T> attempts to create a new instance of the given type by calling the
// types' constructor with parameters retrieved from the IoC container, except for the values given to the GetInstance method.
// When passing only the memoryLogger instance below all the other parameters will be retrieved from IoC.
// By default the parameters passed here is forced through the whole IoC chain for the created instance.
var myService = IoCUtils.GetInstance<MyService>(memoryLogger);
// Invoke something to test.
myService.DoSomething();
// Include log data in the result
result.AddCodeData(memoryLogger.ToString(), "Log");When a test is executed a context object is created for the current request that can be accessed through static methods on TKTestContext. This can be used in e.g. proxy tests to include some extra logging or timings. The context methods only have any effect when the request executed a test.
TKTestContext.Log("Start of test")Add some log data to the result.TKTestContext.StartTiming("Parsing data")Start timing with the given description. Can be stopped withTKTestContext.EndTimingor continues until the end of the test method is reached.TKTestContext.WithCurrentResult(x => x.AddTextData("Something")); Access theTestResult` object for the running test if any.
If the audit log module is used, actions by other modules will be logged and can be viewed in the audit log module interface.
UseModule(new TKAuditLogModule(new TKAuditLogModuleOptions() {
AuditEventService = IAuditEventStorage implementation,
// Optional strip sensitive information in parts of audit event data
SensitiveDataStripper = (value) => {
value = TKSensitiveDataUtils.MaskNorwegianNINs(value);
// MaskAllEmails defaults to masking partially, e.g: ***my@****in.com
value = TKSensitiveDataUtils.MaskAllEmails(value);
return value;
}
}));// Built in implementation example
// Optionally include blob storage for larger data (e.g. copy of executed code if enabled)
var blobFolder = HostingEnvironment.MapPath("~/App_Data/AuditEventBlobs");
var blobService = new FlatFileAuditBlobStorage(blobFolder, maxEventAge: TimeSpan.FromDays(1));
IAuditEventStorage auditEventStorage = new FlatFileAuditEventStorage(HostingEnvironment.MapPath("~/App_Data/AuditEventStorage.json"), maxEventAge: TimeSpan.FromDays(30), blobStorage: blobService);UI for searching through logfiles.
UseModule(new TKLogViewerModule(new TKLogViewerModuleOptions() { LogSearcherService = ILogSearcherService implementation() }));// Built in implementation example
var logSearcherOptions = new FlatFileLogSearcherServiceOptions()
.IncludeLogFilesInDirectory(HostingEnvironment.MapPath("~/App_Data/TestLogs/"), filter: "*.log", recursive: true);
ILogSearcherService logSearcherService = new FlatFileLogSearcherService(logSearcherOptions);When not using regex the search supports the following syntax:
- Or: (a|b|c)
- And: a b c
- Exact: "a b c"
E.g. the query (Exception|Error) "XR 442" order details means that the resulting contents must contain either Exception or Error, and contain both order, details and XR 442.
If an ISiteEventService is provided any events will be retrieved from it and can be shown in a UI. Call StoreEvent(..) on this service from other places in the code to register new events.
Test methods can register events if executed through <TestRunnerService>.ExecuteTests(..), a site event service is given, and the TestResult from a method includes a SiteEvent. When executing a method from the UI the site event data will be ignored.
Site events are grouped on SiteEvent.EventTypeId and extend their duration when multiple events are registered after each other.
UseModule(new TKSiteEventsModule(new TKSiteEventsModuleOptions() { SiteEventService = ISiteEventService implementation }));// Built in implementation example
// Flatfile storages should be injected as singletons, for epi storage implementation see further below
ISiteEventStorage flatfileStorage = new FlatFileSiteEventStorage(HostingEnvironment.MapPath("~/App_Data/SiteEventStorage.json"), maxEventAge: TimeSpan.FromDays(30));
ISiteEventService siteEventService = new SiteEventService(flatfileStorage);Example
[RuntimeTest]
public TestResult CheckIntegrationX()
{
// Use the same event type id when reporting and resolving the event.
var eventTypeId = "IntegrationXAvailability";
try {
...
// Methods that include site events should always include a resolved event
// when the method runs successfully. The event will then be marked as resolved.
return TestResult.CreateResolvedSiteEvent(
testResultMessage: "Integration X seems to be alive.",
eventTypeid: eventTypeId,
resolvedMessage: "Integration X seems to be working again.");
}
catch(Exception ex)
{
// On error include a site event in the result
return TestResult.CreateError(ex.Message)
.SetSiteEvent(new SiteEvent(SiteEventSeverity.Error, eventTypeId,
title: "Integration X availability reduced",
description: "There seems to be some instabilities at the moment " +
"and feature Y and Z might temporarily experience reduced functionality."));
}
}The included class TKSiteEventUtils can optionally be used to quickly register events. (If nothing happens when calling the methods, verify that TKGlobalConfig.DefaultInstanceResolver is configured to your resolver.)
Example
// When something fails you can register an event
TKSiteEventUtils.TryRegisterNewEvent(SiteEventSeverity.Error, "api_x_error", "Oh no! API X is broken!", "How could this happen to us!?",
developerDetails: "Error code X, reason Y etc.",
config: x => x.AddRelatedLink("Status page", "https://status.otherapi.com"));
}
// When the event has been resolved you can mark it as resolved using the same id:
TKSiteEventUtils.TryRegisterResolvedEvent("api_x_error", "Seems it fixed itself somehow.");
// The following could be executed from a scheduled job to resolve events you deem no longer failing based on some criteria.
var unresolvedEvents = TKSiteEventUtils.TryGetAllUnresolvedEvents();
foreach (var unresolvedEvent in unresolvedEvents)
{
// Basic check, it would probably be better to store somewhere statically when the event ids last worked,
// and compare against that to check if the issue should be marked as resolved.
var timeSince = DateTimeOffset.Now - (unresolvedEvent.Timestamp + TimeSpan.FromMinutes(unresolvedEvent.Duration));
if (timeSince > TimeSpan.FromMinutes(15))
{
TKSiteEventUtils.TryMarkEventAsResolved(unresolvedEvent.Id, "Seems to be fixed now.");
}
}Shows the last n requests per endpoint, including stack trace of any unhandled exceptions, statuscodes etc.
For requests to be logged and viewable a few things needs to be configured:
nuget package must be added.
- A set of action filters will need to be registered.
- Optionally run a utility method on startup to generate definitions from all controller actions.
UseModule(new TKRequestLogModule(new TKRequestLogModuleOptions() { RequestLogService = IRequestLogStorage implementation }));View full setup details
// Built in implementation example
IRequestLogStorage storage = new FlatFileRequestLogStorage(HostingEnvironment.MapPath("~/App_Data/RequestLog.json");
var options = new RequestLogServiceOptions
{
MaxCallCount = 3,
MaxErrorCount = 5,
CallStoragePolicy = RequestLogCallStoragePolicy.RemoveOldest,
ErrorStoragePolicy = RequestLogCallStoragePolicy.RemoveOldest
};
IRequestLogService service = new RequestLogService(storage, options);// Register MVC action filters.
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new RequestLogActionFilter());
filters.Add(new RequestLogErrorFilter());
..
}
// Register WebAPI action filter.
public static void RegisterWebApiFilters(HttpFilterCollection filters)
{
filters.Add(new RequestLogWebApiActionFilter());
..
}// Optionally call this method on startup to generate endpoint definitions
Task.Run(() => RequestLogUtil.EnsureDefinitionsFromTypes(RequestLogServiceAccessor.Current, new[] { <your assemblies that contain controllers> }));// The following utility method can be called to register requests that the filters can't capture
RequestLogUtils.HandleRequest(RequestLogServiceAccessor.Current, GetType(), Request);// In some cases or if IoC is not used the static
// RequestLogServiceAccessor.Current property must be set to an instance of the service.
RequestLogServiceAccessor.Current = .. service instanceOptionally decorate methods or classes with the RequestLogInfoAttribute attribute to hide endpoints/classes from the log, or to provide additional details. Any method/class decorated with any attribute named HideFromRequestLogAttribute will also hide it from the log.
Provides a monaco-editor IDE where C# scripts can be stored and executed in the context of the web application to extract data, debug issues or other things. Requires an additional nuget package installed
Should be heavily locked down if used other places than localhost, optimally behind MFA.
UseModule(new TKDynamicCodeExecutionModule(new TKDynamicCodeExecutionModuleOptions() {
// Provide the entry assembly of the web application
TargetAssembly = typeof(MyType).Assembly,
// Optionally provide a IDynamicCodeScriptStorage to allow online script storage.
// The provided FlatFileDynamicCodeScriptStorage can be used:
ScriptStorage = new FlatFileDynamicCodeScriptStorage(@"D:\Server\DCE_Script_Storage.data"),
// PreProcessors = ...
// Pre-processors modify code before exectution, and return the modified code back to the frontend.
// * Included types: BasicAutoCreateUsingsPreProcessor, WrapUsingsInRegionPreProcessor, FuncPreProcessor
// Validators = ..
// Validators check the code that is about to be executed and can halt execution with a message.
// * Included types: FuncCodeValidator
// StaticSnippets = ..
// Snippets can be inserted by entering @@@.
}));The module allows for storing e.g. incoming/outgoing api requests that failed. The data is listed with simple filtering, can be repaired and be retried processed again.
A default implementation TKDataRepeaterService is provided that picks up any registered ITKDataRepeaterStream streams.
// Register your streams and service
services.AddSingleton<ITKDataRepeaterStream, MyStreamA>();
services.AddSingleton<ITKDataRepeaterStream, MyStreamB>();
services.AddSingleton<ITKDataRepeaterService, TKDataRepeaterService>();// Use module in tk controller
UseModule(new TKDataRepeaterModule(new TKDataRepeaterModuleOptions
{
Service = ITKDataRepeaterService implementation
}));// Example usage, store data when something fails:
var streamItem = TestOrderStreamItem.CreateFrom(myModel, myModel.ExternalId, "From \"Jimmy Smithy\" - 1234$")
.AddTags("Capture failed")
.SetError("Capture failed because of server downtime.", exception);
await myStream.AddItemAsync(streamItem);
// Alternatively using the static util:
var streamItem = TestOrderStreamItem.CreateFrom(myModel, myModel.ExternalId, "From \"Jimmy Smithy\" - 1234$")
.AddTags("Capture failed")
.SetError("Capture failed because of server downtime.", exception);
TKDataRepeaterUtils.AddStreamItem<ExampleDataRepeaterStream>(item); // or AddStreamItemAsync<T>
// TKDataRepeaterUtils contains various other shortcuts for setting item properties by the custom id used. E.g. external id above.
// Modify stored items when their statuses changes, e.g. something that failed now works again.
TKDataRepeaterUtils.SetAllowItemRetryAsync<ExampleDataRepeaterStream>(itemId, true);
TKDataRepeaterUtils.AddItemTagAsync<ExampleDataRepeaterStream>(itemId, "Tag X");
// Extension methods exist for streams with shortcuts to item modification methods with only item id and not the guid id. E.g:
await myStream.AddItemTagAsync(itemId, "Tag X");Example stream
public class MyModelFromEgApi
{
public string ExternalId { get; set; }
public decimal Amount { get; set; }
}
public class MyStreamItem : TKDefaultDataRepeaterStreamItem<MyModelFromEgApi, MyStreamItem> { }
public class ExampleDataRepeaterStream : TKDataRepeaterStreamBase<MyStreamItem>
{
public override string StreamDisplayName => "Order Captures";
public override string StreamGroupName => "Orders";
public override string StreamItemsName => "Orders";
public override string ItemIdDisplayName => "Order number";
public override string RetryActionName => "Retry capture";
public override string RetryDescription => "Attempts to perform the capture action again.";
public override List<string> InitiallySelectedTags => new List<string> { "Failed" };
public override List<string> FilterableTags => new List<string> { "Failed", "Retried", "Fixed" };
public override List<ITKDataRepeaterStreamItemAction> Actions => new List<ITKDataRepeaterStreamItemAction>
{
new ExampleDataRepeaterStreamItemActionToggleAllow()
};
public override List<ITKDataRepeaterStreamItemBatchAction> BatchActions => new List<ITKDataRepeaterStreamItemBatchAction>()
{
new ExampleDataRepeaterStreamBatchActionRenameTag()
};
// override AllowedAccessRoles or Categories for more granular access control.
public TestOrderDataRepeaterStream()
: base(/* ITKDataRepeaterStreamItemStorage implementation here, TKFlatFileDataRepeaterStreamItemStorage<TItem> exists for flatfile or check below for epi */)
{
}
// Resolve optional extra details for the a given item.
protected override Task<TKDataRepeaterStreamItemDetails> GetItemDetailsAsync(MyStreamItem item)
{
var details = new TKDataRepeaterStreamItemDetails
{
DescriptionHtml = "<p>Description here with support for <a href=\"#etc\">html.</a></p>",
Links = new List<TKDataRepeaterStreamItemHyperLink>
{
new TKDataRepeaterStreamItemHyperLink("Some link", "/etc1"),
new TKDataRepeaterStreamItemHyperLink("Details page", "/etc2")
}
};
return Task.FromResult(details);
}
// Analyze is called when adding items through the default service and base stream, and optionally manually from the interface.
// Use to categorize using tags, skip inserting if not needed etc.
protected override Task<TKDataRepeaterItemAnalysisResult> AnalyzeItemAsync(MyStreamItem item, bool isManualAnalysis = false)
{
var result = new TKDataRepeaterItemAnalysisResult();
// item.AllowRetry = false;
// result.TagsThatShouldExist.Add("etc");
// result.TagsThatShouldNotExist.Add("etc");
result.Message = $"Result from analysis here.";
return Task.FromResult(result);
}
protected override Task<TKDataRepeaterRetryResult> RetryItemAsync(MyStreamItem item)
{
// Retry whatever failed initially here.
// ...
// And return the result of the attempted retry.
var result = new TKDataRepeaterRetryResult
{
Success = true,
Message = $"Success! New {item.Data.ExternalId} amount is ${item.Data.Amount}",
AllowRetry = false,
Delete = false,
RemoveAllTags = true,
TagsThatShouldExist = new List<string> { "Processed" }
};
return Task.FromResult(result);
}
}Example stream item action
// Simple example action that forces AllowRetry on or off.
public class ExampleDataRepeaterStreamItemActionToggleAllow : TKDataRepeaterStreamItemActionBase<ExampleDataRepeaterStreamItemActionToggleAllow.Parameters>
{
public override string DisplayName => "Set allow retry";
public override string Description => "Forces AllowRetry property to the given value.";
public override string ExecuteButtonLabel => "Set";
// override AllowedAccessRoles or Categories for more granular access control.
// Optionally override to disable disallowed actions
// public override Task<TKDataRepeaterStreamItemActionAllowedResult> ActionIsAllowedForAsync(ITKDataRepeaterStreamItem item)
protected override Task<TKDataRepeaterStreamItemActionResult> PerformActionAsync(ITKDataRepeaterStream stream, ITKDataRepeaterStreamItem item, Parameters parameters)
{
var result = new TKDataRepeaterStreamItemActionResult
{
Success = true,
AllowRetry = parameters.Allowed,
Message = $"AllowRetry was set to {parameters.Allowed}."
};
return Task.FromResult(result);
}
public class Parameters
{
[TKCustomProperty]
public bool Allowed { get; set; }
}
}Example stream item action that modifies data
// Simple example action that modifies stream item data
public class ExampleDataRepeaterStreamItemActionModify : TKDataRepeaterStreamItemActionBase<ExampleDataRepeaterStreamItemActionModify.Parameters>
{
public override string DisplayName => "Modify data example";
public override string Description => "Example that modifies item data";
public override string ExecuteButtonLabel => "Update";
protected override Task<TKDataRepeaterStreamItemActionResult> PerformActionAsync(ITKDataRepeaterStream stream, ITKDataRepeaterStreamItem item, Parameters parameters)
{
var result = TKDataRepeaterStreamItemActionResult.CreateSuccess("Data updated.");
// To perform item modifications from an action, use the SetStreamItemModification<TStreamItem>:
result.SetStreamItemModification<MyStreamItem>(streamItem =>
{
streamItem.ForcedStatus = TKDataRepeaterStreamItemStatus.Error;
// To update the Data property use ModifyData:
streamItem.ModifyData(d => d.Something = "Updated");
});
return Task.FromResult(result);
}
public class Parameters { }
}Example stream batch action
// Example action that renames tags.
public class ExampleDataRepeaterStreamBatchActionRenameTag : TKDataRepeaterStreamItemBatchActionBase<ExampleDataRepeaterStreamBatchActionRenameTag.Parameters>
{
public override string DisplayName => "Rename tag";
public override string Description => "Renames a tag on all items.";
public override string ExecuteButtonLabel => "Rename";
protected override Task<TKDataRepeaterStreamItemBatchActionResult> PerformBatchActionAsync(ITKDataRepeaterStreamItem item, Parameters parameters, TKDataRepeaterStreamBatchActionResult batchResult)
{
if (!item.Tags.Contains(parameters.TagToRename))
{
return Task.FromResult(TKDataRepeaterStreamItemBatchActionResult.CreateNotAttemptedUpdated());
}
item.Tags.Remove(parameters.TagToRename);
item.Tags.Add(parameters.NewTagName);
var shouldStopJob = batchResult.AttemptedUpdatedCount + 1 >= parameters.MaxItemsToUpdate;
return Task.FromResult(TKDataRepeaterStreamItemBatchActionResult.CreateSuccess(shouldStopJob));
}
public class Parameters
{
[TKCustomProperty(UIHints = TKUIHint.NotNull)]
public string TagToRename { get; set; }
[TKCustomProperty(UIHints = TKUIHint.NotNull)]
public string NewTagName { get; set; }
[TKCustomProperty(UIHints = TKUIHint.NotNull)]
public int MaxItemsToUpdate { get; set; }
}
}Requires an additional nuget package installed .
The module allows for filtering and exporting data. The type of data source you have available determines how to filter it.
- IQueryable: Lets the user enter a linq query to filter on.
- IEnumerable<T>: Lets the user filter the data either using an entered linq query or custom parameter inputs depending on your stream implementation.
A default implementation TKDataExportService is provided that picks up any registered ITKDataExportStream streams.
If you dare allow raw SQL queries, you can inherit a stream from TKSqlExportStreamBase<TKSqlExportStreamParameters>. The stream requires a registered ITKSqlExportStreamQueryExecutor, TKDataExportExportSqlQueryExecutor in the nuget package can be used unless you want to create your own implementation.
If the request only has access to load presets + export, a simplified version of the interface will be displayed where the only actions available is to select a stream, preset and export format.
// Register your streams and service
services.AddSingleton<ITKDataExportStream, MyDataExportStreamA>();
services.AddSingleton<ITKDataExportStream, MyDataExportStreamB>();
services.AddSingleton<ITKDataExportService, TKDataExportService>();
// Optionally register a preset storage if you want preset save/load functionality enabled
services.AddSingleton<ITKDataExportPresetStorage>(x => new TKFlatFileDataExportPresetStorage(@"your\location\TKDataExportPresets.json"));// Use module in tk controller
UseModule(new TKDataExportModule(new TKDataExportModuleOptions
{
Service = dataExportService,
// Optionally provide preset storage if needed
PresetStorage = dataExportPresetStorage
// Exporters = ..
})
// By default CSV (semicolon + comma), TSV, XML and JSON exporters are configured.
// Excel exporter can be found in the nuget package QoDL.Toolkit.Module.DataExport.Exporter.Excel
.AddExporter(new TKDataExportExporterXlsx())
);Example stream
public class MyDataExportStreamA : TKDataExportStreamBase<MyModel>
{
public override string StreamDisplayName => "My stream A";
public override string StreamDescription => "Some optional description of the stream.";
// Number of items to export fetch per batch during export
public override int ExportBatchSize => 500;
// The Method parameter decides what method will be used to retrieve data.
// - Queryable uses GetQueryableItemsAsync()
// - Enumerable uses GetEnumerableItemsAsync(int pageIndex, int pageSize, Func<MyModel, bool> predicate)
// - EnumerableWithCustomFilter GetEnumerableWithCustomFilterAsync(..)
public override ITKDataExportStream.QueryMethod Method => ITKDataExportStream.QueryMethod.Queryable;
// Optionally set any allowed column formatters. Defaults to allowing all built-in implementations.
// public override IEnumerable<ITKDataExportValueFormatter> ValueFormatters => new[] { new TKDataExportDateTimeValueFormatter() };
// Optional stream group name
// public override string StreamGroupName => null;
// Optional stream access
// public override object AllowedAccessRoles => RuntimeTestAccessRole.WebAdmins;
// Optional stream categories
// public override List<string> Categories => null;
// Optionally ignore members on model:
// public override TKMemberFilterRecursive IncludedMemberFilter { get; } = new TKMemberFilterRecursive { ... }
// Get queryable
protected override Task<IQueryable<MyModel>> GetQueryableItemsAsync()
=> await _someService.GetItems().AsQueryable();
}Example stream with custom parameters
public class MyDataExportStreamB : TKDataExportStreamBase<MyModel, MyDataExportStreamB.Parameters>
{
public override string StreamDisplayName => "My stream B";
public override string StreamDescription => "Some optional description of the stream.";
public override int ExportBatchSize => 500;
// Optionally override SupportsQuery to true if you want a predicate available in addition to custom inputs.
// public override bool SupportsQuery() => true;
protected override Task<TypedEnumerableResult> GetEnumerableItemsAsync(TKDataExportFilterDataTyped<MyModel, MyDataExportStreamB.Parameters> filter)
{
var matches = await _something.GetDataAsync(filter.Parameters.StringParameter, filter.Parameters.SomeValue, filter.Parameters.AnotherValue);
var pageItems = matches
.Skip(filter.PageIndex * filter.PageSize)
.Take(filter.PageSize);
return new TypedEnumerableResult
{
PageItems = pageItems,
TotalCount = matches.Count()
};
}
}
// Add any properties to filter on here.
public class Parameters
{
public string StringParameter { get; set; }
public int? SomeValue { get; set; }
// Optionally configure inputs using the TKCustomProperty attribute.
[TKCustomProperty]
public DateTime AnotherValue { get; set; }
}
}The Content Permutation module helps find different content to e.g. test. Create a class, and a set of instances will be generated with permuted values, allowing you to quickly find example contents in different states using implemented handlers.
// Register your handlers and service
services.AddSingleton<ITKContentPermutationContentHandler, MyExampleAPermutationHandler>();
services.AddSingleton<ITKContentPermutationContentHandler, MyExampleBPermutationHandler>();
services.AddSingleton<ITKContentPermutationContentDiscoveryService, TKContentPermutationContentDiscoveryService>();// Use module in tk controller
UseModule(new TKContentPermutationModule(new TKContentPermutationModuleOptions
{
AssembliesContainingPermutationTypes = new[] { /* your assembly here */ },
Service = permutationContentDiscoveryService
}));Example implementation
// Define your model to generate permutations from.
// Be carefull not to use too many properties or you will be stuck for a while :-)
// Currently only bool and enum types are supported.
[TKContentPermutationType(Name = "Example", Description = "Example description here.")]
public class ExampleAPermutations
{
public ExampleStatusEnum Status { get; set; }
// Optionally decorate properties with TKCustomProperty to override name and add descriptions.
[TKCustomProperty(Name = "Is exported", Description = "Some description here.")]
public bool IsExported { get; set; }
}
// Then create a handler to fetch content by inheriting from TKContentPermutationContentHandlerBase<YourModelClass>
public class MyExampleAPermutationHandler : TKContentPermutationContentHandlerBase<ExampleAPermutations>
{
public override Task<List<TKPermutatedContentItemViewModel>> GetContentForAsync(TKGetContentPermutationContentOptions<ExampleAPermutations> options)
{
// options.Permutations is an instance of the selected permutation in the UI.
var permutation = options.Permutation;
// Get your content enumerable/query..
var content = yourContentSource.GetEnumerable();
// ..filter it based on the input permutation
content = content
.Where(x => x.Status == permutation.Status
&& x.IsExported == permutation.IsExported)
// ..and limit count by options.MaxCount
var matchingContent = content.Take(options.MaxCount);
var models = matchingContent
// Convert to viewmodels, optionally include urls, image url etc.
.Select(x => new TKPermutatedContentItemViewModel(x.Details, x.PublicUrl))
.ToList();
return Task.FromResult(models);
}
}The Comparison module is a simplified interface where content can be searched and compared against each other for debugging purposes.
The built in differ TKComparisonDifferSerializedJson can be used to compare serialized versions of content.
// Register your handlers, differs and service
// - Handlers allow comparing new types
services.AddSingleton<ITKComparisonTypeHandler, MyExampleAComparisonTypeHandler>();
services.AddSingleton<ITKComparisonTypeHandler, MyExampleBComparisonTypeHandler>();
// - Differs compare instances of content in different ways
services.AddSingleton<ITKComparisonDiffer, MyCustomDiffer>();
services.AddSingleton<ITKComparisonDiffer, TKComparisonDifferSerializedJson>();
// - The service handles the boring parts
services.AddSingleton<ITKComparisonService, TKComparisonService>();// Use module in tk controller
UseModule(new TKComparisonModule(new TKComparisonModuleOptions
{
Service = comparisonService
}));Example content handler implementation
public class MyExampleAComparisonTypeHandler : TKComparisonTypeHandlerBase<MyContentType>
{
public override string Description => "Some description for this type.";
// Find instances to select in the UI based in input search string
public override Task<List<TKComparisonInstanceSelection>> GetFilteredOptionsAsync(TKComparisonTypeFilter filter)
{
var items = MyEnumerable()
.Where(x => x.Id.ToString().Contains(filter.Input))
.Take(10)
.Select(x => new TKComparisonInstanceSelection
{
Id = x.Id.ToString(),
Name = x.Name,
Description = x.Description
})
.ToList();
return Task.FromResult(items);
}
// Get a single instance from its id to compare
public override Task<DummyThing> GetInstanceWithIdOfAsync(string id)
=> Task.FromResult(_items.FirstOrDefault(x => x.Id.ToString() == id));
// Get a suitable name displayed in some places
public override string GetInstanceDisplayNameOf(DummyThing instance) => instance.Name;
}Example differ implementation
// Either extend TKComparisonDifferBase with your content type to allow the differ to be used on, or implement ITKComparisonDiffer directly for more control.
public class MyCustomDiffer : TKComparisonDifferBase<MyContentType>
{
public override string Name => "Investigate possible conflicts";
public override Task<TKComparisonDifferOutput> CompareInstancesAsync(MyContentType left, MyContentType right, string leftName, string rightName)
{
// Use the methods on TKComparisonDifferOutput to create the output to display for the diff.
return Task.FromResult(
new TKComparisonDifferOutput()
.AddNote("A note", "Note title")
.AddSideNotes("Left side note", "Right side note", "Side notes title")
.AddHtml($"Some custom <b>HTML</b>.", "Html title")
.AddSideHtml($"This ones name is <b>'{leftName}'</b>", $"And this ones name is <b>'{rightName}'</b>", "Side html title")
);
}
}Simple module to display mapping of data.
// Register service
services.AddSingleton<ITKMappedDataService, TKMappedDataService>();// Use module in tk controller
UseModule(new TKMappedDataModule(new TKMappedDataModuleOptions
{
Service = mappeddataService,
IncludedAssemblies = new[] { typeof(YourModel).Assembly }
}));Example mapping
- Use <=> to indicate a mapping of values.
- Wrap mapped values in [] to indicate that they are mapped from multiple other values.
- Mapped values within quotes (") indicates hardcoded values.
- Lines starting with // will be included as comments.
- To map complex properties, do like in the address example below.
- Override names etc using available attribute properties.
[TKMappedClass(@"
ExternalId <=> MyRemoteModel.Id
// Name is joined from first and last name.
FullName <=> [MyRemoteModel.FirstName, ""Middle"", MyRemoteModel.LastName]
Address {
StreetName <=> MyRemoteModel.HomeAddress.Street,
StreetNo <=> MyRemoteModel.HomeAddress.StreetNo,
City <=> MyRemoteModel.HomeAddress.City,
Zip <=> MyRemoteModel.HomeAddress.ZipCode
}
Something <=> MyRemoteModel.SomeIndexableThing[1].Etc
Another <=> MyRemoteModel.IndexerCanContainAnything[last].Etc
")]
public class MyLocalModel
{
public string ExternalId { get; set; }
public string FullName { get; set; }
public MyAddressModel Address { get; set; }
}
[TKMappedReferencedType]
public class MyRemoteModel { ... }- Optionally use
TKMappedDataUtils.SetExampleFor(myInstance);to display example values in the UI. Only supported for classes decorated withTKMappedClass.
A very simplified search that allows only a single result per type. Use to quickly find something by e.g. an id that is not normally searchable other places.
// Register your resolvers
services.AddSingleton<ITKGoToResolver, MyAGotoResolver>();
services.AddSingleton<ITKGoToResolver, MyBGotoResolver>();
// And the built in service
services.AddSingleton<ITKGoToService, TKGoToService>();// Use module in tk controller
UseModule(new TKGoToModule(new TKGoToModuleOptions
{
Service = goToService
}));Example goto resolver implementation
public class CustomerGotoResolver : ITKGoToResolver
{
public string Name => "Customer";
public async Task<TKGoToResolvedData> TryResolveAsync(string input)
{
var match = await _myCustomerService.GetCustomerById(input);
if (match == null) return null;
return new TKGoToResolvedData
{
Name = match.Name,
Description = match.Description,
ResolvedFrom = nameof(MyCustomer.Id),
Urls = new List<TKGoToResolvedUrl> {
new TKGoToResolvedUrl("Customer Profile", $"/some-url")
}
};
}
}Some special querystrings are supported on the goto page.
| Querystrings | |
|---|---|
| query=MyQuery | Prefill the input with the given value. |
| auto=true | Automatically search on page load. |
| autoNav=true | Automatically navigate to the first result if theres only one. |
Combine them all to e.g. make a browser search to quickly goto any content directly.
If the Dataflow module is enabled the dataflow tab will become available where custom data can be shown. The module can show a filtered list of any data and was made for showing latest imported data per id to quickly verify that incoming data was correct.
A default implementation DefaultDataflowService is provided where custom data streams can be registered. Data can be fetched in the ui for each registered stream, optionally filtered on and each property given a hint for how to be displayed. Only Raw and HTML types have any effect when not expanded.
UseModule(new TKDataflowModule<RuntimeTestAccessRole>(new TKDataflowModuleOptions<RuntimeTestAccessRole>() {
DataflowService = IDataflowService implementation
}));// Built in implementation example
var options = new DefaultDataflowServiceOptions() {
Streams = ..your streams,
// UnifiedSearches = ..any searches if needed
};
IDataflowService service = new DefaultDataflowService(options);A default abstract stream FlatFileStoredDataflowStream<TEntry, TEntryId> is provided and can be used to store and retrieve latest entries per id to a flatfile + optionally limit the age of entries.
- Use
.InsertEntries(..)method to insert new entries. - Use
IsVisibleproperty to set stream visibility in the UI. - Use
AllowInsertproperty to optionally ignore any new data attempted to be inserted. - Override
RolesWithAccessproperty to set who has access to view the stream data. - If used make sure the services are registered as singletons, they are thread safe but only within their own instances.
GenericDataflowStreamObject.Createcan optionally be used to include a subset of an existing types properties instead of creating a new model.
Simple example stream
public class MySimpleStream : FlatFileStoredDataflowStream<YourAccessRolesEnum, YourDataModel, string>
{
public override Maybe<YourAccessRolesEnum> RolesWithAccess =>new Maybe<YourAccessRolesEnum>(YourAccessRolesEnum.SystemAdmins);
public override string Name => $"My Simple Stream";
public override string Description => $"The simplest of streams.";
public MySimpleStream()
: base(
@"e:\storage\path\my_simple_stream.json",
idSelector: (e) => e.Code,
idSetter: (e, id) => e.Code = id
) {
// To attempt auto-creation of filters for some suitable
// property types the AutoCreateFilters method can be used.
AutoCreateFilters<YourDataModel>();
}
}Example stream using a few more options
public class MyStream : FlatFileStoredDataflowStream<YourAccessRolesEnum, YourDataModel, string>
{
public override Maybe<YourAccessRolesEnum> RolesWithAccess =>new Maybe<YourAccessRolesEnum>(YourAccessRolesEnum.SystemAdmins);
public override string Name => $"My Stream";
public override string Description => $"A stream using a few more options.";
public MyStream(IConfig yourOptionalConfigService)
: base(
@"e:\storage\path\my_stream.json",
idSelector: (e) => e.Code,
idSetter: (e, id) => e.Code = id,
maxEntryAge: TimeSpan.FromDays(7)
)
{
// Optionally toggle some options at runtime
IsVisible = () => yourOptionalConfigService.ShowMyStream;
AllowInsert = () => yourOptionalConfigService.EnableMyStreamInserts;
// Optionally customize object property data
ConfigureProperty(nameof(YourDataModel.Code), "Product Code").SetFilterable();
ConfigureProperty(nameof(YourDataModel.Details))
.SetUIHint(DataFlowPropertyDisplayInfo.DataFlowPropertyUIHint.Dictionary);
.SetVisibility(DataFlowPropertyDisplayInfo.DataFlowPropertyUIVisibilityOption.OnlyWhenExpanded);
}
// Override FilterEntries method to implement any custom filtering.
// To show a filter in frontend IsFilterable must be set to true in ConfigureProperty above.
protected override Task<IEnumerable<YourDataModel>> FilterEntries(DataflowStreamFilter filter, IEnumerable<YourDataModel> entries)
{
// Get user input for Code property
var codeFilter = filter.GetPropertyFilterInput(nameof(YourDataModel.Code));
// Filter on property
entries = entries.Where(x => codeFilter == null || x.Code.ToLower().Contains(codeFilter));
// Or use the FilterContains shortcut for the same effect
entries = filter.FilterContains(entries, nameof(YourDataModel.Code), x => x.Code);
return Task.FromResult(entries);
}
}Example search across streams
public class ExampleSearch : ITKDataflowUnifiedSearch<YourAccessRolesEnum>
{
public Maybe<YourAccessRolesEnum> RolesWithAccess => null;
public string Name => "Example Search";
public string Description => "Searches some streams.";
public string QueryPlaceholder => "Search..";
public string GroupName => "Searches";
public string GroupByLabel { get; }
public Dictionary<Type, string> StreamNamesOverrides { get; }
public Dictionary<Type, string> GroupByStreamNamesOverrides { get; }
public Func<bool> IsVisible { get; } = () => true;
public IEnumerable<Type> StreamTypesToSearch { get; } = new[] { typeof(MyStreamA), typeof(MyStreamB), typeof(MyStreamC) };
public Dictionary<string, string> CreateStreamPropertyFilter(IDataflowStream<YourAccessRolesEnum> stream, string query)
{
var filter = new Dictionary<string, string>();
// Create property filter per stream
if (stream.GetType() == typeof(TestStreamA)) filter[nameof(MyStreamItemA.Title)] = query;
else if (stream.GetType() == typeof(TestStreamB)) filter[nameof(MyStreamItemB.Text)] = query;
else if (stream.GetType() == typeof(TestStreamC)) filter[nameof(MyStreamItemC.Name)] = query;
return filter;
}
public TKDataflowUnifiedSearchResultItem CreateResultItem(Type streamType, IDataflowEntry entry)
{
var item = entry as MyStreamItem;
var result = new TKDataflowUnifiedSearchResultItem
{
Title = item.Title,
Body = item.Text
};
// Optionally try to include all item data
result.TryCreatePopupBodyFrom(item);
return result;
}
}Allows custom settings to be configured.
UseModule(new TKSettingsModule(new TKSettingsModuleOptions() {
SettingsService = ITKSettingsService implementation,
ModelType = typeof(YourSettingsModel)
}));// Built in implementation examples
SettingsService = new TKDefaultSettingsService(new TKFlatFileStringDictionaryStorage(@"D:\settings.json"));Example
// Create a custom model for your settings
public class YourSettingsModel
{
public string PropertyX { get; set; }
[TKSetting(GroupName = "Service X")]
public bool Enabled { get; set; }
[TKSetting(GroupName = "Service X")]
public int ThreadLimit { get; set; } = 2;
[TKSetting(GroupName = "Service X", Description = "Some description here")]
public int NumberOfThings { get; set; } = 321;
[TKSetting(GroupName = "Service X", Description = "When to start")]
public DateTime StartAt { get; set; };
}// Retrieve settings model using the GetSettings<T> method.
service.GetSettings<YourSettingsModel>().EnabledAllows access tokens to be generated with limited access and duration. Tokens are stored hashed and salted in the given IAccessManagerTokenStorage implementation. The data being hashed includes given roles, module options, categories and expiration to prevent tampering. Tokens can be used in e.g. querystring to share quick and easy access to limited parts of the toolkit functionality.
UseModule(new TKAccessTokensModule(new TKAccessTokensModuleOptions()
{
TokenStorage = IAccessManagerTokenStorage implementation
}));// Built in implementation example
new FlatFileAccessManagerTokenStorage(@"e:\config\access-tokens.json")Enables notifications of custom events. Rules for notifications can be edited in a UI and events are easily triggered from code. Notifications are delivered through implementations of IEventNotifier. Built-in implementations: DefaultEventDataSink, TKWebHookEventNotifier, TKMailEventNotifierBase.
Events can be filtered on their id, stringified payload or properties on their payload, and limits and distinctions can be set.
UseModule(new TKEventNotificationsModule(new TKEventNotificationsModuleOptions() {
EventSink = IEventDataSink implementation
}));// Built in implementation examples
var notificationConfigStorage = new FlatFileEventSinkNotificationConfigStorage(@"e:\config\eventconfigs.json");
var notificationDefinitionStorage = new FlatFileEventSinkKnownEventDefinitionsStorage(@"e:\config\eventconfig_defs.json");
var eventSink = new DefaultEventDataSink(notificationConfigStorage, notificationDefinitionStorage)
// Setup any notifiers that should be available
.AddNotifier(new MyNotifier())
.AddNotifier(new WebHookEventNotifier())
// Add any custom placeholders
.AddPlaceholder("NOW", () => DateTime.Now.ToString())
.AddPlaceholder("ServerName", () => Environment.MachineName);Example
// Implement any custom notifiers
public class MyNotifier : IEventNotifier
{
public string Id => "my_notifier";
public string Name => "My Notifier";
public string Description => "Does nothing, just an example.";
public Func<bool> IsEnabled { get; set; } = () => true;
public HashSet<string> PlaceholdersWithOnlyNames => null;
public Dictionary<string, Func<string>> Placeholders { get; } = new Dictionary<string, Func<string>>
{
{ "Custom_Placeholder", () => "Custom placeholder replaced successfully." }
};
public Type OptionsModelType => typeof(MyNotifierOptions);
public async Task<string> NotifyEvent(NotifierConfig notifierConfig, string eventId, Dictionary<string, string> payloadValues)
{
var options = optionsObject as MyNotifierOptions;
var message = options.Message;
try
{
Console.WriteLine(message);
// The latest 10 returned strings will be stored and displayed in the UI.
return await Task.FromResult($"Message '{message}' was outputted.");
}
catch (Exception ex)
{
return $"Failed to create message '{message}'. {ex.Message}";
}
}
public class MyNotifierOptions
{
[EventNotifierOption(description: "Text that will be outputted")]
public string Message { get; set; }
}
}// Register events from interesting places..
// ..without any additional details
eventSink.RegisterEvent("new_order");
// ..with a payload that can be stringified
eventSink.RegisterEvent("order_exception", errorMessage);
// ..with a payload with stringifiable properties
eventSink.RegisterEvent("new_order", new { PaymentType = 'Invoice', Warnings = 0 });
// The static TryRegisterEvent method can be used for quick and easy access.
EventSinkUtil.TryRegisterEvent("thing_imported", () => new { Type = "etc", Value = 321 })Store sent messages and view the latest ones sent from the system, optionally along with any error message. Can be used for e.g. outgoing mail and sms.
The following storage implementations are included, both contains options for max counts and time to live and should not be used with more than max a few hundred items per inbox max:
TKMemoryMessageStore: keeps the latest messages in memory without storing anything.TKFlatFileMessageStore: keeps the latest messages in memory and saves data delayed to a flatfile.
UseModule(new TKMessagesModule(new TKMessagesModuleOptions()
{ MessageStorage = ITKMessageStorage implementation }
// Define any inboxes you want to be visible in the UI
.DefineInbox("mail", "Mail", "All outgoing mail.")
.DefineInbox("sms", "SMS", "All outgoing sms.")
));// Built in implementation examples:
... new TKMemoryMessageStore();
// Flatfile storages should be registered as singletons
... new TKFlatFileMessageStore(@"e:\etc\tk_messages");// Create message item to store
var message = new TKDefaultMessageItem("RE: Hi there",
"[email protected]", "[email protected]",
"<b>Some mail</b> content here.",
isHtml: true);
// Optionally add any error to the message
message.SetError("Failed to send because of invalid email.");
// Send to storage implementation
_messageStore.StoreMessage(inboxId: "mail", message);Requires an additional nuget package installed .
Decorate mvc and webapi actions with TKControlledEndpointAttribute or TKControlledApiEndpointAttribute to allow for a bit of spam control by setting conditional rules at runtime using the interface. The module can also show the latest requests sent to decorated endpoints, including a few graphs.
Also allows for optionally handling blocked requests in code manually, and only count requests that reach a certain step in the code. See usage example below.
Adding attributes to actions will not block anything until you add some rules in the interface.
The default response when request is blocked is a 409 with either a text for GET requests and a json for any other method.
UseModule(new TKEndpointControlModule(new TKEndpointControlModuleOptions()
{
EndpointControlService = IEndpointControlService implementation,
RuleStorage = IEndpointControlRuleStorage implementation,
DefinitionStorage = IEndpointControlEndpointDefinitionStorage implementation,
HistoryStorage = IEndpointControlRequestHistoryStorage implementation
}));// Built in implementation examples
// Flatfile storages should be registered as singletons
... new FlatFileEndpointControlRuleStorage("e:\etc\ec_rules.json");
... new FlatFileEndpointControlEndpointDefinitionStorage(@"e:\etc\ec_definitions.json");
... new FlatFileEndpointControlRequestHistoryStorage(@"e:\etc\ec_history.json");
// DefaultEndpointControlService can be scoped or singleton depending on your DI framework
...RegisterSingleton<IEndpointControlService, DefaultEndpointControlService>();Simple usage
[HttpPost]
// Just decorate with this attribute.
[TKControlledEndpoint]
public ActionResult Submit(MyModel model)
{
// ...
}Custom handling
[HttpPost]
// Set CustomBlockedHandling to not block the request automatically,
// check on EndpointControlUtils.CurrentRequestWasDecidedBlocked() manually.
[TKControlledEndpoint(CustomBlockedHandling = true)]
public ActionResult Submit(MyModel model)
{
if (EndpointControlUtils.CurrentRequestWasDecidedBlocked())
{
return HttpNotFound();
}
// ...
}Conditional request counting
[HttpPost]
// Set ManuallyCounted to not store/count the request automatically,
// invoke EndpointControlUtils.CountCurrentRequest() manually to store it.
[TKControlledEndpoint(ManuallyCounted = true)]
public ActionResult Submit(MyModel model)
{
if (!ModelState.IsValid)
{
return ..;
}
// E.g. store after validation to only count valid requests.
// This way you can set more logical request count limits.
EndpointControlUtils.CountCurrentRequest();
// ...
}To override the default blocked result when not using CustomBlockedHandling, create your own attributes inheriting from the provided ones and override CreateBlockedResult.
To create new types of results that can be selected in the UI, create custom implementations of IEndpointControlRequestResult and add them to the endpoint control service through AddCustomBlockedResult(..).
Built in custom types:
EndpointControlForwardedRequestResult: Forwards request to a given url without blocking them. (Currently only for .Net Framework)EndpointControlContentResult: Allows custom content on block, e.g. some json.EndpointControlRedirectResult: Redirects to a given url on block.
Requires an additional nuget package installed .
Blocks everything except for whitelisted requests. Supports CIDR notations, generating links that can be used to add new ip's to whitelist, log of recently blocked ips, etc.
Supports multiple ways of blocking requests:
TKIPWhitelistMiddlewaremiddleware for .net core.TKIPWhitelistHttpModulehttp module for .net framework.TKIPWhitelistApiAttributefilter attribute for .net framework webapi.TKIPWhitelistAttributefilter attribute for .net framework mvc.
UseModule(new TKIPWhitelistModule(new TKIPWhitelistModuleOptions
{
Service = ITKIPWhitelistService implementation,
ConfigStorage = ITKIPWhitelistConfigStorage implementation,
RuleStorage = ITKIPWhitelistRuleStorage implementation,
LinkStorage = ITKIPWhitelistLinkStorage implementation,
IPStorage = ITKIPWhitelistIPStorage implementation
}));// Built in implementations
// TKIPWhitelistService can be scoped or singleton depending on your DI framework
...RegisterSingleton(x => new TKIPWhitelistServiceOptions {
DisableForLocalhost = true,
// e.g. whitelist login
ShouldAlwaysAllowRequest = (r) => Task.FromResult(!r.Path.StartsWith("/login"))
});
...RegisterSingleton<ITKIPWhitelistService, TKIPWhitelistService>();
// Flatfile storages should be registered as singletons
... new TKIPWhitelistConfigFlatFileStorage("e:\etc\wl_config.json");
... new TKIPWhitelistIPFlatFileStorage(@"e:\etc\wl_ips.json");
... new TKIPWhitelistLinkFlatFileStorage(@"e:\etc\wl_links.json");
... new TKIPWhitelistRuleFlatFileStorage(@"e:\etc\wl_rules.json");The downloads module allow files to be made available for download, optionally protected by password, expiration date and download count limit. Downloads are tracked in the audit log. Built-in implementations: FlatFileSecureFileDownloadDefinitionStorage for download definition storage, and 3 file storage implementations: FolderFileStorage, UrlFileStorage and TKEpiserverBlobFileStorage (in epi package).
UseModule(new TKSecureFileDownloadModule(new TKSecureFileDownloadModuleOptions()
{
DefinitionStorage = ISecureFileDownloadDefinitionStorage implementation,
FileStorages = new ISecureFileDownloadFileStorage[]
{
// By default FolderFileStorage only allows uploading new files.
// Optionally configure it to allow selecting existing files etc. Uploaded files can't manually be selected later.
new FolderFileStorage("files_testA", "Disk storage (upload only)", @"e:\files\folderA") { SupportsSelectingFile = false, SupportsUpload = true },
new FolderFileStorage("files_testB", "Disk storage (download only)", @"e:\files\folderB") { SupportsSelectingFile = true, SupportsUpload = false },
new FolderFileStorage("files_testC", "Disk storage (upload and download)", @"e:\files\folderC") { SupportsSelectingFile = true, SupportsUpload = true },
new UrlFileStorage("url", "External url")
}
}));// Built in implementation examples
var downloadDefinitionStorage = new FlatFileSecureFileDownloadDefinitionStorage(@"e:\config\download_definitions.json");;Very simple module that outputs some metrics you can track manually through TKMetricsContext statically, to e.g. verify that some methods are not called too often, or to include extra details on every page (timings, errors, notes, etc).
TKMetricsUtil.AllowTrackRequestMetrics must be configured on startup to select what when tracking is allowed. By default false is returned and no context will be created, causing any attempt to track metrics to be ignored.
TKMetricsUtil.AllowTrackRequestMetrics = (r) => r.Url?.Contains("some=key") || !r.HasRequestContext;To enable the module to view globally tracked metrics register <ITKMetricsStorage, TKMemoryMetricsStorage> as a singleton to store data in memory and enable the module:
UseModule(new TKMetricsModule(new TKMetricsModuleOptions()
{
// Register TKMemoryMetricsStorage as a singleton and pass to storage here
Storage = ITKMetricsStorage instance
}));To include metrics data on every page when any metrics are available use CreateContextSummaryHtml() in a view to create the html:
@if (allowMetrics)
{
// If no data has been logged through `TKMetricsContext` for the current request null will be returned.
@Html.Raw(QoDL.Toolkit.Core.Modules.Metrics.Context.TKMetricsUtil.CreateContextSummaryHtml())
}Call static shortcuts on TKMetricsContext to track details for the current request.
- Global methods
IncrementGlobalCounterandAddGlobalValuestores data for display in the module, non-global methods only stores data temporarily to be shown usingCreateContextSummaryHtml. - Data is stored to the registered
ITKMetricsStorageinstance when the context object for the request is disposed, so expect some delays before data shows up in the module interface if used.
TKMetricsContext.StartTiming("LoadData()");
// .. do something to be timed here
TKMetricsContext.EndTiming();
// Count something
TKMetricsContext.IncrementGlobalCounter("GetRequestInformation()", 1);
// Add a value that will be stored with counter, min, max and average values
TKMetricsContext.AddGlobalValue("Some value", 42);
// Include some error details
TKMetricsContext.AddError("etc", ex);Work in progress. At the moment sequence diagrams and flowcharts generated from decorated code will be shown.
The default implementations searches through any given assemblies for methods decorated with SequenceDiagramStepAttribute and FlowChartStepAttribute and generate diagrams using them.
UseModule(new TKDocumentationModule(new TKDocumentationModuleOptions()
{
SequenceDiagramService = ISequenceDiagramService implementation,
FlowChartsService = IFlowChartsService implementation
}));// Built in implementation examples
SequenceDiagramService = new DefaultSequenceDiagramService(new DefaultSequenceDiagramServiceOptions()
{
DefaultSourceAssemblies = new[] { typeof(MyController).Assembly }
}),
FlowChartsService = new DefaultFlowChartService(new DefaultFlowChartServiceOptions()
{
DefaultSourceAssemblies = new[] { typeof(MyController).Assembly }
})Simple module that shows release notes e.g. generated during the build process. Supports dev-mode where developers can see any extra details.
UseModule(new TKReleaseNotesModule(new TKReleaseNotesModuleOptions {
// Note: inject TKJsonFileReleaseNotesProvider as a singleton
ReleaseNotesProvider = new TKJsonFileReleaseNotesProvider(HostingEnvironment.MapPath(@"~\App_Data\ReleaseNotes.json"))
{
IssueUrlFactory = (id) => $"https://www.yourjira.com/etc/{id}",
IssueLinkTitleFactory = (id) => $"Jira {id}",
PullRequestUrlFactory = (number) => $"https://github.com/yourOrg/yourProject/pull/{number}",
}
}));To include a floating release notes button on every page when any notes are available use CreateReleaseNotesSummaryHtml() in a view to create the html. The button pulses when new notes are available.
@if (showReleaseNotes)
{
// If there's nothing to display it outputs a html comment with the reason why.
@Html.Raw(QoDL.Toolkit.Core.Modules.ReleaseNotes.Util.TKReleaseNotesUtil.CreateReleaseNotesSummaryHtml(/* optionally pass true here to include dev details */))
}An integrated login dialog is included, but custom authentication logic must be provided. To enable the dialog two steps are required.
-
The main controller uses readonly session behaviour that can cause some login logic dependent on sessions to fail, so a new controller is required that handles the login request. Inherit from
ToolkitLoginControllerBaseand implementHandleLoginRequest.public class TKLoginController : ToolkitLoginControllerBase { protected override TKIntegratedLoginResult HandleLoginRequest(TKIntegratedLoginRequest request) { var success = _myAccessService.AuthenticateUser(request.Username, request.Password); // also validate request.TwoFactorCode if enabled return TKIntegratedLoginResult.CreateResult(success, "Wrong username or password, try again or give up."); } }
-
Enable the dialog by setting the
IntegratedLoginEndpointproperty to the url of theLoginaction on the controller in step 1.protected override void ConfigureAccess(HttpRequestBase request,AccessConfig<RuntimeTestAccessRole> config) { ... config.IntegratedLoginConfig = new TKIntegratedLoginConfig("/tklogin/login") // Optionally require 2FA input using OTP, TOTP or WebAuthn .EnableOneTimePasswordWithCodeRequest("/tklogin/Request2FACode") .EnableTOTP() // <-- requires separate nuget package below .EnableWebAuthn(); }
Any requests to the index action of the main controller that does not have access to any of the content will now be shown the login dialog. On a successfull login the page will refresh and the user will have access to any content you granted the request.
To add TOTP MFA you can add the package. If you already have code for validation of TOTP codes in your project this package is not needed.
- For it to work you need to store a 2FA secret per user to validate the codes against. The secret must be a base32 string and can be generated using e.g.
TKMfaTotpUtil.GenerateOTPSecret(). - Validate codes using the
TKMfaTotpUtil.ValidateTotpCode(userSecret, code)method. - Enable on IntegratedLoginConfig easily by using the extension method
.EnableTOTP() - Bitwarden and most authenticator apps supports TOTP and can be used to generate codes from the generated secret.
To add WebAuthn MFA you can add the package.
You can use the included TKWebAuthnHelper to register FIDO2 keys and create data secrets to store on your user objects.
- Enable on IntegratedLoginConfig easily by using the method
.EnableWebAuthn()
Example setup
-
Create your implementation of
ITKWebAuthnCredentialManagerthat will store and retrieve WebAuthn credential data. -
In the toolkit controller specify desired WebAuthn mode for the login page.
config.IntegratedLoginConfig = new TKIntegratedLoginConfig { // ... WebAuthnMode = TKIntegratedLoginConfig.TKLoginWebAuthnMode.Required };
-
In the login controller add a factory method to create the
TKWebAuthnHelper.private TKWebAuthnHelper CreateWebAuthnHelper() => new TKWebAuthnHelper(new TKWebAuthnHelperOptions { ServerDomain = "localhost", ServerName = "My fancy site", Origin = Request.Headers["Origin"] }, new TKMemoryWebAuthnCredentialManager() /* Add your own implementation here that actually stores data */ ); private TKWebAuthnHelper GetWebAuthnHelper()
-
Override
CreateWebAuthnAssertionOptionsJsonin the login controller with e.g. the following:protected override string CreateWebAuthnAssertionOptionsJson(TKIntegratedLoginCreateWebAuthnAssertionOptionsRequest request) { var webauthn = GetWebAuthnHelper(); var options = webauthn.CreateAssertionOptions(request.Username); return options?.ToJson(); }
-
Verify the new data in
HandleLoginRequest.protected override TKIntegratedLoginResult HandleLoginRequest(TKIntegratedLoginRequest request) { //... username/pass validation etc // Verify WebAuthn payload if (request.WebAuthnPayload?.Id == null) { return TKIntegratedLoginResult.CreateError("Invalid FIDO key assertion data."); } var webauthn = GetWebAuthnHelper(); var jsonOptions = GetWebAuthnAssertionOptionsJsonForSession(); var options = AssertionOptions.FromJson(jsonOptions); var webAuthnResult = AsyncUtils.RunSync(() => webauthn.VerifyAssertion(options, request.WebAuthnPayload)); if (!webAuthnResult.Success) { return TKIntegratedLoginResult.CreateError(webAuthnResult.Error); } return TKIntegratedLoginResult.CreateSuccess(); }
To send a one-time-use code to the user instead of using TOTP you can set the Send2FACodeEndpoint option to target the Request2FACode action on the login controller. A button to send a code to the user will be shown in the login form, and you can override Handle2FACodeRequest to handle what happens when the button is clicked.
Example logic using built in helper methods for creating 2FA codes in session:
protected override TKIntegratedLogin2FACodeRequestResult Handle2FACodeRequest(TKIntegratedLoginRequest2FACodeRequest request)
{
if (string.IsNullOrWhiteSpace(request.Username))
{
return TKIntegratedLogin2FACodeRequestResult.CreateError("You must enter your username first.");
}
var code = CreateSession2FACode(request.Username);
// E.g. send code by mail or sms to user here
return TKIntegratedLogin2FACodeRequestResult.CreateSuccess($"Code has been sent.", codeExpiresIn: TimeSpan.FromMinutes(5));
} protected override TKIntegratedLoginResult HandleLoginRequest(TKIntegratedLoginRequest request)
{
if (!_myAccessService.AuthenticateUser(request.Username, request.Password))
{
return TKIntegratedLoginResult.CreateError("Wrong username or password.");
}
else if (!ValidateSession2FACode(request.Username, request.TwoFactorCode))
{
return TKIntegratedLoginResult.CreateError("Two-factor code was wrong, try again.");
}
return TKIntegratedLogin2FACodeRequestResult.CreateSuccess(
message: "<b>Success!</b> Your code has been sent.",
showAsHtml: true,
codeExpiresIn: TimeSpan.FromMinutes(5)
);
// Or the simple way without any extra details
// return TKIntegratedLoginResult.CreateSuccess();
}Set IntegratedProfileConfig to show a profile button that displays the username, resolved toolkit roles, and optionally add/remove/elevate access for TOTP and WebAuthn.
config.IntegratedProfileConfig = new TKIntegratedProfileConfig
{
Username = CurrentRequestInformation.UserName,
// BodyHtml = "Here is some custom content.<ul><li><a href=\"https://www.google.com\">A link here</a></li></ul>",
// ShowTotpElevation = ..
// TotpElevationLogic = (code) => ..
// ...
};The built in flatfile storage classes should work fine for most use cases when a persistent folder is available. If used make sure they are registered as singletons, they are thread safe but only within their own instances.
For Episerver/Optimizely projects storage implementations can optionally be used from and the other episerver packages for specific modules. If used they should be registered as singletons for optimal performance.
Cache can optionally be set to null in constructor if not wanted, or the included memory cache TKSimpleMemoryCache can be used as a singleton. For load balanced environments TKSimpleMemoryCacheForEpiLoadBalanced can optionally be used (not much tested yet).
The storage implementations are not optimized for load balanced environments, if desired the TKSimpleMemoryCacheForEpiLoadBalanced cache can be used.
Example IoC setup
// Cache required by most of the epi blob implementations below. Choose one.
// For single server instances
context.Services.AddSingleton<ITKCache, TKSimpleMemoryCache>();
// For multiple server instances
context.Services.AddSingleton<ITKCache, TKSimpleMemoryCacheForEpiLoadBalanced>(); // Audit log (defaults to storing the last 10000 events/30 days)
context.Services.AddSingleton<IAuditEventStorage, TKEpiserverBlobAuditEventStorage>();
// Messages
context.Services.AddSingleton<ITKMessageStorage, TKEpiserverBlobMessagesStore<TKDefaultMessageItem>>();
// AccessTokens
context.Services.AddSingleton<IAccessManagerTokenStorage, TKEpiserverBlobAccessTokenStorage>();
// Settings
context.Services.AddSingleton<ITKStringDictionaryStorage, TKEpiserverBlobStringDictionaryStorage>();
context.Services.AddSingleton<ITKSettingsService, TKDefaultSettingsService>();
// DynamicCodeExecution
context.Services.AddSingleton<IDynamicCodeScriptStorage, TKEpiserverBlobDynamicCodeScriptStorage>();
// Endpoint control
context.Services.AddSingleton<IEndpointControlRuleStorage, TKEpiserverBlobEndpointControlRuleStorage>();
context.Services.AddSingleton<IEndpointControlEndpointDefinitionStorage, TKEpiserverBlobEndpointControlEndpointDefinitionStorage>();
context.Services.AddSingleton<IEndpointControlRequestHistoryStorage, TKEpiserverBlobEndpointControlRequestHistoryStorage>();
context.Services.AddSingleton<IEndpointControlService, DefaultEndpointControlService>();
// Dataflow
context.Services.AddSingleton<TestDataStream>();
context.Services.AddSingleton((s) => new DefaultDataflowServiceOptions<AccessRoles>
{
Streams = new[] {
s.GetInstance<TestDataStream>()
}
});
context.Services.AddSingleton<IDataflowService<AccessRoles>, DefaultDataflowService<AccessRoles>>();
// Site events (defaults to storing the last 1000 events/30 days)
context.Services.AddSingleton<ISiteEventStorage, TKEpiserverBlobSiteEventStorage>();
// DataExport
context.Services.AddSingleton<ITKDataExportPresetStorage, TKEpiserverBlobDataExportPresetStorage>();
// IP Whitelist
context.Services.AddSingleton<ITKIPWhitelistRuleStorage, TKEpiserverBlobIPWhitelistRuleStorage>();
context.Services.AddSingleton<ITKIPWhitelistConfigStorage, TKEpiserverBlobIPWhitelistConfigStorage>();
context.Services.AddSingleton<ITKIPWhitelistLinkStorage, TKEpiserverBlobIPWhitelistLinkStorage>();
context.Services.AddSingleton<ITKIPWhitelistIPStorage, TKEpiserverBlobIPWhitelistIPStorage>();
// DataRepeater
// Example setup:
/// public class SomeExistingModel {}
/// public class MyStreamItemA : TKDefaultDataRepeaterStreamItem<SomeExistingModel, MyStreamItemA> {}
/// public class MyStreamStorageA : TKEpiserverBlobDataRepeaterStreamItemStorage<MyStreamItemA>, IMyStreamStorageA
// {
// protected override Guid ContainerId => Guid.Parse("c0254918-1234-1234-1234-062ed6a11aaa"); // <-- set a unique guid per stream
// public MyStreamStorageA(IBlobFactory blobFactory, Core.Abstractions.ITKCache cache) : base(blobFactory, cache) {}
// }
// public class MyStreamA : TKDataRepeaterStreamBase<MyStreamItemA> {
// public MyStreamA(MyStreamStorageA storage) : base(storage) { }
// }
context.Services.AddSingleton<MyStreamStorageA>();
context.Services.AddSingleton<ITKDataRepeaterStream, MyStreamA>();
// services.AddSingleton<ITKDataRepeaterStream, MyStreamB>(); etc
// File download
context.Services.AddSingleton<ISecureFileDownloadFileStorage, TKEpiserverBlobFileStorage>();Various utility classes can be found below the QoDL.Toolkit.Core.Util namespace.
TKSensitiveDataUtils- Util methods for stripping numbers of given lengths, emails etc from texts.TKIPAddressUtils- Parse strings to IP address models.TKExceptionUtils- Get a summary of exceptions to include in test results.TKConnectivityUtils- Ping or send webrequests to check if a host is alive and returnTestResultobjects.TKTimeUtils- Prettify durations.TKIoCUtils- Get instances of types with partial IoC etc.TKAsyncUtils- Invoke async through reflection, run async synchronous.TKRequestData- Quickly get/set some data in request items.- Memory loggers for any interface can be created at runtime by using
TKLogTypeBuilder.CreateMemoryLoggerFor<TInterface>included in the nuget packageQoDL.Toolkit.Utility.Reflection. QoDL.Toolkit.Core.Config.TKGlobalConfigcontains some global static options that can be configured at startup:- Dependency resolver override (must be configured for .NET Core).
- Types and namespaces ignored in data serialization.
- Current request IP resolver logic override.
TKSelfUptimeChecker- Can be used to trigger actions when the site is back up after a certain amount of downtime.TKEpiserverUtils- Some utils using epi implementations, e.g. uptime checking using dds storage.
If something doesn't work as expected it might be a silenced internal exception. To handle such exceptions subscribe to TKGlobalConfig.OnExceptionEvent and handle as needed.