diff --git a/.editorconfig b/.editorconfig index a8a1bab..e04cffc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,7 +2,7 @@ # Default severity for all analyzer diagnostics dotnet_analyzer_diagnostic.severity = error -csharp_using_directive_placement=inside_namespace:error +csharp_using_directive_placement = inside_namespace:error # IDE0032: Use auto property dotnet_diagnostic.IDE0032.severity = none diff --git a/.gitignore b/.gitignore index 35a9140..610eaa3 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ Words.Core/dist Words.Console/normalized_to_originals.csv Words.Console/word_permutations.csv Words.Console/words.json +internal-nlog-AspNetCore.txt diff --git a/Words.Console/Program.cs b/Words.Console/Program.cs index e9f0a64..b40b34a 100644 --- a/Words.Console/Program.cs +++ b/Words.Console/Program.cs @@ -134,7 +134,11 @@ private static void Run(string connectionString, string wordsFilename) foreach (WordFinder wordFinder in new[] { ternary, succinct }.Where(x => x != null)) { stopwatch.Restart(); - List matches = wordFinder.Matches(input, 2); + List matches = wordFinder.Matches( + input, + 2, + x => x, + x => new[] { x }); stopwatch.Stop(); if (matches.Count > 0) { diff --git a/Words.Web.Core/App_Data/placeholder.txt b/Words.Web.Core/App_Data/placeholder.txt new file mode 100644 index 0000000..e69de29 diff --git a/Words.Web.Core/App_Data/words.json b/Words.Web.Core/App_Data/words.json new file mode 100644 index 0000000..2e836b6 --- /dev/null +++ b/Words.Web.Core/App_Data/words.json @@ -0,0 +1,190 @@ +[ + { + "Number": 1, + "Data": { + "EncodingBytes": [ + "u7tbbutWrbta6qXWqtW1bburqqmqlVtVq3bV21VlVVVVWraVVWqtWtrVVZUpKVatVVVVVWqtrba6k1Sq", + "lVVWpVVWqlVVa1Vqq0VSSJSqqqqqqqqqqqqqqqqoUqqUpVKpKVVVVSqqoSoRIoqUqqqiUSSikipApCA" + ], + "EncodingBits": 953, + "LetterBytes": [ + "AHMAdABmAHj__QBwAHYAbABkAGgAZQB1AGIAYQBtAGUAdQBpAHIAaQBjAGUAaQBnAG0AbgBuAHQAYQAA", + "AHMAZQBuAHYAcABhAGn__QBwAGUAbwBvAGIAawBuAGEAbgBhAGsAcABvAGkAbwBzADoAeABhAGsAbgAA", + "AGQAcABz__0AcgAAAHMAagBzAG8AaAB1AGEAbwB0AGwAZQBvAHIAdABpAGwAagBsAGkAbgByAGYAYQB1", + "AGUAcgAAAGEAaQBiAGUAZQBvAG4AYQBoAHIAdgBzAHQAZQBrAG8AYQByAHIAdQBiAAAAbwBuAGEAY__9", + "AHUAdQByAHgAbgBlAGUAaQBpAGoAZQBlAGwAZQBvAGUAcQBvAHQAYwBlAGEAdQAAAHMAYgBqAAAAawBr", + "AAAAaQAAAGEAYQAAAHQAaQByAGkAYwBr__0AbAB0AGsAeQB1AGQAbABjAG4AbAB0AGsAbgB0AGcAbAB6", + "AHQAYQBoAHUAcgBuAHMAYQB1AHIAaQBlAHQAbwB1AGIAAABoAAAAaQBtAHMAdAAAAHQAdQBpAGcAbAAA", + "AHQAZQBpAHAAYQBoAGEAdABpAG8AbwByAAAAbQBmAGkAaABiAGQAaQBuAHQAYQBsAGEAbQAAAHAAcgBh", + "AGQAYwBrAHgAdABwAHkAYwByAGsAdwBhAHkAbgBmAHIAbwBoAHUAZQAAAAAAbgB0AG0AdQAAAGEAAABn", + "AAAAZAAAAAAAegAAAHQAZQAAAGkAbwBnAGkAZwBvAGkAcgBuAGkAbwB0AG7__QByAGQAeABhAG4AZQBw", + "AGEAZABiAGEAbwBlAG8AdABlAG8AawBzAG___QAgAGMAc__9AG8AbABvAGsAaQBj__0AZQBlAHQAaQBu", + "AAAAAAAAAG8AdAAAAGcAYwBnAHAAZQBuAAAAaQBnAAAAawBpAAAAdABhAGEAYQAAAGkAcgBhAG8AAABv", + "AAAAZABsAAAAYQBsAGYAbwBoAG0AcABhAGkAcABqAGwAeQAAAGkAegBvAGsAcgBlAGkAcwBhAAAAAAAA", + "AGgAAABwAG4AeQAAAAAAAABhAAAAAABsAAAAbgAAAAAAawBrAAAAAABsAGkAZgAAAGEAbwAAAGcAbgBy", + "AGQAbwB0AGUAbwBsAGwAAAAAAHMAAABtAGUAAAAAAG0AAABhAAAAZQAAAGsAbgAAAAAAdABlAAD__QAA", + "AGcAAAAAAG0AYQBpAAAAaQAAAAAAAAAAAAAAZwBhAAAAawAAAAAAAABuAAAAAAAAAAAAAA" + ] + } + }, + { + "Number": 2, + "Data": { + "EncodingBytes": [ + "ttW1WuqqrVVVVVVVVVVKqqiqqSqKiA" + ], + "EncodingBits": 175, + "LetterBytes": [ + "AHT__QBzAHIAcgB0AGEAdABhAGEAbABzAGcAbgBuAHYAYQBoAHkAbABmAHIAaQBzAGcAcwBpAGwAeQBv", + "__0AaQBuAGf__QBoAGkAcwBrAHQAbgBuAGcAZQBuAGkAdABrAG8AcAB0AHQAAABuAGcAbgBh__0AcwBh", + "AGUAaQAAAAAAZwBrAGwAaQByAG8AZgAAAGUAAABkAGEAcgBhAAAAAABkAG8AZAAAAAAAYQAA" + ] + } + }, + { + "Number": 3, + "Data": { + "EncodingBytes": [ + "u3VW1VrdVWrXaqqqqrtVVVVVq1VVVVVaqqqqqqqqrSqqqqqqqiqqqqqqipVVVVUiVVVVClVVCFQg" + ], + "EncodingBits": 453, + "LetterBytes": [ + "AHQAbwBnAHYAcgBhAGUAYgBz__0AcwBlAG4AbwBhAG0AbAByAGsAawB0AG4AZQBm__0Aa__9AGoAbQBs", + "AGsAdwBlAHUAcgBhAHQAdAByAGwAcwBhAGkAawBlAHAAZAB2AG8AcgBzAGcAawBlAHL__f_9AHAAbQBu", + "AG8AZABhAHUAbABuAHMAbwBuAGEAYgB2AHMAcv_9AHAAcABsAGIAdABuAGkAbABnAGr__QBpAGkAYQB0", + "AGQAcABh__0AdAB3AGsAcABwAGEAYQBlAHMAZQBs__0AawBhAHQAZwBjAHIAAABvAGkAcgByAHIAbwBp", + "AG8AZgB0AGwAcgB1AGQAYQBu__0AaABwAGEAawBzAHYAAAAAAG4AbQBlAHIAZwBzAGUAdABhAGYAbQBy", + "AG4AawBuAGUAbwB2AGQAYwAAAAAAZQBjAGsAAAB0AHMAYQB5AGEAaQBlAGIAaQBzAGQAbABkAHUAaAAA", + "AGsAAAAAAHQAAABhAGMAcwB2AG8AZQBsAGkAaQByAGUAYgAAAAAAAABlAG0AAABrAGwAYQBuAHMAYQBs", + "AGEAAAAAAAAAaQAAAAAAAABnAGwAbgAAAAAAAABrAAA" + ] + } + }, + { + "Number": 4, + "Data": { + "EncodingBytes": [ + "u3dtu1VVW7atVatbbW1VVVVVatbVVVVVVVW1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVKqqqq", + "qqqqqqilVVVVVVVSpFVVVVVKpAqqqVRBVKSJIA" + ], + "EncodingBits": 705, + "LetterBytes": [ + "AHMAdABvAHYAeQBvAHIAbABw__0Adf_9AHL__QBjAHAAZwBz__0AYwBuAGEAcgBzAHQAdgBlAGQAawBy", + "AGEAdABnAGEAYQBhAGsAYQBtAG4AbABhAHQAYQBwAGUAawBwAGX__QBhAG4AZQBvAG4AeABsAGUAdgBi", + "AG8AZgBr__0AdQB0AHQAYQBlAHQAdgBnAHAAcgBvAGEAcgBkAG0AYgBvAGQAZwBrAGwAcgBiAHIAbwBu", + "AGkAaABlAHQAeQBuAG8AZQByAHMAZAB0__0AZQBnAGIAbgBwAGIAYgBmAHIAcABkAGwAbwBjAHQAeQB0", + "AGYAbABhAHMAbABqAG4AdQBoAHQAcgByAGkAdABpAGsAbgByAHIAZQBvAHAAbwBhAGkAaQBvAHP__QB0", + "AGUAaQB0AHIAdABhAHMAaQBk__0AYwBwAGUAbwB0AHQAYQBtAGsAcgBsAGEAYQBsAG0AZQBtAGsAbABz", + "AHIAYQByAGwAbgBmAGEAbwBlAGcAbQBzAGUAcgBoAHAAdAByAGEAYQByAGEAeQB5AGkAbgBk__0AAABy", + "AGIAYQB0AHQAbwByAGQAZQB0AGkAcgBzAHIAaQBhAHQAbABuAG8AbABzAHIAZABuAGsAbQByAGMAZwBk", + "AGUAbgAAAAAAZABlAAAAcwBlAGEAdABlAGUAYgBrAHMAcwB0AGUAbgBzAGMAYQBkAGgAZQB0AGEAbQBp", + "AGsAAABlAHIAYQAAAHIAAAAAAGcAdAByAHL__QBvAGgAZQBpAG4AaQBsAGsAZwBlAGUAbAAAAGwAYQBz", + "AGEAAABhAAAAAAAAAAAAAABhAHMAcABhAHIAcgB0AG7__QAAAGEAcwB0AGwAAAAAAHQAAAAAAAAAAABp", + "AHYAYQBpAAAAZwBwAAAAawAAAGEAAAAAAGUAAABzAAAAcAAAAAAAAAAAAAA" + ] + } + }, + { + "Number": 5, + "Data": { + "EncodingBytes": [ + "ut1t21Vda7VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVSqqqqqlSqqqoglShQA" + ], + "EncodingBits": 415, + "LetterBytes": [ + "AGoAdQBhAHMAbgBuAGIAbwBrAHQAZwB0AGsAZQBjAGYAagB0AHIAbgByAHYAZgBpAHQAdABoAGYAYwBt", + "__0AcgBwAHkAYQBsAHAAYQBo__0AcgBvAGkAYQBvAHAAawBtAGwAaQBpAGQAcwB5AHUAbgBhAGwAdQB4", + "AGUAbABrAG8AZQBhAHYAbgBuAGQAaQB4AG4AcwBpAGkAbwBpAGIAdABsAHQAcgByAHQAZwBuAGIAIABy", + "AGsAcABiAHMAbABkAHUAagBhAGEAZgBwAGUAdABkAHUAZwBlAHQAbwBvAG8AagBhAGL__QBkAHT__QBy", + "AHIAcgBvAGsAbwBuAGIAbgB4AGwAYQBuAGIAbgBiAGkAbABhAGEAbwBrAGUAcgBvAGUAZABuAGUAAAB0", + "AGwAcwBvAHMAbAB0AHAAcwB0AHQAZQB2AHYAZQBpAHIAAABhAHQAbAAAAGEAYQBpAGEAbwB0AG4AZQBh", + "AHIAbgBhAAAAAABsAAAAAAAAAAAAcgAAAGcAcgBrAAAAZwBkAAAAAAAAAGEAYQAAAAAAAAAA" + ] + } + }, + { + "Number": 6, + "Data": { + "EncodingBytes": [ + "u21XaqrrVVa6qqqqqqqqtVVVVVVVVVVVVVVaqqqqqqqqqqqqqqpVVVVVFVVUqVVVIlVQSg" + ], + "EncodingBits": 419, + "LetterBytes": [ + "AHIAZQBiAHMAawBvAGkAaABwAHQAbABsAG4AdQBkAGsAcgBlAGkAYQBsAGQAcwB1AGkAaQBuAGkAbgBu", + "AGcAbQBzAG4AaABiAG8AdgBuAG8AbQBvAGQAaf_9AGUAav_9AGn__QBiAHMAaQBrAGwAZQBwAHAAbwBs", + "AHIAaQB0AG4AbABlAGsAbgBvAHQAbAByAGUAcgByAHMAZQBuAHQAZwBsAGwAdgBuAG0AaQB0AHYAcgBv", + "AGIAcABrAGcAbgBzAHMAcP_9AG8AcwBtAG___QBhAGcAbwBvAG8AZQBpAHQAbgBhAGsAbABoAHQAaQBs", + "AHIAdAByAGUAbABuAGwAbgBp__0AbgBsAHQAYQBwAHMAZQBkAGkAYQBuAGEAbwAAAGcAZAByAGUAaQBh", + "AG4AcgBlAHIAZQBvAG0AZABuAG0AAAAAAGEAcgBjAHIAZP_9AHIAYQBzAG4AAABlAGQAaQAAAGEAawBl", + "AGUAdgBpAG4AbQBzAAAAZQAAAAAAYQAAAGwAYQBuAHMAYQBrAAAAAAAAAAAAZwAAAHT__QAAAAAAAA" + ] + } + }, + { + "Number": 7, + "Data": { + "EncodingBytes": [ + "u3VbVVW6qqqqqrVVVVVVqqqtVVVVVVVVVVVVVVVVVVVVVVVKqqqpKqqQqUUI" + ], + "EncodingBits": 359, + "LetterBytes": [ + "AHMAeQBr__0AcwBrAHkAaABtAGwAdABhAGwAbwBvAGcAaQBkAGUAdABzAG4AbQBlAGYAbgBlAHUAZQBt", + "AHQAawB0AG8AbgByAGkAZABzAHIAawBl__0Acv_9AG0Aaf_9AG0AaQBrAHMAcgBwAHAAYQBrAG8AaABu", + "AGEAZQBlAHMAaQBsAHYAcwBmAHQAZABlAHMAbABpAGwAagBmAHQAYf_9AHAAYQBlAGkAdAB0AGkAbgBi", + "AHUAaQBpAG4AeABvAGsAbgBmAHMAZQBzAGQAeQBrAHgAcwBlAGwAZQB0AHMAaQBiAHIAdAB1AGcAZABl", + "AGsAcgBpAHMAaQBrAGUAZQBzAGkAcwBnAG8AcgAAAGEAbgBpAHMAYQByAHIAaABzAHQAYQBtAGEAAABn", + "AAAAawBwAGH__QBvAGsAcgByAAAAZAAAAAAAAABkAHYAcAAAAGkAZQAAAAAAYQBwAAAAAAAAAGEAAA" + ] + } + }, + { + "Number": 8, + "Data": { + "EncodingBytes": [ + "ut1tVVVqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpVVSpUSA" + ], + "EncodingBits": 267, + "LetterBytes": [ + "AGsAYQBkAHMAbQByAGcAcABtAHQAZQBvAGkAZQBmAGUAdQByAHIAZwByAG7__QB0AGwAYf_9AGEAbABl", + "AG8AZwBzAHQAZgBu__0AaQBrAG0AZQBrAGkAZgBnAHYAYgB0AGYAbABvAHIAaQBzAGUAZQB2AG8AaQBt", + "AGUAYQBlAHIAcgBlAHIAbgBwAHMAbABsAHYAYQByAHMAZgBlAGkAaQBhAGEAbABrAGsAbAB0AHMAawB2", + "AGsAaQBhAG4AdQBlAHQAbgBnAGEAcwBuAGkAZQBuAGUAYQBpAAAAbQBkAG4AbgBzAG4AbgBmAAAAZQBn", + "AHMAAAB0AGQAdAAAAAAAYQAAAGUAAAAAAAA" + ] + } + }, + { + "Number": 9, + "Data": { + "EncodingBytes": [ + "tdttVW1VWqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqVVVSqqpKlKkg" + ], + "EncodingBits": 353, + "LetterBytes": [ + "AHUAdABsAHMAaQBhAG0AZQB2AHYAYQB2AGsAYQByAGUAaQBzAGsAdABsAGgAcgBp__0AbgBzAHMAdABh", + "AGn__QB1AGsAbgB0AGQAbgB0AG8AbABtAG4AdgBuAG8AdABlAGkAaQBzAHMAYQBzAHUAYQByAHYAZgBu", + "AGwAaQBwAHQAbQBkAGQAaQBpAGkAZwBzAG4AZQBmAGEAZwBzAHQAcwB4AHMAcwB0AG7__QBrAHUAYQBl", + "AGUAZQBoAGoAbwBzAHIAdABkAG4AdABtAHIAbwB1AGwAaQBzAHMAcwBwAHP__QBhAHQAawBlAG___QBv", + "AHQAYQBzAHIAZABhAGQAcgBuAG0AcgBqAHMAawBrAAAAZABvAGH__QByAGT__QBzAHkAbgAAAG0AbgBy", + "AGkAbgBuAGEAZABpAAAAcwAAAG4AaQBzAAAAZABuAAAAZwBuAHQAAABnAAAAZwAAAAAAAA" + ] + } + }, + { + "Number": 10, + "Data": { + "EncodingBytes": [ + "u1qrqraqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqoqolSKA" + ], + "EncodingBits": 349, + "LetterBytes": [ + "AGYAeQBiAGcAcv_9AGwAcgBwAGEAcgBv__0AcgBrAHMAIABzAGMAbgBlAGUAcgBuAGEAdAB2AGsAcwBz", + "AHIAZQB1AG4AaQBh__3__QB0AHMAZABtAG4AbAByAHYAdgBhAG8AaQBtAGkAbABzAGUAZQBuAG4AdABl", + "AG4AIABuAHIAcgBkAHYAdgByAGcAbgBlAHMAcwBhAGH__QBwAHMAaQBkAGsAawBqAGwAcgByAGsAbwBs", + "AHIAcv_9AHMAZABlAG8ALf_9AGkAaQBtAGsAZQBzAG0AbABnAGQAZABmAGEAcgBlAG3__QBnAGEAYf_9", + "AG0AaQBuAGkAcgBuAG4AbgByAHAAbgB0AHMAYQBpAGQAZABlAGEAZwBhAHMAcgBuAGUAZQBsAG4AcwB0", + "AGkAZQBnAAAAAABzAGoAYv_9AG8AAAAAAGUAAABlAHIAbgAAAHQAAAAAAHkAZwAA" + ] + } + } +] \ No newline at end of file diff --git a/Words.Web.Core/Controllers/HomeController.cs b/Words.Web.Core/Controllers/HomeController.cs deleted file mode 100644 index e537f60..0000000 --- a/Words.Web.Core/Controllers/HomeController.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Words.Web.Core.Models; - -namespace Words.Web.Core.Controllers -{ - public class HomeController : Controller - { - private readonly ILogger _logger; - - public HomeController(ILogger logger) - { - _logger = logger; - } - - public IActionResult Index() - { - return View(); - } - - public IActionResult Privacy() - { - return View(); - } - - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public IActionResult Error() - { - return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); - } - } -} diff --git a/Words.Web.Core/Extensions.cs b/Words.Web.Core/Extensions.cs new file mode 100644 index 0000000..015fe5e --- /dev/null +++ b/Words.Web.Core/Extensions.cs @@ -0,0 +1,54 @@ +namespace Words.Web.Core +{ + public static class Extensions + { + private static readonly Action information; + private static readonly Action error; + private static readonly Action cacheItemRemoved; + private static readonly Action queryElapsed; + + static Extensions() + { + int eventId = 1; + information = LoggerMessage.Define( + LogLevel.Information, + new EventId(eventId++, nameof(Information)), + "Informational message: {Information}"); + + error = LoggerMessage.Define( + LogLevel.Error, + new EventId(eventId++, nameof(Error)), + "Error message: {Error}"); + + cacheItemRemoved = LoggerMessage.Define( + LogLevel.Information, + new EventId(eventId++, nameof(CacheItemRemoved)), + "Cache item {Key} removed due to {Reason}: {@Value}"); + + queryElapsed = LoggerMessage.Define( + LogLevel.Information, + new EventId(eventId++, nameof(QueryElapsed)), + "Query '{Text}',{MilliSeconds:F2}"); + } + + public static void Information(this ILogger logger, string message) + { + information.Invoke(logger, message, null); + } + + public static void Error(this ILogger logger, string message) + { + error.Invoke(logger, message, null); + } + + public static void CacheItemRemoved(this ILogger logger, object key, object reason, object value) + { + cacheItemRemoved.Invoke(logger, key, reason, value, null); + } + + public static void QueryElapsed(this ILogger logger, string text, double milliseconds) + { + queryElapsed.Invoke(logger, text, milliseconds, null); + } + } +} diff --git a/Words.Web.Core/Models/ErrorViewModel.cs b/Words.Web.Core/Models/ErrorViewModel.cs deleted file mode 100644 index 01238a2..0000000 --- a/Words.Web.Core/Models/ErrorViewModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Words.Web.Core.Models -{ - public class ErrorViewModel - { - public string RequestId { get; set; } = null!; - - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - } -} diff --git a/Words.Web.Core/NLog.config b/Words.Web.Core/NLog.config new file mode 100644 index 0000000..c0260ec --- /dev/null +++ b/Words.Web.Core/NLog.config @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Words.Web.Core/Program.cs b/Words.Web.Core/Program.cs index 9799389..9f453d2 100644 --- a/Words.Web.Core/Program.cs +++ b/Words.Web.Core/Program.cs @@ -1,19 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - namespace Words.Web.Core { + using Microsoft.Extensions.Logging; + using NLog; + using NLog.Web; + using Npgsql.Logging; + public class Program { public static void Main(string[] args) { - CreateHostBuilder(args).Build().Run(); + NpgsqlLogManager.IsParameterLoggingEnabled = true; + NpgsqlLogManager.Provider = new SqlLoggingProvider(); + var logger = NLogBuilder.ConfigureNLog("NLog.config").GetCurrentClassLogger(); + try + { + logger.Debug("starting"); + CreateHostBuilder(args).Build().Run(); + } + catch (Exception ex) + { + logger.Error(ex, "unhandled exception"); + } + finally + { + logger.Debug("stopping"); + LogManager.Shutdown(); + } } public static IHostBuilder CreateHostBuilder(string[] args) => @@ -21,6 +33,13 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - }); + }) + .ConfigureLogging(logging => + { + logging + .ClearProviders() + .SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); + }) + .UseNLog(); } } diff --git a/Words.Web.Core/SqlLoggingProvider.cs b/Words.Web.Core/SqlLoggingProvider.cs new file mode 100644 index 0000000..7f1b5b5 --- /dev/null +++ b/Words.Web.Core/SqlLoggingProvider.cs @@ -0,0 +1,61 @@ +namespace Words.Web.Core +{ + using NLog; + using Npgsql.Logging; + + public class SqlLoggingProvider : INpgsqlLoggingProvider + { + public NpgsqlLogger CreateLogger(string name) + { + return new CustomLogger(LogManager.GetLogger(name)); + } + + public class CustomLogger : NpgsqlLogger + { + private readonly Logger logger; + + public CustomLogger(Logger logger) + { + this.logger = logger; + } + + public override bool IsEnabled(NpgsqlLogLevel level) + { + return logger.IsEnabled(ToNLogLogLevel(level)); + } + + public override void Log(NpgsqlLogLevel level, int connectorId, string msg, Exception? exception = null) + { + LogEventInfo ev = new( + ToNLogLogLevel(level), + logger.Name, + msg); + if (exception != null) + { + ev.Exception = exception; + } + + if (connectorId != 0) + { + ev.Properties["ConnectorId"] = connectorId; + } + + logger.Log(ev); + } + + private static LogLevel ToNLogLogLevel(NpgsqlLogLevel level) + { + return level switch + { + NpgsqlLogLevel.Trace => LogLevel.Trace, + NpgsqlLogLevel.Debug => LogLevel.Debug, + NpgsqlLogLevel.Info => LogLevel.Info, + NpgsqlLogLevel.Warn => LogLevel.Warn, + NpgsqlLogLevel.Error => LogLevel.Error, + NpgsqlLogLevel.Fatal => LogLevel.Fatal, + _ => throw new ArgumentOutOfRangeException(nameof(level)), + }; + } + } + } +} diff --git a/Words.Web.Core/Startup.cs b/Words.Web.Core/Startup.cs index 1b71eec..9335294 100644 --- a/Words.Web.Core/Startup.cs +++ b/Words.Web.Core/Startup.cs @@ -1,29 +1,37 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - namespace Words.Web.Core { + using System.Data; + using System.Text; + using System.Text.Json; + using Npgsql; + public class Startup { - public Startup(IConfiguration configuration) + public Startup(IConfiguration configuration, IWebHostEnvironment env) { Configuration = configuration; + WebHostEnvironment = env; } public IConfiguration Configuration { get; } + public IWebHostEnvironment WebHostEnvironment {get;set;} + // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + WordsOptions wordsOptions = + Configuration + .GetSection(WordsOptions.Words) + .Get(); + services.Configure(Configuration.GetSection(WordsOptions.Words)); services.AddControllersWithViews(); + services.AddScoped(sp => { + NpgsqlConnection connection = new(wordsOptions.ConnectionString); + connection.Open(); + return connection; + }); + services.AddSingleton(LoadWordFinders(Path.Combine(WebHostEnvironment.ContentRootPath, "App_Data", "words.json"))); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -37,21 +45,39 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); + app.UseHsts(); // ? } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); - app.UseAuthorization(); + app.UseAuthorization(); // ? - app.UseEndpoints(endpoints => + app.UseEndpoints(endpoints => // ? { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); + + AppDomain.CurrentDomain.SetData( + "DataDirectory", + Path.Combine(env.ContentRootPath, "App_Data")); + } + + private static WordFinders LoadWordFinders(string filename) + { + string path = Path.Combine("DataDirectory", filename); + string succinctTreeDataJson = File.ReadAllText(path, Encoding.UTF8); + Bucket[]? buckets = JsonSerializer.Deserialize(succinctTreeDataJson); + if (buckets is null) + { + throw new InvalidOperationException("deserialization failed"); + } + + WordFinders wordFinders = new(buckets); + return wordFinders; } } } diff --git a/Words.Web.Core/TagHelpers/BoldTagHelper.cs b/Words.Web.Core/TagHelpers/BoldTagHelper.cs new file mode 100644 index 0000000..d79f92a --- /dev/null +++ b/Words.Web.Core/TagHelpers/BoldTagHelper.cs @@ -0,0 +1,16 @@ +namespace Words.Web.Core +{ + using Microsoft.AspNetCore.Razor.TagHelpers; + + [HtmlTargetElement("bold")] + [HtmlTargetElement(Attributes = "bold")] + public class BoldTagHelper : TagHelper + { + public override void Process(TagHelperContext context, TagHelperOutput output) + { + output.Attributes.RemoveAll("bold"); + output.PreContent.SetHtmlContent(""); + output.PostContent.SetHtmlContent(""); + } + } +} diff --git a/Words.Web.Core/TagHelpers/EmailTagHelper.cs b/Words.Web.Core/TagHelpers/EmailTagHelper.cs new file mode 100644 index 0000000..aaf8312 --- /dev/null +++ b/Words.Web.Core/TagHelpers/EmailTagHelper.cs @@ -0,0 +1,20 @@ +namespace Words.Web.Core.TagHelpers +{ + using System.Threading.Tasks; + using Microsoft.AspNetCore.Razor.TagHelpers; + + public class EmailTagHelper : TagHelper + { + private const string EmailDomain = "contoso.com"; + public string MailTo { get; set; } = null!; + + public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + output.TagName = "a"; + string address = $"{MailTo}@{EmailDomain}"; + output.Attributes.SetAttribute("href", $"mailto:{address}"); + output.Content.SetContent(address); + return Task.CompletedTask; + } + } +} diff --git a/Words.Web.Core/Views/Home/Index.cshtml b/Words.Web.Core/Views/Home/Index.cshtml index 08a5a5c..5f79311 100644 --- a/Words.Web.Core/Views/Home/Index.cshtml +++ b/Words.Web.Core/Views/Home/Index.cshtml @@ -1,8 +1,146 @@ -@{ - ViewData["Title"] = "Home Page"; -} +@model QueryViewModel + +@* +@helper Table(string header, List results) +{ +
+

