diff --git a/ReadMe.md b/ReadMe.md index 54720a4..66459ec 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -5,16 +5,20 @@ ### Modules * NB All projects upgraded to .Net 6, 28Nov23. -* SACS Standard Base - * Base class library. .Net Standard 2.1. C#. -* SACS Standard Dbas - * Base database interface library. .Net Standard 2.1. C#. -* SACS Standard Accounts - * Base Acounts system interface library. .Net Standard 2.1. C#. -* SACS Standard Mob - * Base Mobile application library. .Net Standard 2.1. C#. -* SACS Core Web - * Base Web application library. [C# .Net 6] -* SACS Core Unit Tests - * Unit Tests. [C# .Net 6] +* **SACS Base** + * C# Base class library, .Net6 (was .Net Standard 2.1). +* **SACS Dbas** + * C# Base database interface library, .Net6 (was .Net Standard 2.1). +* **SACS Proc** + * C# Base processes library, .Net6. +* **SACS Maui** + * C# Maui Mobile library, .Net6 (added Apr24). +* **SACS Xamarin** (Apr24 SACS Standard Mob) + * C# Xamarin Mobile library, .Net6 (was .Net Standard 2.1). +* **SACS Web** + * C# Base Web library, .Net 6. +* **SACS Accounts** + * C# Base Accounts system interface library, .Net6 (was .Net Standard 2.1). +* **SACS Unit Tests** + * C# Unit Tests, .Net 6. diff --git a/SacsCoreUnitTests/SacsCoreUnitTests.csproj b/SacsCoreUnitTests/SacsCoreUnitTests.csproj index 8d897ad..63a75b7 100644 --- a/SacsCoreUnitTests/SacsCoreUnitTests.csproj +++ b/SacsCoreUnitTests/SacsCoreUnitTests.csproj @@ -19,6 +19,7 @@ + diff --git a/SacsCoreUnitTests/Standard/Base/Utils/StringUtil_Tester.cs b/SacsCoreUnitTests/Standard/Base/Utils/StringUtil_Tester.cs index 9e1c803..5b3cf9c 100644 --- a/SacsCoreUnitTests/Standard/Base/Utils/StringUtil_Tester.cs +++ b/SacsCoreUnitTests/Standard/Base/Utils/StringUtil_Tester.cs @@ -1,4 +1,5 @@ using System; +using System.Text.RegularExpressions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Sarand.Net.Standard.Base.Utils; @@ -43,6 +44,61 @@ namespace Sarand.UnitTests.Core.Base.Utils Assert.AreEqual(false, "Abc".IsBlank()); } + //------------------------------------------------------------------- + // Regular Expression Matching. + //------------------------------------------------------------------- + [TestMethod] + public void StringUtil_Match() + { + // Match. + Assert.AreEqual(true, "ABCDEF".Match("CDE")); + Assert.AreEqual(true, "ABCDEF".MatchCI("cDe")); + Assert.AreEqual(true, "ABCDEF".Match("CDE", System.Text.RegularExpressions.RegexOptions.None)); + Assert.AreEqual(true, "ABCDEF".Match(new Regex("CDE", System.Text.RegularExpressions.RegexOptions.None))); + Assert.AreEqual(true, "ABCDEF".Match(new Regex("cDe", System.Text.RegularExpressions.RegexOptions.IgnoreCase))); + + // Match with escaped content. + Assert.AreEqual(true, "ABC\\sDEF".Match("\\\\s")); + Assert.AreEqual(true, "AB(CD)EF".Match("(..)")); + Assert.AreEqual(true, "AB(CD)EF".Match("\\(CD\\)")); + + // Not match. + Assert.AreEqual(false, "ABCDEF".Match("cDe")); + Assert.AreEqual(false, "ABCDEF".MatchCI("eFg")); + Assert.AreEqual(false, "ABCDEF".Match("cDe", System.Text.RegularExpressions.RegexOptions.None)); + Assert.AreEqual(false, "ABCDEF".Match(new Regex("cDe", System.Text.RegularExpressions.RegexOptions.None))); + } + + [TestMethod] + public void StringUtil_IsMatch() + { + // IsMatch. + Assert.AreEqual(true, "ABCDEF".IsMatch("CDE")); + Assert.AreEqual(true, "ABCDEF".IsMatchCI("cDe")); + Assert.AreEqual(true, "ABCDEF".IsMatch("CDE", System.Text.RegularExpressions.RegexOptions.None)); + Assert.AreEqual(true, "ABCDEF".IsMatch(new Regex("CDE", System.Text.RegularExpressions.RegexOptions.None))); + Assert.AreEqual(true, "ABCDEF".IsMatch(new Regex("cDe", System.Text.RegularExpressions.RegexOptions.IgnoreCase))); + + // IsMatch with escaped content. + Assert.AreEqual(true, "ABC\\sDEF".IsMatch("\\\\s")); + Assert.AreEqual(true, "AB(CD)EF".IsMatch("(..)")); + Assert.AreEqual(true, "AB(CD)EF".IsMatch("\\(CD\\)")); + + // Not match. + Assert.AreEqual(false, "ABCDEF".IsMatch("cDe")); + Assert.AreEqual(false, "ABCDEF".IsMatchCI("eFg")); + Assert.AreEqual(false, "ABCDEF".IsMatch("cDe", System.Text.RegularExpressions.RegexOptions.None)); + Assert.AreEqual(false, "ABCDEF".IsMatch(new Regex("cDe", System.Text.RegularExpressions.RegexOptions.None))); + } + + [TestMethod] + public void StringUtil_MatchCount() + { + // Match. + Assert.AreEqual(1, "ABCDEF".MatchCount(new Regex("CDE"))); + Assert.AreEqual(2, "ABCDEFGHICDEJKL".MatchCount(new Regex("CDE"))); + } + //------------------------------------------------------------------- // Padding. //------------------------------------------------------------------- diff --git a/SacsCoreUnitTests/Standard/Proc/Excel/SaMsSpreadsheet_Tester.cs b/SacsCoreUnitTests/Standard/Proc/Excel/SaMsSpreadsheet_Tester.cs new file mode 100644 index 0000000..35a4839 --- /dev/null +++ b/SacsCoreUnitTests/Standard/Proc/Excel/SaMsSpreadsheet_Tester.cs @@ -0,0 +1,42 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Sarand.Net.Standard.Proc.Excel; +using System; + +namespace Sacs.UnitTests.Net.Standard.Proc.Excel +{ + [TestClass] + public class SaMsSpreadsheet_Tester + { + //------------------------------------------------------------------- + // Data members. + private Random random = new Random(); + + //------------------------------------------------------------------- + // GetPosForColId. + //------------------------------------------------------------------- + [TestMethod] + public void GetPosForColId_IsEmpty() + { + Assert.AreEqual(1, SaMsSpreadsheet.GetPosForColId("A")); + Assert.AreEqual(1, SaMsSpreadsheet.GetPosForColId("a")); + Assert.AreEqual(2, SaMsSpreadsheet.GetPosForColId("B")); + Assert.AreEqual(3, SaMsSpreadsheet.GetPosForColId("C")); + Assert.AreEqual(24, SaMsSpreadsheet.GetPosForColId("X")); + Assert.AreEqual(25, SaMsSpreadsheet.GetPosForColId("Y")); + Assert.AreEqual(26, SaMsSpreadsheet.GetPosForColId("Z")); + Assert.AreEqual(27, SaMsSpreadsheet.GetPosForColId("AA")); + Assert.AreEqual(28, SaMsSpreadsheet.GetPosForColId("AB")); + Assert.AreEqual(29, SaMsSpreadsheet.GetPosForColId("AC")); + Assert.AreEqual(128, SaMsSpreadsheet.GetPosForColId("DX")); + Assert.AreEqual(155, SaMsSpreadsheet.GetPosForColId("EY")); + Assert.AreEqual(182, SaMsSpreadsheet.GetPosForColId("FZ")); + + // Invalid. + Assert.AreEqual(0, SaMsSpreadsheet.GetPosForColId("")); + Assert.AreEqual(0, SaMsSpreadsheet.GetPosForColId("1")); + Assert.AreEqual(0, SaMsSpreadsheet.GetPosForColId("A1")); + } + + //------------------------------------------------------------------- + } +} diff --git a/SacsCoreWeb/Web/Components/SaController.cs b/SacsCoreWeb/Web/Components/SaController.cs index a31853e..b518979 100644 --- a/SacsCoreWeb/Web/Components/SaController.cs +++ b/SacsCoreWeb/Web/Components/SaController.cs @@ -141,7 +141,7 @@ namespace Sacs.Web.Components //if (SacsSystem.DebugMode) SaLogger.Debug(SysUtil.Fn + "\n\tData: " + queryString + " \n\tEncrypted: " + encrypted); return encrypted; } - catch (Exception exc) { SaLogger.Warn(SysUtil.Fn + " " + ExceptionUtil.GetErrorStateMessage(exc)); } + catch (Exception exc) { SaLogger.Warn($"{SysUtil.Fn} {ExceptionUtil.GetErrorStateMessage(exc)}"); } return ""; } public static NameValueCollection DecryptParameters(String encrypted, String base64IV, String base64Key) @@ -253,10 +253,11 @@ namespace Sacs.Web.Components public class ItemData_Base { // Common item properties. - public String Id { get; set; } - public String Notes { get; set; } - public String StatusCode { get; set; } - public String StatusDesc { get; set; } + public String Id { get; set; } = null; + private String notes; + public String Notes { get { return StringUtil.OnBlank(notes, null); } set { notes = StringUtil.EmptyOnNull(value).Trim(); } } + public String StatusCode { get; set; } = null; + public String StatusDesc { get; set; } = null; // Flags for common status values. // Return true or null so that property is not serialised when false. public bool? Awaiting { get { return StatusCode.EqualsCI(SacsSystem.GetStatusCode(SaStatus.Awaiting)) ? true : (bool?)null; } } diff --git a/SacsStandardBase/Base/Core/SacsDisposable.cs b/SacsStandardBase/Base/Core/SacsDisposable.cs index 001e1db..aff00bc 100644 --- a/SacsStandardBase/Base/Core/SacsDisposable.cs +++ b/SacsStandardBase/Base/Core/SacsDisposable.cs @@ -6,7 +6,7 @@ namespace Sarand.Net.Standard.Base.Core public abstract class SacsDisposable : IDisposable { //------------------------------------------------------------------- - // Data memebrs. + // Data members. private bool disposed = false; //------------------------------------------------------------------- diff --git a/SacsStandardBase/Base/Dbas/DbQuery.cs b/SacsStandardBase/Base/Dbas/DbQuery.cs index 1b617a7..cd527e7 100644 --- a/SacsStandardBase/Base/Dbas/DbQuery.cs +++ b/SacsStandardBase/Base/Dbas/DbQuery.cs @@ -626,7 +626,7 @@ namespace Sarand.Net.Standard.Base.Dbas { if (bit > 0 && bit <= 32) { - // The operation depands on whether the value should be matched or not. + // The operation depends on whether the value should be matched or not. String condition = (match? "<>" : "="); // Test is achieved by shifting the value so as to drop the target bit, shift back one bit // and identify those value that then do not match the original value shifted by one place less. @@ -727,7 +727,7 @@ namespace Sarand.Net.Standard.Base.Dbas { // Must have an 'Order By' clause to use 'Offset ... Fetch Next ...' if (orderByData.IsEmpty()) - throw new NotSupportedException("Query paging requires sort ofrder to be specified"); + throw new NotSupportedException("Query paging requires sort order to be specified"); if (DbGlobalDbms.Type != DbmsType.MsSql) throw new NotSupportedException("Query paging only supported for MS SQL"); query += " OFFSET " + ((paging.PageNo - 1) * paging.PageLength) + " ROWS FETCH NEXT " + paging.PageLength + " ROWS ONLY"; diff --git a/SacsStandardBase/Base/Helpers/SacsHttpClientHelper.cs b/SacsStandardBase/Base/Helpers/SacsHttpClientHelper.cs index 9c43106..c3484c8 100644 --- a/SacsStandardBase/Base/Helpers/SacsHttpClientHelper.cs +++ b/SacsStandardBase/Base/Helpers/SacsHttpClientHelper.cs @@ -1,5 +1,7 @@ -using Sarand.Net.Standard.Base.App; +using Microsoft.Net.Http.Headers; +using Sarand.Net.Standard.Base.App; using Sarand.Net.Standard.Base.Core; +using Sarand.Net.Standard.Base.Net; using Sarand.Net.Standard.Base.Tasks; using Sarand.Net.Standard.Base.Utils; using System; @@ -29,7 +31,7 @@ namespace Sarand.Net.Standard.Base.Helpers /// Request timeout. Default: 5 secs. /// NB Not currently supported as can only be changed when static HTTP Client not in use. /// - public int TimeoutSecs { get; set; } = RequestTimeoutSecs_Default; + public int TimeoutSecs { get; set; } = SacsHttpClient.RequestTimeoutSecs_Default; /// /// Headers to send with request. /// Accept, Accept-Charset, Accept-Encoding, Accept-Language, User-Agent, Transfer-Encoding, Pragma, Expect etc. @@ -57,55 +59,28 @@ namespace Sarand.Net.Standard.Base.Helpers //------------------------------------------------------------------- /// Response data - public class ResponseData + public class ResponseData : SacsHttpResponseData { - public DateTime RequestStarted { get; set; } - public DateTime RequestCompleted { get; set; } - /// Response headers. - public Dictionary> ResponseHeaders { get; } = new Dictionary>(); - /// Response content headers. - public Dictionary> ContentHeaders { get; } = new Dictionary>(); - /// Indicates whether Status Code represents success. - public bool Success { get; set; } - /// HTTP Status Code. - public HttpStatusCode StatusCode { get; set; } - /// HTTP message version. Default: 1.1 - public Version Version { get; set; } - /// HTTP Reason Code. - public String ReasonPhrase { get; set; } = ""; - /// Response data. - public String Data { get; set; } = ""; - /// Elapsed time for request. - public TimeSpan Elapsed { get; set; } - /// Exception thrown during handling of request. - public Exception Exception { get; set; } - /// Error state message - generally exception type and message. - public String ErrorStateMessage { get; set; } } //------------------------------------------------------------------- // Data members. - private static readonly HttpClient httpClient; //------------------------------------------------------------------- // Static initialisation. - static SacsHttpClientHelper () + static SacsHttpClientHelper() { - // Configure client. - httpClient = new HttpClient(); - httpClient.Timeout = new TimeSpan(0, 0, RequestTimeoutSecs_Default); } //------------------------------------------------------------------- // Properties. - public static HttpClient HttpClient { get { return httpClient; } } - public static int RequestTimeoutSecs_Default { get; } = 5; - public static String HeaderName_ContentType { get; } = "Content-Type"; + public static HttpClient HttpClient { get { return SacsHttpClient.HttpClient; } } + public static int RequestTimeoutSecs_Default { get { return SacsHttpClient.RequestTimeoutSecs_Default; } } + public static String HeaderName_ContentType { get {; return SacsHttpClient.HeaderName_ContentType; } } //------------------------------------------------------------------- - public static async Task DoRequest (RequestData requestData) + public static async Task DoRequest(RequestData requestData) { - String fn = "SacsHttpClientHelper.DoRequest"; requestData.ThrowIfCancellationRequested(); ResponseData responseData = new ResponseData(); @@ -114,14 +89,14 @@ namespace Sarand.Net.Standard.Base.Helpers if (requestData.FormData.Count > 0) { postContent = new FormUrlEncodedContent(requestData.FormData); - if (!requestData.ContentHeaders.ContainsKey(HeaderName_ContentType)) - requestData.ContentHeaders.Add(HeaderName_ContentType, "application/x-www-form-urlencoded; charset=utf-8"); + if (!requestData.ContentHeaders.ContainsKey(SacsHttpClient.HeaderName_ContentType)) + requestData.ContentHeaders.Add(SacsHttpClient.HeaderName_ContentType, "application/x-www-form-urlencoded; charset=utf-8"); } else if (!StringUtil.IsEmpty(requestData.PostData) || requestData.ForcePost) { postContent = new StringContent(StringUtil.EmptyOnNull(requestData.PostData), Encoding.UTF8); - if (!requestData.ContentHeaders.ContainsKey(HeaderName_ContentType)) - requestData.ContentHeaders.Add(HeaderName_ContentType, "text/plain; charset=utf-8"); + if (!requestData.ContentHeaders.ContainsKey(SacsHttpClient.HeaderName_ContentType)) + requestData.ContentHeaders.Add(SacsHttpClient.HeaderName_ContentType, "text/plain; charset=utf-8"); } requestData.ThrowIfCancellationRequested(); @@ -143,7 +118,7 @@ namespace Sarand.Net.Standard.Base.Helpers } // Default request headers. - if (HttpClient.DefaultRequestHeaders.Accept.Count == 0 && request.Headers.Accept.Count == 0) + if (SacsHttpClient.HttpClient.DefaultRequestHeaders.Accept.Count == 0 && request.Headers.Accept.Count == 0) request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/*")); // Configure request. @@ -163,7 +138,7 @@ namespace Sarand.Net.Standard.Base.Helpers } // Send request. responseData.RequestStarted = DateTime.Now; - response = await HttpClient.SendAsync(request, requestData.CancellationToken); + response = await SacsHttpClient.HttpClient.SendAsync(request, requestData.CancellationToken); responseData.RequestCompleted = DateTime.Now; } else @@ -171,7 +146,7 @@ namespace Sarand.Net.Standard.Base.Helpers // GET. request.Method = HttpMethod.Get; responseData.RequestStarted = DateTime.Now; - response = await HttpClient.SendAsync(request, requestData.CancellationToken); + response = await SacsHttpClient.HttpClient.SendAsync(request, requestData.CancellationToken); responseData.RequestCompleted = DateTime.Now; } @@ -205,7 +180,7 @@ namespace Sarand.Net.Standard.Base.Helpers exc = exc.InnerException; } String msg = exc.GetType().Name + ": " + exc.Message; - SaLogger.Warn(fn + " " + msg + ExceptionUtil.GetStackEntries(exc, 6).Concat("", "\n\t")); + SaLogger.Warn(SysUtil.Fn + " " + msg + ExceptionUtil.GetStackEntries(exc, 6).Concat("", "\n\t")); responseData.ErrorStateMessage = msg; } requestData.ThrowIfCancellationRequested(); @@ -218,33 +193,9 @@ namespace Sarand.Net.Standard.Base.Helpers //------------------------------------------------------------------- // R e s p o n s e S t a t u s D e s c r i p t i o n //------------------------------------------------------------------- - public static String GetResponseStatusDescription (HttpStatusCode httpStatus) + public static String GetResponseStatusDescription(HttpStatusCode httpStatus) { - // Ref. https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml - switch (httpStatus) - { - // 1xx: Informational - Request received, continuing process. - case HttpStatusCode.Continue: return "Continue"; - // 2xx: Success - The action was successfully received, understood, and accepted. - case HttpStatusCode.OK: return "OK"; - case HttpStatusCode.Created: return "Created"; - case HttpStatusCode.Accepted: return "Accepted"; - // 3xx: Redirection - Further action must be taken in order to complete the request. - case HttpStatusCode.MultipleChoices: return "Multiple choices"; - case HttpStatusCode.MovedPermanently: return "Moved permanently"; - case HttpStatusCode.Found: return "Found"; - // 4xx: Client Error -The request contains bad syntax or cannot be fulfilled. - case HttpStatusCode.BadRequest: return "Bad request"; - case HttpStatusCode.Unauthorized: return "Unauthorized"; - case HttpStatusCode.PaymentRequired: return "Payment required"; - case HttpStatusCode.Forbidden: return "Forbidden"; - case HttpStatusCode.NotFound: return "Not found"; - case HttpStatusCode.MethodNotAllowed: return "Method not allowed"; - case HttpStatusCode.NotAcceptable: return "Not acceptable"; - // 5xx: Server Error -The server failed to fulfill an apparently valid request. - case HttpStatusCode.InternalServerError: return "Internal server error"; - } - return httpStatus.ToString(); + return SacsHttpClient.GetResponseStatusDescription(httpStatus); } //------------------------------------------------------------------- diff --git a/SacsStandardBase/Base/Helpers/SacsRandomHelper.cs b/SacsStandardBase/Base/Helpers/SacsRandomHelper.cs index 6b5bee7..96624a9 100644 --- a/SacsStandardBase/Base/Helpers/SacsRandomHelper.cs +++ b/SacsStandardBase/Base/Helpers/SacsRandomHelper.cs @@ -15,6 +15,7 @@ namespace Sarand.Net.Standard.Base.Helpers private static Random random; public static Random Random { get { if (random == null) random = new Random(GetInt32(Int32.MaxValue)); return random; } } + //------------------------------------------------------------------- public static void Fill(byte[] byteArray) { RandomNumberGenerator.Fill(byteArray); diff --git a/SacsStandardBase/Base/Net/SacsHttpClient.cs b/SacsStandardBase/Base/Net/SacsHttpClient.cs index 493ca49..8e1ebc3 100644 --- a/SacsStandardBase/Base/Net/SacsHttpClient.cs +++ b/SacsStandardBase/Base/Net/SacsHttpClient.cs @@ -1,23 +1,31 @@ -using Sarand.Net.Standard.Base.App; +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; +using Sarand.Net.Standard.Base.App; using Sarand.Net.Standard.Base.Core; +using Sarand.Net.Standard.Base.Helpers; using Sarand.Net.Standard.Base.Tasks; using Sarand.Net.Standard.Base.Utils; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; +using System.IO.Compression; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Reflection.Metadata; using System.Text; using System.Threading; using System.Threading.Tasks; +using ZstdNet; namespace Sarand.Net.Standard.Base.Net { //======================================================================= public class SacsHttpRequestData : SacsTaskData { - public SacsHttpRequestData (CancellationToken? cancellationToken) : base(cancellationToken) { } + public SacsHttpRequestData(CancellationToken? cancellationToken) : base(cancellationToken) { } /// URL to send request to. public String Url { get; set; } /// @@ -50,6 +58,21 @@ namespace Sarand.Net.Standard.Base.Net public bool ForcePost { get; set; } } + //======================================================================= + public class SacsHttpCookieData + { + public String HeaderValue { get; set; } + public String Name { get; set; } + public String Value { get; set; } + public DateTime Expires { get; set; } + public long MaxAge { get; set; } + public String Path { get; set; } + public String Domain { get; set; } + public bool Secure { get; set; } + public bool HttpOnly { get; set; } + public Dictionary Properties { get; } = new Dictionary(); + } + //======================================================================= public class SacsHttpResponseData { @@ -59,10 +82,14 @@ namespace Sarand.Net.Standard.Base.Net public Dictionary> ResponseHeaders { get; } = new Dictionary>(); /// Response content headers. public Dictionary> ContentHeaders { get; } = new Dictionary>(); + /// Response cookies from 'Set-Cookie' header. + public Dictionary Cookies { get; } = new Dictionary(); /// Indicates whether Status Code represents success. public bool Success { get; set; } /// HTTP Status Code. public HttpStatusCode StatusCode { get; set; } + /// HTTP message version. Default: 1.1 + public Version Version { get; set; } /// HTTP Reason Code. public String ReasonPhrase { get; set; } = ""; /// Response data. @@ -80,26 +107,28 @@ namespace Sarand.Net.Standard.Base.Net { //------------------------------------------------------------------- // Data members. - private static readonly HttpClient client; + private static readonly HttpClient httpClient; //------------------------------------------------------------------- // Static initialisation. - static SacsHttpClient () + static SacsHttpClient() { // Configure client. - client = new HttpClient(); - client.Timeout = new TimeSpan(0, 0, RequestTimeoutSecs_Default); + httpClient = new HttpClient(); + httpClient.Timeout = new TimeSpan(0, 0, RequestTimeoutSecs_Default); + ProductInfoHeaderValue productInfoHeaderValue = new ProductInfoHeaderValue("Saratoga", "1.2.3"); + httpClient.DefaultRequestHeaders.UserAgent.Add(productInfoHeaderValue); } //------------------------------------------------------------------- // Properties. + public static HttpClient HttpClient { get { return httpClient; } } public static int RequestTimeoutSecs_Default { get; } = 5; - public static String HeaderName_ContentType { get; } = "Content-Type"; + public static String HeaderName_ContentType { get; } = HeaderNames.ContentType; //------------------------------------------------------------------- - public static async Task DoRequest (SacsHttpRequestData requestData) + public static async Task DoRequest(SacsHttpRequestData requestData) { - String fn = "SacsHttpClient.DoRequest"; SacsHttpResponseData responseData = new SacsHttpResponseData(); requestData.ThrowIfCancellationRequested(); @@ -109,13 +138,13 @@ namespace Sarand.Net.Standard.Base.Net { postContent = new FormUrlEncodedContent(requestData.FormData); if (!requestData.ContentHeaders.ContainsKey(HeaderName_ContentType)) - requestData.ContentHeaders.Add(HeaderName_ContentType,"application/x-www-form-urlencoded; charset=utf-8"); + requestData.ContentHeaders.Add(HeaderName_ContentType, "application/x-www-form-urlencoded; charset=utf-8"); } else if (!StringUtil.IsEmpty(requestData.PostData) || requestData.ForcePost) { postContent = new StringContent(StringUtil.EmptyOnNull(requestData.PostData), Encoding.UTF8); if (!requestData.ContentHeaders.ContainsKey(HeaderName_ContentType)) - requestData.ContentHeaders.Add(HeaderName_ContentType,"text/plain; charset=utf-8"); + requestData.ContentHeaders.Add(HeaderName_ContentType, "text/plain; charset=utf-8"); } requestData.ThrowIfCancellationRequested(); @@ -137,7 +166,7 @@ namespace Sarand.Net.Standard.Base.Net } // Default request headers. - if (client.DefaultRequestHeaders.Accept.Count == 0 && request.Headers.Accept.Count == 0) + if (httpClient.DefaultRequestHeaders.Accept.Count == 0 && request.Headers.Accept.Count == 0) request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/*")); // Configure request. @@ -157,7 +186,7 @@ namespace Sarand.Net.Standard.Base.Net } // Send request. responseData.RequestStarted = DateTime.Now; - response = await client.SendAsync(request, requestData.CancellationToken); + response = await httpClient.SendAsync(request, requestData.CancellationToken); responseData.RequestCompleted = DateTime.Now; } else @@ -165,25 +194,61 @@ namespace Sarand.Net.Standard.Base.Net // GET. request.Method = HttpMethod.Get; responseData.RequestStarted = DateTime.Now; - response = await client.SendAsync(request, requestData.CancellationToken); + response = await httpClient.SendAsync(request, requestData.CancellationToken); responseData.RequestCompleted = DateTime.Now; } - // Extract resopnse data. + // Extract response data. + responseData.Version = response.Version; responseData.Success = response.IsSuccessStatusCode; responseData.StatusCode = response.StatusCode; responseData.ReasonPhrase = response.ReasonPhrase; + // Extract response headers. if (response.Headers != null) { foreach (var header in response.Headers) responseData.ResponseHeaders[header.Key] = new List(header.Value); } + // Extract response content headers. if (response.Content != null && response.Content.Headers != null) { foreach (var header in response.Content.Headers) responseData.ContentHeaders[header.Key] = new List(header.Value); } - responseData.Data = await response.Content.ReadAsStringAsync(); + // Extract response cookies. + if (response.Headers.Contains(HeaderNames.SetCookie)) + { + foreach (var cookieHeader in response.Headers.GetValues(HeaderNames.SetCookie)) + { + SacsHttpCookieData cookie = ExtractCookieFromHeader(cookieHeader); + if (cookie != null && !StringUtil.IsBlank(cookie.Name)) + responseData.Cookies.Add(cookie.Name, cookie); + } + } + // Extract response payload. + var contentEncodings = responseData.ContentHeaders.GetValueOrDefault(HeaderNames.ContentEncoding, new List()); + using (var responseStream = await response.Content.ReadAsStreamAsync()) + { + // Check if response needs to be decompressed, as directed by 'content-encoding' header. + Stream decompressedStream = null; + if (contentEncodings.ContainsCI("gzip")) + decompressedStream = new GZipStream(responseStream, CompressionMode.Decompress); + else if (contentEncodings.ContainsCI("deflate")) + decompressedStream = new DeflateStream(responseStream, CompressionMode.Decompress); + else if (contentEncodings.ContainsCI("br")) + decompressedStream = new BrotliStream(responseStream, CompressionMode.Decompress); + else if (contentEncodings.ContainsCI("zstd")) + decompressedStream = new DecompressionStream(responseStream); + + // Read the response payload, decompressed if necessary. + using (var streamReader = new StreamReader(decompressedStream != null ? decompressedStream : responseStream)) + responseData.Data = await streamReader.ReadToEndAsync(); + + // Clean-up. + if (decompressedStream != null) + decompressedStream.Dispose(); + } + // Clean-up. if (response != null) response.Dispose(); } } @@ -198,7 +263,7 @@ namespace Sarand.Net.Standard.Base.Net exc = exc.InnerException; } String msg = exc.GetType().Name + ": " + exc.Message; - SaLogger.Warn(fn + " " + msg + ExceptionUtil.GetStackEntries(exc, 6).Concat("", "\n\t")); + SaLogger.Warn(SysUtil.Fn + " " + msg + ExceptionUtil.GetStackEntries(exc, 6).Concat("", "\n\t")); responseData.ErrorStateMessage = msg; } requestData.ThrowIfCancellationRequested(); @@ -208,10 +273,57 @@ namespace Sarand.Net.Standard.Base.Net return responseData; } + //------------------------------------------------------------------- + // Cookies. + //------------------------------------------------------------------- + public static SacsHttpCookieData ExtractCookieFromHeader(String cookieHeader) + { + SacsHttpCookieData cookie = null; + String[] cookieParts = cookieHeader.Split(';'); + if (cookieParts.Count() > 1) + { + cookie = new SacsHttpCookieData(); + cookie.HeaderValue = cookieHeader; + // Name and value. + String[] cookieNameValue = cookieParts[0].Trim().Split(new char[] { '=' }, 2); + cookie.Name = cookieNameValue[0].Trim(); + cookie.Value = cookieNameValue.Count() > 1 ? cookieNameValue[1].Trim() : ""; + // Other properties. + for (int i = 1; i < cookieParts.Length; i++) + { + string[] properties = cookieParts[i].Trim().Split(new char[] { '=' }, 2); + String propertyName = properties[0].Trim(); + String propertyValue = properties.Count() > 1 ? properties[1].Trim() : "True"; + cookie.Properties.Add(propertyName, propertyValue); + if (propertyName.EqualsCI("Domain")) + cookie.Domain = propertyValue; + else if (propertyName.EqualsCI("Path")) + cookie.Path = propertyValue; + else if (propertyName.EqualsCI("Expires")) + { + cookie.Expires = DateUtil.ParseExact(propertyValue, "ddd, d MMM yyyy HH:mm:ss GMT"); + if (!DateUtil.IsValid(cookie.Expires)) + { + DateTime expires; + DateTime.TryParse(propertyValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out expires); + cookie.Expires = expires; + } + } + else if (propertyName.EqualsCI("Max-Age")) + cookie.MaxAge = StringUtil.ToLong(propertyValue); + else if (propertyName.EqualsCI("Secure")) + cookie.Secure = StringUtil.ToBool(propertyValue); + else if (propertyName.EqualsCI("HttpOnly")) + cookie.HttpOnly = StringUtil.ToBool(propertyValue); + } + } + return cookie; + } + //------------------------------------------------------------------- // R e s p o n s e S t a t u s D e s c r i p t i o n //------------------------------------------------------------------- - public static String GetResponseStatusDescription (HttpStatusCode httpStatus) + public static String GetResponseStatusDescription(HttpStatusCode httpStatus) { // Ref. https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml switch (httpStatus) diff --git a/SacsStandardBase/Base/Net/WebUtil.cs b/SacsStandardBase/Base/Net/WebUtil.cs index 2e42283..8311b6e 100644 --- a/SacsStandardBase/Base/Net/WebUtil.cs +++ b/SacsStandardBase/Base/Net/WebUtil.cs @@ -1,4 +1,5 @@ -using Sarand.Net.Standard.Base.Core; +using Org.BouncyCastle.Asn1.Ocsp; +using Sarand.Net.Standard.Base.Core; using Sarand.Net.Standard.Base.Utils; using System; using System.Collections.Generic; @@ -13,13 +14,13 @@ namespace Sarand.Net.Standard.Base.Net // H T M L E n c o d i n g //------------------------------------------------------------------- /// Converts a string to an HTML-encoded string. - public static String HtmlEncode (this String value) + public static String HtmlEncode(this String value) { if (StringUtil.IsEmpty(value)) return value; return WebUtility.HtmlEncode(value); } /// Converts a string that has been HTML-encoded for HTTP transmission into a decoded string. - public static String HtmlDecode (this String value) + public static String HtmlDecode(this String value) { if (StringUtil.IsEmpty(value)) return value; return WebUtility.HtmlDecode(value); @@ -29,18 +30,64 @@ namespace Sarand.Net.Standard.Base.Net // U R L E n c o d i n g //------------------------------------------------------------------- /// Converts a string to an HTML-encoded string. - public static String UrlEncode (this String value) + public static String UrlEncode(this String value) { if (StringUtil.IsEmpty(value)) return value; return WebUtility.UrlEncode(value); } /// Converts a string that has been HTML-encoded for HTTP transmission into a decoded string. - public static String UrlDecode (this String value) + public static String UrlDecode(this String value) { if (StringUtil.IsEmpty(value)) return value; return WebUtility.UrlDecode(value); } + //------------------------------------------------------------------- + // URI Builder. + + /// + /// Configure HTTP URI builder. + /// + public static UriBuilder ConfigureUriBuilder(this UriBuilder uriBuilder, String host, String path, String query = null) + { + uriBuilder.Scheme = "http"; + uriBuilder.Host = StringUtil.OnBlank(host); + uriBuilder.Path = StringUtil.OnBlank(path); + uriBuilder.Query = StringUtil.OnBlank(query); + return uriBuilder; + } + + /// + /// Create HTTP URI builder. + /// + public static UriBuilder CreateUriBuilder(String host, String path, String query = null) + { + UriBuilder uriBuilder = new UriBuilder(); + return uriBuilder.ConfigureUriBuilder(host, path, query); + } + + /// + /// Configure HTTPS URI builder. + /// + public static UriBuilder ConfigureSslUriBuilder(this UriBuilder uriBuilder, String host, String path, String query = null) + { + uriBuilder.Scheme = "https"; + uriBuilder.Port = 443; + uriBuilder.Host = StringUtil.OnBlank(host); + uriBuilder.Path = StringUtil.OnBlank(path); + uriBuilder.Query = StringUtil.OnBlank(query); + return uriBuilder; + } + + /// + /// Create HTTPS URI builder. + /// + public static UriBuilder CreateSslUriBuilder(String host, String path, String query = null) + { + UriBuilder uriBuilder = new UriBuilder(); + return uriBuilder.ConfigureSslUriBuilder(host, path, query); + } + //------------------------------------------------------------------- } } diff --git a/SacsStandardBase/Base/Tasks/SacsTaskData.cs b/SacsStandardBase/Base/Tasks/SacsTaskData.cs index a7bdfe5..386ab51 100644 --- a/SacsStandardBase/Base/Tasks/SacsTaskData.cs +++ b/SacsStandardBase/Base/Tasks/SacsTaskData.cs @@ -12,7 +12,7 @@ namespace Sarand.Net.Standard.Base.Tasks //------------------------------------------------------------------- // Data members. // Cancellation token source - allows task cancellation to be triggered - // if the cancellation tokden wasn't supplied on construction. + // if the cancellation token wasn't supplied on construction. private CancellationTokenSource cancellationTokenSource = null; //------------------------------------------------------------------- diff --git a/SacsStandardBase/Base/Utils/ExceptionUtil.cs b/SacsStandardBase/Base/Utils/ExceptionUtil.cs index 2248291..d5c6dc6 100644 --- a/SacsStandardBase/Base/Utils/ExceptionUtil.cs +++ b/SacsStandardBase/Base/Utils/ExceptionUtil.cs @@ -59,9 +59,12 @@ namespace Sarand.Net.Standard.Base.Utils public static List GetStackEntries(this Exception exc, int entries = 0) { List stackEntries = new List(); - stackEntries.AddRange(exc.StackTrace.Split(new String[] { "\r\n" }, (entries > 0 ? entries : 999), StringSplitOptions.None)); - for (int idx = 0; idx < stackEntries.Count; idx++) - stackEntries[idx] = stackEntries[idx].Replace(" at ", ""); + if (exc.StackTrace != null) + { + stackEntries.AddRange(exc.StackTrace.Split(new String[] { "\r\n" }, (entries > 0 ? entries : 999), StringSplitOptions.None)); + for (int idx = 0; idx < stackEntries.Count; idx++) + stackEntries[idx] = stackEntries[idx].Replace(" at ", ""); + } return stackEntries; } diff --git a/SacsStandardBase/Base/Utils/ListUtil.cs b/SacsStandardBase/Base/Utils/ListUtil.cs index ebf3538..415644a 100644 --- a/SacsStandardBase/Base/Utils/ListUtil.cs +++ b/SacsStandardBase/Base/Utils/ListUtil.cs @@ -9,7 +9,7 @@ namespace Sarand.Net.Standard.Base.Utils public static class ListUtil { //------------------------------------------------------------------- - public static void ClearAndDispose (this IList values) where T : IDisposable + public static void ClearAndDispose(this IList values) where T : IDisposable { while (values.Count > 0) { @@ -50,7 +50,12 @@ namespace Sarand.Net.Standard.Base.Utils /// Unique values only - ignores number of instances of each value. UniqueValuesOnly = 0x02, } - public static bool Equals (this IList sourceList, IList targetList, Equality equality) + /// + /// Compare content of two lists. + /// - Exact ensures precise number and sequence of elements. + /// - Unique allows any number and sequence as long as each item is present in both lists. + /// + public static bool Equals(this IList sourceList, IList targetList, Equality equality) { bool sameLength = (sourceList.Count == targetList.Count); if (equality == Equality.Exact) @@ -69,6 +74,22 @@ namespace Sarand.Net.Standard.Base.Utils return true; } + //------------------------------------------------------------------- + /// + /// Remove the specified range of items from the list to a new list. + /// + public static List ExtractRange(this List values, int index, int count) + { + List extractedValues = new List(); + if (values.Any()) + { + count = Math.Min(values.Count(), count); + extractedValues.AddRange(values.GetRange(index, count)); + values.RemoveRange(index, count); + } + return extractedValues; + } + //------------------------------------------------------------------- } //======================================================================= diff --git a/SacsStandardBase/Base/Utils/NewtonsoftJsonUtil.cs b/SacsStandardBase/Base/Utils/NewtonsoftJsonUtil.cs new file mode 100644 index 0000000..11ba571 --- /dev/null +++ b/SacsStandardBase/Base/Utils/NewtonsoftJsonUtil.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace Sarand.Net.Standard.Base.Utils +{ + //======================================================================= + public static class NewtonsoftJsonUtil + { + //------------------------------------------------------------------- + // Extracting. + //------------------------------------------------------------------- + + //------------------------------------------------------------------- + // Parsing. + //------------------------------------------------------------------- + + /// Parse data as JSON Token. + public static JToken ParseJsonToken(string jsonData) + { + return JToken.Parse(jsonData); + } + + /// Parse data as JSON Token. + public static JToken ParseJsonToken(string jsonData, out Exception excOut) + { + excOut = null; + try + { + return JToken.Parse(jsonData); + } + catch (Exception exc) + { + excOut = exc; + } + return null; + } + + //------------------------------------------------------------------- + + /// Parse data as JSON Object. + public static JObject ParseJsonObject(string jsonData) + { + return JObject.Parse(jsonData); + } + + /// Parse data as JSON Object. + public static JObject ParseJsonObject(string jsonData, out Exception excOut) + { + excOut = null; + try + { + return JObject.Parse(jsonData); + } + catch (Exception exc) + { + excOut = exc; + } + return null; + } + + //------------------------------------------------------------------- + /// Parse data as JSON Object. + public static JArray ParseJsonArray(string jsonData) + { + return JArray.Parse(jsonData); + } + + /// Parse data as JSON Object. + public static JArray ParseJsonArray(string jsonData, out Exception excOut) + { + excOut = null; + try + { + return JArray.Parse(jsonData); + } + catch (Exception exc) + { + excOut = exc; + } + return null; + } + + //------------------------------------------------------------------- + } + //======================================================================= +} diff --git a/SacsStandardBase/Base/Utils/StringUtil.cs b/SacsStandardBase/Base/Utils/StringUtil.cs index ba15816..7bf92cc 100644 --- a/SacsStandardBase/Base/Utils/StringUtil.cs +++ b/SacsStandardBase/Base/Utils/StringUtil.cs @@ -66,7 +66,7 @@ namespace Sarand.Net.Standard.Base.Utils // Regular expressions. //------------------------------------------------------------------- /// - /// Create Regex from (quoted) expression and apply Regex.Match to String value and return Match.Success. + /// Apply Regex.Match to String value and return Match.Success. /// - Case sensitive. /// public static bool Match(this String value, String expression) @@ -74,7 +74,7 @@ namespace Sarand.Net.Standard.Base.Utils return value.Match(expression, RegexOptions.None); } /// - /// Create Regex from (quoted) expression and apply Regex.Match to String value and return Match.Success. + /// Apply Regex.Match to String value and return Match.Success, ignoring case. /// - Case insensitive. /// public static bool MatchCI(this String value, String expression) @@ -82,12 +82,11 @@ namespace Sarand.Net.Standard.Base.Utils return value.Match(expression, RegexOptions.IgnoreCase); } /// - /// Create Regex from (quoted) expression and apply Regex.Match to String value and return Match.Success. - /// - Supply options as required. + /// Apply Regex.Match to String value and return Match.Success. /// public static bool Match(this String value, String expression, RegexOptions options) { - return new Regex(Regex.Escape(expression), options).Match(value).Success; + return Regex.Match(value, expression, options).Success; } /// /// Apply Regex.Match to String value and return Match.Success. @@ -96,12 +95,67 @@ namespace Sarand.Net.Standard.Base.Utils { return re.Match(value).Success; } + /// - /// Apply Regex.Matches to String value and return true if any matches found. + /// Apply Regex.IsMatch to String value. + /// - Case sensitive. /// - public static bool Matches(this String value, Regex re) + public static bool IsMatch(this String value, String expression) { - return re.Matches(value).Count < 1; + return value.IsMatch(expression, RegexOptions.None); + } + /// + /// Apply Regex.IsMatch to String value, ignoring case. + /// - Case insensitive. + /// + public static bool IsMatchCI(this String value, String expression) + { + return value.IsMatch(new Regex(Regex.Escape(expression), RegexOptions.IgnoreCase)); + } + /// + /// Apply Regex.IsMatch to String value. + /// + public static bool IsMatch(this String value, String expression, RegexOptions options) + { + return Regex.IsMatch(value, expression, options); + } + /// + /// Apply Regex.IsMatch to String value. + /// + public static bool IsMatch(this String value, Regex re) + { + return re.IsMatch(value); + } + + /// + /// Apply Regex.Matches to String value and return number of matches found. + /// - Case sensitive. + /// + public static int MatchCount(this String value, String expression) + { + return value.MatchCount(expression, RegexOptions.None); + } + /// + /// Apply Regex.Matches to String value and return number of matches found. + /// - Case insensitive. + /// + public static int MatchCountCI(this String value, String expression) + { + return value.MatchCount(expression, RegexOptions.IgnoreCase); + } + /// + /// Apply Regex.Matches to String value and return number of matches found. + /// + public static int MatchCount(this String value, String expression, RegexOptions options) + { + return Regex.Matches(value, expression, options).Count; + } + /// + /// Apply Regex.Matches to String value and return number of matches found. + /// + public static int MatchCount(this String value, Regex re) + { + return re.Matches(value).Count; } //------------------------------------------------------------------- diff --git a/SacsStandardBase/Base/Utils/WebUtil.cs b/SacsStandardBase/Base/Utils/WebUtil.cs deleted file mode 100644 index 889c969..0000000 --- a/SacsStandardBase/Base/Utils/WebUtil.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Sarand.Net.Standard.Base.Core; -using System; -using System.Collections.Generic; -using System.Net; -using System.Text; - -namespace Sarand.Net.Standard.Base.Utils -{ - public static class WebUtil - { - //------------------------------------------------------------------- - // H T M L E n c o d i n g - //------------------------------------------------------------------- - /// Converts a string to an HTML-encoded string. - public static String HtmlEncode (this String value) - { - if (StringUtil.IsEmpty(value)) return value; - return WebUtility.HtmlEncode(value); - } - /// Converts a string that has been HTML-encoded for HTTP transmission into a decoded string. - public static String HtmlDecode (this String value) - { - if (StringUtil.IsEmpty(value)) return value; - return WebUtility.HtmlDecode(value); - } - - //------------------------------------------------------------------- - // U R L E n c o d i n g - //------------------------------------------------------------------- - /// Converts a string to an HTML-encoded string. - public static String UrlEncode (this String value) - { - if (StringUtil.IsEmpty(value)) return value; - return WebUtility.UrlEncode(value); - } - /// Converts a string that has been HTML-encoded for HTTP transmission into a decoded string. - public static String UrlDecode (this String value) - { - if (StringUtil.IsEmpty(value)) return value; - return WebUtility.UrlDecode(value); - } - - //------------------------------------------------------------------- - } -} diff --git a/SacsStandardBase/SacsStandardBase.csproj b/SacsStandardBase/SacsStandardBase.csproj index 1b41118..cd7772e 100644 --- a/SacsStandardBase/SacsStandardBase.csproj +++ b/SacsStandardBase/SacsStandardBase.csproj @@ -7,11 +7,12 @@ - + + diff --git a/SacsStandardDbas/Dbas/Base/DatabaseDefinitionGenerator.cs b/SacsStandardDbas/Dbas/Base/DatabaseDefinitionGenerator.cs index 86e559d..65562fe 100644 --- a/SacsStandardDbas/Dbas/Base/DatabaseDefinitionGenerator.cs +++ b/SacsStandardDbas/Dbas/Base/DatabaseDefinitionGenerator.cs @@ -20,7 +20,7 @@ namespace Sarand.Net.Standard.Dbas.Base { VS2017, /// - /// Visual Studio 2019 version. StringListUtil rather than StringListUtil. + /// Visual Studio 2019 version. StringListUtil rather than StringList. /// VS2019 } @@ -36,7 +36,7 @@ namespace Sarand.Net.Standard.Dbas.Base } public class Config { - public SarandCommonVersion SarandCommonVersion { get; set; } = SarandCommonVersion.VS2017; + public SarandCommonVersion SarandCommonVersion { get; set; } = SarandCommonVersion.VS2019; public DotNetVersion DotNetVersion { get; set; } = DotNetVersion.Framework; public SacsDatabase Database { get; set; } public Dbms DbmsOutput { get; set; } = Dbms.ODBC; @@ -46,10 +46,12 @@ namespace Sarand.Net.Standard.Dbas.Base public class SchemaData { // Tables to be included (all others excluded) or those to be excluded. - public List Tables_Included { get; } = new List(); - public List Tables_Excluded { get; } = new List(); + public HashSet Tables_Included { get; } = new HashSet(StringComparer.InvariantCultureIgnoreCase); + public HashSet Tables_Excluded { get; } = new HashSet(StringComparer.InvariantCultureIgnoreCase); + // Schema capitalisation - for class name prefix etc - where capitalising first letter is insufficient. + public Dictionary SchemaNameCapitalisation { get; } = new Dictionary(StringComparer.InvariantCultureIgnoreCase); // Alternative table names. - public Dictionary> AlternativeTableNames { get; } = new Dictionary>(); + public Dictionary> AlternativeTableNames { get; } = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); public void AddAlternativeTableName(String tableName, String tableNameAlternative) { if (!AlternativeTableNames.ContainsKey(tableName)) AlternativeTableNames[tableName] = new List(); @@ -66,7 +68,7 @@ namespace Sarand.Net.Standard.Dbas.Base public class DefinitionClassConfig { /// Using namespaces for database definition and table class usings. - public List Usings { get; } = new List() { "Sarand.Net.Standard.Base.Core", "Sarand.Net.Standard.Base.Dbas" }; + public HashSet Usings { get; } = new HashSet() { "Sarand.Net.Standard.Base.Core", "Sarand.Net.Standard.Base.Dbas" }; public String Namespace { get; set; } = "Sarand.Application.Core.Base"; public String Classname { get; set; } = "SaDbDfn"; } @@ -75,7 +77,7 @@ namespace Sarand.Net.Standard.Dbas.Base public class TableClassConfig { /// Using namespaces for table classes. - public List Usings { get; } = new List() { "Sarand.Net.Standard.Base.Core", "Sarand.Net.Standard.Base.Dbas" }; + public HashSet Usings { get; } = new HashSet() { "Sarand.Net.Standard.Base.Core", "Sarand.Net.Standard.Base.Dbas" }; public String Namespace_BaseClass { get; set; } = "Sarand.Net.Standard.Dbas.Data"; public String Namespace { get; set; } = "Sarand.Application.Dbas"; public String BaseClass { get; set; } = "DbDataItem"; @@ -89,6 +91,7 @@ namespace Sarand.Net.Standard.Dbas.Base //------------------------------------------------------------------- // Data members. private Config config = null; + private const String DefaultSchema_MSSQL = "dbo"; private static String /*lf = "\n",*/ crlf = "\r\n", tab1 = "\t", tab2 = "\t\t", tab3 = "\t\t\t", tab4 = "\t\t\t\t", tab5 = "\t\t\t\t\t"; // Reserved words. private static readonly String[] reservedWords = @@ -313,10 +316,19 @@ namespace Sarand.Net.Standard.Dbas.Base file.WriteLine(tab2 + "{"); file.WriteLine(tab3 + "int tableIdx = (int)table - 1;"); file.WriteLine(tab3 + "if (tableIdx < 0 || tableIdx >= tableCount) return String.Empty;"); - file.WriteLine(tab3 + "String tableName = tables[(int)tableIdx];"); + file.WriteLine(tab3 + "String tableName = tables[(int)tableIdx].Name;"); file.WriteLine(tab3 + "if (delimitReservedWords) tableName = DelimitReservedWord(tableName);"); file.WriteLine(tab3 + "return tableName;"); file.WriteLine(tab2 + "}"); + file.WriteLine(tab2 + "public static String GetTableNameQual (TableIdx table, bool delimitReservedWords = true)"); + file.WriteLine(tab2 + "{"); + file.WriteLine(tab3 + "int tableIdx = (int)table - 1;"); + file.WriteLine(tab3 + "if (tableIdx < 0 || tableIdx >= tableCount) return String.Empty;"); + file.WriteLine(tab3 + "String schemaName = tables[(int)tableIdx].Schema;"); + file.WriteLine(tab3 + "String tableName = tables[(int)tableIdx].Name;"); + file.WriteLine(tab3 + "if (delimitReservedWords) { schemaName = DelimitReservedWord(schemaName); tableName = DelimitReservedWord(tableName); }"); + file.WriteLine(tab3 + "return (StringUtil.IsBlank(schemaName) ? \"\" : schemaName + \".\") + tableName;"); + file.WriteLine(tab2 + "}"); file.WriteLine(crlf + tab2 + "//-------------------------------------------------------------------"); // Table for Column method. @@ -369,8 +381,15 @@ namespace Sarand.Net.Standard.Dbas.Base file.WriteLine(tab2 + "}"); file.WriteLine(tab2 + "public static String DelimitReservedWord (String word)"); file.WriteLine(tab2 + "{"); - file.WriteLine(tab3 + "if (!IsReservedWord(word)) return word;"); - file.WriteLine(tab3 + "switch (reservedWordDelim)"); + file.WriteLine(tab3 + "return IsReservedWord(word) ? DelimitWord(word) : word;"); + file.WriteLine(tab2 + "}"); + file.WriteLine(tab2 + "public static String DelimitWord (String word)"); + file.WriteLine(tab2 + "{"); + file.WriteLine(tab3 + "return DelimitWord(word, reservedWordDelim);"); + file.WriteLine(tab2 + "}"); + file.WriteLine(tab2 + "public static String DelimitWord (String word, DbReservedWordDelim wordDelim)"); + file.WriteLine(tab2 + "{"); + file.WriteLine(tab3 + "switch (wordDelim)"); file.WriteLine(tab3 + "{"); file.WriteLine(tab4 + "case DbReservedWordDelim.None:"); file.WriteLine(tab5 + "break;"); @@ -447,12 +466,22 @@ namespace Sarand.Net.Standard.Dbas.Base file.WriteLine(tab2 + "}"); file.WriteLine(crlf + tab2 + "//-------------------------------------------------------------------"); - // Table definitions. + // Table names. file.WriteLine(tab2 + "// Table definitions."); - file.WriteLine(tab2 + "private static readonly String[] tables ="); + file.WriteLine(tab2 + "private class TableDfn"); + file.WriteLine(tab2 + "{"); + file.WriteLine(tab3 + "internal String Schema { get; set; }"); + file.WriteLine(tab3 + "internal String Name { get; set; }"); + file.WriteLine(tab3 + "internal TableDfn (String schema, String name)"); + file.WriteLine(tab3 + "{"); + file.WriteLine(tab4 + "this.Schema= schema;"); + file.WriteLine(tab4 + "this.Name = name;"); + file.WriteLine(tab3 + "}"); + file.WriteLine(tab2 + "}"); + file.WriteLine(tab2 + "private static readonly TableDfn[] tables ="); file.WriteLine(tab2 + "{"); foreach (DbDfnTable tableDfn in tableDfns) - file.WriteLine(tab3 + "\"" + tableDfn.Name + "\", "); + file.WriteLine(tab3 + "new TableDfn(\"" + tableDfn.Schema + "\",\"" + tableDfn.Name + "\"),"); file.WriteLine(tab2 + "};"); file.WriteLine(crlf + tab2 + "//-------------------------------------------------------------------"); @@ -476,7 +505,6 @@ namespace Sarand.Net.Standard.Dbas.Base file.WriteLine(tab4 + "this.DataLen = dataLen;"); file.WriteLine(tab3 + "}"); file.WriteLine(tab2 + "}"); - file.WriteLine(crlf + tab2 + "//-------------------------------------------------------------------"); file.WriteLine(tab2 + "private static readonly ColumnDfn[] columns ="); file.WriteLine(tab2 + "{"); uint tableIdx = 0; @@ -505,6 +533,10 @@ namespace Sarand.Net.Standard.Dbas.Base private void GenerateTableInterfaceClass(DbDfnTable tableDfn) { String className = tableDfn.Name + "_Db"; + if (!StringUtil.IsEmpty(tableDfn.Schema) && !tableDfn.Schema.EqualsCI("dbo")) + { + className = config.Schema.SchemaNameCapitalisation.GetValueOrDefault(tableDfn.Schema, StringUtil.CapitaliseFirst(tableDfn.Schema)) + "_" + className; + } List primaryColumnNames = tableDfn.GetPrimaryColumnNames(); String identityColumnName = tableDfn.Columns.GetIdentityColumnName(); String identityColumnVariableTypeName = tableDfn.Columns.GetIdentityColumnVariableTypeName(); diff --git a/SacsStandardDbas/Dbas/Base/SacsDatabaseConsole.cs b/SacsStandardDbas/Dbas/Base/SacsDatabaseConsole.cs index 77c0402..f4e43ed 100644 --- a/SacsStandardDbas/Dbas/Base/SacsDatabaseConsole.cs +++ b/SacsStandardDbas/Dbas/Base/SacsDatabaseConsole.cs @@ -141,7 +141,7 @@ namespace Sarand.Net.Standard.Dbas.Base if (StringUtil.IsEmpty(config.DatabaseName)) config.DatabaseName = sqlDatabase; - SaLogger.Info("DatabaseConsole.GenerateConnectionString Driver: " + sqlDriver + ". Server: " + sqlServer + ". Database: " + sqlDatabase + ". Connection: " + connectionString); + SaLogger.Info($"{SysUtil.Fn} Driver: " + sqlDriver + ". Server: " + sqlServer + ". Database: " + sqlDatabase + ". Connection: " + connectionString); } return connectionString; } @@ -219,7 +219,7 @@ namespace Sarand.Net.Standard.Dbas.Base else { #if DEBUG - SaLogger.Debug("DropDbIndex Table: " + tableName + "; Index: " + indexName + "; Exception: " + database.ErrorStateMessage); + SaLogger.Debug($"{SysUtil.Fn} Table: " + tableName + "; Index: " + indexName + "; Exception: " + database.ErrorStateMessage); #endif SetErrorState("Failed to drop index " + indexName + " on " + tableName + " - " + database.ErrorStateMessage); } @@ -240,7 +240,7 @@ namespace Sarand.Net.Standard.Dbas.Base else { #if DEBUG - SaLogger.Debug("DropDbIndex Table: " + tableName + "; Index: " + indexName + "; Exception: " + database.ErrorStateMessage); + SaLogger.Debug($"{SysUtil.Fn} Table: " + tableName + "; Index: " + indexName + "; Exception: " + database.ErrorStateMessage); #endif SetErrorState("Failed to drop primary key " + indexName + " on " + tableName + " - " + database.ErrorStateMessage); } @@ -288,7 +288,7 @@ namespace Sarand.Net.Standard.Dbas.Base else { #if DEBUG - SaLogger.Debug("CreatePrimaryDbIndex Table: %" + tableName + "; Index: " + indexName + "; Exception: " + database.ErrorStateMessage); + SaLogger.Debug($"{SysUtil.Fn} Table: %" + tableName + "; Index: " + indexName + "; Exception: " + database.ErrorStateMessage); #endif SetErrorState("Failed to create primary index " + indexName + " on " + tableName + " - " + database.ErrorStateMessage); } @@ -322,7 +322,7 @@ namespace Sarand.Net.Standard.Dbas.Base else { #if DEBUG - SaLogger.Debug("CreateDbIndex Table: " + tableName + "; Index: " + indexName + "; Exception: " + database.ErrorStateMessage); + SaLogger.Debug($"{SysUtil.Fn} Table: " + tableName + "; Index: " + indexName + "; Exception: " + database.ErrorStateMessage); #endif SetErrorState("Failed to create index " + indexName + " on " + tableName + " - " + database.ErrorStateMessage); } diff --git a/SacsStandardDbas/Dbas/CodeTypes/SaCodeTypeDbDfnGenerator.cs b/SacsStandardDbas/Dbas/CodeTypes/SaCodeTypeDbDfnGenerator.cs index e8caf78..0b381e7 100644 --- a/SacsStandardDbas/Dbas/CodeTypes/SaCodeTypeDbDfnGenerator.cs +++ b/SacsStandardDbas/Dbas/CodeTypes/SaCodeTypeDbDfnGenerator.cs @@ -57,8 +57,8 @@ namespace Sarand.Net.Standard.Dbas.CodeType OutputDirectory = directory, }; // Schema - Tables. - config.Schema.Tables_Included.AddRange(new String[] { "SaCodeType", "SaCode" }); - config.Schema.Tables_Excluded.AddRange(new String[] { }); + config.Schema.Tables_Included.UnionWith(new String[] { "SaCodeType", "SaCode" }); + config.Schema.Tables_Excluded.UnionWith(new String[] { }); // Schema - Columns. config.Schema.InsertOnlyColumns.Add(".*", "CreateOn,CreateBy,CreateHost"); config.Schema.InsertOnlyColumns.Add("SaCodeType", "Code"); diff --git a/SacsStandardDbas/Dbas/Data/SacsDatabaseOdbc.cs b/SacsStandardDbas/Dbas/Data/SacsDatabaseOdbc.cs index f62fcc7..81471d4 100644 --- a/SacsStandardDbas/Dbas/Data/SacsDatabaseOdbc.cs +++ b/SacsStandardDbas/Dbas/Data/SacsDatabaseOdbc.cs @@ -384,7 +384,7 @@ namespace Sarand.Net.Standard.Dbas.Data // Only populate table names and/or table schemas where not previously done. if (tables.Count < 1 || tables.Exists(item => !item.HasColumns())) { - // Use a single connection for table and schaema population. + // Use a single connection for table and schema population. using (DbConnection dbConnection = CreateConnection(true)) { dbConnection.Open(); diff --git a/SacsStandardDbas/Dbas/Interfaces/SaInterfaceDbDfnGenerator.cs b/SacsStandardDbas/Dbas/Interfaces/SaInterfaceDbDfnGenerator.cs index baeabb0..25e757e 100644 --- a/SacsStandardDbas/Dbas/Interfaces/SaInterfaceDbDfnGenerator.cs +++ b/SacsStandardDbas/Dbas/Interfaces/SaInterfaceDbDfnGenerator.cs @@ -58,8 +58,8 @@ namespace Sarand.Net.Standard.Dbas.Interface OutputDirectory = directory, }; // Schema - Tables. - config.Schema.Tables_Included.AddRange(new String[] { "SaInterface", "SaInterfaceProcess", "SaInterfaceProcessMessage", "SaInterfaceDataItem", "SaInterfaceProcessDataItem" }); - config.Schema.Tables_Excluded.AddRange(new String[] { }); + config.Schema.Tables_Included.UnionWith(new String[] { "SaInterface", "SaInterfaceProcess", "SaInterfaceProcessMessage", "SaInterfaceDataItem", "SaInterfaceProcessDataItem" }); + config.Schema.Tables_Excluded.UnionWith(new String[] { }); config.Schema.AddAlternativeTableName("SaInterface", "Interface"); config.Schema.AddAlternativeTableName("SaInterfaceProcess", "InterfaceProcess"); config.Schema.AddAlternativeTableName("SaInterfaceProcessMessage", "InterfaceProcessMessage"); diff --git a/SacsStandardDbas/Dbas/ItemData/SaItemDataDbDfnGenerator.cs b/SacsStandardDbas/Dbas/ItemData/SaItemDataDbDfnGenerator.cs index 8b1d962..46a83d1 100644 --- a/SacsStandardDbas/Dbas/ItemData/SaItemDataDbDfnGenerator.cs +++ b/SacsStandardDbas/Dbas/ItemData/SaItemDataDbDfnGenerator.cs @@ -58,8 +58,8 @@ namespace Sarand.Net.Standard.Dbas.ItemData OutputDirectory = directory, }; // Schema - Tables. - config.Schema.Tables_Included.AddRange(new String[] { "SaItemData" }); - config.Schema.Tables_Excluded.AddRange(new String[] { }); + config.Schema.Tables_Included.UnionWith(new String[] { "SaItemData" }); + config.Schema.Tables_Excluded.UnionWith(new String[] { }); // Schema - Columns. config.Schema.InsertOnlyColumns.Add(".*", "CreateOn,CreateBy,CreateHost"); config.Schema.InsertOnlyColumns.Add("SaItemData", "ItemType,ItemId,Name,Context"); diff --git a/SacsStandardDbas/Dbas/ItemDocs/SaItemDocsDbDfnGenerator.cs b/SacsStandardDbas/Dbas/ItemDocs/SaItemDocsDbDfnGenerator.cs index 42afc61..99edbea 100644 --- a/SacsStandardDbas/Dbas/ItemDocs/SaItemDocsDbDfnGenerator.cs +++ b/SacsStandardDbas/Dbas/ItemDocs/SaItemDocsDbDfnGenerator.cs @@ -62,8 +62,8 @@ namespace Sarand.Net.Standard.Dbas.ItemDocs OutputDirectory = directory, }; // Schema - Tables. - config.Schema.Tables_Included.AddRange(new String[] { "SaItemDoc" }); - config.Schema.Tables_Excluded.AddRange(new String[] { }); + config.Schema.Tables_Included.UnionWith(new String[] { "SaItemDoc" }); + config.Schema.Tables_Excluded.UnionWith(new String[] { }); // Schema - Columns. config.Schema.InsertOnlyColumns.Add(".*", "CreateOn,CreateBy,CreateHost"); config.Schema.InsertOnlyColumns.Add("SaItemDocs", "ItemType,ItemId,Name,Context"); diff --git a/SacsStandardDbas/Dbas/ItemOptions/SaItemOptionsDbDfnGenerator.cs b/SacsStandardDbas/Dbas/ItemOptions/SaItemOptionsDbDfnGenerator.cs index a74607b..9593523 100644 --- a/SacsStandardDbas/Dbas/ItemOptions/SaItemOptionsDbDfnGenerator.cs +++ b/SacsStandardDbas/Dbas/ItemOptions/SaItemOptionsDbDfnGenerator.cs @@ -58,8 +58,8 @@ namespace Sarand.Net.Standard.Dbas.ItemOptions OutputDirectory = directory, }; // Schema - Tables. - config.Schema.Tables_Included.AddRange(new String[] { "SaItemOption" }); - config.Schema.Tables_Excluded.AddRange(new String[] { }); + config.Schema.Tables_Included.UnionWith(new String[] { "SaItemOption" }); + config.Schema.Tables_Excluded.UnionWith(new String[] { }); // Schema - Columns. config.Schema.InsertOnlyColumns.Add(".*", "CreateOn,CreateBy,CreateHost"); config.Schema.InsertOnlyColumns.Add("SaItemOption", "ItemType,ItemId,Name,Context"); diff --git a/SacsStandardDbas/Dbas/ReferenceData/SaReferenceDataDbDfnGenerator.cs b/SacsStandardDbas/Dbas/ReferenceData/SaReferenceDataDbDfnGenerator.cs index b44d912..2f1aa3e 100644 --- a/SacsStandardDbas/Dbas/ReferenceData/SaReferenceDataDbDfnGenerator.cs +++ b/SacsStandardDbas/Dbas/ReferenceData/SaReferenceDataDbDfnGenerator.cs @@ -58,8 +58,8 @@ namespace Sarand.Net.Standard.Dbas.ReferenceData OutputDirectory = directory, }; // Schema - Tables. - config.Schema.Tables_Included.AddRange(new String[] { "Language", "Region", "Currency", "TaxCode", "UnitMeasure" }); - config.Schema.Tables_Excluded.AddRange(new String[] { }); + config.Schema.Tables_Included.UnionWith(new String[] { "Language", "Region", "Currency", "TaxCode", "UnitMeasure" }); + config.Schema.Tables_Excluded.UnionWith(new String[] { }); // Schema - Columns. config.Schema.InsertOnlyColumns.Add(".*", "CreateOn,CreateBy,CreateHost"); config.Schema.InsertOnlyColumns.Add("Language", ""); diff --git a/SacsStandardDbas/Dbas/Security/SaSecurityDbDfnGenerator.cs b/SacsStandardDbas/Dbas/Security/SaSecurityDbDfnGenerator.cs index e4a3cf8..54b91c8 100644 --- a/SacsStandardDbas/Dbas/Security/SaSecurityDbDfnGenerator.cs +++ b/SacsStandardDbas/Dbas/Security/SaSecurityDbDfnGenerator.cs @@ -57,8 +57,8 @@ namespace Sarand.Net.Standard.Dbas.Security OutputDirectory = directory, }; // Schema - Tables. - config.Schema.Tables_Included.AddRange(new String[] { "SaSecItem", "SaSecLink" }); - config.Schema.Tables_Excluded.AddRange(new String[] { }); + config.Schema.Tables_Included.UnionWith(new String[] { "SaSecItem", "SaSecLink" }); + config.Schema.Tables_Excluded.UnionWith(new String[] { }); config.Schema.AddAlternativeTableName("SaSecItem", "SaSecurityItem"); config.Schema.AddAlternativeTableName("SaSecLink", "SaSecurityLink"); // Schema - Columns. diff --git a/SacsStandardDbas/Dbas/UpdateLog/SaUpdateLogDbDfnGenerator.cs b/SacsStandardDbas/Dbas/UpdateLog/SaUpdateLogDbDfnGenerator.cs index 1a2cf18..220c102 100644 --- a/SacsStandardDbas/Dbas/UpdateLog/SaUpdateLogDbDfnGenerator.cs +++ b/SacsStandardDbas/Dbas/UpdateLog/SaUpdateLogDbDfnGenerator.cs @@ -58,8 +58,8 @@ namespace Sarand.Net.Standard.Dbas.UpdateLog OutputDirectory = directory, }; // Schema - Tables. - config.Schema.Tables_Included.AddRange(new String[] { "SaAttrUpdate" }); - config.Schema.Tables_Excluded.AddRange(new String[] { }); + config.Schema.Tables_Included.UnionWith(new String[] { "SaAttrUpdate" }); + config.Schema.Tables_Excluded.UnionWith(new String[] { }); // SChema - Columns. config.Schema.InsertOnlyColumns.Add(".*", "CreateOn,CreateBy,CreateHost"); config.Schema.InsertOnlyColumns.Add("SaAttrUpdate", "ItemType,ItemId,Logged,Attribute"); diff --git a/SacsStandardProc/Proc/Excel/SaMsSpreadsheet.cs b/SacsStandardProc/Proc/Excel/SaMsSpreadsheet.cs index 7738189..18a4c4a 100644 --- a/SacsStandardProc/Proc/Excel/SaMsSpreadsheet.cs +++ b/SacsStandardProc/Proc/Excel/SaMsSpreadsheet.cs @@ -587,14 +587,14 @@ namespace Sarand.Net.Standard.Proc.Excel public static int GetPosForColId(String colId) { colId = StringUtil.TrimWhitespace(StringUtil.OnBlank(colId)).ToUpper(); - if (!colId.Matches(new Regex("[A-Z]+"))) + if (!colId.IsMatch("^[A-Z]{1,2}$")) { SaLogger.Warn(SysUtil.Fn + " Invalid column Id supplied: '" + colId + "'"); - return -1; + return 0; } - int col = (colId[0] - 'A'); + int col = (colId[0] - 'A' + 1); if (colId.Length > 1) - col = ((colId[1] - 'A') * 26) + (colId[0] - 'A'); + col = (col * 26) + (colId[1] - 'A' + 1); return col; } diff --git a/SacsStandardProc/Proc/Excel/SaMsSpreadsheet_POI.cs b/SacsStandardProc/Proc/Excel/SaMsSpreadsheet_POI.cs index 0fa0a68..1b3dc90 100644 --- a/SacsStandardProc/Proc/Excel/SaMsSpreadsheet_POI.cs +++ b/SacsStandardProc/Proc/Excel/SaMsSpreadsheet_POI.cs @@ -845,7 +845,7 @@ namespace Sarand.Net.Standard.Proc.Excel try { if (IsCellDate(cell)) - return new DataValue(cell.DateCellValue); + return new DataValue((DateTime)(cell.DateCellValue ?? DateTime.MinValue)); if (IsCellNumeric(cell)) { double value = cell.NumericCellValue; @@ -971,7 +971,7 @@ namespace Sarand.Net.Standard.Proc.Excel try { IRow row = GetRowByPos(rowPos, true); - for (int valueIdx = 0; valueIdx < values.Count; valueIdx++) + for (int valueIdx = 0; valueIdx < values.Count; valueIdx++) { uint colPos = (uint)Math.Max(1, colPosStart + valueIdx); Object value = values[valueIdx]; diff --git a/SacsStandardProc/Proc/Procs/SaProcess.cs b/SacsStandardProc/Proc/Procs/SaProcess.cs index 1589d89..e1e0469 100644 --- a/SacsStandardProc/Proc/Procs/SaProcess.cs +++ b/SacsStandardProc/Proc/Procs/SaProcess.cs @@ -1,4 +1,6 @@ -using Sarand.Net.Standard.Base.Procs; +using Sarand.Net.Standard.Base.App; +using Sarand.Net.Standard.Base.Dbas; +using Sarand.Net.Standard.Base.Procs; using System; using System.Collections.Generic; using System.Text; @@ -13,8 +15,11 @@ namespace Sarand.Net.Standard.Proc.Procs // Definitions. public new class Config : SaProcess_Base.Config { - protected Config() : base() - { } + protected Config(DbUpdateInfo dbUpdateInfo) : base() + { + this.DbUpdateInfo = dbUpdateInfo; + } + public DbUpdateInfo DbUpdateInfo { get; } } //------------------------------------------------------------------- diff --git a/SacsStandardProc/SacsStandardProc.csproj b/SacsStandardProc/SacsStandardProc.csproj index 98bb737..0c39eae 100644 --- a/SacsStandardProc/SacsStandardProc.csproj +++ b/SacsStandardProc/SacsStandardProc.csproj @@ -10,8 +10,8 @@ - - + +