@header

+
+ foreach (string item in results) + { +
+
+ + + + + + + + + + +
@item
+ + Google + + + + Wikipedia + + + + NE + + + + Synonymer + +
+
+
+ } +}*@ -
-

Welcome

-

Learn about building Web apps with ASP.NET Core.

+
+
+ @using (Html.BeginForm( + "Search", + "Home", + null, + FormMethod.Post, + null, + new { @class = "well form-search input-append" })) + { + + @Html.TextBoxFor( + m => m.Text, + new + { + @class = "input-large", + placeholder = "Ange sökterm", + autocorrect = "off", + autocapitalize = "off", + autocomplete = "off", + autofocus = "autofocus" + }) + + } +
+
+@* +
+ @Html.Partial("_Help")
+ +@if (Model.Results != null) +{ +
+
+

+ Sökningen tog @Model.Results.ElapsedMilliseconds.ToString("N2") ms. + @if (Model.Results.Count == 0) + { + @:Hittade 0 resultat. + } + else + { + @:Hittade @Model.Results.Count resultat: + } +

+
+
+ + if (Model.Results.Words.Any()) + { +
+ @Table("Ord", Model.Results.Words) +
+ } + + if (Model.Results.Anagrams.Any()) + { +
+ @Table("Anagram", Model.Results.Anagrams) +
+ } + + if (Model.Results.Near.Any()) + { +
+ @Table("Nära", Model.Results.Near) +
+ } +} + +@if (Model.Recent.Any()) +{ +
+

Nyligen gjorda sökningar:

+
+
+
+ + @foreach (RecentQuery item in Model.Recent) + { + + + + } +
@Html.ActionLink(item.Text, "Index", new { id = item.QueryId })
+
+
+} +*@ + +

Some additional information

+Is this bold? +Daniel diff --git a/Words.Web.Core/Views/Shared/Error.cshtml b/Words.Web.Core/Views/Shared/Error.cshtml deleted file mode 100644 index bfbc079..0000000 --- a/Words.Web.Core/Views/Shared/Error.cshtml +++ /dev/null @@ -1,27 +0,0 @@ -@model ErrorViewModel -@{ - ViewData["Title"] = "Error"; -} - -

Error.

-

An error occurred while processing your request.

- -@if (Model?.ShowRequestId ?? false) -{ -

- Request ID: @Model.RequestId -

-} - -

Development Mode

-

- Swapping to Development environment will display more detailed information about the error that - occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the - ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

diff --git a/Words.Web.Core/Views/_ViewImports.cshtml b/Words.Web.Core/Views/_ViewImports.cshtml index 011c509..8730a1e 100644 --- a/Words.Web.Core/Views/_ViewImports.cshtml +++ b/Words.Web.Core/Views/_ViewImports.cshtml @@ -1,3 +1,3 @@ -@using Words.Web.Core -@using Words.Web.Core.Models +@using Words.Web.ViewModels @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Words.Web.Core diff --git a/Words.Web.Core/WordFinders.cs b/Words.Web.Core/WordFinders.cs new file mode 100644 index 0000000..fc03854 --- /dev/null +++ b/Words.Web.Core/WordFinders.cs @@ -0,0 +1,47 @@ +namespace Words.Web +{ + using System.Collections.Generic; + using Dapper; + using Npgsql; + + public class WordFinders + { + private readonly IDictionary wordFinders; + + public WordFinders(Bucket[] buckets) + { + wordFinders = + buckets.ToDictionary( + x => x.Number, + x => + WordFinder.CreateSuccinct( + x.Data, + Language.Swedish, + y => Array.Empty(), + y => Array.Empty())); + } + + public List Matches(string text, SearchType searchType, int limit) + { + return new List(); + } + + private static string[] GetOriginal(NpgsqlConnection connection, string[] normalized) + { + IEnumerable query = + connection.Query( + "select original from normalized where normalized = any(@normalized)", + new { normalized }); + return query.ToArray(); + } + + private static string[] GetPermutations(NpgsqlConnection connection, string normalized) + { + IEnumerable query = + connection.Query( + "select permutation from permutation where normalized = @normalized", + new { normalized }); + return query.ToArray(); + } + } +} diff --git a/Words.Web.Core/Words.Web.Core.csproj b/Words.Web.Core/Words.Web.Core.csproj index 183a136..87885d2 100644 --- a/Words.Web.Core/Words.Web.Core.csproj +++ b/Words.Web.Core/Words.Web.Core.csproj @@ -3,5 +3,28 @@ net6.0 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Words.Web.Core/WordsOptions.cs b/Words.Web.Core/WordsOptions.cs new file mode 100644 index 0000000..009fef4 --- /dev/null +++ b/Words.Web.Core/WordsOptions.cs @@ -0,0 +1,9 @@ +namespace Words.Web.Core +{ + public class WordsOptions + { + public const string Words = "Words"; + + public string ConnectionString { get; set; } = null!; + } +} diff --git a/Words.Web.Core/appsettings.Development.json b/Words.Web.Core/appsettings.Development.json index dba68eb..8983e0f 100644 --- a/Words.Web.Core/appsettings.Development.json +++ b/Words.Web.Core/appsettings.Development.json @@ -1,9 +1,9 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" } } } diff --git a/Words.Web.Core/appsettings.json b/Words.Web.Core/appsettings.json index 81ff877..dc2be70 100644 --- a/Words.Web.Core/appsettings.json +++ b/Words.Web.Core/appsettings.json @@ -1,9 +1,13 @@ { + "Words": { + "ConnectionString": "Host=localhost;Username=prisma;Password=prisma;Database=words" + }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.Hosting.Lifetime": "Information", + "Npgsql": "Trace" } }, "AllowedHosts": "*" diff --git a/Words.Web/Controllers/AbstractController.cs b/Words.Web/Controllers/AbstractController.cs new file mode 100644 index 0000000..2b55039 --- /dev/null +++ b/Words.Web/Controllers/AbstractController.cs @@ -0,0 +1,130 @@ +#if NET40 +#nullable enable +#endif + +namespace Words.Web +{ +#if NET + using System.Data; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Caching.Memory; + using Words.Web.Core; +#else + using System; + using System.Collections.Generic; + using System.Data; + using System.Threading; + using System.Threading.Tasks; + using System.Web.Caching; + using System.Web.Mvc; + using NLog; +#endif + + public abstract class AbstractController : Controller + { +#if NET + private readonly IMemoryCache memoryCache; + private readonly IDbConnection connection; + private readonly ILogger logger; + private readonly WordFinders wordFinders; + + protected AbstractController( + IMemoryCache memoryCache, + IDbConnection connection, + ILogger logger, + WordFinders wordFinders) + { + this.memoryCache = memoryCache; + this.connection = connection; + this.logger = logger; + this.wordFinders = wordFinders; + } +#endif + + protected object? CacheGet(string cacheKey) + { +#if NET + return memoryCache.Get(cacheKey); +#else + return HttpContext.Cache.Get(cacheKey); +#endif + } + + protected void CachePut(string key, object item, TimeSpan expirationDelay) + { +#if NET + MemoryCacheEntryOptions opts = new() + { + AbsoluteExpirationRelativeToNow = expirationDelay + }; + opts.RegisterPostEvictionCallback(EvictionCallback); + memoryCache.Set(key, item, opts); +#else + _ = HttpContext.Cache.Add( + key, + item, + null, + Cache.NoAbsoluteExpiration, + expirationDelay, + CacheItemPriority.Normal, + OnCacheItemRemoved); +#endif + } + + protected async Task Transact( + Func> func, + CancellationToken cancellationToken) + { +#if NET + IDbTransaction tran = connection.BeginTransaction(); + TResult result = await func.Invoke(connection, tran); + tran.Commit(); + return result; +#else + return await MvcApplication.Transact( + func, + cancellationToken); +#endif + } + + protected async Task Transact( + Func action, + CancellationToken cancellationToken) + { +#if NET + _ = await Transact(async (conn, tran) => + { + await action.Invoke(conn, tran); + return false; + }, + cancellationToken); +#else + await MvcApplication.Transact( + action, + cancellationToken); +#endif + } + + protected List Matches(string text, SearchType searchType, int limit) + { +#if NET + return wordFinders.Matches(text, searchType, limit); +#else + return MvcApplication.Matches(text, searchType, limit); +#endif + } + + +#if NET + private void EvictionCallback(object key, object value, EvictionReason reason, object state) + { + logger.CacheItemRemoved(key, reason, value); + } +#else + private void OnCacheItemRemoved(string key, object value, CacheItemRemovedReason reason) + { + LogManager.GetCurrentClassLogger().Info("Cache item {key} removed due to {reason}: {@value}", key, reason, value); + } +#endif + } +} diff --git a/Words.Web/Controllers/HomeController.cs b/Words.Web/Controllers/HomeController.cs index 7dde8fd..6e3178c 100644 --- a/Words.Web/Controllers/HomeController.cs +++ b/Words.Web/Controllers/HomeController.cs @@ -1,7 +1,25 @@ -#nullable enable +#if NET +#nullable enable +#endif namespace Words.Web.Controllers { +#if NET + using System; + using System.Data; + using System.Diagnostics; + using static System.FormattableString; + using System.Text; + using Dapper; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Caching.Memory; + using Words.Web.Entities; + using Words.Web.Infrastructure; + using Words.Web.Models; + using Words.Web.ViewModels; + using Microsoft.Net.Http.Headers; + using Words.Web.Core; +#else using System; using System.Collections.Generic; using System.Diagnostics; @@ -18,19 +36,36 @@ namespace Words.Web.Controllers using ViewModels; using Words.Web.Entities; using Words.Web.Infrastructure; +#endif - public class HomeController : Controller + public class HomeController : AbstractController { +#if !NET private static readonly Logger Log = LogManager.GetCurrentClassLogger(); +#else + private readonly ILogger logger; + + public HomeController( + IMemoryCache memoryCache, + ILogger logger, + IDbConnection connection, + WordFinders wordFinders) + : base(memoryCache, connection, logger, wordFinders) + { + this.logger = logger; + logger.Error("here i am"); + } +#endif public async Task Index(int? id, CancellationToken cancellationToken) { + logger.Information("index daniel"); if (id != null) { - string text = await MvcApplication.Transact(async (conn, tran) => + string text = await Transact(async (conn, tran) => await conn.QuerySingleAsync("select text from query where query_id = @id", new { id }), cancellationToken); - if (HttpContext.Cache.Get($"query-{id}") is ResultsViewModel results) + if (CacheGet($"query-{id}") is ResultsViewModel results) { QueryViewModel cachedModel = new() { Text = text, Results = results }; if (results.Count == 0) @@ -43,17 +78,10 @@ public async Task Index(int? id, CancellationToken cancellationTok // perform search again Stopwatch sw = Stopwatch.StartNew(); - List matches = MvcApplication.Matches(text, SearchType.All, 100); + List matches = Matches(text, SearchType.All, 100); sw.Stop(); results = new ResultsViewModel(text, matches, sw.Elapsed.TotalMilliseconds); - _ = HttpContext.Cache.Add( - $"query-{id}", - results, - null, - Cache.NoAbsoluteExpiration, - TimeSpan.FromDays(1), - CacheItemPriority.Normal, - OnCacheItemRemoved); + CachePut($"query-{id}", results, TimeSpan.FromDays(1)); QueryViewModel model = new() { Text = text, Results = results }; if (results.Count == 0) { @@ -63,37 +91,63 @@ public async Task Index(int? id, CancellationToken cancellationTok return View(model); } + logger.Information("get recent queries"); + // get recent queries RecentQuery[] recentQueries = await GetRecentQueries(cancellationToken); + logger.Information(string.Join(", ", recentQueries.Select(x => x.Text))); return View(new QueryViewModel { Recent = recentQueries }); } +#if NET + [ValidateAntiForgeryToken] + [HttpPost("search")] +#else [HttpPost] +#endif public async Task Search(QueryViewModel q, CancellationToken cancellationToken) { + logger.Information("begin search"); if (ModelState.IsValid == false || q.Text is null) { - return View(q); + logger.Information("view index"); + return View("Index", q); } - if (HttpContext.Cache.Get($"query-{Encoding.UTF8.GetBytes(q.Text).ComputeHash()}") is QueryId cachedQueryId) + if (CacheGet($"query-{Encoding.UTF8.GetBytes(q.Text).ComputeHash()}") is QueryId cachedQueryId) { + logger.Information("cache found"); return RedirectToAction(nameof(Index), new { id = cachedQueryId.Id }); } Stopwatch sw = Stopwatch.StartNew(); - List matches = MvcApplication.Matches(q.Text, SearchType.All, 100); + List matches = Matches(q.Text, SearchType.All, 100); sw.Stop(); ResultsViewModel results = new(q.Text, matches, sw.Elapsed.TotalMilliseconds); +#if NET + logger.QueryElapsed(q.Text, sw.Elapsed.TotalMilliseconds); +#else Log.Info(CultureInfo.InvariantCulture, "Query '{0}',{1:F2}", q.Text, sw.Elapsed.TotalMilliseconds); +#endif // save query - int queryId = await MvcApplication.Transact(async (connection, tran) => + int queryId = await Transact(async (connection, tran) => { int id = await connection.QuerySingleAsync(@" insert into query(type, text, elapsed_milliseconds, created_date, user_agent, user_host_address, browser_screen_pixels_height, browser_screen_pixels_width) values (@type, @text, @elapsedmilliseconds, @createddate, @useragent, @userhostaddress::cidr, @browserscreenpixelsheight, @browserscreenpixelswidth) returning query_id", +#if NET + new Query( + Type: QueryType.Word.ToString(), + Text: q.Text, + ElapsedMilliseconds: (int)Math.Round(sw.Elapsed.TotalMilliseconds), + CreatedDate: DateTime.UtcNow, + UserAgent: Request.Headers[HeaderNames.UserAgent], + UserHostAddress: Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? string.Empty, + BrowserScreenPixelsHeight: 480, + BrowserScreenPixelsWidth: 640), +#else new Query( Type: QueryType.Word.ToString(), Text: q.Text, @@ -103,41 +157,34 @@ insert into query(type, text, elapsed_milliseconds, created_date, user_agent, us UserHostAddress: Request.UserHostAddress, BrowserScreenPixelsHeight: Request.Browser.ScreenPixelsHeight, BrowserScreenPixelsWidth: Request.Browser.ScreenPixelsWidth), +#endif tran); return id; }, cancellationToken); - _ = HttpContext.Cache.Add( + CachePut( $"query-{queryId}", results, - null, - Cache.NoAbsoluteExpiration, - TimeSpan.FromDays(1), - CacheItemPriority.Normal, - OnCacheItemRemoved); + TimeSpan.FromDays(1)); - _ = HttpContext.Cache.Add( + CachePut( $"query-{Encoding.UTF8.GetBytes(q.Text).ComputeHash()}", new QueryId(queryId, q.Text), - null, - Cache.NoAbsoluteExpiration, - TimeSpan.FromDays(1), - CacheItemPriority.Normal, - OnCacheItemRemoved); + TimeSpan.FromDays(1)); return RedirectToAction(nameof(Index), new { id = queryId }); } private async Task GetRecentQueries(CancellationToken cancellationToken) { - if (HttpContext.Cache.Get("recent-queries") is RecentQuery[] cachedRecentQueries) + if (CacheGet("recent-queries") is RecentQuery[] cachedRecentQueries) { return cachedRecentQueries; } RecentQuery[] recentQueries = - await MvcApplication.Transact(async (connection, tran) => + await Transact(async (connection, tran) => { IEnumerable qs = await connection.QueryAsync(@" SELECT q.query_id as queryid @@ -147,22 +194,13 @@ SELECT q.query_id as queryid return qs.ToArray().Randomize().Take(20).OrderBy(x => x.CreatedDate).ToArray(); }, cancellationToken); - _ = HttpContext.Cache.Add( + CachePut( "recent-queries", recentQueries, - null, - Cache.NoAbsoluteExpiration, - TimeSpan.FromDays(1), - CacheItemPriority.Normal, - OnCacheItemRemoved); + TimeSpan.FromDays(1)); return recentQueries; } - private void OnCacheItemRemoved(string key, object value, CacheItemRemovedReason reason) - { - Log.Info("Cache item {key} removed due to {reason}: {@value}", key, reason, value); - } - private record QueryId(int Id, string Text); } } diff --git a/Words.Web/Global.asax.cs b/Words.Web/Global.asax.cs index eac48db..f982b22 100644 --- a/Words.Web/Global.asax.cs +++ b/Words.Web/Global.asax.cs @@ -27,13 +27,20 @@ public class MvcApplication : HttpApplication { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private static string connectionString = null!; private static IDictionary? wordFinders; public static List Matches(string text, SearchType searchType, int limit) { int bucket = Bucket.ToBucket(text.Length); return wordFinders!.TryGetValue(bucket, out WordFinder? wordFinder) - ? wordFinder.Matches(text, 0, searchType, limit) + ? wordFinder.Matches( + text, + 0, + x => GetOriginal(connectionString, x), + x => GetPermutations(connectionString, x), + searchType, + limit) : throw new Exception($"no word finder found for word: {text}"); } @@ -124,7 +131,7 @@ protected void Application_Start() string appDataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory").ToString(); string filename = Path.Combine(appDataDirectory, "words.json"); - string connectionString = ConfigurationManager.ConnectionStrings["Words"].ConnectionString; + connectionString = ConfigurationManager.ConnectionStrings["Words"].ConnectionString; wordFinders = LoadWordFinders(filename, connectionString); Log.Info("Dictionary loaded"); NpgsqlLogManager.Provider = new SqlLoggingProvider(); @@ -186,27 +193,27 @@ private IDictionary LoadWordFinders(string filename, string con y => GetPermutations(connectionString, y), y => GetOriginal(connectionString, y))); return wordFinders; + } - static string[] GetOriginal(string connectionString, string[] normalized) - { - using IDbConnection connection = new NpgsqlConnection(connectionString); - connection.Open(); - IEnumerable query = - connection.Query( - "select original from normalized where normalized = any(@normalized)", - new { normalized }); - return query.ToArray(); - } + private static string[] GetOriginal(string connectionString, string[] normalized) + { + using IDbConnection connection = new NpgsqlConnection(connectionString); + connection.Open(); + IEnumerable query = + connection.Query( + "select original from normalized where normalized = any(@normalized)", + new { normalized }); + return query.ToArray(); + } - static string[] GetPermutations(string connectionString, string normalized) - { - using IDbConnection connection = new NpgsqlConnection(connectionString); - IEnumerable query = - connection.Query( - "select permutation from permutation where normalized = @normalized", - new { normalized }); - return query.ToArray(); - } + private static string[] GetPermutations(string connectionString, string normalized) + { + using IDbConnection connection = new NpgsqlConnection(connectionString); + IEnumerable query = + connection.Query( + "select permutation from permutation where normalized = @normalized", + new { normalized }); + return query.ToArray(); } } -} \ No newline at end of file +} diff --git a/Words.Web/Infrastructure/Extensions.cs b/Words.Web/Infrastructure/Extensions.cs index 9710572..c77ff05 100644 --- a/Words.Web/Infrastructure/Extensions.cs +++ b/Words.Web/Infrastructure/Extensions.cs @@ -9,12 +9,18 @@ namespace Words.Web.Infrastructure using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; +#if NET + using Microsoft.AspNetCore.Mvc.Routing; +#else using System.Web.Hosting; using System.Web.Mvc; +#endif using Dapper; + using System.Globalization; public static class UrlExtensions { +#if !NET public static string ContentCacheBreak(this UrlHelper url, string contentPath) { if (contentPath == null) @@ -38,15 +44,20 @@ public static string ContentCacheBreak(this UrlHelper url, string contentPath) return $"{url.Content(contentPath)}{hashPart}"; } +#endif public static string ComputeHash(this byte[] bytes) { - using MD5 md5 = MD5.Create(); + using SHA512 md5 = SHA512.Create(); byte[] hash = md5.ComputeHash(bytes); StringBuilder hashBuilder = new(); foreach (byte b in hash) { +#if NET + hashBuilder.Append(CultureInfo.InvariantCulture, $"{b:x2}"); +#else _ = hashBuilder.Append($"{b:x2}"); +#endif } return hashBuilder.ToString(); @@ -63,4 +74,4 @@ public static async Task> QueryAsync( return result; } } -} \ No newline at end of file +} diff --git a/Words.Web/Models/Query.cs b/Words.Web/Models/QueryType.cs similarity index 100% rename from Words.Web/Models/Query.cs rename to Words.Web/Models/QueryType.cs diff --git a/Words.Web/ViewModels/QueryViewModel.cs b/Words.Web/ViewModels/QueryViewModel.cs index 034dfeb..2b78714 100644 --- a/Words.Web/ViewModels/QueryViewModel.cs +++ b/Words.Web/ViewModels/QueryViewModel.cs @@ -8,12 +8,17 @@ public class QueryViewModel { public QueryViewModel() { +#if NET + Recent = Array.Empty(); +#else Recent = new RecentQuery[0]; +#endif } [Required(ErrorMessage = "*")] [MinLength(1)] [MaxLength(255)] + [DataType(DataType.Text)] public string? Text { get; set; } public ResultsViewModel? Results { get; set; } diff --git a/Words.Web/Words.Web.csproj b/Words.Web/Words.Web.csproj index 5430cad..b1ffe28 100644 --- a/Words.Web/Words.Web.csproj +++ b/Words.Web/Words.Web.csproj @@ -97,6 +97,7 @@ + @@ -107,7 +108,7 @@ - + @@ -200,4 +201,4 @@ --> - \ No newline at end of file + diff --git a/Words/WordFinder.cs b/Words/WordFinder.cs index d73ea1f..5aa0761 100644 --- a/Words/WordFinder.cs +++ b/Words/WordFinder.cs @@ -115,7 +115,13 @@ public static WordFinder CreateSuccinct( return wordFinder; } - public List Matches(string input, int d, SearchType searchType = SearchType.All, int limit = 100) + public List Matches( + string input, + int d, + Func getOriginal, + Func getPermutations, + SearchType searchType = SearchType.All, + int limit = 100) { if (input == null) { @@ -123,7 +129,14 @@ public List Matches(string input, int d, SearchType searchType = SearchTy } List matches = new(); - Matches(input, matches.Add, d, searchType, limit); + Matches( + input, + matches.Add, + d, + getOriginal, + getPermutations, + searchType, + limit); return matches; } @@ -131,6 +144,8 @@ private void Matches( string input, Action action, int d, + Func getOriginal, + Func getPermutations, SearchType searchType, int limit) {