diff --git a/.github/workflows/test.yml b/.github/workflows/unittest-verification.yml similarity index 82% rename from .github/workflows/test.yml rename to .github/workflows/unittest-verification.yml index ff6d68d..a820d77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/unittest-verification.yml @@ -1,4 +1,4 @@ -name: UnitTests +name: UnitTest-Verification on: workflow_dispatch: @@ -10,6 +10,9 @@ on: jobs: build-and-test: runs-on: ubuntu-latest + defaults: + run: + working-directory: src steps: - name: Checkout code uses: actions/checkout@v3 @@ -17,7 +20,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 9.x + dotnet-version: 10.x - name: Restore dependencies run: dotnet restore diff --git a/icon.png b/icon.png index 7ce9270..01bf63f 100644 Binary files a/icon.png and b/icon.png differ diff --git a/src/WebExpress.WebCore.Test/Fixture/AssertExtensions.cs b/src/WebExpress.WebCore.Test/Fixture/AssertExtensions.cs index f241278..e6fb0b4 100644 --- a/src/WebExpress.WebCore.Test/Fixture/AssertExtensions.cs +++ b/src/WebExpress.WebCore.Test/Fixture/AssertExtensions.cs @@ -23,6 +23,14 @@ public static partial class AssertExtensions /// The actual string to compare. public static void EqualWithPlaceholders(string expected, string actual) { + if (expected is null && actual is null) + { + return; + } + + Assert.NotNull(expected); + Assert.NotNull(actual); + var str = RemoveLineBreaks(actual?.ToString()); Assert.True(AreEqualWithPlaceholders(expected, str), $"Expected: {expected}{Environment.NewLine}Actual: {str}"); } @@ -47,15 +55,15 @@ public static void EqualWithPlaceholders(string expected, IHtmlNode actual) /// True if the actual string matches the expected string with placeholders; otherwise, false. private static bool AreEqualWithPlaceholders(string expected, string actual) { - if (expected == null && actual == null) + if (expected is null && actual is null) { return true; } - else if (expected != null && actual == null) + else if (expected is not null && actual is null) { return false; } - else if (expected == null && actual != null) + else if (expected is null && actual is not null) { return false; } diff --git a/src/WebExpress.WebCore.Test/Fixture/UnitTestFixture.cs b/src/WebExpress.WebCore.Test/Fixture/UnitTestFixture.cs index bf3684b..aa472fd 100644 --- a/src/WebExpress.WebCore.Test/Fixture/UnitTestFixture.cs +++ b/src/WebExpress.WebCore.Test/Fixture/UnitTestFixture.cs @@ -98,7 +98,7 @@ public static ComponentHub CreateAndRegisterComponentHubMock() /// The content of the request. /// The URI of the request. /// A fake request for testing. - public static Request CrerateRequestMock(string content = "", string uri = "") + public static IRequest CreateRequestMock(string content = "", string uri = "") { var context = CreateHttpContextMock(content); @@ -112,6 +112,25 @@ public static Request CrerateRequestMock(string content = "", string uri = "") return request; } + /// + /// Create a fake request. + /// + /// The URI of the request. + /// A fake request for testing. + public static IRequest CreateRequestMock(IUri uri) + { + var context = CreateHttpContextMock(); + + var request = context.Request; + + if (uri is not null) + { + request.Uri = uri as UriEndpoint; + } + + return request; + } + /// /// Create a fake http context. /// @@ -191,7 +210,7 @@ public static WebMessage.HttpContext CreateHttpContextMock(string content = "") /// A mock render context for testing. public static RenderContext CrerateRenderContextMock(IApplicationContext applicationContext = null, IEnumerable scopes = null) { - var request = CrerateRequestMock(); + var request = CreateRequestMock(); return new RenderContext(null, CreratePageContextMock(applicationContext, scopes), request); } diff --git a/src/WebExpress.WebCore.Test/Html/UnitTestDeterministicId.cs b/src/WebExpress.WebCore.Test/Html/UnitTestDeterministicId.cs new file mode 100644 index 0000000..d2aec90 --- /dev/null +++ b/src/WebExpress.WebCore.Test/Html/UnitTestDeterministicId.cs @@ -0,0 +1,83 @@ +using WebExpress.WebCore.WebHtml; + +namespace WebExpress.WebCore.Test.Html +{ + /// + /// Unit tests for the DeterministicId class. + /// + [Collection("NonParallelTests")] + public class UnitTestDeterministicId + { + /// + /// Tests the create method. + /// + [Fact] + public void CreateSameCallsite() + { + // act + var id1 = CallCreate(); + var id2 = CallCreate(); + + // validation + Assert.NotEqual(id1, id2); + } + + /// + /// Tests the create method. + /// + [Fact] + public void CreateIndexes() + { + // act + var id1 = CallCreate(0); + var id2 = CallCreate(1); + + // validation + Assert.NotEqual(id1, id2); + } + + /// + /// Tests the create method. + /// + [Fact] + public void CreateDifferentCallsites() + { + // arrange + var CallsiteA = new Func(() => CallCreate()); + var CallsiteB = new Func(() => CallCreate()); + + // act + var id1 = CallsiteA(); + var id2 = CallsiteB(); + + // validation + Assert.NotEqual(id1, id2); + } + + + /// + /// Generates a deterministic identifier. + /// + /// A string that represents the generated deterministic identifier. + /// + private string CallCreate() + { + return DeterministicId.Create(); + } + + /// + /// Generates a deterministic identifier based on the specified index. + /// + /// + /// The optional content used to influence the generated identifier. If + /// null, a default identifier is created. + /// + /// + /// A string that represents the generated deterministic identifier. + /// + private string CallCreate(object contet = null) + { + return DeterministicId.Create(contet); + } + } +} diff --git a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElement.cs b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElement.cs index e4df43e..bfcc0ef 100644 --- a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElement.cs +++ b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElement.cs @@ -14,7 +14,7 @@ public class UnitTestHtmlElement [Fact] public void FindSingel() { - // preconditions + // arrange var html = new HtmlElementTextContentDiv ( new HtmlElementTextSemanticsI(), @@ -22,7 +22,7 @@ public void FindSingel() new HtmlElementTextSemanticsB() ); - // test execution + // act var res = html.Find(x => x is HtmlElementTextSemanticsSpan).FirstOrDefault(); // validation @@ -35,7 +35,7 @@ public void FindSingel() [Fact] public void Find() { - // preconditions + // arrange var html = new HtmlElement[] { new HtmlElementTextContentDiv @@ -47,7 +47,7 @@ public void Find() new HtmlElementMultimediaImg() }; - // test execution + // act var res = html.Find(x => x is HtmlElementTextSemanticsSpan).FirstOrDefault(); // validation @@ -60,10 +60,10 @@ public void Find() [Fact] public void AddClassTest() { - // preconditions + // arrange var div = new HtmlElementTextContentDiv(); - // test execution + // act div.AddClass("test-class"); // validation @@ -76,11 +76,11 @@ public void AddClassTest() [Fact] public void RemoveClassTest() { - // preconditions + // arrange var div = new HtmlElementTextContentDiv(); div.AddClass("test-class"); - // test execution + // act div.RemoveClass("test-class"); // validation @@ -93,10 +93,10 @@ public void RemoveClassTest() [Fact] public void AddStyleTest() { - // preconditions + // arrange var div = new HtmlElementTextContentDiv(); - // test execution + // act div.AddStyle("color:red;"); // validation @@ -109,11 +109,11 @@ public void AddStyleTest() [Fact] public void RemoveStyleTest() { - // preconditions + // arrange var div = new HtmlElementTextContentDiv(); div.AddStyle("color", "red"); - // test execution + // act div.RemoveStyle("color"); // validation @@ -126,10 +126,10 @@ public void RemoveStyleTest() [Fact] public void AddMultipleClassesTest() { - // preconditions + // arrange var div = new HtmlElementTextContentDiv(); - // test execution + // act div.AddClass("class1"); div.AddClass("class2"); @@ -143,12 +143,12 @@ public void AddMultipleClassesTest() [Fact] public void RemoveOneOfMultipleClassesTest() { - // preconditions + // arrange var div = new HtmlElementTextContentDiv(); div.AddClass("class1"); div.AddClass("class2"); - // test execution + // act div.RemoveClass("class1"); // validation @@ -162,10 +162,10 @@ public void RemoveOneOfMultipleClassesTest() [Fact] public void AddMultipleStylesTest() { - // preconditions + // arrange var div = new HtmlElementTextContentDiv(); - // test execution + // act div.AddStyle("color:red;"); div.AddStyle("background:blue;"); @@ -180,12 +180,12 @@ public void AddMultipleStylesTest() [Fact] public void RemoveOneOfMultipleStylesTest() { - // preconditions + // arrange var div = new HtmlElementTextContentDiv(); div.AddStyle("color:red;"); div.AddStyle("background:blue;"); - // test execution + // act div.RemoveStyle("color:red;"); // validation @@ -199,7 +199,7 @@ public void RemoveOneOfMultipleStylesTest() [Fact] public void ToStringEmptyDivTest() { - // preconditions + // arrange var div = new HtmlElementTextContentDiv(); // validation @@ -212,7 +212,7 @@ public void ToStringEmptyDivTest() [Fact] public void ToStringWithChildrenTest() { - // preconditions + // arrange var div = new HtmlElementTextContentDiv( new HtmlElementTextSemanticsB(), new HtmlElementTextSemanticsI() diff --git a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementExtension.cs b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementExtension.cs index 7bfe0a0..3375d00 100644 --- a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementExtension.cs +++ b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementExtension.cs @@ -22,10 +22,10 @@ public class UnitTestHtmlElementExtension [InlineData(null, "new", "new")] public void AddClass(string initial, string toAdd, string expected) { - // preconditions + // arrange var element = new HtmlElementTextContentDiv { Class = initial } as IHtmlNode; - // test execution + // act element.AddClass(toAdd); Assert.Equal(expected, (element as IHtmlElement).Class); @@ -42,10 +42,10 @@ public void AddClass(string initial, string toAdd, string expected) [InlineData(null, "gamma", "")] public void RemoveClass(string initial, string toRemove, string expected) { - // preconditions + // arrange var element = new HtmlElementTextContentDiv { Class = initial } as IHtmlNode; - // test execution + // act element.RemoveClass(toRemove); // validation @@ -68,10 +68,10 @@ public void RemoveClass(string initial, string toRemove, string expected) [InlineData(null, "new", "new")] public void AddStyle(string initial, string toAdd, string expected) { - // preconditions + // arrange var element = new HtmlElementTextContentDiv { Style = initial } as IHtmlNode; - // test execution + // act element.AddStyle(toAdd); // validation @@ -91,10 +91,10 @@ public void AddStyle(string initial, string toAdd, string expected) [InlineData(null, "gamma", "")] public void RemoveStyle(string initial, string toRemove, string expected) { - // preconditions + // arrange var element = new HtmlElementTextContentDiv { Style = initial } as IHtmlNode; - // test execution + // act element.RemoveStyle(toRemove); // validation @@ -109,10 +109,10 @@ public void RemoveStyle(string initial, string toRemove, string expected) [InlineData("data-id", "42", "data-id=42")] public void AddUserAttribute(string name, string value, string expected) { - // preconditions + // arrange var element = new HtmlElementTextContentDiv() as IHtmlNode; - // test execution + // act if (!string.IsNullOrWhiteSpace(value)) { element.AddUserAttribute(name, value); @@ -145,11 +145,11 @@ public void AddUserAttribute(string name, string value, string expected) [InlineData("data-id", "42")] public void RemoveUserAttribute(string name, string value) { - // preconditions + // arrange var element = new HtmlElementTextContentDiv() as IHtmlNode; element.AddUserAttribute(name, value); - // test execution + // act element.RemoveUserAttribute(name); // validation @@ -162,7 +162,7 @@ public void RemoveUserAttribute(string name, string value) [Fact] public void Find() { - // preconditions + // arrange var root = new HtmlElementTextContentDiv(); var node = root as IHtmlNode; var child1 = new HtmlElementTextSemanticsSpan(); @@ -170,7 +170,7 @@ public void Find() root.Add(child1); root.Add(child2); - // test execution + // act var result = node.Find(e => e is HtmlElementTextSemanticsSpan).ToList(); // validation diff --git a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementFieldLabel.cs b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementFieldLabel.cs index cd716f1..de21028 100644 --- a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementFieldLabel.cs +++ b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementFieldLabel.cs @@ -14,7 +14,7 @@ public class UnitTestHtmlElementFieldLabel [Fact] public void Empty() { - // test execution + // act var html = new HtmlElementFieldLabel(); Assert.Equal(@"", html.Trim()); @@ -26,7 +26,7 @@ public void Empty() [Fact] public void TextAtInstancing() { - // test execution + // act var html = new HtmlElementFieldLabel("abcdef"); Assert.Equal(@"", html.Trim()); @@ -38,7 +38,7 @@ public void TextAtInstancing() [Fact] public void TextAtProperty() { - // test execution + // act var html = new HtmlElementFieldLabel { Text = "abcdef" @@ -53,7 +53,7 @@ public void TextAtProperty() [Fact] public void TextAtHtmlText() { - // test execution + // act var html = new HtmlElementFieldLabel(new HtmlText("abc"), new HtmlText("def")); Assert.Equal(@"", html.Trim()); @@ -65,7 +65,7 @@ public void TextAtHtmlText() [Fact] public void TextWithId() { - // test execution + // act var html = new HtmlElementFieldLabel() { Id = "identity" @@ -80,7 +80,7 @@ public void TextWithId() [Fact] public void Inline() { - // test execution + // act var html = new HtmlElementFieldLabel(); Assert.False(html.Inline); @@ -92,7 +92,7 @@ public void Inline() [Fact] public void CloseTag() { - // test execution + // act var html = new HtmlElementFieldLabel(); Assert.True(html.CloseTag); @@ -104,7 +104,7 @@ public void CloseTag() [Fact] public void Class() { - // test execution + // act var html = new HtmlElementFieldLabel() { Class = "abc" @@ -119,7 +119,7 @@ public void Class() [Fact] public void Style() { - // test execution + // act var html = new HtmlElementFieldLabel() { Style = "abc" diff --git a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementMetadataBase.cs b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementMetadataBase.cs index 8a80013..67fdea8 100644 --- a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementMetadataBase.cs +++ b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementMetadataBase.cs @@ -14,10 +14,10 @@ public class UnitTestHtmlElementMetadataBase [Fact] public void Constructor() { - // preconditions + // arrange var url = "https://example.com"; - // test execution + // act var element = new HtmlElementMetadataBase(url); // validation @@ -32,10 +32,10 @@ public void Constructor() [InlineData("https://example.com", "https://example.com")] public void Href(string uri, string expected) { - // preconditions + // arrange var element = new HtmlElementMetadataBase(); - // test execution + // act element.Href = uri; // validation diff --git a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementRootHtml.cs b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementRootHtml.cs index d5115e3..c167356 100644 --- a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementRootHtml.cs +++ b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementRootHtml.cs @@ -14,10 +14,10 @@ public class UnitTestHtmlElementRootHtml [Fact] public void HeadBase() { - // preconditions + // arrange var url = "https://example.com"; - // test execution + // act var element = new HtmlElementRootHtml(); element.Head.Base = url; diff --git a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementTextContentP.cs b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementTextContentP.cs index 555fdfa..1ec474d 100644 --- a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementTextContentP.cs +++ b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlElementTextContentP.cs @@ -14,7 +14,7 @@ public class UnitTestHtmlElementTextContentP [Fact] public void Empty() { - // test execution + // act var html = new HtmlElementTextContentP(); Assert.Equal(@"

", html.Trim()); @@ -26,7 +26,7 @@ public void Empty() [Fact] public void TextAtInstancing() { - // test execution + // act var html = new HtmlElementTextContentP("abcdef"); Assert.Equal(@"

abcdef

", html.Trim()); @@ -38,7 +38,7 @@ public void TextAtInstancing() [Fact] public void TextAtProperty() { - // test execution + // act var html = new HtmlElementTextContentP { Text = "abcdef" @@ -53,7 +53,7 @@ public void TextAtProperty() [Fact] public void TextAtHtmlText() { - // test execution + // act var html = new HtmlElementTextContentP(new HtmlText("abc"), new HtmlText("def")); var str = html.ToString(); @@ -66,7 +66,7 @@ public void TextAtHtmlText() [Fact] public void TextWithId() { - // test execution + // act var html = new HtmlElementTextContentP() { Id = "identity" @@ -81,7 +81,7 @@ public void TextWithId() [Fact] public void Inline() { - // test execution + // act var html = new HtmlElementTextContentP(); Assert.False(html.Inline); @@ -93,7 +93,7 @@ public void Inline() [Fact] public void CloseTag() { - // test execution + // act var html = new HtmlElementTextContentP(); Assert.True(html.CloseTag); @@ -105,7 +105,7 @@ public void CloseTag() [Fact] public void Class() { - // test execution + // act var html = new HtmlElementTextContentP() { Class = "abc" @@ -120,7 +120,7 @@ public void Class() [Fact] public void Style() { - // test execution + // act var html = new HtmlElementTextContentP() { Style = "abc" diff --git a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlImage.cs b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlImage.cs index f5c9d85..5679f85 100644 --- a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlImage.cs +++ b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlImage.cs @@ -14,7 +14,7 @@ public class UnitTestHtmlImage [Fact] public void Empty() { - // test execution + // act var html = new HtmlElementMultimediaImg(); Assert.Equal(@"", html.ToString().Trim()); diff --git a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlText.cs b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlText.cs index 4d499af..a89b3b4 100644 --- a/src/WebExpress.WebCore.Test/Html/UnitTestHtmlText.cs +++ b/src/WebExpress.WebCore.Test/Html/UnitTestHtmlText.cs @@ -14,7 +14,7 @@ public class UnitTestHtmlText [Fact] public void Empty() { - // test execution + // act var html = new HtmlText(); Assert.Null(html.Value); @@ -26,7 +26,7 @@ public void Empty() [Fact] public void TextAtInstancing() { - // test execution + // act var html = new HtmlText("abcdef"); Assert.Equal(@"abcdef", html.Value); @@ -38,7 +38,7 @@ public void TextAtInstancing() [Fact] public void TextAtProperty() { - // test execution + // act var html = new HtmlText { Value = "abcdef" diff --git a/src/WebExpress.WebCore.Test/Html/UnitTestRandomId.cs b/src/WebExpress.WebCore.Test/Html/UnitTestRandomId.cs new file mode 100644 index 0000000..6152894 --- /dev/null +++ b/src/WebExpress.WebCore.Test/Html/UnitTestRandomId.cs @@ -0,0 +1,100 @@ +using System.Collections.Concurrent; +using WebExpress.WebCore.WebHtml; + +namespace WebExpress.WebCore.Test.Html +{ + /// + /// Unit tests for the RandomId class. + /// + [Collection("NonParallelTests")] + public class UnitTestRandomId + { + /// + /// Tests the create method. + /// + [Fact] + public void CreateDifferent() + { + // act + var id1 = RandomId.Create(); + var id2 = RandomId.Create(); + + // validation + Assert.NotEqual(id1, id2); + } + + /// + /// Tests the create method. + /// + [Fact] + public void Prefix() + { + // act + var id = RandomId.Create(); + + // validation + Assert.StartsWith("id_", id); + } + + /// + /// Tests the create method. + /// + [Fact] + public void Length() + { + // act + var id = RandomId.Create(); + + // validation + Assert.Equal("id_".Length + 32, id.Length); + } + + /// + /// Tests the create method. + /// + [Fact] + public void HexCharacters() + { + // act + var id = RandomId.Create(); + + // validation + var hex = id.Substring("id_".Length); + Assert.Matches("^[0-9A-F]+$", hex); + } + + /// + /// Tests the create method. + /// + [Fact] + public void ThreadSafe() + { + // arrange + var results = new ConcurrentBag(); + + Parallel.For(0, 5000, _ => + { + // act + results.Add(RandomId.Create()); + }); + + // validation + var distinct = results.Distinct().Count(); + + Assert.Equal(results.Count, distinct); + } + + /// + /// Tests the create method. + /// + [Fact] + public void NotNullOrEmpty() + { + // act + var id = RandomId.Create(); + + // validation + Assert.False(string.IsNullOrWhiteSpace(id)); + } + } +} diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestApplication.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestApplicationManager.cs similarity index 87% rename from src/WebExpress.WebCore.Test/Manager/UnitTestApplication.cs rename to src/WebExpress.WebCore.Test/Manager/UnitTestApplicationManager.cs index db76fe1..d7e011b 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestApplication.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestApplicationManager.cs @@ -9,7 +9,7 @@ namespace WebExpress.WebCore.Test.Manager /// Test the application manager. ///
[Collection("NonParallelTests")] - public class UnitTestApplication + public class UnitTestApplicationManager { /// /// Test the register function of the application manager. @@ -17,13 +17,14 @@ public class UnitTestApplication [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var pluginManager = componentHub.PluginManager as PluginManager; - // test execution + // act pluginManager.Register(); + // validation Assert.Equal(3, componentHub.ApplicationManager.Applications.Count()); Assert.Equal("webexpress.webcore.test.testapplicationa", componentHub.ApplicationManager.GetApplications(typeof(TestApplicationA)).FirstOrDefault()?.ApplicationId); Assert.Equal("webexpress.webcore.test.testapplicationb", componentHub.ApplicationManager.GetApplications(typeof(TestApplicationB)).FirstOrDefault()?.ApplicationId); @@ -36,19 +37,20 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var applicationManager = componentHub.ApplicationManager as ApplicationManager; var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); - // test execution + // act applicationManager.Remove(plugin); - Assert.Empty(componentHub.ApplicationManager.Applications); + // validation + Assert.Empty(applicationManager.Applications); } /// - /// Test the name property of the application. + /// Test the id property of the application. /// [Theory] [InlineData(typeof(TestApplicationA), "webexpress.webcore.test.testapplicationa")] @@ -56,11 +58,11 @@ public void Remove() [InlineData(typeof(TestApplicationC), "webexpress.webcore.test.testapplicationc")] public void Id(Type applicationType, string id) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act Assert.Equal(id, application.ApplicationId); } @@ -73,11 +75,11 @@ public void Id(Type applicationType, string id) [InlineData(typeof(TestApplicationC), "TestApplicationC")] public void Name(Type applicationType, string name) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act Assert.Equal(name, application.ApplicationName); } @@ -90,11 +92,11 @@ public void Name(Type applicationType, string name) [InlineData(typeof(TestApplicationC), "application.description")] public void Description(Type applicationType, string description) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act Assert.Equal(description, application.Description); } @@ -107,11 +109,11 @@ public void Description(Type applicationType, string description) [InlineData(typeof(TestApplicationC), "/server/assets/img/Logo.png")] public void Icon(Type applicationType, string icon) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act Assert.Equal(icon, application.Icon.ToString()); } @@ -124,11 +126,11 @@ public void Icon(Type applicationType, string icon) [InlineData(typeof(TestApplicationC), "/server")] public void ContextPath(Type applicationType, string contextPath) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act Assert.Equal(contextPath, application.Route.ToString()); } @@ -138,15 +140,15 @@ public void ContextPath(Type applicationType, string contextPath) [Theory] [InlineData(typeof(TestApplicationA), "/asseta")] [InlineData(typeof(TestApplicationB), "/assetb")] - [InlineData(typeof(TestApplicationC), "/")] + [InlineData(typeof(TestApplicationC), "*/")] public void AssetPath(Type applicationType, string assetPath) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution - Assert.Equal(assetPath, application.AssetPath); + // act + AssertExtensions.EqualWithPlaceholders(assetPath, application.AssetPath); } /// @@ -155,15 +157,15 @@ public void AssetPath(Type applicationType, string assetPath) [Theory] [InlineData(typeof(TestApplicationA), "/dataa")] [InlineData(typeof(TestApplicationB), "/datab")] - [InlineData(typeof(TestApplicationC), "/")] + [InlineData(typeof(TestApplicationC), "*/")] public void DataPath(Type applicationType, string dataPath) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution - Assert.Equal(dataPath, application.DataPath); + // act + AssertExtensions.EqualWithPlaceholders(dataPath, application.DataPath); } /// @@ -172,10 +174,10 @@ public void DataPath(Type applicationType, string dataPath) [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.ApplicationManager.GetType())); } @@ -185,10 +187,10 @@ public void IsIComponentManager() [Fact] public void IsIContext() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act foreach (var application in componentHub.ApplicationManager.Applications) { Assert.True(typeof(IContext).IsAssignableFrom(application.GetType()), $"Application context {application.GetType().Name} does not implement IContext."); diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestAssetManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestAssetManager.cs index 1974d85..b150178 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestAssetManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestAssetManager.cs @@ -19,10 +19,10 @@ public class UnitTestAssetManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(12, componentHub.AssetManager.Assets.Count()); } @@ -32,12 +32,12 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); var resourceManager = componentHub.AssetManager as AssetManager; - // test execution + // act resourceManager.Remove(plugin); Assert.Empty(componentHub.AssetManager.Assets); @@ -59,12 +59,12 @@ public void Remove() [InlineData(typeof(TestApplicationC), "webexpress.webcore.test.js.myjavascript.mini.js")] public void Id(Type applicationType, string id) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var asset = componentHub.AssetManager.GetAssets(application)?.FirstOrDefault(x => x.EndpointId.ToString() == id); - // test execution + // act Assert.Equal(id, asset?.EndpointId.ToString()); } @@ -84,13 +84,13 @@ public void Id(Type applicationType, string id) [InlineData(typeof(TestApplicationC), "/server/assets/js/myjavascript.mini.js")] public void Uri(Type applicationType, string route) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var asset = componentHub.AssetManager.GetAssets(application)? .FirstOrDefault(x => x.Route.ToString() == route); - // test execution + // act Assert.Equal(route, asset?.Route.ToString()); } @@ -110,14 +110,14 @@ public void Uri(Type applicationType, string route) [InlineData("http://localhost:8080/server/assets/js/myjavascript.mini.js", "js/myjavascript.mini.js")] public void Request(string uri, string resource) { - // preconditions + // arrange var embeddedResource = UnitTestFixture.GetEmbeddedResource(resource); var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var context = UnitTestFixture.CreateHttpContextMock(); var httpServerContext = UnitTestFixture.CreateHttpServerContextMock(); componentHub.SitemapManager.Refresh(); - // test execution + // act var searchResult = componentHub.SitemapManager.SearchResource(new System.Uri(uri), new SearchContext() { HttpServerContext = httpServerContext, @@ -127,7 +127,7 @@ public void Request(string uri, string resource) var response = componentHub .EndpointManager - .HandleRequest(UnitTestFixture.CrerateRequestMock("", uri), searchResult.EndpointContext); + .HandleRequest(UnitTestFixture.CreateRequestMock("", uri), searchResult.EndpointContext); Assert.Equal($"webexpress.webcore.test.{resource.Replace('/', '.')}", searchResult?.EndpointContext?.EndpointId.ToString()); Assert.IsNotType(response); @@ -140,10 +140,10 @@ public void Request(string uri, string resource) [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.AssetManager.GetType())); } @@ -153,10 +153,10 @@ public void IsIComponentManager() [Fact] public void IsIContext() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act foreach (var asset in componentHub.AssetManager.Assets) { Assert.True(typeof(IContext).IsAssignableFrom(asset.GetType()), $"Asset context {asset.GetType().Name} does not implement IContext."); diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestComponentManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestComponentManager.cs index d0ebaaf..52a5a9a 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestComponentManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestComponentManager.cs @@ -14,10 +14,10 @@ public class UnitTestComponentManager [Fact] public void PluginManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); - // test execution + // act Assert.NotNull(componentHub.PluginManager); } @@ -27,10 +27,10 @@ public void PluginManager() [Fact] public void ApplicationManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); - // test execution + // act Assert.NotNull(componentHub.ApplicationManager); } } diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestEventManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestEventManager.cs index 9c68326..09573b2 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestEventManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestEventManager.cs @@ -16,10 +16,10 @@ public class UnitTestEventManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(6, componentHub.EventManager.EventHandlers.Count()); } @@ -29,12 +29,12 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var eventManager = componentHub.EventManager as EventManager; var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); - // test execution + // act eventManager.Remove(plugin); Assert.Empty(componentHub.EventManager.EventHandlers); @@ -46,10 +46,10 @@ public void Remove() [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.EventManager.GetType())); } @@ -63,14 +63,14 @@ public void IsIComponentManager() [InlineData(typeof(TestApplicationB), typeof(TestEventB), "webexpress.webcore.test.testeventhandlerb")] public void Id(Type applicationType, Type eventType, string id) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act var eventHandlers = componentHub.EventManager.GetEventHandlers(application, eventType); - if (id == null) + if (id is null) { Assert.Empty(eventHandlers); return; @@ -89,14 +89,14 @@ public void Id(Type applicationType, Type eventType, string id) [InlineData(typeof(TestApplicationB), typeof(TestEventB), "webexpress.webcore.test.testeventb")] public void EventId(Type applicationType, Type eventType, string id) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act var eventHandlers = componentHub.EventManager.GetEventHandlers(application, eventType); - if (id == null) + if (id is null) { Assert.Empty(eventHandlers); return; @@ -111,11 +111,11 @@ public void EventId(Type applicationType, Type eventType, string id) [Fact] public void RaiseEventA1() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplication(); - // test execution + // act var eventArgument = new TestEventArgument() { TestProperty = false }; componentHub.EventManager.RaiseEvent(application, this, eventArgument); @@ -129,11 +129,11 @@ public void RaiseEventA1() [Fact] public void RaiseEventB1() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplication(); - // test execution + // act var eventArgument = new TestEventArgument() { TestProperty = false }; componentHub.EventManager.RaiseEvent(application, this, eventArgument); diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestFragmentManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestFragmentManager.cs index 35df62c..66859b6 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestFragmentManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestFragmentManager.cs @@ -19,10 +19,10 @@ public class UnitTestFragmentManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(15, componentHub.FragmentManager.Fragments.Count()); } @@ -32,14 +32,15 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var fragmentManager = componentHub.FragmentManager as FragmentManager; var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); - // test execution + // act fragmentManager.Remove(plugin); + // validation Assert.Empty(componentHub.FragmentManager.Fragments); } @@ -49,10 +50,10 @@ public void Remove() [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.EventManager.GetType())); } @@ -71,19 +72,20 @@ public void IsIComponentManager() [InlineData(typeof(TestApplicationC), typeof(TestFragmentC), "webexpress.webcore.test.testfragmentc")] public void Id(Type applicationType, Type fragmentType, string id) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act var fragment = componentHub.FragmentManager.GetFragments(application, fragmentType); - if (id == null) + if (id is null) { Assert.Empty(fragment); return; } + // validation Assert.Contains(id, fragment.Select(x => x.FragmentId?.ToString())); } @@ -98,14 +100,39 @@ public void Id(Type applicationType, Type fragmentType, string id) [InlineData(typeof(TestApplicationB), typeof(About), 1)] public void GetFragments(Type applicationType, Type scopeType, int count) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); var renderContext = UnitTestFixture.CrerateRenderContextMock(application, [scopeType]); - // test execution + // act var fragments = componentHub.FragmentManager.GetFragments(application, renderContext?.PageContext?.Scopes).ToList(); + // validation + Assert.NotNull(fragments); + Assert.Equal(count, fragments.Count); + } + + /// + /// Test the get fragment function of the fragment. + /// + [Theory] + [InlineData(typeof(TestApplicationA), typeof(IScope), 0)] + [InlineData(typeof(TestApplicationA), typeof(TestScopeA), 1)] + [InlineData(typeof(TestApplicationA), typeof(About), 1)] + [InlineData(typeof(TestApplicationB), typeof(IScope), 0)] + [InlineData(typeof(TestApplicationB), typeof(About), 1)] + public void GetFragmentsBase(Type applicationType, Type scopeType, int count) + { + // arrange + var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); + var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); + var renderContext = UnitTestFixture.CrerateRenderContextMock(application, [scopeType]); + + // act + var fragments = componentHub.FragmentManager.GetFragments(application, renderContext?.PageContext?.Scopes).ToList(); + + // validation Assert.NotNull(fragments); Assert.Equal(count, fragments.Count); } @@ -121,15 +148,16 @@ public void GetFragments(Type applicationType, Type scopeType, int count) [InlineData(typeof(TestApplicationA), typeof(TestSectionA), typeof(TestScopeD), true)] public void Render(Type applicationType, Type sectionType, Type scopeType, bool empty) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); var renderContext = UnitTestFixture.CrerateRenderContextMock(application, [scopeType]); var visualTree = new VisualTree(); - // test execution + // act var html = componentHub.FragmentManager.Render(renderContext, visualTree, sectionType); + // validation Assert.NotNull(html); if (!empty) diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestIdentityManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestIdentityManager.cs index 9f381a0..6e1b337 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestIdentityManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestIdentityManager.cs @@ -18,10 +18,10 @@ public class UnitTestIdentityManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(9, componentHub.IdentityManager.Permissions.Count()); Assert.Equal(6, componentHub.IdentityManager.Policies.Count()); } @@ -32,12 +32,12 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); var identityManager = componentHub.IdentityManager as IdentityManager; - // test execution + // act identityManager.Remove(plugin); Assert.Empty(componentHub.IdentityManager.Permissions); @@ -50,10 +50,10 @@ public void Remove() [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.IdentityManager.GetType())); } @@ -72,13 +72,13 @@ public void IsIComponentManager() [InlineData(typeof(TestApplicationA), "Charlie", typeof(TestIdentityPermissionC), false)] public void CheckAccessIdentity(Type application, string identityName, Type permission, bool expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var identityManager = componentHub.IdentityManager as IdentityManager; var applicationContext = componentHub.ApplicationManager.GetApplications(application).FirstOrDefault(); var identity = MockIdentityFactory.GetIdentity(identityName); - // test execution + // act var access = identityManager.CheckAccess(applicationContext, identity, permission); Assert.Equal(expected, access); @@ -99,13 +99,13 @@ public void CheckAccessIdentity(Type application, string identityName, Type perm [InlineData(typeof(TestApplicationA), "Guests", typeof(TestIdentityPermissionC), false)] public void CheckAccessGroup(Type application, string groupName, Type permission, bool expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var identityManager = componentHub.IdentityManager as IdentityManager; var applicationContext = componentHub.ApplicationManager.GetApplications(application).FirstOrDefault(); var group = MockIdentityFactory.GetIdentityGroup(groupName); - // test execution + // act var access = identityManager.CheckAccess(applicationContext, group, permission); Assert.Equal(expected, access); @@ -123,12 +123,12 @@ public void CheckAccessGroup(Type application, string groupName, Type permission [InlineData(typeof(TestApplicationA), typeof(TestIdentityPolicyB), typeof(TestIdentityPermissionC), false)] public void CheckAccess(Type application, Type policy, Type permission, bool expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var identityManager = componentHub.IdentityManager as IdentityManager; var applicationContext = componentHub.ApplicationManager.GetApplications(application).FirstOrDefault(); - // test execution + // act var access = identityManager.CheckAccess(applicationContext, policy, permission); Assert.Equal(expected, access); @@ -142,16 +142,16 @@ public void CheckAccess(Type application, Type policy, Type permission, bool exp [InlineData("Alice", "123", false)] public void Login(string identityName, string password, bool expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var identityManager = componentHub.IdentityManager as IdentityManager; - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); var identity = MockIdentityFactory.GetIdentity(identityName); var securePassword = new SecureString(); password.ToList().ForEach(x => securePassword.AppendChar(x)); securePassword.MakeReadOnly(); - // test execution + // act var res = identityManager.Login(request, identity, securePassword); Assert.Equal(expected, res); @@ -166,17 +166,17 @@ public void Login(string identityName, string password, bool expected) [InlineData("Charlie", "abc")] public void Logout(string identityName, string password) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var identityManager = componentHub.IdentityManager as IdentityManager; - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); var identity = MockIdentityFactory.GetIdentity(identityName); var securePassword = new SecureString(); password.ToList().ForEach(x => securePassword.AppendChar(x)); securePassword.MakeReadOnly(); identityManager.Login(request, identity, securePassword); - // test execution + // act identityManager.Logout(request); var res = identityManager.GetCurrentIdentity(request); @@ -192,17 +192,17 @@ public void Logout(string identityName, string password) [InlineData("Charlie", "abc")] public void GetCurrentIdentity(string identityName, string password) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var identityManager = componentHub.IdentityManager as IdentityManager; - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); var identity = MockIdentityFactory.GetIdentity(identityName); var securePassword = new SecureString(); password.ToList().ForEach(x => securePassword.AppendChar(x)); securePassword.MakeReadOnly(); identityManager.Login(request, identity, securePassword); - // test execution + // act var res = identityManager.GetCurrentIdentity(request); Assert.Equal(identity, res); diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestIncludeManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestIncludeManager.cs index c194bdd..bf1755e 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestIncludeManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestIncludeManager.cs @@ -16,10 +16,10 @@ public class UnitTestIncludeManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(12, componentHub.IncludeManager.Includes.Count()); } @@ -29,12 +29,12 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); var includeManager = componentHub.IncludeManager as IncludeManager; - // test execution + // act includeManager.Remove(plugin); // validation @@ -59,12 +59,12 @@ public void Remove() [InlineData(typeof(TestApplicationC), typeof(TestIncludeCssB), "webexpress.webcore.test.testincludecssb")] public void Id(Type applicationType, Type includeType, string expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var include = componentHub.IncludeManager.GetIncludes(application, includeType)?.FirstOrDefault(); - // test execution + // act var id = include?.IncludeId.ToString(); // validation @@ -89,12 +89,12 @@ public void Id(Type applicationType, Type includeType, string expected) [InlineData(typeof(TestApplicationC), typeof(TestIncludeCssB), "/myX.css;/myY.css;/myZ.css")] public void Files(Type applicationType, Type resourceType, string expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var include = componentHub.IncludeManager.GetIncludes(application, resourceType)?.FirstOrDefault(); - // test execution + // act var files = include.Files.Select(x => x.FileName); // validation @@ -119,12 +119,12 @@ public void Files(Type applicationType, Type resourceType, string expected) [InlineData(typeof(TestApplicationC), typeof(TestIncludeCssB), "StyleSheet;StyleSheet;StyleSheet")] public void FileType(Type applicationType, Type resourceType, string expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var include = componentHub.IncludeManager.GetIncludes(application, resourceType)?.FirstOrDefault(); - // test execution + // act var files = include.Files.Select(x => x.Type); // validation @@ -137,10 +137,10 @@ public void FileType(Type applicationType, Type resourceType, string expected) [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.IncludeManager.GetType())); } @@ -150,10 +150,10 @@ public void IsIComponentManager() [Fact] public void IsIContext() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act foreach (var include in componentHub.IncludeManager.Includes) { Assert.True(typeof(IContext).IsAssignableFrom(include.GetType()), $"Include context '{include.GetType().Name}' does not implement IContext."); diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestInternationalization.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestInternationalization.cs index 9b3247c..f33ce9d 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestInternationalization.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestInternationalization.cs @@ -18,11 +18,11 @@ public class UnitTestInternationalization [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var pluginManager = componentHub.PluginManager as PluginManager; - // test execution + // act pluginManager.Register(); Assert.Equal("This is a test", I18N.Translate("webexpress.webcore.test:unit.test.message")); @@ -34,12 +34,12 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var internationalizationManager = componentHub.InternationalizationManager as InternationalizationManager; var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); - // test execution + // act internationalizationManager.Remove(plugin); Assert.Equal("webexpress.webcore.test:unit.test.message", I18N.Translate("webexpress.webcore.test:unit.test.message")); @@ -51,10 +51,10 @@ public void Remove() [Fact] public void GetDefaultCulture() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(CultureInfo.GetCultureInfo("en"), InternationalizationManager.DefaultCulture); } @@ -72,52 +72,51 @@ public void GetDefaultCulture() [InlineData("non.existent.key", "non.existent.key", "de")] public void Translate(string key, string excepted, string cultureName = null, string pluginID = null, params object[] param) { - // preconditions + // arrange UnitTestFixture.CreateAndRegisterComponentHubMock(); - if (cultureName == null && !param.Any()) + if (cultureName is null && param.Length == 0) { - // test execution + // act var result = I18N.Translate(key); Assert.Equal(excepted, result); } - if (cultureName == null && param.Any()) + if (cultureName is null && param.Length != 0) { - // test execution + // act var result = I18N.Translate(key, param); Assert.Equal(excepted, result); } - if (cultureName != null && pluginID == null && !param.Any()) + if (cultureName is not null && pluginID is null && param.Length == 0) { - // test execution + // act var result = I18N.Translate(CultureInfo.GetCultureInfo(cultureName), key); Assert.Equal(excepted, result); } - if (cultureName != null && pluginID == null && param.Any()) + if (cultureName is not null && pluginID is null && param.Length != 0) { - // test execution + // act var result = I18N.Translate(CultureInfo.GetCultureInfo(cultureName), key, param); Assert.Equal(excepted, result); } - if (cultureName != null && pluginID != null && !param.Any()) + if (cultureName is not null && pluginID is not null && param.Length == 0) { - // test execution + // act var result = I18N.Translate(CultureInfo.GetCultureInfo(cultureName), pluginID, key); Assert.Equal(excepted, result); } - if (cultureName != null && pluginID != null && param.Any()) + if (cultureName is not null && pluginID is not null && param.Length != 0) { - // test execution + // act var result = I18N.Translate(CultureInfo.GetCultureInfo(cultureName), pluginID, key, param); Assert.Equal(excepted, result); } - } /// @@ -126,10 +125,10 @@ public void Translate(string key, string excepted, string cultureName = null, st [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.InternationalizationManager.GetType())); } } diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestJobManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestJobManager.cs index 3f4eb85..1d347c3 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestJobManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestJobManager.cs @@ -16,10 +16,10 @@ public class UnitTestJobManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(3, componentHub.JobManager.Jobs.Count()); } @@ -29,12 +29,12 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); var jobManager = componentHub.JobManager as JobManager; - // test execution + // act jobManager.Remove(plugin); Assert.Empty(componentHub.JobManager.Jobs); @@ -46,10 +46,10 @@ public void Remove() [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.ResourceManager.GetType())); } @@ -59,10 +59,10 @@ public void IsIComponentManager() [Fact] public void IsIContext() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act foreach (var job in componentHub.JobManager.Jobs) { Assert.True(typeof(IContext).IsAssignableFrom(job.GetType()), $"Job context {job.GetType().Name} does not implement IContext."); @@ -79,12 +79,12 @@ public void IsIContext() public void Id(Type applicationType, Type jobType, string id) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); var job = componentHub.JobManager.GetJob(application, jobType); - // test execution + // act Assert.Equal(id, job?.JobId.ToString()); } @@ -97,12 +97,12 @@ public void Id(Type applicationType, Type jobType, string id) [InlineData(typeof(TestApplicationC), typeof(TestJobA), 50, 8, 31, new[] { 1, 2 }, 0)] public void Cron(Type applicationType, Type jobType, int minute, int hour, int day, int[] month, int weekday) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); var job = componentHub.JobManager.GetJob(application, jobType); - // test execution + // act Assert.Equal(minute, job?.Cron.Minute.FirstOrDefault() ?? -1); Assert.Equal(hour, job?.Cron.Hour.FirstOrDefault() ?? -1); Assert.Equal(day, job?.Cron.Day.FirstOrDefault() ?? -1); diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestLogManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestLogManager.cs index 96f8c3d..2a94866 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestLogManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestLogManager.cs @@ -16,11 +16,11 @@ public class UnitTestLogManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var logManager = componentHub.LogManager as LogManager; - // test execution + // act Assert.NotNull(logManager); } @@ -30,11 +30,11 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var logManager = componentHub.LogManager as LogManager; - // test execution + // act Assert.NotNull(logManager); } @@ -44,11 +44,11 @@ public void Remove() [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var logManager = componentHub.LogManager as LogManager; - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(logManager.GetType())); } } diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestPackageManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestPackageManager.cs index 93ce7da..5814623 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestPackageManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestPackageManager.cs @@ -19,11 +19,11 @@ public class UnitTestPackageManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var packageManager = componentHub.PackageManager as PackageManager; - // test execution + // act Assert.NotNull(packageManager); } @@ -33,11 +33,11 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var packageManager = componentHub.PackageManager as PackageManager; - // test execution + // act Assert.NotNull(packageManager); } @@ -47,11 +47,11 @@ public void Remove() [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var packageManager = componentHub.PackageManager as PackageManager; - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(packageManager.GetType())); } @@ -61,7 +61,7 @@ public void IsIComponentManager() [Fact] public void AddPackageEvent() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var packageManager = componentHub.PackageManager as PackageManager; bool eventFired = false; @@ -70,7 +70,7 @@ public void AddPackageEvent() // create dummy package var package = new PackageCatalogItem() { Id = "test", File = "test.wxp", State = PackageCatalogeItemState.Active }; - // test execution + // act var method = typeof(PackageManager).GetMethod("OnAddPackage", BindingFlags.NonPublic | BindingFlags.Instance); method.Invoke(packageManager, [package]); @@ -84,7 +84,7 @@ public void AddPackageEvent() [Fact] public void RemovePackageEvent() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var packageManager = componentHub.PackageManager as PackageManager; bool eventFired = false; @@ -93,7 +93,7 @@ public void RemovePackageEvent() // create dummy package var package = new PackageCatalogItem() { Id = "test", File = "test.wxp", State = PackageCatalogeItemState.Active }; - // test execution + // act var method = typeof(PackageManager).GetMethod("OnRemovePackage", BindingFlags.NonPublic | BindingFlags.Instance); method.Invoke(packageManager, [package]); @@ -106,7 +106,7 @@ public void RemovePackageEvent() [Fact] public void ScanDetectsNewPackage() { - // preconditions + // arrange var httpServerContext = UnitTestFixture.CreateHttpServerContextMock(); var componentHub = UnitTestFixture.CreateComponentHubMock(httpServerContext); var packageManager = componentHub.PackageManager as PackageManager; @@ -131,7 +131,7 @@ public void ScanDetectsNewPackage() "); } - // test execution - scan should detect the new file + // act - scan should detect the new file packageManager.Scan(); // validation @@ -152,7 +152,7 @@ public void ScanDetectsNewPackage() [Fact] public void ScanDetectsRemovedPackage() { - // preconditions + // arrange var httpServerContext = UnitTestFixture.CreateHttpServerContextMock(); var componentHub = UnitTestFixture.CreateComponentHubMock(httpServerContext); var packageManager = componentHub.PackageManager as PackageManager; @@ -183,7 +183,7 @@ public void ScanDetectsRemovedPackage() // remove file and scan again File.Delete(dummyFile); - // test execution - scan should detect the removed file + // act - scan should detect the removed file packageManager.Scan(); // validation @@ -204,7 +204,7 @@ public void ScanDetectsRemovedPackage() [Fact] public void LoadPackageReadsSpec() { - // preconditions + // arrange var httpServerContext = UnitTestFixture.CreateHttpServerContextMock(); var componentHub = UnitTestFixture.CreateComponentHubMock(httpServerContext); var packageManager = componentHub.PackageManager as PackageManager; @@ -231,7 +231,7 @@ public void LoadPackageReadsSpec() // use private LoadPackage method via reflection var method = typeof(PackageManager).GetMethod("LoadPackage", BindingFlags.NonPublic | BindingFlags.Instance); - // test execution + // act var result = method.Invoke(packageManager, [dummyFile]) as PackageCatalogItem; // validation diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestPageManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestPageManager.cs index fc39f30..cd35974 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestPageManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestPageManager.cs @@ -17,10 +17,10 @@ public class UnitTestPageManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(33, componentHub.PageManager.Pages.Count()); } @@ -30,12 +30,12 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); var pageManager = componentHub.PageManager as PageManager; - // test execution + // act pageManager.Remove(plugin); Assert.Empty(componentHub.PageManager.Pages); @@ -58,12 +58,12 @@ public void Remove() [InlineData(typeof(TestApplicationA), typeof(WWW.Products.Details.Index), "webexpress.webcore.test.www.products.details.index")] public void Id(Type applicationType, Type pageType, string id) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var page = componentHub.PageManager.GetPages(pageType, application)?.FirstOrDefault(); - // test execution + // act Assert.Equal(id, page.EndpointId.ToString()); } @@ -83,12 +83,12 @@ public void Id(Type applicationType, Type pageType, string id) public void Title(Type applicationType, Type resourceType, string title) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var page = componentHub.PageManager.GetPages(resourceType, application)?.FirstOrDefault(); - // test execution + // act Assert.Equal(title, page.PageTitle); } @@ -107,12 +107,12 @@ public void Title(Type applicationType, Type resourceType, string title) [InlineData(typeof(TestApplicationC), typeof(Contact), "/server/contact")] public void RoutePath(Type applicationType, Type resourceType, string path) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var page = componentHub.PageManager.GetPages(resourceType, application)?.FirstOrDefault(); - // test execution + // act Assert.Equal(path, page.Route.ToString()); } @@ -122,10 +122,10 @@ public void RoutePath(Type applicationType, Type resourceType, string path) [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.PageManager.GetType())); } @@ -135,10 +135,10 @@ public void IsIComponentManager() [Fact] public void IsIContext() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act foreach (var pages in componentHub.PageManager.Pages) { Assert.True(typeof(IContext).IsAssignableFrom(pages.GetType()), $"Page context {pages.GetType().Name} does not implement IContext."); diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestPluginManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestPluginManager.cs index 2b1d5d7..815f347 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestPluginManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestPluginManager.cs @@ -16,11 +16,11 @@ public class UnitTestPluginManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var pluginManager = componentHub.PluginManager as PluginManager; - // test execution + // act pluginManager.Register(); Assert.Single(componentHub.PluginManager.Plugins); @@ -33,7 +33,7 @@ public void Register() [Fact] public void RegisterEvent() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var pluginManager = componentHub.PluginManager as PluginManager; var i = 0; @@ -41,7 +41,7 @@ public void RegisterEvent() componentHub.PluginManager.AddPlugin += (s, e) => { i++; triggered = true; }; - // test execution + // act pluginManager.Register(); Assert.Single(componentHub.PluginManager.Plugins); @@ -56,13 +56,13 @@ public void RegisterEvent() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var pluginManager = componentHub.PluginManager as PluginManager; pluginManager.Register(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); - // test execution + // act pluginManager.Remove(plugin); Assert.Empty(componentHub.PluginManager.Plugins); @@ -74,7 +74,7 @@ public void Remove() [Fact] public void RemoveEvent() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var pluginManager = componentHub.PluginManager as PluginManager; var i = 1; @@ -84,7 +84,7 @@ public void RemoveEvent() pluginManager.Register(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); - // test execution + // act pluginManager.Remove(plugin); Assert.Empty(componentHub.PluginManager.Plugins); @@ -98,10 +98,10 @@ public void RemoveEvent() [Fact] public void GetPluginById() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act var plugin = componentHub.PluginManager.GetPlugin("webexpress.webcore.test"); Assert.Equal("webexpress.webcore.test", plugin?.PluginId.ToString()); @@ -113,10 +113,10 @@ public void GetPluginById() [Fact] public void GetPluginByType() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); Assert.Equal("webexpress.webcore.test", plugin?.PluginId.ToString()); @@ -128,11 +128,11 @@ public void GetPluginByType() [Fact] public void Id() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); - // test execution + // act Assert.Equal(typeof(TestPlugin).Namespace.ToLower(), plugin.PluginId.ToString()); } @@ -142,11 +142,11 @@ public void Id() [Fact] public void GetName() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); - // test execution + // act Assert.Equal("TestPlugin", plugin.PluginName); } @@ -156,11 +156,11 @@ public void GetName() [Fact] public void GetDescription() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); - // test execution + // act Assert.Equal("plugin.description", plugin.Description); } @@ -170,11 +170,11 @@ public void GetDescription() [Fact] public void GetIcon() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); - // test execution + // act Assert.Equal("/server/assets/img/Logo.png", plugin.Icon.ToString()); } @@ -188,13 +188,13 @@ public void GetIcon() [InlineData(null, null)] public void Boot(string pluginId, string expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var pluginManager = componentHub.PluginManager as PluginManager; pluginManager.Register(); var plugin = componentHub.PluginManager.GetPlugin(pluginId); - // test execution + // act pluginManager.Boot(plugin); Assert.Single(componentHub.PluginManager.Plugins); @@ -211,13 +211,13 @@ public void Boot(string pluginId, string expected) [InlineData(null, null)] public void ShutDown(string pluginId, string expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateComponentHubMock(); var pluginManager = componentHub.PluginManager as PluginManager; pluginManager.Register(); var plugin = componentHub.PluginManager.GetPlugin(pluginId); - // test execution + // act pluginManager.ShutDown(plugin); Assert.Single(componentHub.PluginManager.Plugins); @@ -230,11 +230,11 @@ public void ShutDown(string pluginId, string expected) [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var pluginManager = componentHub.PluginManager as PluginManager; - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(pluginManager.GetType())); } @@ -244,10 +244,10 @@ public void IsIComponentManager() [Fact] public void IsIContext() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act foreach (var plugin in componentHub.PluginManager.Plugins) { Assert.True(typeof(IContext).IsAssignableFrom(plugin.GetType()), $"Plugin context {plugin.GetType().Name} does not implement IContext."); diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestResourceManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestResourceManager.cs index 7a6728d..71fefd6 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestResourceManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestResourceManager.cs @@ -17,10 +17,10 @@ public class UnitTestResourceManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(12, componentHub.ResourceManager.Resources.Count()); } @@ -30,12 +30,12 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); var resourceManager = componentHub.ResourceManager as ResourceManager; - // test execution + // act resourceManager.Remove(plugin); Assert.Empty(componentHub.ResourceManager.Resources); @@ -59,12 +59,12 @@ public void Remove() [InlineData(typeof(TestApplicationC), typeof(TestResourceD), "webexpress.webcore.test.www.resources.testresourced")] public void Id(Type applicationType, Type resourceType, string id) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var resource = componentHub.ResourceManager.GetResorces(resourceType, application)?.FirstOrDefault(); - // test execution + // act Assert.Equal(id, resource?.EndpointId.ToString()); } @@ -87,12 +87,12 @@ public void Id(Type applicationType, Type resourceType, string id) public void RoutePath(Type applicationType, Type resourceType, string path) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var resource = componentHub.ResourceManager.GetResorces(resourceType, application)?.FirstOrDefault(); - // test execution + // act Assert.Equal(path, resource.Route.ToString()); } @@ -102,10 +102,10 @@ public void RoutePath(Type applicationType, Type resourceType, string path) [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.ResourceManager.GetType())); } @@ -115,10 +115,10 @@ public void IsIComponentManager() [Fact] public void IsIContext() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act foreach (var resources in componentHub.ResourceManager.Resources) { Assert.True(typeof(IContext).IsAssignableFrom(resources.GetType()), $"Resource context {resources.GetType().Name} does not implement IContext."); diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestRestApiManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestRestApiManager.cs index 9c18eba..c4fdff7 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestRestApiManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestRestApiManager.cs @@ -1,9 +1,10 @@ using WebExpress.WebCore.Test.Fixture; -using WebExpress.WebCore.Test.WWW.Api._1; +using WebExpress.WebCore.Test.WWW.Api._1_; using WebExpress.WebCore.Test.WWW.Api._2; -using WebExpress.WebCore.Test.WWW.Api._3; +using WebExpress.WebCore.Test.WWW.Api.V3; using WebExpress.WebCore.WebComponent; using WebExpress.WebCore.WebMessage; +using WebExpress.WebCore.WebParameter; using WebExpress.WebCore.WebRestApi; namespace WebExpress.WebCore.Test.Manager @@ -20,10 +21,10 @@ public class UnitTestRestApiManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(9, componentHub.RestApiManager.RestApis.Count()); } @@ -33,12 +34,12 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); var apiManager = componentHub.RestApiManager as RestApiManager; - // test execution + // act apiManager.Remove(plugin); // validation @@ -49,23 +50,23 @@ public void Remove() /// Test the id property of the rest api. /// [Theory] - [InlineData(typeof(TestApplicationA), typeof(TestRestApiA), "webexpress.webcore.test.www.api._1.testrestapia")] + [InlineData(typeof(TestApplicationA), typeof(TestRestApiA), "webexpress.webcore.test.www.api._1_.testrestapia")] [InlineData(typeof(TestApplicationA), typeof(TestRestApiB), "webexpress.webcore.test.www.api._2.testrestapib")] - [InlineData(typeof(TestApplicationA), typeof(TestRestApiC), "webexpress.webcore.test.www.api._3.testrestapic")] - [InlineData(typeof(TestApplicationB), typeof(TestRestApiA), "webexpress.webcore.test.www.api._1.testrestapia")] + [InlineData(typeof(TestApplicationA), typeof(TestRestApiC), "webexpress.webcore.test.www.api.v3.testrestapic")] + [InlineData(typeof(TestApplicationB), typeof(TestRestApiA), "webexpress.webcore.test.www.api._1_.testrestapia")] [InlineData(typeof(TestApplicationB), typeof(TestRestApiB), "webexpress.webcore.test.www.api._2.testrestapib")] - [InlineData(typeof(TestApplicationB), typeof(TestRestApiC), "webexpress.webcore.test.www.api._3.testrestapic")] - [InlineData(typeof(TestApplicationC), typeof(TestRestApiA), "webexpress.webcore.test.www.api._1.testrestapia")] + [InlineData(typeof(TestApplicationB), typeof(TestRestApiC), "webexpress.webcore.test.www.api.v3.testrestapic")] + [InlineData(typeof(TestApplicationC), typeof(TestRestApiA), "webexpress.webcore.test.www.api._1_.testrestapia")] [InlineData(typeof(TestApplicationC), typeof(TestRestApiB), "webexpress.webcore.test.www.api._2.testrestapib")] - [InlineData(typeof(TestApplicationC), typeof(TestRestApiC), "webexpress.webcore.test.www.api._3.testrestapic")] + [InlineData(typeof(TestApplicationC), typeof(TestRestApiC), "webexpress.webcore.test.www.api.v3.testrestapic")] public void Id(Type applicationType, Type resourceType, string id) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var api = componentHub.RestApiManager.GetRestApi(resourceType, application)?.FirstOrDefault(); - // test execution + // act Assert.Equal(id, api?.EndpointId.ToString()); } @@ -84,31 +85,61 @@ public void Id(Type applicationType, Type resourceType, string id) [InlineData(typeof(TestApplicationC), typeof(TestRestApiC), "/server/api/3/testrestapic")] public void RoutePath(Type applicationType, Type resourceType, string path) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var api = componentHub.RestApiManager.GetRestApi(resourceType, application)?.FirstOrDefault(); - // test execution + // act Assert.Equal(path, api?.Route.ToString()); } + /// + /// Test the version from path property of the rest api. + /// + [Theory] + [InlineData(typeof(TestApplicationA), typeof(TestRestApiA), "1")] + [InlineData(typeof(TestApplicationA), typeof(TestRestApiB), "2")] + [InlineData(typeof(TestApplicationA), typeof(TestRestApiC), "3")] + [InlineData(typeof(TestApplicationB), typeof(TestRestApiA), "1")] + [InlineData(typeof(TestApplicationB), typeof(TestRestApiB), "2")] + [InlineData(typeof(TestApplicationB), typeof(TestRestApiC), "3")] + [InlineData(typeof(TestApplicationC), typeof(TestRestApiA), "1")] + [InlineData(typeof(TestApplicationC), typeof(TestRestApiB), "2")] + [InlineData(typeof(TestApplicationC), typeof(TestRestApiC), "3")] + public void Version(Type applicationType, Type resourceType, string expected) + { + // arrange + var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); + var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); + var api = componentHub.RestApiManager.GetRestApi(resourceType, application)?.FirstOrDefault(); + componentHub.SitemapManager.Refresh(); + var uri = componentHub.SitemapManager.GetUri(resourceType, application); + + // act + var version = uri.Parameters + .Where(x => x.Key == "_apiversion") + .FirstOrDefault(); + + // act + Assert.Equal(expected, version.Value); + } + /// /// Test the context path property of the rest api. /// [Theory] - [InlineData(typeof(TestApplicationA), typeof(TestRestApiA), CrudMethod.POST)] - [InlineData(typeof(TestApplicationA), typeof(TestRestApiA), CrudMethod.GET)] - [InlineData(typeof(TestApplicationA), typeof(TestRestApiB), CrudMethod.GET)] - [InlineData(typeof(TestApplicationA), typeof(TestRestApiC), CrudMethod.GET)] - public void Method(Type applicationType, Type resourceType, CrudMethod method) + [InlineData(typeof(TestApplicationA), typeof(TestRestApiA), RequestMethod.POST)] + [InlineData(typeof(TestApplicationA), typeof(TestRestApiA), RequestMethod.GET)] + [InlineData(typeof(TestApplicationA), typeof(TestRestApiB), RequestMethod.GET)] + public void Method(Type applicationType, Type resourceType, RequestMethod method) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var api = componentHub.RestApiManager.GetRestApi(resourceType, application)?.FirstOrDefault(); - // test execution + // act Assert.Contains(method, api?.Methods); } @@ -118,10 +149,10 @@ public void Method(Type applicationType, Type resourceType, CrudMethod method) [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.RestApiManager.GetType())); } @@ -131,10 +162,10 @@ public void IsIComponentManager() [Fact] public void IsIContext() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act foreach (var api in componentHub.RestApiManager.RestApis) { Assert.True(typeof(IContext).IsAssignableFrom(api.GetType()), $"Api context {api.GetType().Name} does not implement IContext."); @@ -150,11 +181,11 @@ public void IsIContext() [InlineData(" ")] public void ValidateRequire(string input) { - // preconditions - var request = UnitTestFixture.CrerateRequestMock($"name={input}"); + // arrange + var request = UnitTestFixture.CreateRequestMock($"name={input}"); request.AddParameter(new Parameter("name", input, ParameterScope.Parameter)); - // test execution + // act var validator = new RestApiValidator(request) .Require("name"); @@ -172,11 +203,11 @@ public void ValidateRequire(string input) [InlineData("ab")] public void ValidateMinLength(string input) { - // preconditions - var request = UnitTestFixture.CrerateRequestMock(); + // arrange + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("code", input, ParameterScope.Parameter)); - // test execution + // act var validator = new RestApiValidator(request) .MinLength("code", 3); @@ -192,12 +223,12 @@ public void ValidateMinLength(string input) [InlineData(300)] public void ValidateMaxLength(int length) { - // preconditions + // arrange var input = new string('x', length); - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("bio", input, ParameterScope.Parameter)); - // test execution + // act var validator = new RestApiValidator(request) .MaxLength("bio", 255); @@ -215,11 +246,11 @@ public void ValidateMaxLength(int length) [InlineData("@nouser.com")] public void ValidateEmail(string email) { - // preconditions - var request = UnitTestFixture.CrerateRequestMock(); + // arrange + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("email", email, ParameterScope.Parameter)); - // test execution + // act var validator = new RestApiValidator(request) .Email("email"); @@ -237,11 +268,11 @@ public void ValidateEmail(string email) [InlineData("123a")] public void ValidateIsInt(string input) { - // preconditions - var request = UnitTestFixture.CrerateRequestMock(); + // arrange + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("age", input, ParameterScope.Parameter)); - // test execution + // act var validator = new RestApiValidator(request) .IsInt("age"); @@ -258,11 +289,11 @@ public void ValidateIsInt(string input) [InlineData("WrongCase")] public void ValidateEqualTo(string input) { - // preconditions - var request = UnitTestFixture.CrerateRequestMock(); + // arrange + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("role", input, ParameterScope.Parameter)); - // test execution + // act var validator = new RestApiValidator(request) .EqualTo("role", "admin"); @@ -279,7 +310,7 @@ public void ValidateEqualTo(string input) [InlineData("101")] public void ValidateRange(string input) { - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("level", input, ParameterScope.Parameter)); var validator = new RestApiValidator(request) @@ -297,7 +328,7 @@ public void ValidateRange(string input) [InlineData("xyz-start")] public void ValidateStartsWith(string input) { - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("code", input, ParameterScope.Parameter)); var validator = new RestApiValidator(request) @@ -315,7 +346,7 @@ public void ValidateStartsWith(string input) [InlineData("document.pdf")] public void ValidateEndsWith(string input) { - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("filename", input, ParameterScope.Parameter)); var validator = new RestApiValidator(request) @@ -333,7 +364,7 @@ public void ValidateEndsWith(string input) [InlineData("anonymous")] public void ValidateIn(string input) { - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("role", input, ParameterScope.Parameter)); var validator = new RestApiValidator(request) @@ -351,7 +382,7 @@ public void ValidateIn(string input) [InlineData("foo bar")] public void ValidateContains(string input) { - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("description", input, ParameterScope.Parameter)); var validator = new RestApiValidator(request) @@ -371,7 +402,7 @@ public enum Difficulty { Easy, Medium, Hard } [InlineData("easy-peasy")] public void ValidateMatchesEnum(string value) { - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("difficulty", value, ParameterScope.Parameter)); var validator = new RestApiValidator(request) @@ -389,7 +420,7 @@ public void ValidateMatchesEnum(string value) [InlineData("31/31/2020")] public void ValidateIsDate(string input) { - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("date", input, ParameterScope.Parameter)); var validator = new RestApiValidator(request) @@ -408,7 +439,7 @@ public void ValidateIsDate(string input) [InlineData("nonpirate")] public void ValidateCustom(string input) { - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("nickname", input, ParameterScope.Parameter)); var validator = new RestApiValidator(request) @@ -431,7 +462,7 @@ public void ValidateCustom(string input) [InlineData("true", "")] public void ValidateWhen_ConditionalRequire(string subscribe, string email) { - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("subscribe", subscribe, ParameterScope.Parameter)); request.AddParameter(new Parameter("email", email, ParameterScope.Parameter)); @@ -451,7 +482,7 @@ public void ValidateWhen_ConditionalRequire(string subscribe, string email) [InlineData(null, "")] public void ValidateWhen_ConditionFalse(string subscribe, string email) { - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); request.AddParameter(new Parameter("subscribe", subscribe, ParameterScope.Parameter)); request.AddParameter(new Parameter("email", email, ParameterScope.Parameter)); diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestSessionManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestSessionManager.cs index e7bf41e..afd01d4 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestSessionManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestSessionManager.cs @@ -1,6 +1,6 @@ using WebExpress.WebCore.Test.Fixture; using WebExpress.WebCore.WebComponent; -using WebExpress.WebCore.WebMessage; +using WebExpress.WebCore.WebParameter; using WebExpress.WebCore.WebSession.Model; namespace WebExpress.WebCore.Test.Manager @@ -17,10 +17,10 @@ public class UnitTestSessionManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.NotNull(componentHub.SessionManager); } @@ -30,10 +30,10 @@ public void Register() [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.SessionManager.GetType())); } @@ -43,11 +43,11 @@ public void IsIComponentManager() [Fact] public void GetSession() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); - // test execution + // act var session = componentHub.SessionManager.GetSession(request); Assert.NotNull(session); @@ -59,12 +59,12 @@ public void GetSession() [Fact] public void AddPropertyToSession() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); var session = componentHub.SessionManager.GetSession(request); - // test execution + // act session.SetProperty(new SessionPropertyParameter(new Parameter("test", "test param", ParameterScope.Session))); var testProperty = session.GetProperty(); @@ -81,12 +81,12 @@ public void AddPropertyToSession() [Fact] public void RemovePropertyFromSession() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - var request = UnitTestFixture.CrerateRequestMock(); + var request = UnitTestFixture.CreateRequestMock(); var session = componentHub.SessionManager.GetSession(request); - // test execution + // act session.SetProperty(new SessionPropertyParameter(new Parameter("test", "test param", ParameterScope.Session))); session.RemoveProperty(); diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestSettingPageManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestSettingPageManager.cs index bab64fb..098c40d 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestSettingPageManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestSettingPageManager.cs @@ -17,10 +17,10 @@ public class UnitTestSettingPageManager [Fact] public void RegisterSettingPages() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(9, componentHub.SettingPageManager.SettingPages.Count()); } @@ -30,10 +30,10 @@ public void RegisterSettingPages() [Fact] public void RegisterSettingCategories() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(9, componentHub.SettingPageManager.SettingCategories.Count()); } @@ -43,10 +43,10 @@ public void RegisterSettingCategories() [Fact] public void RegisterSettingGroups() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(9, componentHub.SettingPageManager.SettingGroups.Count()); } @@ -56,12 +56,12 @@ public void RegisterSettingGroups() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); var settingPageManager = componentHub.SettingPageManager as SettingPageManager; - // test execution + // act settingPageManager.Remove(plugin); Assert.Empty(componentHub.SettingPageManager.SettingPages); @@ -82,12 +82,12 @@ public void Remove() [InlineData(typeof(TestApplicationC), typeof(TestSettingPageC), "webexpress.webcore.test.www.settings.testsettingpagec")] public void Id(Type applicationType, Type resourceType, string id) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var settingPage = componentHub.SettingPageManager.GetSettingPages(resourceType, application)?.FirstOrDefault(); - // test execution + // act Assert.Equal(id, settingPage.EndpointId.ToString()); } @@ -103,12 +103,12 @@ public void Id(Type applicationType, Type resourceType, string id) [InlineData(typeof(TestApplicationC), typeof(TestSettingPageB), "webindex:settingpageb.label")] public void Title(Type applicationType, Type resourceType, string title) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var settingPage = componentHub.SettingPageManager.GetSettingPages(resourceType, application)?.FirstOrDefault(); - // test execution + // act Assert.Equal(title, settingPage.PageTitle); } @@ -127,12 +127,12 @@ public void Title(Type applicationType, Type resourceType, string title) [InlineData(typeof(TestApplicationC), typeof(TestSettingPageC), "/server/settings/testsettingpagec")] public void RoutePath(Type applicationType, Type resourceType, string path) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var settingPage = componentHub.SettingPageManager.GetSettingPages(resourceType, application)?.FirstOrDefault(); - // test execution + // act Assert.Equal(path, settingPage.Route.ToString()); } @@ -142,10 +142,10 @@ public void RoutePath(Type applicationType, Type resourceType, string path) [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.SettingPageManager.GetType())); } @@ -155,10 +155,10 @@ public void IsIComponentManager() [Fact] public void IsIContext() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act foreach (var settingPages in componentHub.SettingPageManager.SettingPages) { Assert.True(typeof(IContext).IsAssignableFrom(settingPages.GetType()), $"Page context {settingPages.GetType().Name} does not implement IContext."); @@ -174,12 +174,12 @@ public void IsIContext() [InlineData(typeof(TestApplicationC), new[] { "SettingCategory A", "SettingCategory B", "SettingCategory C" })] public void CategoryName(Type applicationType, params string[] names) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var settingCategories = componentHub.SettingPageManager.GetSettingCategories(application); - // test execution + // act Assert.Equal([.. names], [.. settingCategories.Select(x => x.Name)]); } @@ -192,12 +192,12 @@ public void CategoryName(Type applicationType, params string[] names) [InlineData(typeof(TestApplicationC), new[] { "WebExpress.WebCore.Test.TestIconBell", "WebExpress.WebCore.Test.TestIconProfile", null })] public void CategoryIcon(Type applicationType, params string[] icons) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var settingCategories = componentHub.SettingPageManager.GetSettingCategories(application); - // test execution + // act Assert.Equal([.. icons], [.. settingCategories.Select(x => x.Icon?.ToString())]); } @@ -210,12 +210,12 @@ public void CategoryIcon(Type applicationType, params string[] icons) [InlineData(typeof(TestApplicationC), new[] { "Description of category a.", "Description of category b.", "Description of category c." })] public void CategoryDescription(Type applicationType, params string[] descriptions) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var settingCategories = componentHub.SettingPageManager.GetSettingCategories(application); - // test execution + // act Assert.Equal([.. descriptions], [.. settingCategories.Select(x => x.Description)]); } @@ -228,12 +228,12 @@ public void CategoryDescription(Type applicationType, params string[] descriptio [InlineData(typeof(TestApplicationC), new[] { SettingSection.Preferences, SettingSection.Primary, SettingSection.Secondary })] public void CategorySection(Type applicationType, params SettingSection[] sections) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var settingCategories = componentHub.SettingPageManager.GetSettingCategories(application); - // test execution + // act Assert.Equal([.. sections], [.. settingCategories.Select(x => x.Section)]); } @@ -248,13 +248,13 @@ public void CategorySection(Type applicationType, params SettingSection[] sectio [InlineData(typeof(TestApplicationA), null, new[] { "SettingGroup C" })] public void GroupName(Type applicationType, Type settingCategoryType, params string[] names) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var settingCategory = componentHub.SettingPageManager.GetSettingCategories(application).FirstOrDefault(x => x.CategoryId.ToString() == settingCategoryType?.FullName.ToLower()); var settinGroups = componentHub.SettingPageManager.GetSettingGroups(application, settingCategory); - // test execution + // act Assert.Equal([.. names], [.. settinGroups.Select(x => x.Name)]); } @@ -269,13 +269,13 @@ public void GroupName(Type applicationType, Type settingCategoryType, params str [InlineData(typeof(TestApplicationA), null, new[] { "Description of group c." })] public void GroupDescription(Type applicationType, Type settingCategoryType, params string[] descriptions) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var settingCategory = componentHub.SettingPageManager.GetSettingCategories(application).FirstOrDefault(x => x.CategoryId.ToString() == settingCategoryType?.FullName.ToLower()); var settinGroups = componentHub.SettingPageManager.GetSettingGroups(application, settingCategory); - // test execution + // act Assert.Equal([.. descriptions], [.. settinGroups.Select(x => x.Description)]); } @@ -290,13 +290,13 @@ public void GroupDescription(Type applicationType, Type settingCategoryType, par [InlineData(typeof(TestApplicationA), null, new[] { SettingSection.Secondary })] public void GroupSection(Type applicationType, Type settingCategoryType, params SettingSection[] sections) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var settingCategory = componentHub.SettingPageManager.GetSettingCategories(application).FirstOrDefault(x => x.CategoryId.ToString() == settingCategoryType?.FullName.ToLower()); var settinGroups = componentHub.SettingPageManager.GetSettingGroups(application, settingCategory); - // test execution + // act Assert.Equal([.. sections], [.. settinGroups.Select(x => x.Section)]); } @@ -311,13 +311,13 @@ public void GroupSection(Type applicationType, Type settingCategoryType, params [InlineData(typeof(TestApplicationA), null)] public void GroupCategory(Type applicationType, Type settingCategoryType) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var settingCategory = componentHub.SettingPageManager.GetSettingCategories(application).FirstOrDefault(x => x.CategoryId.ToString() == settingCategoryType?.FullName.ToLower()); var settinGroups = componentHub.SettingPageManager.GetSettingGroups(application, settingCategory); - // test execution + // act Assert.Equal(settinGroups.Count(), settinGroups.Where(x => x.SettingCategory == settingCategory).Count()); } @@ -332,14 +332,16 @@ public void GroupCategory(Type applicationType, Type settingCategoryType) [InlineData(typeof(TestApplicationA), null, typeof(TestSettingPageC))] public void GetFirstSettingPage(Type applicationType, Type settingCategoryType, Type firstPageType) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); var settingCategory = componentHub.SettingPageManager.GetSettingCategories(application).FirstOrDefault(x => x.CategoryId.ToString() == settingCategoryType?.FullName.ToLower()); - var firstPage = firstPageType != null ? componentHub.SettingPageManager.GetSettingPages(firstPageType, application).FirstOrDefault() : null; + var firstPage = firstPageType is not null + ? componentHub.SettingPageManager.GetSettingPages(firstPageType, application).FirstOrDefault() + : null; var settingPage = componentHub.SettingPageManager.GetFirstSettingPage(application, settingCategory); - // test execution + // act Assert.Equal(firstPage, settingPage); } } diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestSitemapManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestSitemapManager.cs index 3ec7ab8..76c0eb5 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestSitemapManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestSitemapManager.cs @@ -1,7 +1,7 @@ using WebExpress.WebCore.Test.Fixture; -using WebExpress.WebCore.Test.WWW.Api._1; +using WebExpress.WebCore.Test.WWW.Api._1_; using WebExpress.WebCore.Test.WWW.Api._2; -using WebExpress.WebCore.Test.WWW.Api._3; +using WebExpress.WebCore.Test.WWW.Api.V3; using WebExpress.WebCore.Test.WWW.Resources; using WebExpress.WebCore.WebComponent; using WebExpress.WebCore.WebSitemap; @@ -18,16 +18,18 @@ public class UnitTestSitemapManager /// /// Test the refresh function of the sitemap manager. /// - [Fact] - public void Refresh() + [Theory] + [InlineData(106)] + public void Refresh(int expected) { - // preconditions + // arrange var componentManager = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act componentManager.SitemapManager.Refresh(); - Assert.Equal(97, componentManager.SitemapManager.SiteMap.Count()); + // validation + Assert.Equal(expected, componentManager.SitemapManager.SiteMap.Count()); } /// @@ -55,22 +57,22 @@ public void Refresh() [InlineData("http://localhost:8080/server", "webexpress.webcore.test.www.index")] [InlineData("http://localhost:8080/server/about", "webexpress.webcore.test.www.about")] [InlineData("http://localhost:8080/server/contact", "webexpress.webcore.test.www.contact")] - [InlineData("http://localhost:8080/server/appa/api/1/testrestapia", "webexpress.webcore.test.www.api._1.testrestapia")] + [InlineData("http://localhost:8080/server/appa/api/1/testrestapia", "webexpress.webcore.test.www.api._1_.testrestapia")] [InlineData("http://localhost:8080/server/appa/api/2/testrestapib", "webexpress.webcore.test.www.api._2.testrestapib")] - [InlineData("http://localhost:8080/server/appa/api/3/testrestapic", "webexpress.webcore.test.www.api._3.testrestapic")] + [InlineData("http://localhost:8080/server/appa/api/3/testrestapic", "webexpress.webcore.test.www.api.v3.testrestapic")] [InlineData("http://localhost:8080/server/appa/assets/css/mycss.css", "webexpress.webcore.test.css.mycss.css")] [InlineData("http://localhost:8080/server/appa/assets/js/myjavascript.js", "webexpress.webcore.test.js.myjavascript.js")] [InlineData("http://localhost:8080/server/appa/assets/js/myjavascript.mini.js", "webexpress.webcore.test.js.myjavascript.mini.js")] [InlineData("http://localhost:8080/uri/does/not/exist", null)] public void SearchResource(string uri, string id) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var context = UnitTestFixture.CreateHttpContextMock(); var httpServerContext = UnitTestFixture.CreateHttpServerContextMock(); componentHub.SitemapManager.Refresh(); - // test execution + // act var searchResult = componentHub.SitemapManager.SearchResource(new System.Uri(uri), new SearchContext() { HttpServerContext = httpServerContext, @@ -78,8 +80,9 @@ public void SearchResource(string uri, string id) HttpContext = context }); - componentHub.EndpointManager.HandleRequest(UnitTestFixture.CrerateRequestMock(), searchResult?.EndpointContext); + componentHub.EndpointManager.HandleRequest(UnitTestFixture.CreateRequestMock(), searchResult?.EndpointContext); + // validation Assert.Equal(id, searchResult?.EndpointContext?.EndpointId.ToString()); } @@ -110,14 +113,25 @@ public void SearchResource(string uri, string id) [InlineData(typeof(TestApplicationA), typeof(WWW.Products.Details.Index), 2, "/server/appa/products/2")] public void GetUri(Type applicationType, Type resourceType, int? param, string expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); componentHub.SitemapManager.Refresh(); + var parameter = param.HasValue + ? new TestParameterA(param ?? 0) + : new TestParameterA(); - // test execution - var uri = componentHub.SitemapManager.GetUri(resourceType, application, [param.HasValue ? new TestParameterA(param.Value) : null]); + // act + var uri = componentHub.SitemapManager.GetUri + ( + resourceType, + application, + [ + parameter + ] + ); + // validation Assert.Equal(expected, uri?.ToString()); } @@ -133,9 +147,9 @@ public void GetUri(Type applicationType, Type resourceType, int? param, string e [InlineData("http://localhost:8080/server/appa/assets/css/mycss.css", "webexpress.webcore.test.css.mycss.css")] [InlineData("http://localhost:8080/server/appa/assets/js/myjavascript.js", "webexpress.webcore.test.js.myjavascript.js")] [InlineData("http://localhost:8080/server/appa/assets/js/myjavascript.mini.js", "webexpress.webcore.test.js.myjavascript.mini.js")] - [InlineData("http://localhost:8080/server/appa/api/1/testrestapia", "webexpress.webcore.test.www.api._1.testrestapia")] + [InlineData("http://localhost:8080/server/appa/api/1/testrestapia", "webexpress.webcore.test.www.api._1_.testrestapia")] [InlineData("http://localhost:8080/server/appa/api/2/TestRestApiB", "webexpress.webcore.test.www.api._2.testrestapib")] - [InlineData("http://localhost:8080/server/appa/api/3/testrestapic", "webexpress.webcore.test.www.api._3.testrestapic")] + [InlineData("http://localhost:8080/server/appa/api/3/testrestapic", "webexpress.webcore.test.www.api.v3.testrestapic")] [InlineData("http://localhost:8080/server/appa", "webexpress.webcore.test.www.index")] [InlineData("http://localhost:8080/server/appa/", "webexpress.webcore.test.www.index")] [InlineData("http://localhost:8080/server/appa/about", "webexpress.webcore.test.www.about")] @@ -156,14 +170,15 @@ public void GetUri(Type applicationType, Type resourceType, int? param, string e [InlineData("http://localhost:8080/server/appa/products/10E96737-5C72-4C25-9E74-F96D8863D123/", "webexpress.webcore.test.www.products.details.index")] public void GetEndpoint(string uri, string expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); componentHub.SitemapManager.Refresh(); - // test execution + // act var endpoint = componentHub.SitemapManager.GetEndpoint(new UriEndpoint(uri)); - Assert.Equal(expected, endpoint?.EndpointId?.ToString()); + // validation + AssertExtensions.EqualWithPlaceholders(expected, endpoint?.EndpointId?.ToString()); } /// @@ -172,10 +187,10 @@ public void GetEndpoint(string uri, string expected) [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.SitemapManager.GetType())); } } diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestSocketManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestSocketManager.cs new file mode 100644 index 0000000..e3cba42 --- /dev/null +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestSocketManager.cs @@ -0,0 +1,119 @@ +using WebExpress.WebCore.Test.Fixture; +using WebExpress.WebCore.WebComponent; +using WebExpress.WebCore.WebPlugin; +using WebExpress.WebCore.WebSocket; + +namespace WebExpress.WebCore.Test.Manager +{ + /// + /// Test the websocket manager. + /// + [Collection("NonParallelTests")] + public class UnitTestSocketManager + { + /// + /// Test the register function of the socket manager. + /// + [Fact] + public void Register() + { + // arrange + var componentHub = UnitTestFixture.CreateComponentHubMock(); + var pluginManager = componentHub.PluginManager as PluginManager; + var socketManager = componentHub.SocketManager as SocketManager; + + // act + pluginManager.Register(); + + // validation + Assert.Equal(3, socketManager.Sockets.Count()); + Assert.Equal("webexpress.webcore.test.testsocketa", socketManager.GetSockets()?.FirstOrDefault()?.EndpointId?.ToString()); + Assert.Equal("webexpress.webcore.test.testsocketa", socketManager.GetSockets(typeof(TestSocketA))?.FirstOrDefault()?.EndpointId?.ToString()); + } + + /// + /// Test the remove function of the socket manager. + /// + [Fact] + public void Remove() + { + // arrange + var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); + var socketManager = componentHub.SocketManager as SocketManager; + var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); + + // act + socketManager.Remove(plugin); + + // validation + Assert.Empty(socketManager.Sockets); + } + + /// + /// Test the id property of the socket. + /// + [Theory] + [InlineData(typeof(TestApplicationA), "webexpress.webcore.test.testsocketa")] + [InlineData(typeof(TestApplicationB), "webexpress.webcore.test.testsocketa")] + [InlineData(typeof(TestApplicationC), "webexpress.webcore.test.testsocketa")] + public void Id(Type applicationType, string id) + { + // arrange + var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); + var applicationContext = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); + var socket = componentHub.SocketManager.GetSockets(applicationContext) + .FirstOrDefault(); + + // act + Assert.Equal(id, socket.EndpointId?.ToString()); + } + + /// + /// Test the context path property of the socket. + /// + [Theory] + [InlineData(typeof(TestApplicationA), "/server/appa/testsocketa")] + [InlineData(typeof(TestApplicationB), "/server/appb/testsocketa")] + [InlineData(typeof(TestApplicationC), "/server/testsocketa")] + public void ContextPath(Type applicationType, string contextPath) + { + // arrange + var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); + var applicationContext = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); + var socket = componentHub.SocketManager.GetSockets(applicationContext) + .FirstOrDefault(); + + // act + Assert.Equal(contextPath, socket.Route.ToString()); + } + + /// + /// Tests whether the socket manager implements interface IComponentManager. + /// + [Fact] + public void IsIComponentManager() + { + // arrange + var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); + + // act + Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.SocketManager.GetType())); + } + + /// + /// Tests whether the application context implements interface IContext. + /// + [Fact] + public void IsIContext() + { + // arrange + var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); + + // act + foreach (var application in componentHub.SocketManager.Sockets) + { + Assert.True(typeof(IContext).IsAssignableFrom(application.GetType()), $"Socket context {application.GetType().Name} does not implement IContext."); + } + } + } +} diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestStatusPageManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestStatusPageManager.cs index 56ded64..cfcba08 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestStatusPageManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestStatusPageManager.cs @@ -16,10 +16,10 @@ public class UnitTestStatusPageManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(12, componentHub.StatusPageManager.StatusPages.Count()); } @@ -29,12 +29,12 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); var statusPageManager = componentHub.StatusPageManager as StatusPageManager; - // test execution + // act statusPageManager.Remove(plugin); Assert.Empty(componentHub.StatusPageManager.StatusPages); @@ -51,12 +51,12 @@ public void Remove() public void Id(Type applicationType, Type statusPageType, string id) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); var statusPage = componentHub.StatusPageManager.GetStatusPage(application, statusPageType); - // test execution + // act Assert.Equal(id, statusPage.StatusPageId.ToString()); } @@ -78,12 +78,12 @@ public void Id(Type applicationType, Type statusPageType, string id) [InlineData(typeof(TestApplicationC), typeof(TestStatusPage500), "webindex:homepage.label")] public void Title(Type applicationType, Type resourceType, string title) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); var statusPage = componentHub.StatusPageManager.GetStatusPage(application, resourceType); - // test execution + // act Assert.Equal(title, statusPage.StatusTitle); } @@ -105,12 +105,12 @@ public void Title(Type applicationType, Type resourceType, string title) [InlineData(typeof(TestApplicationC), typeof(TestStatusPage500), 500)] public void Code(Type applicationType, Type statusPageType, int? code) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); var statusPage = componentHub.StatusPageManager.GetStatusPage(application, statusPageType); - // test execution + // act Assert.Equal(code, statusPage?.StatusCode); } @@ -132,12 +132,12 @@ public void Code(Type applicationType, Type statusPageType, int? code) [InlineData(typeof(TestApplicationC), typeof(TestStatusPage500), "/server/webexpress/icon.png")] public void Icon(Type applicationType, Type statusPageType, string icon) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); var statusPage = componentHub.StatusPageManager.GetStatusPage(application, statusPageType); - // test execution + // act Assert.Equal(icon, statusPage?.StatusIcon?.ToString()); } @@ -152,12 +152,12 @@ public void Icon(Type applicationType, Type statusPageType, string icon) [InlineData(typeof(TestApplicationA), 500, 500)] public void CreateAndCheckCode(Type applicationType, int statusCode, int? expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); var statusResponse = componentHub.StatusPageManager.CreateStatusResponse("content", statusCode, application, UnitTestFixture.CreateHttpContextMock().Request); - // test execution + // act Assert.Equal(expected, statusResponse?.Status); } @@ -165,18 +165,22 @@ public void CreateAndCheckCode(Type applicationType, int statusCode, int? expect /// Test the CreateStatusResponse function of the status page. /// [Theory] - [InlineData(typeof(TestApplicationA), 400, "content", "content", 78)] - [InlineData(typeof(TestApplicationA), 500, "content", "content", 78)] + [InlineData(typeof(TestApplicationA), 400, "content", "content", 72)] + [InlineData(typeof(TestApplicationA), 500, "content", "content", 72)] public void CreateAndCheckMessage(Type applicationType, int statusCode, string content, string expected, int length) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); + + // act var statusResponse = componentHub.StatusPageManager.CreateStatusResponse(content, statusCode, application, UnitTestFixture.CreateHttpContextMock().Request); - // test execution + // validation + var normalized = statusResponse?.Content?.ToString().Replace("\r\n", "\n").Replace("\r", "\n"); Assert.Contains(expected, statusResponse?.Content?.ToString()); - Assert.Equal(length, statusResponse?.Header?.ContentLength); + Assert.Equal(length, normalized.Length); + Assert.Equal(statusResponse?.Content?.ToString().Length, statusResponse?.Header?.ContentLength); } /// @@ -185,10 +189,10 @@ public void CreateAndCheckMessage(Type applicationType, int statusCode, string c [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.StatusPageManager.GetType())); } @@ -198,10 +202,10 @@ public void IsIComponentManager() [Fact] public void IsIContext() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act foreach (var application in componentHub.StatusPageManager.StatusPages) { Assert.True(typeof(IContext).IsAssignableFrom(application.GetType()), $"Page context {application.GetType().Name} does not implement IContext."); diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestTaskManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestTaskManager.cs index cc5b0db..c15ed63 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestTaskManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestTaskManager.cs @@ -15,10 +15,10 @@ public class UnitTestTaskManager [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.ResourceManager.GetType())); } @@ -28,12 +28,12 @@ public void IsIComponentManager() [Fact] public void IsCompopnent() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); componentHub.TaskManager.CreateTask("test"); - // test execution + // act foreach (var task in componentHub.TaskManager.Tasks) { Assert.True(typeof(IComponent).IsAssignableFrom(task.GetType()), $"Task {task.GetType().Name} does not implement IComponent."); @@ -46,10 +46,10 @@ public void IsCompopnent() [Fact] public void CreateSystemTask() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act var task = componentHub.TaskManager.CreateTask("test"); Assert.Equal("test", task?.Id); } @@ -60,10 +60,10 @@ public void CreateSystemTask() [Fact] public void CreateOwnTask() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act var task = componentHub.TaskManager.CreateTask("test", null, []); Assert.Equal("test", task?.Id); } @@ -74,11 +74,11 @@ public void CreateOwnTask() [Fact] public void ContainsTask() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var task = componentHub.TaskManager.CreateTask("test"); - // test execution + // act var res = componentHub.TaskManager.ContainsTask("test"); Assert.True(res); } @@ -89,11 +89,11 @@ public void ContainsTask() [Fact] public void GetTask() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var task = componentHub.TaskManager.CreateTask("test"); - // test execution + // act var res = componentHub.TaskManager.GetTask("test"); Assert.Equal(task, res); } @@ -104,11 +104,11 @@ public void GetTask() [Fact] public void RemoveTask() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var task = componentHub.TaskManager.CreateTask("test"); - // test execution + // act componentHub.TaskManager.RemoveTask(task); Assert.Empty(componentHub.TaskManager.Tasks); } diff --git a/src/WebExpress.WebCore.Test/Manager/UnitTestThemeManager.cs b/src/WebExpress.WebCore.Test/Manager/UnitTestThemeManager.cs index d7300d8..dfc14a6 100644 --- a/src/WebExpress.WebCore.Test/Manager/UnitTestThemeManager.cs +++ b/src/WebExpress.WebCore.Test/Manager/UnitTestThemeManager.cs @@ -16,10 +16,10 @@ public class UnitTestThemeManager [Fact] public void Register() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.Equal(6, componentHub.ThemeManager.Themes.Count()); } @@ -29,14 +29,15 @@ public void Register() [Fact] public void Remove() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var themeManager = componentHub.ThemeManager as ThemeManager; var plugin = componentHub.PluginManager.GetPlugin(typeof(TestPlugin)); - // test execution + // act themeManager.Remove(plugin); + // validation Assert.Empty(componentHub.ThemeManager.Themes); } @@ -46,10 +47,10 @@ public void Remove() [Fact] public void IsIComponentManager() { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); - // test execution + // act Assert.True(typeof(IComponentManager).IsAssignableFrom(componentHub.ThemeManager.GetType())); } @@ -65,14 +66,15 @@ public void IsIComponentManager() [InlineData(typeof(TestApplicationC), typeof(TestThemeB), "webexpress.webcore.test.testthemeb")] public void Id(Type applicationType, Type themeType, string id) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act var themes = componentHub.ThemeManager.GetThemes(application, themeType); - if (id == null) + // validation + if (id is null) { Assert.Empty(themes); return; @@ -93,14 +95,15 @@ public void Id(Type applicationType, Type themeType, string id) [InlineData(typeof(TestApplicationC), typeof(TestThemeB), "TestThemeB")] public void Name(Type applicationType, Type themeType, string name) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act var themes = componentHub.ThemeManager.GetThemes(application, themeType); - if (name == null) + // validation + if (name is null) { Assert.Empty(themes); return; @@ -121,13 +124,14 @@ public void Name(Type applicationType, Type themeType, string name) [InlineData(typeof(TestApplicationC), typeof(TestThemeB), null)] public void Description(Type applicationType, Type themeType, string expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act var theme = componentHub.ThemeManager.GetThemes(application, themeType).FirstOrDefault(); + // validation Assert.NotNull(theme); Assert.Equal(expected, theme?.Description); } @@ -144,13 +148,14 @@ public void Description(Type applicationType, Type themeType, string expected) [InlineData(typeof(TestApplicationC), typeof(TestThemeB), null)] public void Image(Type applicationType, Type themeType, string expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act var theme = componentHub.ThemeManager.GetThemes(application, themeType).FirstOrDefault(); + // validation Assert.NotNull(theme); Assert.Equal(expected, theme?.Image?.ToString()); } @@ -167,13 +172,14 @@ public void Image(Type applicationType, Type themeType, string expected) [InlineData(typeof(TestApplicationC), typeof(TestThemeB), ThemeMode.Light)] public void Mode(Type applicationType, Type themeType, ThemeMode expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act var theme = componentHub.ThemeManager.GetThemes(application, themeType).FirstOrDefault(); + // validation Assert.NotNull(theme); Assert.Equal(expected, theme?.ThemeMode); } @@ -190,13 +196,14 @@ public void Mode(Type applicationType, Type themeType, ThemeMode expected) [InlineData(typeof(TestApplicationC), typeof(TestThemeB), null)] public void ThemeStyle(Type applicationType, Type themeType, string expected) { - // preconditions + // arrange var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); var application = componentHub.ApplicationManager.GetApplications(applicationType).FirstOrDefault(); - // test execution + // act var theme = componentHub.ThemeManager.GetThemes(application, themeType).FirstOrDefault(); + // validation Assert.NotNull(theme); Assert.Equal(expected, theme?.ThemeStyle?.ToString()); } diff --git a/src/WebExpress.WebCore.Test/Message/UnitTestGetRequest.cs b/src/WebExpress.WebCore.Test/Message/UnitTestGetRequest.cs index a7d6814..141d555 100644 --- a/src/WebExpress.WebCore.Test/Message/UnitTestGetRequest.cs +++ b/src/WebExpress.WebCore.Test/Message/UnitTestGetRequest.cs @@ -15,7 +15,7 @@ public class UnitTestGetRequest public void General() { var content = UnitTestFixture.GetEmbeddedResource("general.get"); - var request = UnitTestFixture.CrerateRequestMock(content); + var request = UnitTestFixture.CreateRequestMock(content); Assert.Equal("http://localhost:8080/abc/xyz/A7BCCCA9-4C7E-4117-9EE2-ECC3381B605A", request.Uri?.ToString()); } @@ -27,7 +27,7 @@ public void General() public void Less() { var content = UnitTestFixture.GetEmbeddedResource("less.get"); - var request = UnitTestFixture.CrerateRequestMock(content); + var request = UnitTestFixture.CreateRequestMock(content); Assert.Equal("http://localhost:8080/abc/xyz/A7BCCCA9-4C7E-4117-9EE2-ECC3381B605A", request.Uri?.ToString()); } @@ -39,7 +39,7 @@ public void Less() public void Massive() { var content = UnitTestFixture.GetEmbeddedResource("massive.get"); - var request = UnitTestFixture.CrerateRequestMock(content); + var request = UnitTestFixture.CreateRequestMock(content); Assert.Equal("http://localhost:8080/abc/xyz/A7BCCCA9-4C7E-4117-9EE2-ECC3381B605A", request.Uri?.ToString()); } @@ -51,7 +51,7 @@ public void Massive() public void GetParameter() { var content = UnitTestFixture.GetEmbeddedResource("param.get"); - var request = UnitTestFixture.CrerateRequestMock(content); + var request = UnitTestFixture.CreateRequestMock(content); var param = request?.GetParameter("a")?.Value; Assert.Equal("http://localhost:8080/abc/xyz/A7BCCCA9-4C7E-4117-9EE2-ECC3381B605A", request.Uri?.ToString()); @@ -65,7 +65,7 @@ public void GetParameter() public void GetParameterWithUmlaut() { var content = UnitTestFixture.GetEmbeddedResource("param_umlaut.get"); - var request = UnitTestFixture.CrerateRequestMock(content); + var request = UnitTestFixture.CreateRequestMock(content); var a = request?.GetParameter("a")?.Value; var b = request?.GetParameter("b")?.Value; diff --git a/src/WebExpress.WebCore.Test/Message/UnitTestPostRequest.cs b/src/WebExpress.WebCore.Test/Message/UnitTestPostRequest.cs index 8b73d5c..856cdf1 100644 --- a/src/WebExpress.WebCore.Test/Message/UnitTestPostRequest.cs +++ b/src/WebExpress.WebCore.Test/Message/UnitTestPostRequest.cs @@ -11,7 +11,7 @@ public class UnitTestPostRequest : UnitTestRequest // Assert.True // ( - // param != null && param == "1" + // param is not null && param == "1" // ); //} @@ -26,9 +26,9 @@ public class UnitTestPostRequest : UnitTestRequest // Assert.True // ( - // a != null && a == "ä" && - // b != null && b == "ö ü" && - // s != null && s == "1" + // a is not null && a == "ä" && + // b is not null && b == "ö ü" && + // s is not null && s == "1" // ); //} diff --git a/src/WebExpress.WebCore.Test/Route/UnitTestRoute.cs b/src/WebExpress.WebCore.Test/Route/UnitTestRoute.cs index 862f497..9d8760b 100644 --- a/src/WebExpress.WebCore.Test/Route/UnitTestRoute.cs +++ b/src/WebExpress.WebCore.Test/Route/UnitTestRoute.cs @@ -19,12 +19,13 @@ public class UnitTestRoute [InlineData("/a/b/c", "/d/e/f", "/a/b/c/d/e/f", 7)] public void ConcatString(string baseRoute, string segment, string expected, int count) { - // preconditions + // arrange var route = new RouteEndpoint(baseRoute); - // test execution + // act var concat = route.Concat(segment); + // validation Assert.Equal(expected, concat.ToString()); Assert.Equal(count, concat.PathSegments.Count()); } @@ -39,12 +40,15 @@ public void ConcatString(string baseRoute, string segment, string expected, int [InlineData("/a/b/c", "/d/e/f", "/a/b/c/d/e/f", 7)] public void ConcatSegment(string baseRoute, string segment, string expected, int count) { - // preconditions + // arrange var route = new RouteEndpoint(baseRoute); - // test execution - var concat = route.Concat(segment != null ? [.. segment?.Split('/').Select(x => new UriPathSegmentConstant(x))] : null); + // act + var concat = route.Concat(segment is not null + ? [.. segment?.Split('/').Select(x => new UriPathSegmentConstant(x))] + : null); + // validation Assert.Equal(expected, concat.ToString()); Assert.Equal(count, concat.PathSegments.Count()); } @@ -59,9 +63,10 @@ public void ConcatSegment(string baseRoute, string segment, string expected, int [InlineData("/a/b/c", "/d/e/f", "/a/b/c/d/e/f")] public void CombinePath(string baseRoute, string pathB, string expected) { - // test execution + // act var combine = RouteEndpoint.Combine([new RouteEndpoint(baseRoute), new RouteEndpoint(pathB)]); + // validation Assert.Equal(expected, combine.ToString()); } @@ -75,9 +80,10 @@ public void CombinePath(string baseRoute, string pathB, string expected) [InlineData("/a/b/c", "/d/e/f", "/a/b/c/d/e/f")] public void CombineRoute(string baseRoute, string pathB, string expected) { - // test execution + // act var combine = RouteEndpoint.Combine(new RouteEndpoint(baseRoute), [pathB]); + // validation Assert.Equal(expected, combine.ToString()); } @@ -91,9 +97,10 @@ public void CombineRoute(string baseRoute, string pathB, string expected) [InlineData("/a/b/c", "/d/e/f", "/a/b/c/d/e/f")] public void CombineSegment(string baseRoute, string segment, string expected) { - // test execution + // act var combine = RouteEndpoint.Combine(new RouteEndpoint(baseRoute), segment); + // validation Assert.Equal(expected, combine.ToString()); } @@ -109,11 +116,13 @@ public void CombineSegment(string baseRoute, string segment, string expected) [InlineData("/a/b/c", "/a/c", "/a/b/c")] public void RemoveSegment(string route, string segment, string expected) { - // test execution + // arrange var routeEndpoint = new RouteEndpoint(route); + // act var removed = routeEndpoint.RemoveSegment(segment); + // validation Assert.Equal(expected, removed.ToString()); } } diff --git a/src/WebExpress.WebCore.Test/Route/UnitTestSegmentAttribute.cs b/src/WebExpress.WebCore.Test/Route/UnitTestSegmentAttribute.cs new file mode 100644 index 0000000..9db7140 --- /dev/null +++ b/src/WebExpress.WebCore.Test/Route/UnitTestSegmentAttribute.cs @@ -0,0 +1,131 @@ +using WebExpress.WebCore.WebAttribute; + +namespace WebExpress.WebCore.Test.Route +{ + /// + /// Tests the segment attribute. + /// + [Collection("NonParallelTests")] + public class UnitTestSegmentAttribute + { + /// + /// Test the constant segment. + /// + [Theory] + [InlineData(null, "")] + [InlineData("abc", "abc")] + public void Constant(string name, string expected) + { + // arrange + var attribute = new SegmentAttribute(name); + + // act + var segment = attribute.ToPathSegment(); + + // validation + Assert.Equal(expected, segment.ToString()); + } + + /// + /// Test the double segment attribute. + /// + [Theory] + [InlineData("${testparametera}")] + public void Double(string expected) + { + // arrange + var attribute = new SegmentDoubleAttribute(""); + + // act + var segment = attribute.ToPathSegment(); + + // validation + Assert.Equal(expected, segment.ToString()); + } + + /// + /// Test the guid segment attribute. + /// + [Theory] + [InlineData("${testparametera}")] + public void Guid(string expected) + { + // arrange + var attribute = new SegmentGuidAttribute(); + + // act + var segment = attribute.ToPathSegment(); + + // validation + Assert.Equal(expected, segment.ToString()); + } + + /// + /// Test the int segment attribute. + /// + [Theory] + [InlineData("${testparametera}")] + public void Int(string expected) + { + // arrange + var attribute = new SegmentIntAttribute(); + + // act + var segment = attribute.ToPathSegment(); + + // validation + Assert.Equal(expected, segment.ToString()); + } + + /// + /// Test the regex segment attribute. + /// + [Theory] + [InlineData(".*", "${testparametera}")] + public void Regex(string regex, string expected) + { + // arrange + var attribute = new SegmentRegexAttribute(regex); + + // act + var segment = attribute.ToPathSegment(); + + // validation + Assert.Equal(expected, segment.ToString()); + } + + /// + /// Test the string segment attribute. + /// + [Theory] + [InlineData("${testparametera}")] + public void String(string expected) + { + // arrange + var attribute = new SegmentStringAttribute(); + + // act + var segment = attribute.ToPathSegment(); + + // validation + Assert.Equal(expected, segment.ToString()); + } + + /// + /// Test the uint segment attribute. + /// + [Theory] + [InlineData("${testparametera}")] + public void UInt(string expected) + { + // arrange + var attribute = new SegmentUIntAttribute(); + + // act + var segment = attribute.ToPathSegment(); + + // validation + Assert.Equal(expected, segment.ToString()); + } + } +} diff --git a/src/WebExpress.WebCore.Test/Route/UnitTestUriPathSegment.cs b/src/WebExpress.WebCore.Test/Route/UnitTestUriPathSegment.cs new file mode 100644 index 0000000..343198e --- /dev/null +++ b/src/WebExpress.WebCore.Test/Route/UnitTestUriPathSegment.cs @@ -0,0 +1,163 @@ +using WebExpress.WebCore.Test.Fixture; +using WebExpress.WebCore.WebUri; + +namespace WebExpress.WebCore.Test.Route +{ + /// + /// Tests the path segment. + /// + [Collection("NonParallelTests")] + public class UnitTestUriPathSegment + { + /// + /// Test the constant segment. + /// + [Theory] + [InlineData(null, "", null)] + [InlineData("abc", "abc", null)] + public void Constant(string value, string expected, string displayText) + { + // arrange + var renderContet = UnitTestFixture.CrerateRenderContextMock(); + + // act + var segment = new UriPathSegmentConstant(value); + + // validation + Assert.Equal(expected, segment.ToString()); + Assert.Equal(displayText, segment.GetDisplayText(renderContet)); + } + + /// + /// Test the int segment. + /// + [Theory] + [InlineData(null, "${testparametera}", null)] + [InlineData("123", "123", "123")] + public void Int(string value, string expected, string displayText) + { + // arrange + var renderContet = UnitTestFixture.CrerateRenderContextMock(); + + // act + var segment = new UriPathSegmentVariableInt() + { + Value = value + }; + + // validation + Assert.Equal(expected, segment.ToString()); + Assert.Equal(displayText, segment.GetDisplayText(renderContet)); + } + + /// + /// Test the uint segment. + /// + [Theory] + [InlineData(null, "${testparametera}", null)] + [InlineData("123", "123", "123")] + public void UInt(string value, string expected, string displayText) + { + // arrange + var renderContet = UnitTestFixture.CrerateRenderContextMock(); + + // act + var segment = new UriPathSegmentVariableUInt() + { + Value = value + }; + + // validation + Assert.Equal(expected, segment.ToString()); + Assert.Equal(displayText, segment.GetDisplayText(renderContet)); + } + + /// + /// Test the double segment. + /// + [Theory] + [InlineData(null, "${testparametera}", null)] + [InlineData("123", "123", "123")] + public void Double(string value, string expected, string displayText) + { + // arrange + var renderContet = UnitTestFixture.CrerateRenderContextMock(); + + // act + var segment = new UriPathSegmentVariableDouble() + { + Value = value + }; + + // validation + Assert.Equal(expected, segment.ToString()); + Assert.Equal(displayText, segment.GetDisplayText(renderContet)); + } + + /// + /// Test the guid segment. + /// + [Theory] + [InlineData(null, "${testparametera}", null)] + [InlineData("123", "123", "")] + public void Guid(string value, string expected, string displayText) + { + // arrange + var renderContet = UnitTestFixture.CrerateRenderContextMock(); + + // act + var segment = new UriPathSegmentVariableGuid() + { + Value = value + }; + + // validation + Assert.Equal(expected, segment.ToString()); + Assert.Equal(displayText, segment.GetDisplayText(renderContet)); + } + + /// + /// Test the regex segment. + /// + [Theory] + [InlineData(null, "${testparametera}", null)] + [InlineData("123", "123", "123")] + public void Regex(string value, string expected, string displayText) + { + // arrange + var renderContet = UnitTestFixture.CrerateRenderContextMock(); + + // act + var segment = new UriPathSegmentVariableRegex(".*") + { + Value = value + }; + + // validation + Assert.Equal(expected, segment.ToString()); + Assert.Equal(displayText, segment.GetDisplayText(renderContet)); + } + + /// + /// Test the regex segment. + /// + [Theory] + [InlineData(null, "${testparametera}", null)] + [InlineData("123", "123", "123")] + public void String(string value, string expected, string displayText) + { + // arrange + var renderContet = UnitTestFixture.CrerateRenderContextMock(); + + // act + var segment = new UriPathSegmentVariableString(".*") + { + Value = value + }; + + // validation + Assert.Equal(expected, segment.ToString()); + Assert.Equal(displayText, segment.GetDisplayText(renderContet)); + } + } +} diff --git a/src/WebExpress.WebCore.Test/Schedule/UnitTestClock.cs b/src/WebExpress.WebCore.Test/Schedule/UnitTestClock.cs index 004ba48..bb2a209 100644 --- a/src/WebExpress.WebCore.Test/Schedule/UnitTestClock.cs +++ b/src/WebExpress.WebCore.Test/Schedule/UnitTestClock.cs @@ -21,7 +21,7 @@ public class UnitTestClock [InlineData(-1, -10, 0, 0, (24 * 60) + (10 * 60))] public void Synchronize(int? days, int? hours, int? minutes, int? seconds, int expected) { - // preconditions + // arrange var dateTime = DateTime.Now; if (days.HasValue) @@ -46,7 +46,7 @@ public void Synchronize(int? days, int? hours, int? minutes, int? seconds, int e var clock = new Clock(dateTime); - // test execution + // act var elapsed = clock.Synchronize(); Assert.Equal(expected, elapsed.Count()); @@ -60,11 +60,11 @@ public void Synchronize(int? days, int? hours, int? minutes, int? seconds, int e [InlineData("2020-12-31 23:59:00", "2021-01-01 00:00:00", false)] public void CompareEquals(string dateTime1, string dateTime2, bool expected) { - // preconditions + // arrange var clock1 = new Clock(DateTime.ParseExact(dateTime1, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); var clock2 = new Clock(DateTime.ParseExact(dateTime2, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); - // test execution + // act Assert.Equal(expected, clock1 == clock2); } @@ -76,11 +76,11 @@ public void CompareEquals(string dateTime1, string dateTime2, bool expected) [InlineData("2020-12-31 23:59:00", "2021-01-01 00:00:00", true)] public void CompareInequality(string dateTime1, string dateTime2, bool expected) { - // preconditions + // arrange var clock1 = new Clock(DateTime.ParseExact(dateTime1, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); var clock2 = new Clock(DateTime.ParseExact(dateTime2, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); - // test execution + // act Assert.Equal(expected, clock1 != clock2); } @@ -93,11 +93,11 @@ public void CompareInequality(string dateTime1, string dateTime2, bool expected) [InlineData("2020-12-31 23:59:00", "2021-01-01 00:00:00", true)] public void CompareLess(string dateTime1, string dateTime2, bool expected) { - // preconditions + // arrange var clock1 = new Clock(DateTime.ParseExact(dateTime1, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); var clock2 = new Clock(DateTime.ParseExact(dateTime2, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); - // test execution + // act Assert.Equal(expected, clock1 < clock2); } @@ -110,11 +110,11 @@ public void CompareLess(string dateTime1, string dateTime2, bool expected) [InlineData("2020-12-31 23:59:00", "2021-01-01 00:00:00", false)] public void CompareGreater(string dateTime1, string dateTime2, bool expected) { - // preconditions + // arrange var clock1 = new Clock(DateTime.ParseExact(dateTime1, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); var clock2 = new Clock(DateTime.ParseExact(dateTime2, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); - // test execution + // act Assert.Equal(expected, clock1 > clock2); } @@ -127,11 +127,11 @@ public void CompareGreater(string dateTime1, string dateTime2, bool expected) [InlineData("2020-12-31 23:59:00", "2021-01-01 00:00:00", true)] public void CompareLessOrEqual(string dateTime1, string dateTime2, bool expected) { - // preconditions + // arrange var clock1 = new Clock(DateTime.ParseExact(dateTime1, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); var clock2 = new Clock(DateTime.ParseExact(dateTime2, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); - // test execution + // act Assert.Equal(expected, clock1 <= clock2); } @@ -144,11 +144,11 @@ public void CompareLessOrEqual(string dateTime1, string dateTime2, bool expected [InlineData("2020-12-31 23:59:00", "2021-01-01 00:00:00", false)] public void CompareGreaterOrEqual(string dateTime1, string dateTime2, bool expected) { - // preconditions + // arrange var clock1 = new Clock(DateTime.ParseExact(dateTime1, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); var clock2 = new Clock(DateTime.ParseExact(dateTime2, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); - // test execution + // act Assert.Equal(expected, clock1 >= clock2); } @@ -167,7 +167,7 @@ public void Tick(string dateTime1, string expected) var clock2 = new Clock(DateTime.ParseExact(expected, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); clock1.Tick(); - // test execution + // act Assert.Equal(clock2, clock1); } } diff --git a/src/WebExpress.WebCore.Test/Schedule/UnitTestCron.cs b/src/WebExpress.WebCore.Test/Schedule/UnitTestCron.cs index 65a1400..4053e43 100644 --- a/src/WebExpress.WebCore.Test/Schedule/UnitTestCron.cs +++ b/src/WebExpress.WebCore.Test/Schedule/UnitTestCron.cs @@ -13,45 +13,45 @@ public class UnitTestCron [Fact] public void Create_1() { - // preconditions + // arrange var context = UnitTestFixture.CreateHttpServerContextMock(); var clock = new Clock(); var cron = new Cron(context, "0-59", "*", "1-31", "1-2,3,4,5,6,7,8-10,11,12"); - // test execution + // act Assert.True(cron.Matching(clock)); } [Fact] public void Create_2() { - // preconditions + // arrange var context = UnitTestFixture.CreateHttpServerContextMock(); var dateTime = DateTime.Now; var clock = new Clock(new DateTime(dateTime.Year, 1, dateTime.Day, dateTime.Hour, dateTime.Minute, 0)); var cron = new Cron(context, "*", "*", "0-33", "2, 1-4, x"); - // test execution + // act Assert.True(cron.Matching(clock)); } [Fact] public void Create_3() { - // preconditions + // arrange var context = UnitTestFixture.CreateHttpServerContextMock(); var dateTime = DateTime.Now; var clock = new Clock(new DateTime(dateTime.Year, 12, 31, dateTime.Hour, dateTime.Minute, 0)); var cron = new Cron(context, "*", "*", "31", "12"); - // test execution + // act Assert.True(cron.Matching(clock)); } [Fact] public void Create_4() { - // preconditions + // arrange var context = UnitTestFixture.CreateHttpServerContextMock(); var dateTime = DateTime.Now; Log.Current.Clear(); @@ -59,27 +59,27 @@ public void Create_4() var clock = new Clock(new DateTime(dateTime.Year, 12, 31, dateTime.Hour, dateTime.Minute, 0)); var cron = new Cron(context, "*", "*", "*", "a"); - // test execution + // act Assert.Equal(1, context.Log.WarningCount); } [Fact] public void Create_5() { - // preconditions + // arrange var context = UnitTestFixture.CreateHttpServerContextMock(); var dateTime = DateTime.Now; var clock = new Clock(new DateTime(dateTime.Year, 12, 31, dateTime.Hour, dateTime.Minute, 0)); var cron = new Cron(context, "*", "*", "*", ""); - // test execution + // act Assert.True(cron.Matching(clock)); } [Fact] public void Create_6() { - // preconditions + // arrange var context = UnitTestFixture.CreateHttpServerContextMock(); var dateTime = DateTime.Now; Log.Current.Clear(); @@ -87,46 +87,46 @@ public void Create_6() var clock = new Clock(new DateTime(dateTime.Year, 12, 31, dateTime.Hour, dateTime.Minute, 0)); var cron = new Cron(context, "99", "*", "*", "*"); - // test execution + // act Assert.Equal(1, context.Log.WarningCount); } [Fact] public void Matching_1() { - // preconditions + // arrange var context = UnitTestFixture.CreateHttpServerContextMock(); var dateTime = DateTime.Now; var clock = new Clock(new DateTime(dateTime.Year, 12, 31, dateTime.Hour, dateTime.Minute, 0)); var cron = new Cron(context, "*", "*", "31", "1-11"); - // test execution + // act Assert.False(cron.Matching(clock)); } [Fact] public void Matching_2() { - // preconditions + // arrange var context = UnitTestFixture.CreateHttpServerContextMock(); var dateTime = DateTime.Now; var clock = new Clock(new DateTime(2020, 1, 1, dateTime.Hour, dateTime.Minute, 0)); // wednesday var cron = new Cron(context, "*", "*", "*", "*", "3"); // wednesday - // test execution + // act Assert.True(cron.Matching(clock)); } [Fact] public void Matching_3() { - // preconditions + // arrange var context = UnitTestFixture.CreateHttpServerContextMock(); var dateTime = DateTime.Now; var clock = new Clock(new DateTime(2020, 1, 1, dateTime.Hour, dateTime.Minute, 0)); // wednesday var cron = new Cron(context, "*", "*", "*", "*", "1"); // sunday - // test execution + // act Assert.False(cron.Matching(clock)); } } diff --git a/src/WebExpress.WebCore.Test/TestApplicationA.cs b/src/WebExpress.WebCore.Test/TestApplicationA.cs index 5b687a7..4e8c0c8 100644 --- a/src/WebExpress.WebCore.Test/TestApplicationA.cs +++ b/src/WebExpress.WebCore.Test/TestApplicationA.cs @@ -22,7 +22,7 @@ public sealed class TestApplicationA : IApplication private TestApplicationA(IApplicationContext applicationContext) { // test the injection - if (applicationContext == null) + if (applicationContext is null) { throw new ArgumentNullException(nameof(applicationContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestApplicationC.cs b/src/WebExpress.WebCore.Test/TestApplicationC.cs index 5adfc91..62f2f66 100644 --- a/src/WebExpress.WebCore.Test/TestApplicationC.cs +++ b/src/WebExpress.WebCore.Test/TestApplicationC.cs @@ -22,13 +22,13 @@ public sealed class TestApplicationC : IApplication private TestApplicationC(IApplicationContext applicationContext, IPluginManager pluginManager) { // test the injection - if (applicationContext == null) + if (applicationContext is null) { throw new ArgumentNullException(nameof(applicationContext), "Parameter cannot be null or empty."); } // test the injection - if (pluginManager == null) + if (pluginManager is null) { throw new ArgumentNullException(nameof(pluginManager), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestConditionAlwaysFalse.cs b/src/WebExpress.WebCore.Test/TestConditionAlwaysFalse.cs index c77a1c1..fa9ff3e 100644 --- a/src/WebExpress.WebCore.Test/TestConditionAlwaysFalse.cs +++ b/src/WebExpress.WebCore.Test/TestConditionAlwaysFalse.cs @@ -13,7 +13,7 @@ public class TestConditionAlwaysFalse : ICondition /// /// The request. /// True if the condition is fulfilled, false otherwise. - public bool Fulfillment(Request request) + public bool Fulfillment(IRequest request) { return false; } diff --git a/src/WebExpress.WebCore.Test/TestEventHandlerA.cs b/src/WebExpress.WebCore.Test/TestEventHandlerA.cs index c93ae04..97c8fdf 100644 --- a/src/WebExpress.WebCore.Test/TestEventHandlerA.cs +++ b/src/WebExpress.WebCore.Test/TestEventHandlerA.cs @@ -19,13 +19,13 @@ public sealed class TestEventHandlerA : IEventHandler private TestEventHandlerA(IEventHandlerContext eventHandlerContext, IApplicationManager applicationManager) { // test the injection - if (eventHandlerContext == null) + if (eventHandlerContext is null) { throw new ArgumentNullException(nameof(eventHandlerContext), "Parameter cannot be null or empty."); } // test the injection - if (applicationManager == null) + if (applicationManager is null) { throw new ArgumentNullException(nameof(applicationManager), "Parameter cannot be null or empty."); } @@ -39,13 +39,13 @@ private TestEventHandlerA(IEventHandlerContext eventHandlerContext, IApplication public void Process(object sender, IEventArgument eventArgument) { // test the parameter - if (sender == null) + if (sender is null) { throw new ArgumentNullException(nameof(sender), "Parameter cannot be null or empty."); } // test the parameter - if (eventArgument == null) + if (eventArgument is null) { throw new ArgumentNullException(nameof(eventArgument), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestEventHandlerB.cs b/src/WebExpress.WebCore.Test/TestEventHandlerB.cs index 542d528..b375a70 100644 --- a/src/WebExpress.WebCore.Test/TestEventHandlerB.cs +++ b/src/WebExpress.WebCore.Test/TestEventHandlerB.cs @@ -19,13 +19,13 @@ public sealed class TestEventHandlerB : IEventHandler private TestEventHandlerB(IEventHandlerContext eventHandlerContext, IApplicationManager applicationManager) { // test the injection - if (eventHandlerContext == null) + if (eventHandlerContext is null) { throw new ArgumentNullException(nameof(eventHandlerContext), "Parameter cannot be null or empty."); } // test the injection - if (applicationManager == null) + if (applicationManager is null) { throw new ArgumentNullException(nameof(applicationManager), "Parameter cannot be null or empty."); } @@ -39,13 +39,13 @@ private TestEventHandlerB(IEventHandlerContext eventHandlerContext, IApplication public void Process(object sender, TestEventArgument eventArgument) { // test the parameter - if (sender == null) + if (sender is null) { throw new ArgumentNullException(nameof(sender), "Parameter cannot be null or empty."); } // test the parameter - if (eventArgument == null) + if (eventArgument is null) { throw new ArgumentNullException(nameof(eventArgument), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestFragmentA.cs b/src/WebExpress.WebCore.Test/TestFragmentA.cs index 8fb3a7a..0fda54e 100644 --- a/src/WebExpress.WebCore.Test/TestFragmentA.cs +++ b/src/WebExpress.WebCore.Test/TestFragmentA.cs @@ -30,13 +30,13 @@ public sealed class TestFragmentA : IFragment public TestFragmentA(IComponentHub componentHub, IFragmentContext fragmentContext, IComponentId id) { // test the injection - if (componentHub == null) + if (componentHub is null) { throw new ArgumentNullException(nameof(componentHub), "Parameter componentHub cannot be null or empty."); } // test the injection - if (fragmentContext == null) + if (fragmentContext is null) { throw new ArgumentNullException(nameof(fragmentContext), "Parameter fragmentContext cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestFragmentB.cs b/src/WebExpress.WebCore.Test/TestFragmentB.cs index 88a58c6..0f179d9 100644 --- a/src/WebExpress.WebCore.Test/TestFragmentB.cs +++ b/src/WebExpress.WebCore.Test/TestFragmentB.cs @@ -27,13 +27,13 @@ public sealed class TestFragmentB : IFragment public TestFragmentB(IComponentHub componentHub, IFragmentContext fragmentContext) { // test the injection - if (componentHub == null) + if (componentHub is null) { throw new ArgumentNullException(nameof(componentHub), "Parameter cannot be null or empty."); } // test the injection - if (fragmentContext == null) + if (fragmentContext is null) { throw new ArgumentNullException(nameof(fragmentContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestFragmentC.cs b/src/WebExpress.WebCore.Test/TestFragmentC.cs index 40be3f3..7f4281f 100644 --- a/src/WebExpress.WebCore.Test/TestFragmentC.cs +++ b/src/WebExpress.WebCore.Test/TestFragmentC.cs @@ -26,13 +26,13 @@ public sealed class TestFragmentC : IFragment public TestFragmentC(IComponentHub componentHub, IFragmentContext fragmentContext) { // test the injection - if (componentHub == null) + if (componentHub is null) { throw new ArgumentNullException(nameof(componentHub), "Parameter cannot be null or empty."); } // test the injection - if (fragmentContext == null) + if (fragmentContext is null) { throw new ArgumentNullException(nameof(fragmentContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestFragmentD.cs b/src/WebExpress.WebCore.Test/TestFragmentD.cs index 3a583fd..913287e 100644 --- a/src/WebExpress.WebCore.Test/TestFragmentD.cs +++ b/src/WebExpress.WebCore.Test/TestFragmentD.cs @@ -27,13 +27,13 @@ public sealed class TestFragmentD : IFragment public TestFragmentD(IComponentHub componentHub, IFragmentContext fragmentContext) { // test the injection - if (componentHub == null) + if (componentHub is null) { throw new ArgumentNullException(nameof(componentHub), "Parameter cannot be null or empty."); } // test the injection - if (fragmentContext == null) + if (fragmentContext is null) { throw new ArgumentNullException(nameof(fragmentContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestJobA.cs b/src/WebExpress.WebCore.Test/TestJobA.cs index 8794b38..1351db7 100644 --- a/src/WebExpress.WebCore.Test/TestJobA.cs +++ b/src/WebExpress.WebCore.Test/TestJobA.cs @@ -16,7 +16,7 @@ public sealed class TestJobA : IJob private TestJobA(IJobContext jobContext) { // test the injection - if (jobContext == null) + if (jobContext is null) { throw new ArgumentNullException(nameof(jobContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestParameterA.cs b/src/WebExpress.WebCore.Test/TestParameterA.cs index e77468b..9896cf7 100644 --- a/src/WebExpress.WebCore.Test/TestParameterA.cs +++ b/src/WebExpress.WebCore.Test/TestParameterA.cs @@ -1,19 +1,33 @@ -using WebExpress.WebCore.WebMessage; +using WebExpress.WebCore.WebParameter; namespace WebExpress.WebCore.Test { /// /// Represents a test parameter. /// - internal class TestParameterA : Parameter + internal class TestParameterA : IParameterStatic { + /// + /// Returns the key that uniquely identifies the parameter in configuration or + /// settings contexts. + /// + public static string Key => "testparametera"; + + /// + /// Returns or sets the scope of the parameter. + /// + public ParameterScope Scope { get; set; } + + /// + /// Returns the value of the parameter. + /// + public string Value { get; set; } + /// /// Initializes a new instance of the class. /// public TestParameterA() - : base("TestParameterA", null, ParameterScope.Url) { - } /// @@ -21,9 +35,8 @@ public TestParameterA() /// /// The value of the parameter. public TestParameterA(int value) - : base("TestParameterA", value, ParameterScope.Url) { - + Value = value.ToString(); } /// @@ -31,9 +44,20 @@ public TestParameterA(int value) /// /// The value of the parameter. public TestParameterA(Guid value) - : base("TestParameterA", value.ToString(), ParameterScope.Url) { + Value = value.ToString(); + } + /// + /// Retrieves the unique key associated with the current instance. + /// + /// + /// A string representing the unique key. This key is used for identifying + /// the instance in various operations. + /// + public string GetKey() + { + return Key; } } } diff --git a/src/WebExpress.WebCore.Test/TestParameterB.cs b/src/WebExpress.WebCore.Test/TestParameterB.cs new file mode 100644 index 0000000..a28e0da --- /dev/null +++ b/src/WebExpress.WebCore.Test/TestParameterB.cs @@ -0,0 +1,61 @@ +using WebExpress.WebCore.WebParameter; + +namespace WebExpress.WebCore.Test +{ + /// + /// Represents a test parameter. + /// + internal class TestParameterB : IParameterStatic + { + // + /// Returns the key that uniquely identifies the parameter in configuration or + /// settings contexts. + /// + public static string Key => "testparameterb"; + + /// + /// Returns or sets the scope of the parameter. + /// + public ParameterScope Scope { get; set; } + + /// + /// Returns the value of the parameter. + /// + public string Value { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public TestParameterB() + { + } + + /// + /// Initializes a new instance of the class with a specified value. + /// + /// The value of the parameter. + public TestParameterB(int value) + { + } + + /// + /// Initializes a new instance of the class with a specified value. + /// + /// The value of the parameter. + public TestParameterB(Guid value) + { + } + + /// + /// Retrieves the unique key associated with the current instance. + /// + /// + /// A string representing the unique key. This key is used for identifying + /// the instance in various operations. + /// + public string GetKey() + { + return Key; + } + } +} diff --git a/src/WebExpress.WebCore.Test/TestRenderContext.cs b/src/WebExpress.WebCore.Test/TestRenderContext.cs index 5b84420..35d6d4c 100644 --- a/src/WebExpress.WebCore.Test/TestRenderContext.cs +++ b/src/WebExpress.WebCore.Test/TestRenderContext.cs @@ -22,7 +22,7 @@ public class TestRenderContext : IRenderContext /// /// Returns the request. /// - public Request Request { get; protected set; } + public IRequest Request { get; protected set; } /// /// Initializes a new instance of the class. @@ -30,7 +30,7 @@ public class TestRenderContext : IRenderContext /// The endpoint associated with the rendering context. /// >The page context. /// The request associated with the rendering context. - public TestRenderContext(IEndpoint endpoint, IPageContext pageContext, Request request) + public TestRenderContext(IEndpoint endpoint, IPageContext pageContext, IRequest request) { Endpoint = endpoint; PageContext = pageContext; diff --git a/src/WebExpress.WebCore.Test/TestSocketA.cs b/src/WebExpress.WebCore.Test/TestSocketA.cs new file mode 100644 index 0000000..e3f70d5 --- /dev/null +++ b/src/WebExpress.WebCore.Test/TestSocketA.cs @@ -0,0 +1,45 @@ +using WebExpress.WebCore.WebSocket; + +namespace WebExpress.WebCore.Test +{ + /// + /// A dummy web socket for testing purposes. + /// + public sealed class TestSocketA : ISocket + { + private readonly ISocketContext _socketContext; + + /// + /// Initializes a new instance of the TestSocketA class using the specified + /// socket context and write stream. + /// + /// + /// The socket context that manages the state and configuration for the socket + /// connection. + /// + /// The connection id. + public TestSocketA(ISocketContext socketContext, Guid connectionId) + { + _socketContext = socketContext ?? throw new ArgumentNullException(nameof(socketContext), "Parameter cannot be null or empty."); + } + + /// + /// Handles logic to be executed when a new connection is established with the + /// socket server. + /// + /// The socket connection. + /// + /// A task that represents the asynchronous operation. + /// + public async Task OnConnectedAsync(ISocketConnection socketConnection) + { + } + + /// + /// Releases all resources used by the current instance of the class. + /// + public void Dispose() + { + } + } +} diff --git a/src/WebExpress.WebCore.Test/TestStatusPage301.cs b/src/WebExpress.WebCore.Test/TestStatusPage301.cs index ad41698..97617eb 100644 --- a/src/WebExpress.WebCore.Test/TestStatusPage301.cs +++ b/src/WebExpress.WebCore.Test/TestStatusPage301.cs @@ -20,7 +20,7 @@ public sealed class TestStatusPage301 : IStatusPage private TestStatusPage301(IStatusPageContext statusPageContext) { // test the injection - if (statusPageContext == null) + if (statusPageContext is null) { throw new ArgumentNullException(nameof(statusPageContext), "Parameter cannot be null or empty."); } @@ -34,7 +34,7 @@ private TestStatusPage301(IStatusPageContext statusPageContext) public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the parameter - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestStatusPage400.cs b/src/WebExpress.WebCore.Test/TestStatusPage400.cs index c005266..0243af1 100644 --- a/src/WebExpress.WebCore.Test/TestStatusPage400.cs +++ b/src/WebExpress.WebCore.Test/TestStatusPage400.cs @@ -27,13 +27,13 @@ public sealed class TestStatusPage400 : IStatusPage private TestStatusPage400(IStatusPageContext statusPageContext, StatusMessage message) { // test the injection - if (statusPageContext == null) + if (statusPageContext is null) { throw new ArgumentNullException(nameof(statusPageContext), "Parameter cannot be null or empty."); } // test the injection - if (message == null) + if (message is null) { throw new ArgumentNullException(nameof(message), "Parameter cannot be null or empty."); } @@ -49,7 +49,7 @@ private TestStatusPage400(IStatusPageContext statusPageContext, StatusMessage me public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the parameter - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestStatusPage404.cs b/src/WebExpress.WebCore.Test/TestStatusPage404.cs index ab0bea6..09165f4 100644 --- a/src/WebExpress.WebCore.Test/TestStatusPage404.cs +++ b/src/WebExpress.WebCore.Test/TestStatusPage404.cs @@ -20,7 +20,7 @@ public sealed class TestStatusPage404 : IStatusPage private TestStatusPage404(IStatusPageContext statusPageContext) { // test the injection - if (statusPageContext == null) + if (statusPageContext is null) { throw new ArgumentNullException(nameof(statusPageContext), "Parameter cannot be null or empty."); } @@ -34,7 +34,7 @@ private TestStatusPage404(IStatusPageContext statusPageContext) public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the parameter - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestStatusPage500.cs b/src/WebExpress.WebCore.Test/TestStatusPage500.cs index f283612..8d585fa 100644 --- a/src/WebExpress.WebCore.Test/TestStatusPage500.cs +++ b/src/WebExpress.WebCore.Test/TestStatusPage500.cs @@ -27,13 +27,13 @@ public sealed class TestStatusPage500 : IStatusPage private TestStatusPage500(IStatusPageContext statusPageContext, StatusMessage message) { // test the injection - if (statusPageContext == null) + if (statusPageContext is null) { throw new ArgumentNullException(nameof(statusPageContext), "Parameter cannot be null or empty."); } // test the injection - if (message == null) + if (message is null) { throw new ArgumentNullException(nameof(message), "Parameter cannot be null or empty."); } @@ -49,7 +49,7 @@ private TestStatusPage500(IStatusPageContext statusPageContext, StatusMessage me public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the parameter - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestTask.cs b/src/WebExpress.WebCore.Test/TestTask.cs index bfb27a3..5c7d70e 100644 --- a/src/WebExpress.WebCore.Test/TestTask.cs +++ b/src/WebExpress.WebCore.Test/TestTask.cs @@ -17,17 +17,17 @@ public TestTask(IComponentHub componentHub, string id, params object[] args) : base(id, args) { // test the injection - if (componentHub == null) + if (componentHub is null) { throw new ArgumentNullException(nameof(componentHub), "Parameter cannot be null or empty."); } - if (id == null) + if (id is null) { throw new ArgumentNullException(nameof(id), "Parameter cannot be null or empty."); } - if (args == null) + if (args is null) { throw new ArgumentNullException(nameof(args), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/TestVisualTree.cs b/src/WebExpress.WebCore.Test/TestVisualTree.cs index 4509ae9..bab7aa8 100644 --- a/src/WebExpress.WebCore.Test/TestVisualTree.cs +++ b/src/WebExpress.WebCore.Test/TestVisualTree.cs @@ -108,8 +108,10 @@ public virtual IHtmlNode Render(IVisualTreeContext context) html.Body.Add(Content); html.Body.Scripts = [.. Scripts.Values]; - html.Head.CssLinks = CssLinks.Where(x => x != null).Select(x => x.ToString()); - html.Head.ScriptLinks = HeaderScriptLinks?.Where(x => x != null).Select(x => x.ToString()); + html.Head.CssLinks = CssLinks.Where(x => x is not null) + .Select(x => x.ToString()); + html.Head.ScriptLinks = HeaderScriptLinks?.Where(x => x is not null) + .Select(x => x.ToString()); return html; } diff --git a/src/WebExpress.WebCore.Test/WWW/About.cs b/src/WebExpress.WebCore.Test/WWW/About.cs index a3326d4..5cdaff6 100644 --- a/src/WebExpress.WebCore.Test/WWW/About.cs +++ b/src/WebExpress.WebCore.Test/WWW/About.cs @@ -29,7 +29,7 @@ public About(IPageContext pageContext) PageContext = pageContext; // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -43,7 +43,7 @@ public About(IPageContext pageContext) public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Api/1/TestRestApiA.cs b/src/WebExpress.WebCore.Test/WWW/Api/_1_/TestRestApiA.cs similarity index 92% rename from src/WebExpress.WebCore.Test/WWW/Api/1/TestRestApiA.cs rename to src/WebExpress.WebCore.Test/WWW/Api/_1_/TestRestApiA.cs index 769e67a..4d110b4 100644 --- a/src/WebExpress.WebCore.Test/WWW/Api/1/TestRestApiA.cs +++ b/src/WebExpress.WebCore.Test/WWW/Api/_1_/TestRestApiA.cs @@ -3,13 +3,11 @@ using WebExpress.WebCore.WebRestApi; using WebExpress.WebCore.WebStatusPage; -namespace WebExpress.WebCore.Test.WWW.Api._1 +namespace WebExpress.WebCore.Test.WWW.Api._1_ { /// /// A dummy class for testing purposes. /// - [Method(CrudMethod.POST)] - [Method(CrudMethod.GET)] public sealed class TestRestApiA : IRestApi { /// @@ -19,7 +17,7 @@ public sealed class TestRestApiA : IRestApi public TestRestApiA(IRestApiContext restApiContext) { // test the injection - if (restApiContext == null) + if (restApiContext is null) { throw new ArgumentNullException(nameof(restApiContext), "Parameter cannot be null or empty."); } @@ -40,6 +38,7 @@ public Response CreateData(Request request) /// /// The request. /// The response containing the result of the operation. + [Method(RequestMethod.GET)] public Response GetData(Request request) { return new ResponseBadRequest(new StatusMessage("Not implemented.")); @@ -50,10 +49,11 @@ public Response GetData(Request request) /// /// The request. /// The response containing the result of the operation. + [Method(RequestMethod.POST)] public Response UpdateData(Request request) { // test the request - if (request == null) + if (request is null) { throw new ArgumentNullException(nameof(request), "Parameter cannot be null or empty."); } @@ -69,7 +69,7 @@ public Response UpdateData(Request request) public Response DeleteData(Request request) { // test the request - if (request == null) + if (request is null) { throw new ArgumentNullException(nameof(request), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Api/2/TestRestApiB.cs b/src/WebExpress.WebCore.Test/WWW/Api/_2/TestRestApiB.cs similarity index 95% rename from src/WebExpress.WebCore.Test/WWW/Api/2/TestRestApiB.cs rename to src/WebExpress.WebCore.Test/WWW/Api/_2/TestRestApiB.cs index d243db1..d5ef343 100644 --- a/src/WebExpress.WebCore.Test/WWW/Api/2/TestRestApiB.cs +++ b/src/WebExpress.WebCore.Test/WWW/Api/_2/TestRestApiB.cs @@ -8,7 +8,6 @@ namespace WebExpress.WebCore.Test.WWW.Api._2 /// /// A dummy class for testing purposes. /// - [Method(CrudMethod.GET)] public sealed class TestRestApiB : IRestApi { /// @@ -18,7 +17,7 @@ public sealed class TestRestApiB : IRestApi public TestRestApiB(IRestApiContext restApiContext) { // test the injection - if (restApiContext == null) + if (restApiContext is null) { throw new ArgumentNullException(nameof(restApiContext), "Parameter cannot be null or empty."); } @@ -39,6 +38,7 @@ public Response CreateData(Request request) /// /// The request. /// The response containing the result of the operation. + [Method(RequestMethod.GET)] public Response GetData(Request request) { return new ResponseBadRequest(new StatusMessage("Not implemented.")); @@ -52,7 +52,7 @@ public Response GetData(Request request) public Response UpdateData(Request request) { // test the request - if (request == null) + if (request is null) { throw new ArgumentNullException(nameof(request), "Parameter cannot be null or empty."); } @@ -68,7 +68,7 @@ public Response UpdateData(Request request) public Response DeleteData(Request request) { // test the request - if (request == null) + if (request is null) { throw new ArgumentNullException(nameof(request), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Api/3/TestRestApiC.cs b/src/WebExpress.WebCore.Test/WWW/Api/_3_/TestRestApiC.cs similarity index 78% rename from src/WebExpress.WebCore.Test/WWW/Api/3/TestRestApiC.cs rename to src/WebExpress.WebCore.Test/WWW/Api/_3_/TestRestApiC.cs index 9b499f1..67c5c75 100644 --- a/src/WebExpress.WebCore.Test/WWW/Api/3/TestRestApiC.cs +++ b/src/WebExpress.WebCore.Test/WWW/Api/_3_/TestRestApiC.cs @@ -4,13 +4,12 @@ using WebExpress.WebCore.WebRestApi; using WebExpress.WebCore.WebStatusPage; -namespace WebExpress.WebCore.Test.WWW.Api._3 +namespace WebExpress.WebCore.Test.WWW.Api.V3 { /// /// A dummy class for testing purposes. /// - [Method(CrudMethod.GET)] - public sealed class TestRestApiC : RestApi + public sealed class TestRestApiC : IRestApi { /// /// Initialization of the rest api resource. Here, for example, managed resources can be loaded. @@ -18,16 +17,15 @@ public sealed class TestRestApiC : RestApi /// The component hub. /// The context of the restapi resource. public TestRestApiC(IComponentHub componentHub, IRestApiContext restApiContext) - : base(restApiContext) { // test the injection - if (componentHub == null) + if (componentHub is null) { throw new ArgumentNullException(nameof(componentHub), "Parameter cannot be null or empty."); } // test the injection - if (restApiContext == null) + if (restApiContext is null) { throw new ArgumentNullException(nameof(restApiContext), "Parameter cannot be null or empty."); } @@ -38,7 +36,7 @@ public TestRestApiC(IComponentHub componentHub, IRestApiContext restApiContext) /// /// The request. /// The response containing the result of the operation. - public override Response CreateData(Request request) + public Response CreateData(Request request) { return new ResponseBadRequest(new StatusMessage("Not implemented.")); } @@ -48,7 +46,8 @@ public override Response CreateData(Request request) /// /// The request. /// The response containing the result of the operation. - public override Response GetData(Request request) + [Method(RequestMethod.GET)] + public Response GetData(Request request) { return new ResponseBadRequest(new StatusMessage("Not implemented.")); } @@ -58,10 +57,10 @@ public override Response GetData(Request request) /// /// The request. /// The response containing the result of the operation. - public override Response UpdateData(Request request) + public Response UpdateData(Request request) { // test the request - if (request == null) + if (request is null) { throw new ArgumentNullException(nameof(request), "Parameter cannot be null or empty."); } @@ -74,22 +73,15 @@ public override Response UpdateData(Request request) /// /// The request. /// The response containing the result of the operation. - public override Response DeleteData(Request request) + public Response DeleteData(Request request) { // test the request - if (request == null) + if (request is null) { throw new ArgumentNullException(nameof(request), "Parameter cannot be null or empty."); } return new ResponseBadRequest(new StatusMessage("Not implemented.")); } - - /// - /// Release of unmanaged resources reserved during use. - /// - public override void Dispose() - { - } } } diff --git a/src/WebExpress.WebCore.Test/WWW/Blog/Index.cs b/src/WebExpress.WebCore.Test/WWW/Blog/Index.cs index ae60598..1115178 100644 --- a/src/WebExpress.WebCore.Test/WWW/Blog/Index.cs +++ b/src/WebExpress.WebCore.Test/WWW/Blog/Index.cs @@ -16,7 +16,7 @@ public sealed class Index : Page private Index(IPageContext pageContext) { // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -30,13 +30,13 @@ private Index(IPageContext pageContext) public override void Process(IRenderContext renderContext, TestVisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } // test the visualTree - if (visualTree == null) + if (visualTree is null) { throw new ArgumentNullException(nameof(visualTree), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Blog/Post/Add.cs b/src/WebExpress.WebCore.Test/WWW/Blog/Post/Add.cs index 6f74a0c..413ef93 100644 --- a/src/WebExpress.WebCore.Test/WWW/Blog/Post/Add.cs +++ b/src/WebExpress.WebCore.Test/WWW/Blog/Post/Add.cs @@ -16,7 +16,7 @@ public sealed class Add : Page private Add(IPageContext pageContext) { // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -30,13 +30,13 @@ private Add(IPageContext pageContext) public override void Process(IRenderContext renderContext, TestVisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } // test the visualTree - if (visualTree == null) + if (visualTree is null) { throw new ArgumentNullException(nameof(visualTree), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Blog/Post/Index.cs b/src/WebExpress.WebCore.Test/WWW/Blog/Post/Index.cs index f9f9098..39cb3be 100644 --- a/src/WebExpress.WebCore.Test/WWW/Blog/Post/Index.cs +++ b/src/WebExpress.WebCore.Test/WWW/Blog/Post/Index.cs @@ -16,7 +16,7 @@ public sealed class Index : Page private Index(IPageContext pageContext) { // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -30,13 +30,13 @@ private Index(IPageContext pageContext) public override void Process(IRenderContext renderContext, TestVisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } // test the visualTree - if (visualTree == null) + if (visualTree is null) { throw new ArgumentNullException(nameof(visualTree), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Blog/Post/PostId/Edit.cs b/src/WebExpress.WebCore.Test/WWW/Blog/Post/PostId/Edit.cs index 67384d7..69bf174 100644 --- a/src/WebExpress.WebCore.Test/WWW/Blog/Post/PostId/Edit.cs +++ b/src/WebExpress.WebCore.Test/WWW/Blog/Post/PostId/Edit.cs @@ -28,7 +28,7 @@ public Edit(IPageContext pageContext) PageContext = pageContext; // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -42,7 +42,7 @@ public Edit(IPageContext pageContext) public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Blog/Post/PostId/Index.cs b/src/WebExpress.WebCore.Test/WWW/Blog/Post/PostId/Index.cs index 130a23d..40123bb 100644 --- a/src/WebExpress.WebCore.Test/WWW/Blog/Post/PostId/Index.cs +++ b/src/WebExpress.WebCore.Test/WWW/Blog/Post/PostId/Index.cs @@ -7,7 +7,7 @@ namespace WebExpress.WebCore.Test.WWW.Blog.Post.PostId /// A dummy class for testing purposes. /// [Title("webindex:index.label")] - [SegmentGuid("segment")] + [SegmentGuid()] public sealed class Index : IPage { /// @@ -29,7 +29,7 @@ public Index(IPageContext pageContext) PageContext = pageContext; // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -43,7 +43,7 @@ public Index(IPageContext pageContext) public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Contact.cs b/src/WebExpress.WebCore.Test/WWW/Contact.cs index 3697d8e..132ca95 100644 --- a/src/WebExpress.WebCore.Test/WWW/Contact.cs +++ b/src/WebExpress.WebCore.Test/WWW/Contact.cs @@ -16,7 +16,7 @@ public sealed class Contact : Page private Contact(IPageContext pageContext) { // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -30,13 +30,13 @@ private Contact(IPageContext pageContext) public override void Process(IRenderContext renderContext, TestVisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } // test the visualTree - if (visualTree == null) + if (visualTree is null) { throw new ArgumentNullException(nameof(visualTree), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Index.cs b/src/WebExpress.WebCore.Test/WWW/Index.cs index cfc2e40..465d0a5 100644 --- a/src/WebExpress.WebCore.Test/WWW/Index.cs +++ b/src/WebExpress.WebCore.Test/WWW/Index.cs @@ -28,7 +28,7 @@ public Index(IPageContext pageContext) PageContext = pageContext; // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -42,7 +42,7 @@ public Index(IPageContext pageContext) public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Products/Details/Index.cs b/src/WebExpress.WebCore.Test/WWW/Products/Details/Index.cs index edba4ad..85bd346 100644 --- a/src/WebExpress.WebCore.Test/WWW/Products/Details/Index.cs +++ b/src/WebExpress.WebCore.Test/WWW/Products/Details/Index.cs @@ -7,7 +7,7 @@ namespace WebExpress.WebCore.Test.WWW.Products.Details /// A dummy class for testing purposes. /// [Title("webindex:index.label")] - [SegmentGuid("")] + [SegmentGuid()] public sealed class Index : IPage { /// @@ -29,7 +29,7 @@ public Index(IPageContext pageContext) PageContext = pageContext; // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -43,7 +43,7 @@ public Index(IPageContext pageContext) public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Products/Index.cs b/src/WebExpress.WebCore.Test/WWW/Products/Index.cs index 4349317..e5e0d65 100644 --- a/src/WebExpress.WebCore.Test/WWW/Products/Index.cs +++ b/src/WebExpress.WebCore.Test/WWW/Products/Index.cs @@ -6,6 +6,7 @@ namespace WebExpress.WebCore.Test.WWW.Products /// /// A dummy class for testing purposes. /// + [SegmentHidden] [Title("webindex:index.label")] public sealed class Index : IPage { @@ -28,7 +29,7 @@ public Index(IPageContext pageContext) PageContext = pageContext; // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -42,7 +43,7 @@ public Index(IPageContext pageContext) public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Products/List.cs b/src/WebExpress.WebCore.Test/WWW/Products/List.cs index 089f34e..9ef0163 100644 --- a/src/WebExpress.WebCore.Test/WWW/Products/List.cs +++ b/src/WebExpress.WebCore.Test/WWW/Products/List.cs @@ -28,7 +28,7 @@ public List(IPageContext pageContext) PageContext = pageContext; // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -42,7 +42,7 @@ public List(IPageContext pageContext) public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceA.cs b/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceA.cs index 4f65bf6..50f6685 100644 --- a/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceA.cs +++ b/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceA.cs @@ -15,7 +15,7 @@ public sealed class TestResourceA : IResource public TestResourceA(IResourceContext resourceContext) { // test the injection - if (resourceContext == null) + if (resourceContext is null) { throw new ArgumentNullException(nameof(resourceContext), "Parameter cannot be null or empty."); } @@ -26,10 +26,10 @@ public TestResourceA(IResourceContext resourceContext) /// /// The request. /// The processed response. - public Response Process(Request request) + public IResponse Process(IRequest request) { // test the request - if (request == null) + if (request is null) { throw new ArgumentNullException(nameof(request), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceB.cs b/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceB.cs index ab0c2b4..e2d5ac8 100644 --- a/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceB.cs +++ b/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceB.cs @@ -20,10 +20,10 @@ public TestResourceB() /// /// The request. /// The processed response. - public Response Process(Request request) + public IResponse Process(IRequest request) { // test the request - if (request == null) + if (request is null) { throw new ArgumentNullException(nameof(request), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceC.cs b/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceC.cs index 538e866..71990fd 100644 --- a/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceC.cs +++ b/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceC.cs @@ -16,13 +16,13 @@ public sealed class TestResourceC : IResource public TestResourceC(IResourceManager resourceManager, IResourceContext resourceContext) { // test the injection - if (resourceManager == null) + if (resourceManager is null) { throw new ArgumentNullException(nameof(resourceManager), "Parameter cannot be null or empty."); } // test the injection - if (resourceContext == null) + if (resourceContext is null) { throw new ArgumentNullException(nameof(resourceContext), "Parameter cannot be null or empty."); } @@ -33,10 +33,10 @@ public TestResourceC(IResourceManager resourceManager, IResourceContext resource /// /// The request. /// The processed response. - public Response Process(Request request) + public IResponse Process(IRequest request) { // test the request - if (request == null) + if (request is null) { throw new ArgumentNullException(nameof(request), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceD.cs b/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceD.cs index 7cc397c..001a328 100644 --- a/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceD.cs +++ b/src/WebExpress.WebCore.Test/WWW/Resources/TestResourceD.cs @@ -16,13 +16,13 @@ public sealed class TestResourceD : IResource public TestResourceD(IResourceContext resourceContext, IResourceManager resourceManager) { // test the injection - if (resourceManager == null) + if (resourceManager is null) { throw new ArgumentNullException(nameof(resourceManager), "Parameter cannot be null or empty."); } // test the injection - if (resourceContext == null) + if (resourceContext is null) { throw new ArgumentNullException(nameof(resourceContext), "Parameter cannot be null or empty."); } @@ -33,10 +33,10 @@ public TestResourceD(IResourceContext resourceContext, IResourceManager resource /// /// The request. /// The processed response. - public Response Process(Request request) + public IResponse Process(IRequest request) { // test the request - if (request == null) + if (request is null) { throw new ArgumentNullException(nameof(request), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Settings/TestSettingPageA.cs b/src/WebExpress.WebCore.Test/WWW/Settings/TestSettingPageA.cs index 81340d0..e3e12a8 100644 --- a/src/WebExpress.WebCore.Test/WWW/Settings/TestSettingPageA.cs +++ b/src/WebExpress.WebCore.Test/WWW/Settings/TestSettingPageA.cs @@ -26,7 +26,7 @@ public TestSettingPageA(ISettingPageContext pageContext) PageContext = pageContext; // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -40,7 +40,7 @@ public TestSettingPageA(ISettingPageContext pageContext) public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Settings/TestSettingPageB.cs b/src/WebExpress.WebCore.Test/WWW/Settings/TestSettingPageB.cs index 4530e2a..81a9582 100644 --- a/src/WebExpress.WebCore.Test/WWW/Settings/TestSettingPageB.cs +++ b/src/WebExpress.WebCore.Test/WWW/Settings/TestSettingPageB.cs @@ -26,7 +26,7 @@ public TestSettingPageB(ISettingPageContext pageContext) PageContext = pageContext; // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -40,7 +40,7 @@ public TestSettingPageB(ISettingPageContext pageContext) public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WWW/Settings/TestSettingPageC.cs b/src/WebExpress.WebCore.Test/WWW/Settings/TestSettingPageC.cs index 3f29a23..0e54eb0 100644 --- a/src/WebExpress.WebCore.Test/WWW/Settings/TestSettingPageC.cs +++ b/src/WebExpress.WebCore.Test/WWW/Settings/TestSettingPageC.cs @@ -24,7 +24,7 @@ public TestSettingPageC(ISettingPageContext pageContext) PageContext = pageContext; // test the injection - if (pageContext == null) + if (pageContext is null) { throw new ArgumentNullException(nameof(pageContext), "Parameter cannot be null or empty."); } @@ -38,7 +38,7 @@ public TestSettingPageC(ISettingPageContext pageContext) public void Process(IRenderContext renderContext, VisualTree visualTree) { // test the context - if (renderContext == null) + if (renderContext is null) { throw new ArgumentNullException(nameof(renderContext), "Parameter cannot be null or empty."); } diff --git a/src/WebExpress.WebCore.Test/WebExpress.WebCore.Test.csproj b/src/WebExpress.WebCore.Test/WebExpress.WebCore.Test.csproj index a2ce933..ee13ee0 100644 --- a/src/WebExpress.WebCore.Test/WebExpress.WebCore.Test.csproj +++ b/src/WebExpress.WebCore.Test/WebExpress.WebCore.Test.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable disable @@ -41,9 +41,9 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/WebExpress.WebCore.Test/Uri/UnitTestUri.cs b/src/WebExpress.WebCore.Test/WebUri/UnitTestUri.cs similarity index 52% rename from src/WebExpress.WebCore.Test/Uri/UnitTestUri.cs rename to src/WebExpress.WebCore.Test/WebUri/UnitTestUri.cs index cb5e36a..24cd78a 100644 --- a/src/WebExpress.WebCore.Test/Uri/UnitTestUri.cs +++ b/src/WebExpress.WebCore.Test/WebUri/UnitTestUri.cs @@ -1,7 +1,9 @@ -using WebExpress.WebCore.WebEndpoint; +using WebExpress.WebCore.Test.Fixture; +using WebExpress.WebCore.Test.WWW; +using WebExpress.WebCore.WebEndpoint; using WebExpress.WebCore.WebUri; -namespace WebExpress.WebCore.Test.Uri +namespace WebExpress.WebCore.Test.WebUri { /// /// Tests an uri. @@ -23,15 +25,16 @@ public class UnitTestUri [InlineData(UriScheme.Http, "example.com", "user", "80", "/abc", "a=1&b=2", "fragment", "http://user@example.com/abc?a=1&b=2#fragment")] public void UriAbsolute(UriScheme scheme, string authority, string user, string port, string path, string query, string fragment, string expected) { - // preconditions - var uriUser = user != null ? user + "@" : ""; - var uriPort = port != null ? ":" + port : null; - var uriQuery = query != null ? "?" + query : ""; - var uriFragment = fragment != null ? "#" + fragment : null; + // arrange + var uriUser = user is not null ? user + "@" : ""; + var uriPort = port is not null ? ":" + port : null; + var uriQuery = query is not null ? "?" + query : ""; + var uriFragment = fragment is not null ? "#" + fragment : null; - // test execution + // act var uri = new UriEndpoint($"{scheme}://{uriUser}{authority}{uriPort}{path}{uriQuery}{uriFragment}"); + // validation Assert.Equal(expected, uri.ToString()); Assert.Equal(scheme, uri.Scheme); Assert.Equal(authority, uri.Authority.Host); @@ -60,13 +63,14 @@ public void UriAbsolute(UriScheme scheme, string authority, string user, string [InlineData("/assets/img/example.svg", null, null, "/assets/img/example.svg")] public void UriRelative(string path, string query, string fragment, string expected) { - // preconditions - var uriQuery = query != null ? "?" + query : ""; - var uriFragment = fragment != null ? "#" + fragment : null; + // arrange + var uriQuery = query is not null ? "?" + query : ""; + var uriFragment = fragment is not null ? "#" + fragment : null; - // test execution + // act var uri = new UriEndpoint($"{path}{uriQuery}{uriFragment}"); + // validation Assert.Equal(expected, uri.ToString()); Assert.Equal(path, !string.IsNullOrWhiteSpace(path) ? "/" + string.Join("/", uri.PathSegments.Skip(1)) @@ -85,16 +89,36 @@ public void UriRelative(string path, string query, string fragment, string expec [InlineData("/a/b/c", "/d/e/f", "/a/b/c/d/e/f", 7)] public void Concat(string path, string segment, string expected, int count) { - // preconditions + // arrange var uri = new UriEndpoint(path); - // test execution + // act var concat = uri.Concat(segment); + // validation Assert.Equal(expected, concat.ToString()); Assert.Equal(count, concat.PathSegments.Count()); } + /// + /// Test the concat method. + /// + [Theory] + [InlineData(null, null, "/")] + [InlineData("a", null, "/?a=")] + [InlineData("b", "x", "/?b=x")] + public void ConcatQuery(string key, string value, string expected) + { + // arrange + var uri = new UriEndpoint(); + + // act + var concat = uri.Concat(key is not null ? new UriQuery(key, value) : null); + + // validation + Assert.Equal(expected, concat.ToString()); + } + /// /// Test the skip method. /// @@ -107,12 +131,13 @@ public void Concat(string path, string segment, string expected, int count) [InlineData("/a/b/c", 5, null)] public void Skip(string path, int skipCount, string expected) { - // preconditions + // arrange var uri = new UriEndpoint(path); - // test execution + // act var skip = uri.Skip(skipCount); + // validation Assert.Equal(expected, skip?.ToString()); } @@ -133,12 +158,36 @@ public void Skip(string path, int skipCount, string expected) [InlineData("/a/b/c", -5, null)] public void Take(string path, int takeCount, string expected) { - // preconditions + // arrange var uri = new UriEndpoint(path); - // test execution + // act var take = uri.Take(takeCount); + // validation + Assert.Equal(expected, take?.ToString()); + } + + /// + /// Test the take last method. + /// + [Theory] + [InlineData("/a/b/c", 0, "/a/b/c")] + [InlineData("/a/b/c", 1, "/c")] + [InlineData("/a/b/c", 2, "/b/c")] + [InlineData("/a/b/c", 3, "/a/b/c")] + [InlineData("/a/b/c", 4, "/a/b/c")] + [InlineData("/a/b/c", 5, "/a/b/c")] + [InlineData("/a/b/c", -5, "/a/b/c")] + public void TakeLast(string path, int takeCount, string expected) + { + // arrange + var uri = new UriEndpoint(path); + + // act + var take = uri.TakeLast(takeCount); + + // validation Assert.Equal(expected, take?.ToString()); } @@ -152,7 +201,7 @@ public void Take(string path, int takeCount, string expected) [InlineData("http://www.example.com/a/$guid/c", "/a/$guid/c", "http://www.example.com/a/$guid/c")] public void Merge(string uri, string route, string expected) { - // preconditions + // arrange var random = Guid.NewGuid().ToString(); var uriEndpoint = new UriEndpoint(uri.Replace("$guid", random)); var routeEndpoint = new RouteEndpoint @@ -160,14 +209,15 @@ public void Merge(string uri, string route, string expected) [.. route.Split('/').Select ( x => (IUriPathSegment)(x == "$guid" - ? new UriPathSegmentVariableGuid("guid") { Value = random } + ? new UriPathSegmentVariableGuid("guid") { Value = random } : new UriPathSegmentConstant(x)) )] ); - // test execution + // act var resourceUri = new UriEndpoint(uriEndpoint, routeEndpoint.PathSegments); + // validation Assert.Equal(expected.Replace("$guid", random), resourceUri?.ToString()); } @@ -179,17 +229,19 @@ [.. route.Split('/').Select [InlineData("http://user@example.com/a/b/c/x/y/z", "http://user@example.com/a/b/c", "http://user@example.com/a/b/c")] public void BasePath(string uri, string baseUri, string expected) { + // act var resourceUri = new UriEndpoint(uri) { BasePath = new UriEndpoint(baseUri) }; + // validation Assert.Equal(uri, resourceUri.ToString()); Assert.Equal(expected, resourceUri.BasePath.ToString()); } /// - /// Test the setfragment method. + /// Test the SetFragment method. /// [Theory] [InlineData("http://user@example.com/x", null, "http://user@example.com/x")] @@ -198,16 +250,145 @@ public void BasePath(string uri, string baseUri, string expected) [InlineData("http://user@example.com/a/b/c", "myfragment", "http://user@example.com/a/b/c#myfragment")] public void SetFragment(string uri, string fragment, string expected) { - // preconditions + // arrange var resourceUri = (IUri)new UriEndpoint(uri) { }; - // test execution + // act resourceUri = resourceUri.SetFragment(fragment); // validation Assert.Equal(expected, resourceUri.ToString()); } + + /// + /// Test the GetDisplayText method. + /// + [Theory] + [InlineData(typeof(TestApplicationA), typeof(WWW.Index), null)] + [InlineData(typeof(TestApplicationA), typeof(About), null)] + [InlineData(typeof(TestApplicationA), typeof(Contact), null)] + [InlineData(typeof(TestApplicationB), typeof(WWW.Index), null)] + [InlineData(typeof(TestApplicationB), typeof(About), null)] + [InlineData(typeof(TestApplicationB), typeof(Contact), null)] + [InlineData(typeof(TestApplicationC), typeof(WWW.Index), null)] + [InlineData(typeof(TestApplicationC), typeof(About), null)] + [InlineData(typeof(TestApplicationC), typeof(Contact), null)] + public void GetDisplayText(Type applicationType, Type resourceType, string expected) + { + // arrange + var componentHub = UnitTestFixture.CreateAndRegisterComponentHubMock(); + var application = componentHub.ApplicationManager.GetApplications(applicationType)?.FirstOrDefault(); + componentHub.SitemapManager.Refresh(); + var page = componentHub.PageManager.GetPages(resourceType, application)?.FirstOrDefault(); + var endpoint = componentHub.SitemapManager.GetEndpoint(page.Route.ToUri()); + var renderContext = UnitTestFixture.CrerateRenderContextMock(application); + + // act + var display = endpoint.Route.ToUri().GetDisplayText(renderContext); + + // validation + Assert.Equal(expected, display); + } + + /// + /// Tests the BindParameters method to verify that parameters are correctly + /// bound to the URI endpoint. + /// + [Fact] + public void BindParametersSingle() + { + // arrange + var pathSegments = new IUriPathSegment[] + { + new UriPathSegmentConstant("a"), + new UriPathSegmentVariableGuid("testparametera"), + new UriPathSegmentConstant("b") + }; + + var uri = new UriEndpoint(pathSegments); + var parameter = new TestParameterA() + { + Value = "CFABEA8C-4223-4E17-AB31-C8FA4454B745" + }; + + // act + var bind = uri.BindParameters(parameter); + + // validation + Assert.Equal("/a/${testparametera}/b", uri.ToString()); + Assert.Equal("/a/CFABEA8C-4223-4E17-AB31-C8FA4454B745/b", bind.ToString()); + } + + /// + /// Tests the BindParameters method to verify that parameters are correctly + /// bound to the URI endpoint. + /// + [Fact] + public void BindParametersMultible() + { + // arrange + var pathSegments = new IUriPathSegment[] + { + new UriPathSegmentConstant("a"), + new UriPathSegmentVariableInt(), + new UriPathSegmentConstant("b"), + new UriPathSegmentVariableInt() + }; + + var uri = new UriEndpoint(pathSegments); + var parameter1 = new TestParameterA() + { + Value = "10" + }; + + var parameter2 = new TestParameterB() + { + Value = "20" + }; + + // act + var bind = uri.BindParameters(parameter1, parameter2); + + // validation + Assert.Equal("/a/${testparametera}/b/${testparameterb}", uri.ToString()); + Assert.Equal("/a/10/b/20", bind.ToString()); + } + + /// + /// Tests the BindParameters method to verify that parameters are correctly + /// bound to the URI endpoint. + /// + [Fact] + public void BindParametersQuery() + { + // arrange + var pathSegments = new IUriPathSegment[] + { + new UriPathSegmentConstant("a"), + new UriPathSegmentVariableInt(), + new UriPathSegmentConstant("b") + }; + + var uri = new UriEndpoint(pathSegments) + .Add(new UriQuery()); + var parameter1 = new TestParameterA() + { + Value = "10" + }; + + var parameter2 = new TestParameterB() + { + Value = "20" + }; + + // act + var bind = uri.BindParameters(parameter1, parameter2); + + // validation + Assert.Equal("/a/${testparametera}/b?testparameterb=", uri.ToString()); + Assert.Equal("/a/10/b?testparameterb=20", bind.ToString()); + } } } diff --git a/src/WebExpress.WebCore/ArgumentParser.cs b/src/WebExpress.WebCore/ArgumentParser.cs index 8b473f7..9ba11f7 100644 --- a/src/WebExpress.WebCore/ArgumentParser.cs +++ b/src/WebExpress.WebCore/ArgumentParser.cs @@ -78,7 +78,7 @@ where x.FullName.Equals(key[1..], StringComparison.OrdinalIgnoreCase) || x.ShortName.Equals(key[1..], StringComparison.OrdinalIgnoreCase) select x).FirstOrDefault(); - if (command != null) + if (command is not null) { argsDict.Add(command.FullName.ToLower(), value.Trim()); } @@ -100,7 +100,7 @@ where x.FullName.Equals(key[1..], StringComparison.OrdinalIgnoreCase) || x.ShortName.Equals(key[1..], StringComparison.OrdinalIgnoreCase) select x).FirstOrDefault(); - if (command != null) + if (command is not null) { argsDict.Add(command.FullName.ToLower(), value.Trim()); } diff --git a/src/WebExpress.WebCore/HttpServer.cs b/src/WebExpress.WebCore/HttpServer.cs index b91e0f5..e5fbcf5 100644 --- a/src/WebExpress.WebCore/HttpServer.cs +++ b/src/WebExpress.WebCore/HttpServer.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -7,21 +7,25 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Net; using System.Net.Sockets; +using System.Reflection; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using WebExpress.WebCore.Config; using WebExpress.WebCore.Internationalization; using WebExpress.WebCore.WebEndpoint; -using WebExpress.WebCore.WebHtml; using WebExpress.WebCore.WebLog; using WebExpress.WebCore.WebMessage; +using WebExpress.WebCore.WebParameter; using WebExpress.WebCore.WebSitemap; +using WebExpress.WebCore.WebSocket; +using WebExpress.WebCore.WebStatusPage; using WebExpress.WebCore.WebUri; namespace WebExpress.WebCore @@ -29,7 +33,7 @@ namespace WebExpress.WebCore /// /// The web server for processing http requests (see RFC 2616). The web server uses Kestrel internally. /// - public class HttpServer : IHost, IHttpApplication + public class HttpServer : IHost, IHttpApplication { /// /// Event is triggered after the web server is started. @@ -44,10 +48,10 @@ public class HttpServer : IHost, IHttpApplication /// /// Server thread termination. /// - private CancellationToken ServerToken { get; } = new CancellationToken(); + private CancellationTokenSource ServerTokenSource { get; } = new CancellationTokenSource(); /// - /// Returns or sets the configuration + /// Returns or sets the configuration. /// public HttpServerConfig Config { get; set; } @@ -71,10 +75,25 @@ public class HttpServer : IHost, IHttpApplication /// public long RequestNumber { get; private set; } + /// + /// Returns the statistics history. + /// + public static List Statistics { get; } = []; + + /// + /// Synchronization object for statistics. + /// + private static readonly Lock _statLock = new(); + + // Variables for CPU usage calculation + private static DateTime _lastCpuTime = DateTime.UtcNow; + private static TimeSpan _lastProcessorTime = Process.GetCurrentProcess().TotalProcessorTime; + private static readonly Process _currentProcess = Process.GetCurrentProcess(); + /// /// Initializes a new instance of the class. /// - /// Der Serverkontext. + /// The server context. public HttpServer(HttpServerContext context) { HttpServerContext = new HttpServerContext @@ -109,7 +128,10 @@ public void Start() } var logger = new LogFactory(); - var transportOptions = new OptionsWrapper(new SocketTransportOptions()); + var transportOptions = new OptionsWrapper + ( + new SocketTransportOptions() + ); var transport = new SocketTransportFactory(transportOptions, logger); var serviceCollection = new ServiceCollection(); @@ -119,10 +141,13 @@ public void Start() x.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); x.AddProvider(logger); }); - serviceCollection.AddHttpLogging(x => - { - x.LoggingFields = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All; - }); + serviceCollection.AddHttpLogging + ( + x => + { + x.LoggingFields = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All; + } + ); var serverOptions = new OptionsWrapper(new KestrelServerOptions() { @@ -130,11 +155,14 @@ public void Start() AllowResponseHeaderCompression = true, AddServerHeader = true, ApplicationServices = serviceCollection.BuildServiceProvider() - }); - serverOptions.Value.Limits.MaxConcurrentConnections = Config?.Limit?.ConnectionLimit > 0 ? Config?.Limit?.ConnectionLimit : serverOptions.Value.Limits.MaxConcurrentConnections; - serverOptions.Value.Limits.MaxRequestBodySize = Config?.Limit?.UploadLimit > 0 ? Config?.Limit?.UploadLimit : serverOptions.Value.Limits.MaxRequestBodySize; + serverOptions.Value.Limits.MaxConcurrentConnections = Config?.Limit?.ConnectionLimit > 0 + ? Config?.Limit?.ConnectionLimit + : serverOptions.Value.Limits.MaxConcurrentConnections; + serverOptions.Value.Limits.MaxRequestBodySize = Config?.Limit?.UploadLimit > 0 + ? Config?.Limit?.UploadLimit + : serverOptions.Value.Limits.MaxRequestBodySize; foreach (var endpoint in Config.Endpoints) { @@ -142,10 +170,13 @@ public void Start() } Kestrel = new KestrelServer(serverOptions, transport, logger); + Kestrel.StartAsync(this, ServerTokenSource.Token); - Kestrel.StartAsync(this, ServerToken); - - HttpServerContext.Log.Info(message: I18N.Translate("webexpress.webcore:httpserver.start"), args: [ExecutionTime.ToShortDateString(), ExecutionTime.ToLongTimeString()]); + HttpServerContext.Log.Info(message: I18N.Translate + ( + "webexpress.webcore:httpserver.start"), + args: [ExecutionTime.ToShortDateString(), ExecutionTime.ToLongTimeString()] + ); Started?.Invoke(this, new EventArgs()); } @@ -176,10 +207,17 @@ private void AddEndpoint(OptionsWrapper serverOptions, End switch (uri.Scheme) { - case "HTTPS": { AddEndpoint(serverOptions, ep, endPoint.PfxFile, endPoint.Password); break; } - default: { AddEndpoint(serverOptions, ep); break; } + case "HTTPS": + { + AddEndpoint(serverOptions, ep, endPoint.PfxFile, endPoint.Password); + break; + } + default: + { + AddEndpoint(serverOptions, ep); + break; + } } - } } catch (Exception ex) @@ -197,7 +235,6 @@ private void AddEndpoint(OptionsWrapper serverOptions, End private void AddEndpoint(OptionsWrapper serverOptions, IPEndPoint endPoint) { serverOptions.Value.Listen(endPoint); - HttpServerContext.Log.Info(message: I18N.Translate("webexpress.webcore:httpserver.listen"), args: endPoint.ToString()); } @@ -213,7 +250,6 @@ private void AddEndpoint(OptionsWrapper serverOptions, IPE serverOptions.Value.Listen(endPoint, configure => { var cert = X509CertificateLoader.LoadPkcs12FromFile(pfxFile, password, X509KeyStorageFlags.DefaultKeySet); - configure.UseHttps(cert); }); @@ -221,27 +257,26 @@ private void AddEndpoint(OptionsWrapper serverOptions, IPE } /// - /// Stops the HTTP(S) server + /// Stops the HTTP(S) server. /// public void Stop() { - // End running threads - Kestrel.StopAsync(ServerToken); + // signal cancellation and stop server + ServerTokenSource.Cancel(); + Kestrel.StopAsync(ServerTokenSource.Token); } /// - /// Handles an incoming request - /// Concurrent execution + /// Handles an incoming request. /// /// The context of the web request. + /// The previously resolved search result for the request. /// The response to be sent back to the caller. - private Response HandleClient(HttpContext context) + private IResponse HandleClient(IHttpContext context, SearchResult searchResult) { var stopwatch = Stopwatch.StartNew(); var request = context.Request; - var response = default(Response); - var culture = request.Culture; - var uri = request?.Uri; + var response = default(IResponse); HttpServerContext.Log.Debug(message: I18N.Translate("webexpress.webcore:httpserver.connected"), args: context.RemoteEndPoint); HttpServerContext.Log.Info(I18N.Translate @@ -252,85 +287,83 @@ private Response HandleClient(HttpContext context) $"{request?.Method} {request?.Uri} {request?.Protocoll}" )); - // search page in sitemap - var searchResult = WebEx.ComponentHub.SitemapManager.SearchResource(context.Uri, new SearchContext() - { - Culture = culture, - HttpContext = context, - HttpServerContext = HttpServerContext - }); + var resourceUri = new UriEndpoint(request.Uri, searchResult.Uri.PathSegments); + request.Uri = resourceUri; - if (searchResult != null) + try { - var resourceUri = new UriEndpoint(request.Uri, searchResult.Uri.PathSegments); - request.Uri = resourceUri; + // execute resource + request.AddParameter(searchResult.Uri.Parameters.Select(x => new Parameter(x.Key, x.Value, ParameterScope.Url))); - try + if (searchResult.EndpointContext != null) { - // execute resource - request.AddParameter(searchResult.Uri.Parameters.Select(x => new Parameter(x.Key, x.Value, ParameterScope.Url))); + response = WebEx.ComponentHub.EndpointManager.HandleRequest(request, searchResult.EndpointContext); - if (searchResult.EndpointContext != null) + if (response is ResponseNotFound) { - response = WebEx.ComponentHub.EndpointManager.HandleRequest(request, searchResult.EndpointContext); - - if (response is ResponseNotFound) - { - response = CreateStatusPage - ( - string.Empty, - request, - searchResult - ); - } - - if - ( - !response.Header.Cookies.Where(x => x.Name.Equals("session")).Any() && - !request.Header.Cookies.Where(x => x.Name.Equals("session")).Any() && - request.Session != null - ) - { - var cookie = new Cookie("session", request.Session.Id.ToString()) { Expires = DateTime.MaxValue }; - response.Header.Cookies.Add(cookie); - } - } - else - { - // Resource not found response = CreateStatusPage ( - "Resource not found", + string.Empty, request, searchResult ); } - } - catch (RedirectException ex) - { - if (ex.Permanet) - { - response = new ResponseMovedPermanently(ex.Uri); - } - else + + if + ( + !response.Header.Cookies.Where(x => x.Name.Equals("session")).Any() && + !request.Header.Cookies.Where(x => x.Name.Equals("session")).Any() && + request.Session != null + ) { - response = new ResponseMovedTemporarily(ex.Uri); + var cookie = new Cookie("session", request.Session.Id.ToString()) { Expires = DateTime.MaxValue }; + response.Header.Cookies.Add(cookie); } } - catch (BadRequestException ex) + else { - var message = $"

Message

{ex.Message}

" + - $"
Source
{ex.Source}

" + - $"
StackTrace
{ex.StackTrace.Replace("\n", "
\n")}"; - - response = CreateStatusPage + // resource not found + response = CreateStatusPage ( - message, + "Resource not found", request, searchResult ); } - catch (Exception ex) + } + catch (RedirectException ex) + { + if (ex.Permanet) + { + response = new ResponseMovedPermanently(ex.Uri); + } + else + { + response = new ResponseMovedTemporarily(ex.Uri); + } + } + catch (BadRequestException ex) + { + var message = $"

Message

{ex.Message}

" + + $"
Source
{ex.Source}

" + + $"
StackTrace
{ex.StackTrace.Replace("\n", "
\n")}"; + + response = CreateStatusPage + ( + message, + request, + searchResult + ); + } + catch (Exception ex) + { + if (ex is TargetInvocationException tie && tie.InnerException is RedirectException rex) + { + response = rex.Permanet + ? new ResponseMovedPermanently(rex.Uri) + : new ResponseMovedTemporarily(rex.Uri); + } + else { HttpServerContext.Log.Exception(ex); @@ -347,14 +380,11 @@ private Response HandleClient(HttpContext context) ); } } - else - { - // Resource not found - response = CreateStatusPage("Resource not found", request); - } stopwatch.Stop(); + UpdateStatistics(response, stopwatch.ElapsedMilliseconds); + HttpServerContext.Log.Info(I18N.Translate ( "webexpress.webcore:httpserver.request.done", @@ -368,91 +398,98 @@ private Response HandleClient(HttpContext context) } /// - /// Sends the response message + /// Updates the request statistics with ring buffer logic (max 24h). /// - /// The context of the request - /// The reply message - /// Sending the message as a task, which is executed concurrently. - private async Task SendResponseAsync(HttpContext context, Response response) + /// The response containing the status code. + /// The duration of the request in milliseconds. + private static void UpdateStatistics(IResponse response, long duration) { - try - { - var responseFeature = context.Features.Get(); - var responseBodyFeature = context.Features.Get(); + var now = DateTime.Now; + var minute = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0); + var isError = response != null && response.Status >= 400; - responseFeature.StatusCode = response.Status; - responseFeature.ReasonPhrase = response.Reason; - responseFeature.Headers.KeepAlive = "true"; + // calculate memory usage in MB + var memUsage = _currentProcess.WorkingSet64 / (1024.0 * 1024.0); - if (response.Header.Location != null) - { - responseFeature.Headers.Location = response.Header.Location; - } + // calculate cpu usage + var currentCpuTime = _currentProcess.TotalProcessorTime; + var currentWallTime = DateTime.UtcNow; + var cpuUsedMs = (currentCpuTime - _lastProcessorTime).TotalMilliseconds; + var totalMsPassed = (currentWallTime - _lastCpuTime).TotalMilliseconds; + var cpuUsage = 0.0; - if (!string.IsNullOrWhiteSpace(response.Header.CacheControl)) - { - responseFeature.Headers.CacheControl = response.Header.CacheControl; - } + if (totalMsPassed > 0) + { + cpuUsage = (cpuUsedMs / (totalMsPassed * Environment.ProcessorCount)) * 100.0; + } - if (!string.IsNullOrWhiteSpace(response.Header.ContentType)) - { - responseFeature.Headers.ContentType = response.Header.ContentType; - } + // update pointers for next calculation + _lastProcessorTime = currentCpuTime; + _lastCpuTime = currentWallTime; - if (response.Header.WWWAuthenticate) + lock (_statLock) + { + // remove entries older than 24 hours (1440 minutes) + while (Statistics.Count >= 1440) { - responseFeature.Headers.WWWAuthenticate = "Basic realm=\"Bereich\""; + Statistics.RemoveAt(0); } - if (response.Header.Cookies.Count != 0) - { - responseFeature.Headers.SetCookie = string.Join(" ", response.Header.Cookies); - } + var current = Statistics.LastOrDefault(); - if (response?.Content is byte[] byteContent) - { - responseFeature.Headers.ContentLength = byteContent.Length; - await responseBodyFeature.Stream.WriteAsync(byteContent); - await responseBodyFeature.Stream.FlushAsync(); - } - else if (response?.Content is string strContent) + if (current != null && current.Timestamp == minute) { - var content = context.Encoding.GetBytes(strContent); + current.Requests++; + if (isError) + { + current.Errors++; + } + + // update min, max and total duration + if (duration < current.MinDuration) + { + current.MinDuration = duration; + } + if (duration > current.MaxDuration) + { + current.MaxDuration = duration; + } + current.TotalDuration += duration; - responseFeature.Headers.ContentLength = content.Length; - await responseBodyFeature.Stream.WriteAsync(content); - await responseBodyFeature.Stream.FlushAsync(); + // calculate moving average for system metrics within this minute + current.CpuUsage += (cpuUsage - current.CpuUsage) / current.Requests; + current.MemoryUsage += (memUsage - current.MemoryUsage) / current.Requests; } - else if (response?.Content is IHtmlNode htmlContent) + else { - var content = context.Encoding.GetBytes(htmlContent?.ToString()); - - responseFeature.Headers.ContentLength = content.Length; - await responseBodyFeature.Stream.WriteAsync(content); - await responseBodyFeature.Stream.FlushAsync(); + Statistics.Add(new HttpServerStatisticItem() + { + Timestamp = minute, + Requests = 1, + Errors = isError ? 1 : 0, + MinDuration = duration, + MaxDuration = duration, + TotalDuration = duration, + CpuUsage = cpuUsage, + MemoryUsage = memUsage + }); } - - responseBodyFeature.Stream.Close(); - } - catch (Exception ex) - { - HttpServerContext.Log.Error(context.RemoteEndPoint + ": " + ex.Message); } } /// - /// Creates a status page + /// Creates a status page. /// /// The error message. /// The request. /// The plugin by searching the status page or null. /// The response. - private static Response CreateStatusPage(string message, Request request, SearchResult searchResult = null) where T : Response, new() + private static Response CreateStatusPage(string message, IRequest request, SearchResult searchResult = null) where T : Response, new() { var response = new T() as Response; var statusPageManager = WebEx.ComponentHub.StatusPageManager; var applicationManager = WebEx.ComponentHub.ApplicationManager; - var route = new RouteEndpoint([.. request.Uri.PathSegments])?.ToString(); + var route = new RouteEndpoint(request.Uri.PathSegments)?.ToString(); var applicationContext = applicationManager.Applications .Where(x => route.StartsWith(x.Route.ToString())) .FirstOrDefault(); @@ -491,48 +528,159 @@ private async Task SendResponseAsync(HttpContext context, Response response) } /// - /// Create an HttpContext with a collection of HTTP features. + /// Creates an appropriate IHttpContext instance (HttpContext or WebSocketContext) + /// based on feature detection. /// - /// A collection of HTTP features to use to create the HttpContext. - /// The HttpContext created. - public HttpContext CreateContext(IFeatureCollection contextFeatures) + /// The feature collection of the request. + /// An IHttpContext instance for the request. + public IHttpContext CreateContext(IFeatureCollection contextFeatures) { try { + var requestFeature = contextFeatures.Get(); + + // check if schema or upgrade header indicates websocket + if (IsWebSocketRequest(requestFeature)) + { + // use WebSocketContext for websocket connections + return new HttpWebSocketContext(contextFeatures, HttpServerContext); + } + + // use regular HttpContext for normal HTTP requests return new HttpContext(contextFeatures, HttpServerContext); } catch (Exception ex) { + // fall back to HttpExceptionContext on error return new HttpExceptionContext(ex, contextFeatures); } } /// /// Processes an http context asynchronously. + /// If the request is a websocket upgrade to a configured endpoint, handle + /// websocket lifecycle instead of request/response. + /// Handles missing sitemap endpoints directly here. /// - /// The http context that the operation processes. + /// The http context that the operation processes. /// Provides an asynchronous operation that handles the http context. - public async Task ProcessRequestAsync(HttpContext context) + public async Task ProcessRequestAsync(IHttpContext httpContext) { - if (context is HttpExceptionContext exceptionContext) + var responseSender = new ResponseSender(); + + if (httpContext is HttpExceptionContext exceptionContext) { var message = "404" + - $"

Message

{exceptionContext.Exception.Message}

" + - $"
Source
{exceptionContext.Exception.Source}

" + - $"
StackTrace
{exceptionContext.Exception.StackTrace.Replace("\n", "
\n")}

" + - $"
InnerException
{exceptionContext.Exception.InnerException?.ToString().Replace("\n", "
\n")}" + - ""; + $"

Message

{exceptionContext.Exception.Message}

" + + $"
Source
{exceptionContext.Exception.Source}

" + + $"
StackTrace
{exceptionContext.Exception.StackTrace.Replace("\n", "
\n")}

" + + $"
InnerException
{exceptionContext.Exception.InnerException?.ToString().Replace("\n", "
\n")}" + + ""; - var response500 = CreateStatusPage(message, context?.Request); + var response500 = CreateStatusPage(message, httpContext?.Request); + + await responseSender.SendAsync(exceptionContext, response500); + + return; + } + + var culture = httpContext?.Request?.Culture; + var searchResult = WebEx.ComponentHub.SitemapManager.SearchResource(httpContext?.Uri, new SearchContext() + { + Culture = culture, + HttpContext = httpContext, + HttpServerContext = HttpServerContext + }); + + if (searchResult == null) + { + var notFoundResponse = CreateStatusPage + ( + "Resource not found", + httpContext.Request + ); + + await responseSender.SendAsync(httpContext, notFoundResponse); + + return; + } + + if (httpContext is HttpWebSocketContext) + { + // try to obtain websocket context and optional handler + var socketContext = searchResult.EndpointContext as ISocketContext; - await SendResponseAsync(exceptionContext, response500); + await HandleWebSocketAsync(httpContext, socketContext); return; } - var response = HandleClient(context); + var response = HandleClient(httpContext, searchResult); - await SendResponseAsync(context, response); + await responseSender.SendAsync(httpContext, response); + } + + /// + /// Handles the complete WebSocket request lifecycle for the given HTTP context. + /// Validates the upgrade request, delegates the connection handling to the socket manager, + /// and returns appropriate HTTP error responses when the handshake or connection setup fails. + /// + /// + /// The current HTTP context containing the incoming WebSocket upgrade request. + /// + /// + /// Optional WebSocket endpoint context resolved from the sitemap. May be null + /// if the endpoint does not define additional metadata. + /// + public async Task HandleWebSocketAsync(IHttpContext httpContext, ISocketContext socketContext) + { + var responseSender = new ResponseSender(); + var socketManager = WebEx.ComponentHub.SocketManager; + + // validate that the request is a websocket upgrade + if (httpContext is not HttpWebSocketContext) + { + // websocket not requested by client; return 400 Bad Request + await responseSender.SendAsync(httpContext, new ResponseBadRequest(new StatusMessage("WebSocket upgrade required"))); + + return; + } + + try + { + await socketManager.HandleConnectionAsync(httpContext, socketContext); + } + catch (SocketHandshakeException) + { + // missing or invalid websocket handshake headers -> respond with 426 + var response = new ResponseUpgradeRequired(new StatusMessage("Invalid WebSocket handshake headers")); + response.Header.Upgrade = "websocket"; + + await responseSender.SendAsync(httpContext, response); + } + catch (SocketMessageTooLargeException ex) + { + // the client sent a WebSocket message exceeding the configured maximum size -> respond with 413 + var response = new ResponsePayloadTooLarge(new StatusMessage($"WebSocket message exceeds the maximum allowed size of {ex.MaxSize} bytes.")); + await responseSender.SendAsync(httpContext, response); + } + catch (SocketException ex) + { + HttpServerContext.Log.Exception(ex); + + // return 500 when socket error + var response = new ResponseInternalServerError(new StatusMessage("A transport-level socket error occurred during WebSocket communication.")); + await responseSender.SendAsync(httpContext, response); + } + catch (Exception ex) + { + // log unhandled exceptions during websocket processing + HttpServerContext.Log.Exception(ex); + + // return 500 when handshake did not succeed and no websocket established + var response = new ResponseInternalServerError(new StatusMessage("An unexpected server error occurred during WebSocket processing.")); + await responseSender.SendAsync(httpContext, response); + } } /// @@ -540,8 +688,31 @@ public async Task ProcessRequestAsync(HttpContext context) /// /// The http context to discard. /// The exception that is thrown if processing did not complete successfully; otherwise null. - public void DisposeContext(HttpContext context, Exception exception) + public void DisposeContext(IHttpContext context, Exception exception) { } + + /// + /// Checks whether the current request is a WebSocket connection. + /// + /// The HTTP request feature instance. + /// True if it is a WebSocket connection; otherwise, false. + private static bool IsWebSocketRequest(IHttpRequestFeature requestFeature) + { + // check scheme and "Upgrade" header for websocket protocol + if (requestFeature == null) + { + return false; + } + + var upgradeHeader = requestFeature.Headers.Upgrade; + var scheme = requestFeature.Scheme; + var isWebSocket = + upgradeHeader.Contains("websocket", StringComparer.OrdinalIgnoreCase) || + string.Equals(scheme, "ws", StringComparison.OrdinalIgnoreCase) || + string.Equals(scheme, "wss", StringComparison.OrdinalIgnoreCase); + + return isWebSocket; + } } -} +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/HttpServerStatisticItem.cs b/src/WebExpress.WebCore/HttpServerStatisticItem.cs new file mode 100644 index 0000000..c2d8719 --- /dev/null +++ b/src/WebExpress.WebCore/HttpServerStatisticItem.cs @@ -0,0 +1,55 @@ +using System; + +namespace WebExpress.WebCore +{ + /// + /// Represents a single data point for server statistics. + /// + public class HttpServerStatisticItem + { + /// + /// Returns or sets the timestamp (grouped by minute). + /// + public DateTime Timestamp { get; set; } + + /// + /// Returns or sets the total number of requests. + /// + public int Requests { get; set; } + + /// + /// Returns or sets the number of erroneous requests (status code >= 400). + /// + public int Errors { get; set; } + + /// + /// Returns or sets the minimum response time in milliseconds. + /// + public long MinDuration { get; set; } + + /// + /// Returns or sets the maximum response time in milliseconds. + /// + public long MaxDuration { get; set; } + + /// + /// Returns or sets the total duration of all requests in this interval in milliseconds. + /// + public long TotalDuration { get; set; } + + /// + /// Returns the average response time in milliseconds. + /// + public double AverageDuration => Requests > 0 ? (double)TotalDuration / Requests : 0; + + /// + /// Returns or sets the average CPU usage in percentage (0-100) during this interval. + /// + public double CpuUsage { get; set; } + + /// + /// Returns or sets the average memory usage in MB during this interval. + /// + public double MemoryUsage { get; set; } + } +} diff --git a/src/WebExpress.WebCore/Internationalization/I18N.cs b/src/WebExpress.WebCore/Internationalization/I18N.cs index ec8a0f7..7a37de9 100644 --- a/src/WebExpress.WebCore/Internationalization/I18N.cs +++ b/src/WebExpress.WebCore/Internationalization/I18N.cs @@ -36,7 +36,7 @@ public static string Translate(string key, params object[] args) /// The request with the language to use. /// The internationalization key. /// The value of the key in the current language. - public static string Translate(Request request, string key) + public static string Translate(IRequest request, string key) { return WebEx.ComponentHub?.InternationalizationManager.Translate(request, key) ?? key; } @@ -48,7 +48,7 @@ public static string Translate(Request request, string key) /// The internationalization key. /// The formatting arguments. /// The value of the key in the current language. - public static string Translate(Request request, string key, params object[] args) + public static string Translate(IRequest request, string key, params object[] args) { return WebEx.ComponentHub?.InternationalizationManager?.Translate(request, key, args) ?? key; } diff --git a/src/WebExpress.WebCore/Internationalization/IInternationalizationManager.cs b/src/WebExpress.WebCore/Internationalization/IInternationalizationManager.cs index cfed473..21a1c3d 100644 --- a/src/WebExpress.WebCore/Internationalization/IInternationalizationManager.cs +++ b/src/WebExpress.WebCore/Internationalization/IInternationalizationManager.cs @@ -30,7 +30,7 @@ public interface IInternationalizationManager : IComponentManager /// The request with the language to use. /// The internationalization key. /// The value of the key in the current language. - string Translate(Request request, string key); + string Translate(IRequest request, string key); /// /// Translates a given key to the specified language. @@ -39,7 +39,7 @@ public interface IInternationalizationManager : IComponentManager /// The internationalization key. /// The formatting arguments. /// The value of the key in the current language. - string Translate(Request request, string key, params object[] args); + string Translate(IRequest request, string key, params object[] args); /// /// Translates a given key to the specified language. diff --git a/src/WebExpress.WebCore/Internationalization/InternationalizationManager.cs b/src/WebExpress.WebCore/Internationalization/InternationalizationManager.cs index bc9ee28..e29fb10 100644 --- a/src/WebExpress.WebCore/Internationalization/InternationalizationManager.cs +++ b/src/WebExpress.WebCore/Internationalization/InternationalizationManager.cs @@ -137,7 +137,7 @@ public void Register(Assembly assembly, string pluginId) /// The context of the plugin containing the key-value pairs to remove. public void Remove(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return; } @@ -182,7 +182,7 @@ public string Translate(string key, params object[] args) /// The request with the language to use. /// The internationalization key. /// The value of the key in the current language. - public string Translate(Request request, string key) + public string Translate(IRequest request, string key) { return Translate(request.Culture, null, key); } @@ -194,7 +194,7 @@ public string Translate(Request request, string key) /// The internationalization key. /// The formatting arguments. /// The value of the key in the current language. - public string Translate(Request request, string key, params object[] args) + public string Translate(IRequest request, string key, params object[] args) { return string.Format(Translate(request, key), args); } diff --git a/src/WebExpress.WebCore/Internationalization/de b/src/WebExpress.WebCore/Internationalization/de index febb19d..cb7a647 100644 --- a/src/WebExpress.WebCore/Internationalization/de +++ b/src/WebExpress.WebCore/Internationalization/de @@ -170,6 +170,10 @@ includemanager.initialization=Der Includemanager wurde initialisiert. includemanager.addinclude=Die Client-Ressource '{0}' wurde in der Anwendung '{1}' registiert. includemanager.removeinclude=Die Client-Ressource '{0}' wurde aus der Anwendung '{1}' entfernt. +socketmanager.initialization=Der Socketmanager wurde initialisiert. +socketmanager.addsocket=Der WebSocket '{0}' wurde in der Anwendung '{1}' registiert. +socketmanager.removesocket=Der WebSocket '{0}' wurde aus der Anwendung '{1}' entfernt. + thememanager.initialization=Der Thememanager wurde initialisiert. thememanager.addtheme=Das Theme '{0}' wurde in der Anwendung '{1}' registiert. thememanager.titel=Designvorlagen: diff --git a/src/WebExpress.WebCore/Internationalization/en b/src/WebExpress.WebCore/Internationalization/en index 2308ed3..155cdbf 100644 --- a/src/WebExpress.WebCore/Internationalization/en +++ b/src/WebExpress.WebCore/Internationalization/en @@ -170,6 +170,10 @@ includemanager.initialization=The include manager has been initialized. includemanager.addinclude=The client resource '{0}' has been registered in the application '{1}'. includemanager.removeinclude=The client resource '{0}' has been removed from the application '{1}'. +socketmanager.initialization=The WebSocket manager has been initialized. +socketmanager.addsocket=The WebSocket '{0}' has been registered in the application '{1}'. +socketmanager.removesocket=The WebSocket '{0}' has been removed from the application '{1}'. + thememanager.initialization=The theme manager has been initialized. thememanager.addtheme=The theme '{0}' has been registered in the application '{1}'. thememanager.titel=Themes: diff --git a/src/WebExpress.WebCore/Rocket.ico b/src/WebExpress.WebCore/Rocket.ico deleted file mode 100644 index c0909c9..0000000 Binary files a/src/WebExpress.WebCore/Rocket.ico and /dev/null differ diff --git a/src/WebExpress.WebCore/WebApplication/ApplicationManager.cs b/src/WebExpress.WebCore/WebApplication/ApplicationManager.cs index 05b6928..a2d338a 100644 --- a/src/WebExpress.WebCore/WebApplication/ApplicationManager.cs +++ b/src/WebExpress.WebCore/WebApplication/ApplicationManager.cs @@ -80,7 +80,7 @@ private void Register(IPluginContext pluginContext) x => x.IsClass && x.IsSealed && x.IsPublic && - x.GetInterface(typeof(IApplication).Name) != null + x.GetInterface(typeof(IApplication).Name) is not null )) { var id = type.FullName?.ToLower(); @@ -88,8 +88,8 @@ private void Register(IPluginContext pluginContext) var icon = string.Empty; var description = string.Empty; var contextPath = string.Empty; - var assetPath = "/"; - var dataPath = "/"; + var assetPath = "./"; + var dataPath = "./"; // determining attributes foreach (var customAttribute in type.CustomAttributes @@ -176,7 +176,7 @@ private void Register(IPluginContext pluginContext) /// The context of the plugin that contains the applications to remove. internal void Remove(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return; } @@ -267,11 +267,11 @@ public IEnumerable GetApplications(Type application) /// The context of the plugin that contains the applications. public void Boot(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return; } - else if (pluginContext.Assembly.GetCustomAttribute() != null) + else if (pluginContext.Assembly.GetCustomAttribute() is not null) { return; } diff --git a/src/WebExpress.WebCore/WebApplication/Model/ApplicationDictionary.cs b/src/WebExpress.WebCore/WebApplication/Model/ApplicationDictionary.cs index d1cff54..3dc6d2d 100644 --- a/src/WebExpress.WebCore/WebApplication/Model/ApplicationDictionary.cs +++ b/src/WebExpress.WebCore/WebApplication/Model/ApplicationDictionary.cs @@ -108,7 +108,7 @@ public IApplicationContext GetApplication(string applicationId) /// The contexts of the applications as an enumeration. public IEnumerable GetApplications(Type application) { - if (application == null) return []; + if (application is null) return []; var items = _dict.Values.SelectMany(x => x.Values) .Where(x => x.ApplicationClass.Equals(application) || application.IsAssignableFrom(x.ApplicationClass)) diff --git a/src/WebExpress.WebCore/WebAsset/Asset.cs b/src/WebExpress.WebCore/WebAsset/Asset.cs index e554e83..d109af0 100644 --- a/src/WebExpress.WebCore/WebAsset/Asset.cs +++ b/src/WebExpress.WebCore/WebAsset/Asset.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using System.Reflection; using WebExpress.WebCore.Internationalization; using WebExpress.WebCore.WebComponent; @@ -46,9 +45,9 @@ public Asset(IComponentHub componentHub, IAssetContext assetContext, IHttpServer /// /// The request. /// The response. - public Response Process(Request request) + public IResponse Process(IRequest request) { - if (_data == null) + if (_data is null) { return new ResponseNotFound(); } @@ -139,7 +138,7 @@ public Response Process(Request request) /// The data. private byte[] GetData(Assembly assembly) { - if (assembly == null || _embeddedResource == null) + if (assembly is null || _embeddedResource is null) { return []; } @@ -157,8 +156,6 @@ private byte[] GetData(Assembly assembly) public void Dispose() { _data = null; - - GC.SuppressFinalize(this); } } } \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebAsset/AssetManager.cs b/src/WebExpress.WebCore/WebAsset/AssetManager.cs index 8d7239d..1a1d4a0 100644 --- a/src/WebExpress.WebCore/WebAsset/AssetManager.cs +++ b/src/WebExpress.WebCore/WebAsset/AssetManager.cs @@ -70,7 +70,7 @@ private AssetManager(IComponentHub componentHub, IHttpServerContext httpServerCo .EndsWith(x.AssetContext.Route.ToString().Replace("/", ".")) ); - if (asset != null) + if (asset is not null) { return asset.Instance.Process(request); } diff --git a/src/WebExpress.WebCore/WebAsset/IAsset.cs b/src/WebExpress.WebCore/WebAsset/IAsset.cs index 78d8e5c..8d4c2f8 100644 --- a/src/WebExpress.WebCore/WebAsset/IAsset.cs +++ b/src/WebExpress.WebCore/WebAsset/IAsset.cs @@ -13,6 +13,6 @@ public interface IAsset : IEndpoint /// /// The request. /// The response. - Response Process(Request request); + IResponse Process(IRequest request); } } diff --git a/src/WebExpress.WebCore/WebAsset/Model/AssetItemDictionary.cs b/src/WebExpress.WebCore/WebAsset/Model/AssetItemDictionary.cs index 4924daa..baad7ec 100644 --- a/src/WebExpress.WebCore/WebAsset/Model/AssetItemDictionary.cs +++ b/src/WebExpress.WebCore/WebAsset/Model/AssetItemDictionary.cs @@ -65,7 +65,7 @@ public bool AddAssetItem(IPluginContext pluginContext, IApplicationContext appli /// An enumeration of asset contexts that were removed. public IEnumerable Remove(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return []; } @@ -97,7 +97,7 @@ public IEnumerable Remove(IPluginContext pluginContext) /// An enumeration of asset contexts that were removed. internal IEnumerable Remove(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { return []; } diff --git a/src/WebExpress.WebCore/WebAttribute/ApplicationAttribute.cs b/src/WebExpress.WebCore/WebAttribute/ApplicationAttribute.cs index adb5ee8..28d3f17 100644 --- a/src/WebExpress.WebCore/WebAttribute/ApplicationAttribute.cs +++ b/src/WebExpress.WebCore/WebAttribute/ApplicationAttribute.cs @@ -8,7 +8,8 @@ namespace WebExpress.WebCore.WebAttribute ///
/// The type of the application. [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class ApplicationAttribute : Attribute, IPluginAttribute where TApplication : class, IApplication + public class ApplicationAttribute : Attribute, IPluginAttribute + where TApplication : class, IApplication { } diff --git a/src/WebExpress.WebCore/WebAttribute/ContextPathAttribute.cs b/src/WebExpress.WebCore/WebAttribute/ContextPathAttribute.cs index d8554f2..0f788ae 100644 --- a/src/WebExpress.WebCore/WebAttribute/ContextPathAttribute.cs +++ b/src/WebExpress.WebCore/WebAttribute/ContextPathAttribute.cs @@ -8,13 +8,18 @@ namespace WebExpress.WebCore.WebAttribute [AttributeUsage(AttributeTargets.All, AllowMultiple = false)] public class ContextPathAttribute : Attribute, IApplicationAttribute, IEndpointAttribute { + /// + /// Returns the context path associated with the current instance. + /// + public string ContextPath { get; } + /// /// Initializes a new instance of the class. /// /// The context path. public ContextPathAttribute(string contetxPath) { - + ContextPath = contetxPath; } } } diff --git a/src/WebExpress.WebCore/WebAttribute/DomainAttribute.cs b/src/WebExpress.WebCore/WebAttribute/DomainAttribute.cs new file mode 100644 index 0000000..19ad50e --- /dev/null +++ b/src/WebExpress.WebCore/WebAttribute/DomainAttribute.cs @@ -0,0 +1,28 @@ +using System; +using WebExpress.WebCore.WebDomain; + +namespace WebExpress.WebCore.WebAttribute +{ + /// + /// Specifies that the decorated class belongs to a particular domain. + /// Domains represent logical application areas such as workspaces, + /// modules or functional segments. Multiple domain attributes may be + /// applied to the same class to associate it with several domains. + /// + /// + /// The domain type implementing that describes + /// the logical area to which the class belongs. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class DomainAttribute : Attribute, IPageAttribute, ISettingPageAttribute + where TDomain : class, IDomain + { + /// + /// Initializes a new instance of the class. + /// + public DomainAttribute() + { + + } + } +} diff --git a/src/WebExpress.WebCore/WebAttribute/ISocketAttribute.cs b/src/WebExpress.WebCore/WebAttribute/ISocketAttribute.cs new file mode 100644 index 0000000..08b82b3 --- /dev/null +++ b/src/WebExpress.WebCore/WebAttribute/ISocketAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace WebExpress.WebCore.WebAttribute +{ + /// + /// Interface of a socket assignment attribute. + /// + public interface ISocketAttribute + { + } +} diff --git a/src/WebExpress.WebCore/WebAttribute/IncludeSubPathsAttribute.cs b/src/WebExpress.WebCore/WebAttribute/IncludeSubPathsAttribute.cs index f14ea97..025fb79 100644 --- a/src/WebExpress.WebCore/WebAttribute/IncludeSubPathsAttribute.cs +++ b/src/WebExpress.WebCore/WebAttribute/IncludeSubPathsAttribute.cs @@ -1,17 +1,25 @@ -namespace WebExpress.WebCore.WebAttribute +using System; + +namespace WebExpress.WebCore.WebAttribute { /// /// Determines whether all resources below the specified path (including segment) are also processed. /// - public class IncludeSubPathsAttribute : System.Attribute, IEndpointAttribute + [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] + public class IncludeSubPathsAttribute : Attribute, IEndpointAttribute { + /// + /// Returns a value indicating whether subpaths are included in the operation. + /// + public bool IncludeSubPaths { get; } + /// /// Initializes a new instance of the class. /// /// All subpaths are included. - public IncludeSubPathsAttribute(bool includeSubPaths) + public IncludeSubPathsAttribute(bool includeSubPaths = true) { - + IncludeSubPaths = includeSubPaths; } } } diff --git a/src/WebExpress.WebCore/WebAttribute/MaxMessageSizeAttribute.cs b/src/WebExpress.WebCore/WebAttribute/MaxMessageSizeAttribute.cs new file mode 100644 index 0000000..68696f4 --- /dev/null +++ b/src/WebExpress.WebCore/WebAttribute/MaxMessageSizeAttribute.cs @@ -0,0 +1,29 @@ +using System; + +namespace WebExpress.WebCore.WebAttribute +{ + /// + /// Specifies the maximum allowed message size for WebSocket messages + /// processed by the decorated endpoint. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class MaxMessageSizeAttribute : Attribute, ISocketAttribute + { + /// + /// Gets the maximum allowed message size in bytes. + /// + public ulong MaxMessageSize { get; } + + /// + /// Initializes a new instance of the class + /// with the specified maximum message size. + /// + /// + /// The maximum allowed message size in bytes. + /// + public MaxMessageSizeAttribute(ulong maxMessageSize) + { + MaxMessageSize = maxMessageSize; + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebAttribute/MessageTypeAttribute.cs b/src/WebExpress.WebCore/WebAttribute/MessageTypeAttribute.cs new file mode 100644 index 0000000..dd8f0ae --- /dev/null +++ b/src/WebExpress.WebCore/WebAttribute/MessageTypeAttribute.cs @@ -0,0 +1,26 @@ +using System; +using WebExpress.WebCore.WebSocket; + +namespace WebExpress.WebCore.WebAttribute +{ + /// + /// Specifies the status code for an HTTP response (see RFC 7231). + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class MessageTypeAttribute : Attribute, ISocketAttribute + { + /// + /// Returns the message type code. + /// + public SocketMessageType MessageType { get; } + + /// + /// Initializes a new instance of the class with the specified status code. + /// + /// The message type. + public MessageTypeAttribute(SocketMessageType messageType) + { + MessageType = messageType; + } + } +} diff --git a/src/WebExpress.WebCore/WebAttribute/MethodAttribute.cs b/src/WebExpress.WebCore/WebAttribute/MethodAttribute.cs index c78a190..8c3f8b4 100644 --- a/src/WebExpress.WebCore/WebAttribute/MethodAttribute.cs +++ b/src/WebExpress.WebCore/WebAttribute/MethodAttribute.cs @@ -1,20 +1,32 @@ using System; -using WebExpress.WebCore.WebRestApi; +using WebExpress.WebCore.WebMessage; namespace WebExpress.WebCore.WebAttribute { /// - /// The range in which the attribute is valid. + /// Specifies the HTTP method or CRUD operation that an endpoint method + /// is intended to handle. This attribute can be applied multiple times + /// to the same method to declare support for multiple request methods. /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class MethodAttribute : Attribute, IEndpointAttribute { /// - /// Initializes a new instance of the class. + /// Returns the CRUD (Create, Read, Update, Delete) operation or request + /// method associated with the decorated endpoint method. /// - public MethodAttribute(CrudMethod crudMethod) - { + public RequestMethod RequestMethod { get; private set; } + /// + /// Initializes a new instance of the class + /// with the specified request method. + /// + /// + /// The request method that the endpoint method should handle. + /// + public MethodAttribute(RequestMethod requestMethod) + { + RequestMethod = requestMethod; } } } diff --git a/src/WebExpress.WebCore/WebAttribute/SegmentAttribute.cs b/src/WebExpress.WebCore/WebAttribute/SegmentAttribute.cs index 6c4884d..0d2edd5 100644 --- a/src/WebExpress.WebCore/WebAttribute/SegmentAttribute.cs +++ b/src/WebExpress.WebCore/WebAttribute/SegmentAttribute.cs @@ -14,20 +14,13 @@ public class SegmentAttribute : Attribute, IEndpointAttribute, ISegmentAttribute /// private string Segment { get; set; } - /// - /// Returns or sets the display string. - /// - private string Display { get; set; } - /// /// Initializes a new instance of the class. /// /// The segment of the uri path. - /// The display string. - public SegmentAttribute(string segment, string display = null) + public SegmentAttribute(string segment) { Segment = segment; - Display = display; } /// @@ -36,7 +29,7 @@ public SegmentAttribute(string segment, string display = null) /// The path segment. public IUriPathSegment ToPathSegment() { - return new UriPathSegmentConstant(Segment, Display) { }; + return new UriPathSegmentConstant(Segment) { }; } } } diff --git a/src/WebExpress.WebCore/WebAttribute/SegmentDoubleAttribute.cs b/src/WebExpress.WebCore/WebAttribute/SegmentDoubleAttribute.cs index 1c2a208..763dcc1 100644 --- a/src/WebExpress.WebCore/WebAttribute/SegmentDoubleAttribute.cs +++ b/src/WebExpress.WebCore/WebAttribute/SegmentDoubleAttribute.cs @@ -1,4 +1,5 @@ using System; +using WebExpress.WebCore.WebParameter; using WebExpress.WebCore.WebUri; namespace WebExpress.WebCore.WebAttribute @@ -6,14 +7,13 @@ namespace WebExpress.WebCore.WebAttribute /// /// Attribute to define a double segment in a URI path. /// + /// + /// The type of parameter to associate with the segment key. + /// [AttributeUsage(AttributeTargets.Class)] - public class SegmentDoubleAttribute : Attribute, IEndpointAttribute, ISegmentAttribute + public class SegmentDoubleAttribute : Attribute, IEndpointAttribute, ISegmentAttribute + where TParameter : IParameterStatic, new() { - /// - /// Returns or sets the name of the variable. - /// - private string VariableName { get; set; } - /// /// Returns or sets the display string. /// @@ -22,11 +22,9 @@ public class SegmentDoubleAttribute : Attribute, IEndpointAttribute, ISegmentAtt /// /// Initializes a new instance of the class. /// - /// The name of the variable. /// The display string. - public SegmentDoubleAttribute(string variableName, string display) + public SegmentDoubleAttribute(string display = null) { - VariableName = variableName; Display = display; } @@ -36,7 +34,7 @@ public SegmentDoubleAttribute(string variableName, string display) /// The path segment. public IUriPathSegment ToPathSegment() { - return new UriPathSegmentVariableDouble(VariableName, Display); + return new UriPathSegmentVariableDouble(Display); } } } diff --git a/src/WebExpress.WebCore/WebAttribute/SegmentGuidAttribute.cs b/src/WebExpress.WebCore/WebAttribute/SegmentGuidAttribute.cs index 57d1d7d..01be48b 100644 --- a/src/WebExpress.WebCore/WebAttribute/SegmentGuidAttribute.cs +++ b/src/WebExpress.WebCore/WebAttribute/SegmentGuidAttribute.cs @@ -1,5 +1,5 @@ using System; -using WebExpress.WebCore.WebMessage; +using WebExpress.WebCore.WebParameter; using WebExpress.WebCore.WebUri; namespace WebExpress.WebCore.WebAttribute @@ -7,34 +7,24 @@ namespace WebExpress.WebCore.WebAttribute /// /// A dynamic path segment of type guid. /// + /// + /// The type of parameter to associate with the segment key. + /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class SegmentGuidAttribute : Attribute, IEndpointAttribute, ISegmentAttribute - where TParameter : Parameter + where TParameter : IParameterStatic, new() { - /// - /// Returns or sets the name of the variable. - /// - private string VariableName { get; set; } - - /// - /// Returns or sets the display string. - /// - private string Display { get; set; } - /// /// Returns or sets the display format. /// - private UriPathSegmentVariableGuid.Format DisplayFormat { get; set; } + private UriPathSegmentVariableGuid.Format DisplayFormat { get; set; } /// /// Initializes a new instance of the class. /// - /// The display string. /// The display format. - public SegmentGuidAttribute(string display, UriPathSegmentVariableGuid.Format displayFormat = UriPathSegmentVariableGuid.Format.Simple) + public SegmentGuidAttribute(UriPathSegmentVariableGuid.Format displayFormat = UriPathSegmentVariableGuid.Format.Simple) { - VariableName = (Activator.CreateInstance() as Parameter)?.Key?.ToLower(); - Display = display; DisplayFormat = displayFormat; } @@ -44,7 +34,7 @@ public SegmentGuidAttribute(string display, UriPathSegmentVariableGuid.Format di /// The path segment. public IUriPathSegment ToPathSegment() { - return new UriPathSegmentVariableGuid(VariableName, Display, DisplayFormat); + return new UriPathSegmentVariableGuid(DisplayFormat); } } } diff --git a/src/WebExpress.WebCore/WebAttribute/SegmentHiddenAttribute.cs b/src/WebExpress.WebCore/WebAttribute/SegmentHiddenAttribute.cs new file mode 100644 index 0000000..586845e --- /dev/null +++ b/src/WebExpress.WebCore/WebAttribute/SegmentHiddenAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace WebExpress.WebCore.WebAttribute +{ + /// + /// Indicates that a segment is hidden. + /// + /// + /// This attribute can be used to determine if the segment should not be displayed in user + /// interfaces. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class SegmentHiddenAttribute : Attribute + { + + } +} diff --git a/src/WebExpress.WebCore/WebAttribute/SegmentIntAttribute.cs b/src/WebExpress.WebCore/WebAttribute/SegmentIntAttribute.cs index ed888a2..5c3c898 100644 --- a/src/WebExpress.WebCore/WebAttribute/SegmentIntAttribute.cs +++ b/src/WebExpress.WebCore/WebAttribute/SegmentIntAttribute.cs @@ -1,4 +1,5 @@ using System; +using WebExpress.WebCore.WebParameter; using WebExpress.WebCore.WebUri; namespace WebExpress.WebCore.WebAttribute @@ -6,14 +7,13 @@ namespace WebExpress.WebCore.WebAttribute /// /// Attribute to define an integer segment in a URI path. /// - [AttributeUsage(AttributeTargets.Class)] - public class SegmentIntAttribute : Attribute, IEndpointAttribute, ISegmentAttribute + /// + /// The type of parameter to associate with the segment key. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class SegmentIntAttribute : Attribute, IEndpointAttribute, ISegmentAttribute + where TParameter : IParameterStatic, new() { - /// - /// Returns or sets the name of the variable. - /// - private string VariableName { get; set; } - /// /// Returns or sets the display string. /// @@ -22,11 +22,9 @@ public class SegmentIntAttribute : Attribute, IEndpointAttribute, ISegmentAttrib /// /// Initializes a new instance of the class. /// - /// The name of the variable. /// The display string. - public SegmentIntAttribute(string variableName, string display) + public SegmentIntAttribute(string display = null) { - VariableName = variableName; Display = display; } @@ -36,7 +34,7 @@ public SegmentIntAttribute(string variableName, string display) /// The path segment. public IUriPathSegment ToPathSegment() { - return new UriPathSegmentVariableInt(VariableName, Display); + return new UriPathSegmentVariableInt(Display); } } } diff --git a/src/WebExpress.WebCore/WebAttribute/SegmentRegexAttribute.cs b/src/WebExpress.WebCore/WebAttribute/SegmentRegexAttribute.cs new file mode 100644 index 0000000..207fb83 --- /dev/null +++ b/src/WebExpress.WebCore/WebAttribute/SegmentRegexAttribute.cs @@ -0,0 +1,47 @@ +using System; +using WebExpress.WebCore.WebParameter; +using WebExpress.WebCore.WebUri; + +namespace WebExpress.WebCore.WebAttribute +{ + /// + /// Attribute to define a regex segment in a URI path. + /// + /// + /// The type of parameter to associate with the segment key. + /// + [AttributeUsage(AttributeTargets.Class)] + public class SegmentRegexAttribute : Attribute, IEndpointAttribute, ISegmentAttribute + where TParameter : IParameterStatic, new() + { + /// + /// Reurns or sets the string representation of the expression. + /// + private string Expression { get; set; } + + /// + /// Returns or sets the tag. + /// + private string Tag { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The regular expression. + /// The tag. + public SegmentRegexAttribute(string expression, string tag = null) + { + Expression = expression; + Tag = tag; + } + + /// + /// Conversion to a path segment. + /// + /// The path segment. + public IUriPathSegment ToPathSegment() + { + return new UriPathSegmentVariableRegex(Expression, Tag); + } + } +} diff --git a/src/WebExpress.WebCore/WebAttribute/SegmentStringAttribute.cs b/src/WebExpress.WebCore/WebAttribute/SegmentStringAttribute.cs index d125473..b52850f 100644 --- a/src/WebExpress.WebCore/WebAttribute/SegmentStringAttribute.cs +++ b/src/WebExpress.WebCore/WebAttribute/SegmentStringAttribute.cs @@ -1,4 +1,5 @@ using System; +using WebExpress.WebCore.WebParameter; using WebExpress.WebCore.WebUri; namespace WebExpress.WebCore.WebAttribute @@ -6,28 +7,25 @@ namespace WebExpress.WebCore.WebAttribute /// /// Attribute to define a segment string in a URI path. /// - [AttributeUsage(AttributeTargets.Class)] - public class SegmentStringAttribute : Attribute, IEndpointAttribute, ISegmentAttribute + /// + /// The type of parameter to associate with the segment key. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class SegmentStringAttribute : Attribute, IEndpointAttribute, ISegmentAttribute + where TParameter : IParameterStatic, new() { /// - /// Returns or sets the name of the variable. + /// Returns or sets the tag. /// - private string VariableName { get; set; } - - /// - /// Returns or sets the display string. - /// - private string Display { get; set; } + private string Tag { get; set; } /// /// Initializes a new instance of the class. /// - /// The name of the variable. - /// The display string. - public SegmentStringAttribute(string variableName, string display) + /// The tag. + public SegmentStringAttribute(string tag = null) { - VariableName = variableName; - Display = display; + Tag = tag; } /// @@ -36,7 +34,7 @@ public SegmentStringAttribute(string variableName, string display) /// The path segment. public IUriPathSegment ToPathSegment() { - return new UriPathSegmentVariableString(VariableName, Display); + return new UriPathSegmentVariableString(Tag); } } } diff --git a/src/WebExpress.WebCore/WebAttribute/SegmentUIntAttribute.cs b/src/WebExpress.WebCore/WebAttribute/SegmentUIntAttribute.cs index fc6b841..1d1d8ba 100644 --- a/src/WebExpress.WebCore/WebAttribute/SegmentUIntAttribute.cs +++ b/src/WebExpress.WebCore/WebAttribute/SegmentUIntAttribute.cs @@ -1,4 +1,5 @@ using System; +using WebExpress.WebCore.WebParameter; using WebExpress.WebCore.WebUri; namespace WebExpress.WebCore.WebAttribute @@ -9,28 +10,25 @@ namespace WebExpress.WebCore.WebAttribute /// /// This attribute is used to specify a segment in the URI path that contains an unsigned integer variable. /// - [AttributeUsage(AttributeTargets.Class)] - public class SegmentUIntAttribute : Attribute, IEndpointAttribute, ISegmentAttribute + /// + /// The type of parameter to associate with the segment key. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class SegmentUIntAttribute : Attribute, IEndpointAttribute, ISegmentAttribute + where TParameter : IParameterStatic, new() { /// - /// Returns or sets the name of the variable. + /// Returns or sets the tag. /// - private string VariableName { get; set; } - - /// - /// Returns or sets the display string. - /// - private string Display { get; set; } + private string Tag { get; set; } /// /// Initializes a new instance of the class. /// - /// The name of the variable. - /// The display string. - public SegmentUIntAttribute(string variableName, string display) + /// The tag. + public SegmentUIntAttribute(string tag = null) { - VariableName = variableName; - Display = display; + Tag = tag; } /// @@ -39,7 +37,7 @@ public SegmentUIntAttribute(string variableName, string display) /// The path segment. public IUriPathSegment ToPathSegment() { - return new UriPathSegmentVariableUInt(VariableName, Display); + return new UriPathSegmentVariableUInt(Tag); } } } diff --git a/src/WebExpress.WebCore/WebAttribute/SettingSectionAttribute.cs b/src/WebExpress.WebCore/WebAttribute/SettingSectionAttribute.cs index 7fc68fb..cb09086 100644 --- a/src/WebExpress.WebCore/WebAttribute/SettingSectionAttribute.cs +++ b/src/WebExpress.WebCore/WebAttribute/SettingSectionAttribute.cs @@ -9,13 +9,18 @@ namespace WebExpress.WebCore.WebAttribute [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class SettingSectionAttribute : Attribute, IEndpointAttribute, ISettingCategoryAttribute, ISettingGroupAttribute { + /// + /// Returns the configuration section associated with the current settings. + /// + public SettingSection Section { get; } + /// /// Initializes a new instance of the class. /// /// The section where the settings page is listed. public SettingSectionAttribute(SettingSection section) { - + Section = section; } } } diff --git a/src/WebExpress.WebCore/WebAttribute/SubProtocolAttribute.cs b/src/WebExpress.WebCore/WebAttribute/SubProtocolAttribute.cs new file mode 100644 index 0000000..51e56de --- /dev/null +++ b/src/WebExpress.WebCore/WebAttribute/SubProtocolAttribute.cs @@ -0,0 +1,26 @@ +using System; +using System.Net.WebSockets; + +namespace WebExpress.WebCore.WebAttribute +{ + /// + /// Specifies the sub protocole for an WebSocket (see RFC 6455). + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class SubProtocolAttribute : Attribute, ISocketAttribute + { + /// + /// Returns the sub protocol. + /// + public string SubProtocol { get; } + + /// + /// Initializes a new instance of the class with the specified sub protocol. + /// + /// The sub protocol. + public SubProtocolAttribute(string subProtocol) + { + SubProtocol = subProtocol; + } + } +} diff --git a/src/WebExpress.WebCore/WebAttribute/TitleAttribute.cs b/src/WebExpress.WebCore/WebAttribute/TitleAttribute.cs index 0fdf584..4fba2a7 100644 --- a/src/WebExpress.WebCore/WebAttribute/TitleAttribute.cs +++ b/src/WebExpress.WebCore/WebAttribute/TitleAttribute.cs @@ -6,12 +6,18 @@ [System.AttributeUsage(System.AttributeTargets.Class)] public class TitleAttribute : System.Attribute, IPageAttribute, ISettingPageAttribute, IStatusPageAttribute { + /// + /// Returns the title associated with the current instance. + /// + public string Title { get; } + /// /// Initializes a new instance of the class. /// /// The display text. public TitleAttribute(string display) { + Title = display; } } } diff --git a/src/WebExpress.WebCore/WebComponent/ComponentActivator.cs b/src/WebExpress.WebCore/WebComponent/ComponentActivator.cs index 6787bbc..036cc1e 100644 --- a/src/WebExpress.WebCore/WebComponent/ComponentActivator.cs +++ b/src/WebExpress.WebCore/WebComponent/ComponentActivator.cs @@ -14,21 +14,37 @@ namespace WebExpress.WebCore.WebComponent public static class ComponentActivator { /// - /// Creates an instance of the specified response type with the component hub and advanced parameters. + /// Creates an instance of the specified response type with the component hub and + /// advanced parameters. /// - /// The type of the response. - /// The type of the response to create. - /// The reference to the context of the host. - /// The component hub to use for dependency injection. - /// Additional parameter with a status message to pass to the response's constructor. - /// Additional parameters to pass to the component's constructor. - /// An instance of the specified response type. - public static T CreateInstance(Type responseType, IHttpServerContext httpServerContext, IComponentHub componentHub, StatusMessage statusMessage, params object[] advancedParameters) where T : Response + /// + /// The type of the response. + /// + /// + /// The type of the response to create. + /// + /// + /// The reference to the context of the host. + /// + /// + /// The component hub to use for dependency injection. + /// + /// + /// Additional parameter with a status message to pass to the response's constructor. + /// + /// + /// Additional parameters to pass to the component's constructor. + /// + /// + /// An instance of the specified response type. + /// + public static TResponse CreateInstance(Type responseType, IHttpServerContext httpServerContext, IComponentHub componentHub, StatusMessage statusMessage, params object[] advancedParameters) + where TResponse : Response { var flags = BindingFlags.NonPublic | BindingFlags.Instance; var constructors = responseType?.GetConstructors(flags); - if (constructors != null) + if (constructors is not null) { foreach (var constructor in constructors.OrderByDescending(x => x.GetParameters().Length)) { @@ -47,29 +63,39 @@ public static T CreateInstance(Type responseType, IHttpServerContext httpServ .FirstOrDefault() ?? null ).ToArray(); - if (constructor.Invoke(parameterValues) is T component) + if (constructor.Invoke(parameterValues) is TResponse component) { return component; } } } - return Activator.CreateInstance(responseType) as T; + return Activator.CreateInstance(responseType) as TResponse; } /// - /// Creates an instance of the specified component type with the provided context, component hub advanced parameters. + /// Creates an instance of the specified component type with the provided context, + /// component hub advanced parameters. /// - /// The type of the component manager, which must implement . - /// The reference to the context of the host. - /// Additional parameters to pass to the component's constructor. - /// An instance of the specified component type. - public static T CreateInstance(IHttpServerContext httpServerContext, params object[] advancedParameters) where T : class, IComponentHub + /// + /// The type of the component manager, which must implement . + /// + /// + /// The reference to the context of the host. + /// + /// + /// Additional parameters to pass to the component's constructor. + /// + /// + /// An instance of the specified component type. + /// + public static TComponentHub CreateInstance(IHttpServerContext httpServerContext, params object[] advancedParameters) + where TComponentHub : class, IComponentHub { var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - var constructors = typeof(T).GetConstructors(flags); + var constructors = typeof(TComponentHub).GetConstructors(flags); - if (constructors != null) + if (constructors is not null) { foreach (var constructor in constructors.OrderByDescending(x => x.GetParameters().Length)) { @@ -83,31 +109,45 @@ public static T CreateInstance(IHttpServerContext httpServerContext, params o .FirstOrDefault() ?? null ).ToArray(); - if (constructor.Invoke(parameterValues) is T component) + if (constructor.Invoke(parameterValues) is TComponentHub component) { return component; } } } - return Activator.CreateInstance(typeof(T), advancedParameters) as T; + return Activator.CreateInstance(typeof(TComponentHub), advancedParameters) as TComponentHub; } /// - /// Creates an instance of the specified component type with the provided context, component hub advanced parameters. + /// Creates an instance of the specified component type with the provided context, + /// component hub advanced parameters. /// - /// The type of the component manager, which must implement . - /// The type of the component to create. - /// The reference to the context of the host. - /// The component hub to use for dependency injection. - /// Additional parameters to pass to the component's constructor. - /// An instance of the specified component type. - public static T CreateInstance(Type componentType, IHttpServerContext httpServerContext, IComponentHub componentHub, params object[] advancedParameters) where T : class, IComponentManager + /// + /// The type of the component manager, which must implement . + /// + /// + /// The type of the component to create. + /// + /// + /// The reference to the context of the host. + /// + /// + /// The component hub to use for dependency injection. + /// + /// + /// Additional parameters to pass to the component's constructor. + /// + /// + /// An instance of the specified component type. + /// + public static TComponentManager CreateInstance(Type componentType, IHttpServerContext httpServerContext, IComponentHub componentHub, params object[] advancedParameters) + where TComponentManager : class, IComponentManager { var flags = BindingFlags.NonPublic | BindingFlags.Instance; var constructors = componentType?.GetConstructors(flags); - if (constructors != null) + if (constructors is not null) { foreach (var constructor in constructors.OrderByDescending(x => x.GetParameters().Length)) { @@ -125,31 +165,45 @@ public static T CreateInstance(Type componentType, IHttpServerContext httpSer .FirstOrDefault() ?? null ).ToArray(); - if (constructor.Invoke(parameterValues) is T component) + if (constructor.Invoke(parameterValues) is TComponentManager component) { return component; } } } - return Activator.CreateInstance(componentType) as T; + return Activator.CreateInstance(componentType) as TComponentManager; } /// - /// Creates an instance of the specified component type with the provided context, component hub advanced parameters. + /// Creates an instance of the specified component type with the provided context, + /// component hub advanced parameters. /// - /// The type of the component manager, which must implement . - /// The reference to the context of the host. - /// The component hub to use for dependency injection. - /// The type of the component to create. - /// Additional parameters to pass to the component's constructor. - /// An instance of the specified component type. - public static T CreateInstance(IHttpServerContext httpServerContext, IComponentHub componentHub, Type componentType, params object[] advancedParameters) where T : IComponent + /// + /// The type of the component manager, which must implement . + /// + /// + /// The reference to the context of the host. + /// + /// + /// The component hub to use for dependency injection. + /// + /// + /// The type of the component to create. + /// + /// + /// Additional parameters to pass to the component's constructor. + /// + /// + /// An instance of the specified component type. + /// + public static TComponent CreateInstance(IHttpServerContext httpServerContext, IComponentHub componentHub, Type componentType, params object[] advancedParameters) + where TComponent : IComponent { var flags = BindingFlags.NonPublic | BindingFlags.Instance; var constructors = componentType?.GetConstructors(flags); - if (constructors != null) + if (constructors is not null) { foreach (var constructor in constructors.OrderByDescending(x => x.GetParameters().Length)) { @@ -167,31 +221,43 @@ public static T CreateInstance(IHttpServerContext httpServerContext, ICompone .FirstOrDefault() ?? null ).ToArray(); - if (constructor.Invoke(parameterValues) is T component) + if (constructor.Invoke(parameterValues) is TComponent component) { return component; } } } - return (T)Activator.CreateInstance(componentType); + return (TComponent)Activator.CreateInstance(componentType); } /// - /// Creates an instance of the specified component type with the provided context, component hub advanced parameters. + /// Creates an instance of the specified component type with the provided context, + /// component hub advanced parameters. /// - /// The type of the component, which must implement . - /// The reference to the context of the host. - /// The component hub to use for dependency injection. - /// Additional parameters to pass to the component's constructor. - /// An instance of the specified component type. - public static T CreateInstance(IHttpServerContext httpServerContext, IComponentHub componentHub, params object[] advancedParameters) where T : class, IComponent + /// + /// The type of the component, which must implement . + /// + /// + /// The reference to the context of the host. + /// + /// + /// The component hub to use for dependency injection. + /// + /// + /// Additional parameters to pass to the component's constructor. + /// + /// + /// An instance of the specified component type. + /// + public static TComponent CreateInstance(IHttpServerContext httpServerContext, IComponentHub componentHub, params object[] advancedParameters) + where TComponent : class, IComponent { var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - var componentType = typeof(T); + var componentType = typeof(TComponent); var constructors = componentType?.GetConstructors(flags); - if (constructors != null) + if (constructors is not null) { foreach (var constructor in constructors.OrderByDescending(x => x.GetParameters().Length)) { @@ -205,32 +271,49 @@ public static T CreateInstance(IHttpServerContext httpServerContext, ICompone properties.Where(x => x.PropertyType == parameter.ParameterType) .FirstOrDefault()? .GetValue(componentHub) ?? - advancedParameters.Where(x => x != null) + advancedParameters.Where(x => x is not null) .Where(x => x.GetType() == parameter.ParameterType) .FirstOrDefault() ?? null ).ToArray(); - if (constructor.Invoke(parameterValues) is T component) + if (constructor.Invoke(parameterValues) is TComponent component) { return component; } } } - return Activator.CreateInstance(componentType) as T; + return Activator.CreateInstance(componentType) as TComponent; } /// - /// Creates an instance of the specified component type with the provided context and component hub and advanced parameters. + /// Creates an instance of the specified component type with the provided context and + /// component hub and advanced parameters. /// - /// The type of the component, which must implement . - /// The type of the context, which must implement . - /// The type of the component to create. - /// The context to pass to the component's constructor. - /// The reference to the context of the host. - /// The component hub to use for dependency injection. - /// Additional parameters to pass to the component's constructor. - /// An instance of the specified component type. + /// + /// The type of the component, which must implement . + /// + /// + /// The type of the context, which must implement . + /// + /// + /// The type of the component to create. + /// + /// + /// The context to pass to the component's constructor. + /// + /// + /// The reference to the context of the host. + /// + /// + /// The component hub to use for dependency injection. + /// + /// + /// Additional parameters to pass to the component's constructor. + /// + /// + /// An instance of the specified component type. + /// public static TComponent CreateInstance(Type componentType, TContext context, IHttpServerContext httpServerContext, IComponentHub componentHub, params object[] advancedParameters) where TComponent : class, IComponent where TContext : IContext @@ -238,13 +321,18 @@ public static TComponent CreateInstance(Type componentType var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var constructors = componentType?.GetConstructors(flags); - if (constructors != null) + if (constructors is not null) { foreach (var constructor in constructors.OrderByDescending(x => x.GetParameters().Length)) { // injection var parameters = constructor.GetParameters(); - var hubProperties = componentHub.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + var components = componentHub.Managers + .Select(x => new + { + type = x.GetType().GetInterfaces().FirstOrDefault(), + value = x + }); var contextIdProperty = context.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(x => x.PropertyType == typeof(IComponentId)) .FirstOrDefault(); @@ -254,9 +342,9 @@ public static TComponent CreateInstance(Type componentType parameter.ParameterType == typeof(IHttpServerContext) ? httpServerContext : parameter.ParameterType == typeof(TContext) ? context : parameter.ParameterType == typeof(IComponentId) ? contextIdProperty?.GetValue(context) : - hubProperties.Where(x => x.PropertyType == parameter.ParameterType) - .FirstOrDefault()? - .GetValue(componentHub) ?? + components.Where(x => x.type == parameter.ParameterType) + .Select(x => x.value) + .FirstOrDefault() ?? advancedParameters.Where(x => x.GetType() == parameter.ParameterType || ( @@ -266,6 +354,10 @@ public static TComponent CreateInstance(Type componentType ( parameter.ParameterType == typeof(IPageContext) && x.GetType().GetInterfaces().Any(x => x == typeof(IPageContext)) + ) || + ( + parameter.ParameterType == typeof(IRequest) && + x.GetType().GetInterfaces().Any(x => x == typeof(IRequest)) ) ) .FirstOrDefault() ?? null @@ -282,22 +374,37 @@ public static TComponent CreateInstance(Type componentType } /// - /// Creates an instance of the specified component type with the provided context and component hub and advanced parameters. + /// Creates an instance of the specified component type with the provided context and + /// component hub and advanced parameters. /// - /// The type of the context, which must implement . - /// The type of the component to create. - /// The context to pass to the component's constructor. - /// The reference to the context of the host. - /// The component hub to use for dependency injection. - /// Additional parameters to pass to the component's constructor. - /// An instance of the specified component type. + /// + /// The type of the context, which must implement . + /// + /// + /// The type of the component to create. + /// + /// + /// The context to pass to the component's constructor. + /// + /// + /// The reference to the context of the host. + /// + /// + /// The component hub to use for dependency injection. + /// + /// + /// Additional parameters to pass to the component's constructor. + /// + /// + /// An instance of the specified component type. + /// public static IComponent CreateInstance(Type componentType, TContext context, IHttpServerContext httpServerContext, IComponentHub componentHub, params object[] advancedParameters) where TContext : IContext { var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var constructors = componentType?.GetConstructors(flags); - if (constructors != null) + if (constructors is not null) { foreach (var constructor in constructors.OrderByDescending(x => x.GetParameters().Length)) { diff --git a/src/WebExpress.WebCore/WebComponent/ComponentHub.cs b/src/WebExpress.WebCore/WebComponent/ComponentHub.cs index ed96034..021e36c 100644 --- a/src/WebExpress.WebCore/WebComponent/ComponentHub.cs +++ b/src/WebExpress.WebCore/WebComponent/ComponentHub.cs @@ -20,6 +20,7 @@ using WebExpress.WebCore.WebSession; using WebExpress.WebCore.WebSettingPage; using WebExpress.WebCore.WebSitemap; +using WebExpress.WebCore.WebSocket; using WebExpress.WebCore.WebStatusPage; using WebExpress.WebCore.WebTask; using WebExpress.WebCore.WebTheme; @@ -53,6 +54,7 @@ public class ComponentHub : IComponentHub private readonly JobManager _jobManager; private readonly TaskManager _taskManager; private readonly IdentityManager _identityManager; + private readonly SocketManager _socketManager; private readonly ThemeManager _themeManager; private int _lastCounter = 0; @@ -91,6 +93,7 @@ public class ComponentHub : IComponentHub _identityManager, _sessionManager, _taskManager, + _socketManager, _themeManager }.Concat(_dictionary.Values.SelectMany(x => x).Select(x => x.ComponentInstance)); @@ -214,6 +217,12 @@ public class ComponentHub : IComponentHub /// The instance of the session manager. public ISessionManager SessionManager => _sessionManager; + /// + /// Returns the socket manager. + /// + /// The instance of the socket manager. + public ISocketManager SocketManager => _socketManager; + /// /// Returns the theme manager. /// @@ -250,6 +259,7 @@ protected ComponentHub(IHttpServerContext httpServerContext) _sessionManager = CreateInstance(typeof(SessionManager)) as SessionManager; _taskManager = CreateInstance(typeof(TaskManager)) as TaskManager; _identityManager = CreateInstance(typeof(IdentityManager)) as IdentityManager; + _socketManager = CreateInstance(typeof(SocketManager)) as SocketManager; _themeManager = CreateInstance(typeof(ThemeManager)) as ThemeManager; _internationalizationManager.Register(typeof(HttpServer).Assembly, typeof(HttpServer).Assembly.GetName().Name?.ToLower()); @@ -277,7 +287,7 @@ protected ComponentHub(IHttpServerContext httpServerContext) /// The instance of the create and initialized component. private IComponentManager CreateInstance(Type componentType) { - if (componentType == null) + if (componentType is null) { return null; } @@ -350,24 +360,28 @@ internal void Register(IPluginContext pluginContext) var assembly = pluginContext.Assembly; - _dictionary.Add(pluginContext, []); - var componentItems = _dictionary[pluginContext]; + // initialize the component entry as an empty list for easier manipulation + var componentList = new List(); + _dictionary.Add(pluginContext, componentList); - foreach (var type in assembly.GetExportedTypes().Where(x => x.IsClass && x.IsSealed && x.GetInterface(typeof(IComponentManager).Name) != null)) + foreach (var type in assembly + .GetExportedTypes() + .Where(x => x.IsClass && x.IsSealed && x.GetInterface(typeof(IComponentManager).Name) is not null)) { var id = type.FullName?.ToLower(); // determining attributes var componentInstance = CreateInstance(type); - if (!componentItems.Where(x => x.ComponentId.Equals(id, StringComparison.OrdinalIgnoreCase)).Any()) + // check for duplicates + if (!componentList.Any(x => x.ComponentId.Equals(id, StringComparison.OrdinalIgnoreCase))) { - _dictionary[pluginContext] = componentItems.Concat([ new ComponentItem() + componentList.Add(new ComponentItem() { ComponentClass = type, ComponentId = id, ComponentInstance = componentInstance - }]); + }); _httpServerContext.Log.Debug ( @@ -386,6 +400,9 @@ internal void Register(IPluginContext pluginContext) } } + // make sure the dictionary uses IEnumerable as value type, if the dictionary requires it + _dictionary[pluginContext] = componentList; + Log(); } @@ -466,20 +483,24 @@ internal void ShutDownComponent(IPluginContext pluginContext) /// The context of the plugin that contains the applications to remove. public void Remove(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return; } - if (_dictionary.TryGetValue(pluginContext, out IEnumerable componentItems)) + // try to get a list for safe removal and iteration + if (_dictionary.TryGetValue(pluginContext, out var componentItems)) { - if (!componentItems.Any()) + // for IEnumerable, first eagerly materialize the enumeration + var items = componentItems.ToList(); + if (items.Count == 0) { return; } - foreach (var componentItem in componentItems) + foreach (var componentItem in items) { + // raise the RemoveComponent event for each item OnRemoveComponent(componentItem.ComponentInstance); _httpServerContext.Log.Debug diff --git a/src/WebExpress.WebCore/WebComponent/IComponentHub.cs b/src/WebExpress.WebCore/WebComponent/IComponentHub.cs index c5bbaef..6ac352d 100644 --- a/src/WebExpress.WebCore/WebComponent/IComponentHub.cs +++ b/src/WebExpress.WebCore/WebComponent/IComponentHub.cs @@ -18,6 +18,7 @@ using WebExpress.WebCore.WebSession; using WebExpress.WebCore.WebSettingPage; using WebExpress.WebCore.WebSitemap; +using WebExpress.WebCore.WebSocket; using WebExpress.WebCore.WebStatusPage; using WebExpress.WebCore.WebTask; using WebExpress.WebCore.WebTheme; @@ -164,6 +165,12 @@ public interface IComponentHub : IComponentManager /// The instance of the session manager. ISessionManager SessionManager { get; } + /// + /// Returns the socket manager. + /// + /// The instance of the socket manager. + ISocketManager SocketManager { get; } + /// /// Returns the theme manager. /// diff --git a/src/WebExpress.WebCore/WebComponent/Model/ComponentDictionary.cs b/src/WebExpress.WebCore/WebComponent/Model/ComponentDictionary.cs index d49fc29..b80e44b 100644 --- a/src/WebExpress.WebCore/WebComponent/Model/ComponentDictionary.cs +++ b/src/WebExpress.WebCore/WebComponent/Model/ComponentDictionary.cs @@ -8,7 +8,7 @@ namespace WebExpress.WebCore.WebComponent.Model /// key = plugin /// value = component item /// - internal class ComponentDictionary : Dictionary> + internal class ComponentDictionary : Dictionary> { } diff --git a/src/WebExpress.WebCore/WebCondition/ICondition.cs b/src/WebExpress.WebCore/WebCondition/ICondition.cs index d0e75d4..4e1ad44 100644 --- a/src/WebExpress.WebCore/WebCondition/ICondition.cs +++ b/src/WebExpress.WebCore/WebCondition/ICondition.cs @@ -12,6 +12,6 @@ public interface ICondition /// /// The request. /// True if the condition is met, false otherwise. - bool Fulfillment(Request request); + bool Fulfillment(IRequest request); } } diff --git a/src/WebExpress.WebCore/WebDomain/IDomain.cs b/src/WebExpress.WebCore/WebDomain/IDomain.cs new file mode 100644 index 0000000..225d1de --- /dev/null +++ b/src/WebExpress.WebCore/WebDomain/IDomain.cs @@ -0,0 +1,12 @@ +namespace WebExpress.WebCore.WebDomain +{ + /// + /// Represents a logical application domain used to categorize or group + /// components, pages or functional areas within the system. Implementations + /// define domain-specific metadata or identifiers that can be used for + /// routing, filtering or contextual association. + /// + public interface IDomain + { + } +} diff --git a/src/WebExpress.WebCore/WebEndpoint/EndpointManager.cs b/src/WebExpress.WebCore/WebEndpoint/EndpointManager.cs index 461dcd8..5bec36d 100644 --- a/src/WebExpress.WebCore/WebEndpoint/EndpointManager.cs +++ b/src/WebExpress.WebCore/WebEndpoint/EndpointManager.cs @@ -92,7 +92,7 @@ public void Remove() where TEndpointContext : IEndpointContext /// An enumeration of endpoint contexts. public IEnumerable GetEndpoints(Type endpointType, IApplicationContext applicationContext = null) { - if (endpointType == null) + if (endpointType is null) { return []; } @@ -106,7 +106,7 @@ public IEnumerable GetEndpoints(Type endpointType, IApplicatio /// The request to handle. /// The context of the endpoint handling the request. /// The response generated by the endpoint. - public Response HandleRequest(Request request, IEndpointContext endpointContext) + public IResponse HandleRequest(IRequest request, IEndpointContext endpointContext) { var registration = _registrations .Where(x => x.Key == endpointContext?.GetType()) @@ -182,42 +182,55 @@ public static IRoute CreateEndpointRoute var name = default(string); var description = default(string); var icon = default(IIcon); + var hidden = false; - var typeName = $"{s.FullNamespace}.Index"; - var segmentInfoType = classType.Assembly.GetType(typeName, throwOnError: false, ignoreCase: true); + var segmentInfoType = classType.Assembly + .GetTypes() + .Where(t => t.IsClass) + .Where(t => t.Namespace?.ToLowerInvariant() == s.FullNamespace.ToLowerInvariant()) + .FirstOrDefault(t => t.Name.StartsWith("Index", StringComparison.OrdinalIgnoreCase)); - if (segmentInfoType != null) + var nameAttr = segmentInfoType?.CustomAttributes + .FirstOrDefault(x => x.AttributeType == typeof(TitleAttribute)); + var descAttr = segmentInfoType?.CustomAttributes + .FirstOrDefault(x => x.AttributeType == typeof(DescriptionAttribute)); + var iconAttr = segmentInfoType?.CustomAttributes + .FirstOrDefault(x => x.AttributeType.IsGenericType && + x.AttributeType.GetGenericTypeDefinition() == typeof(WebIconAttribute<>)); + var hiddenAttr = segmentInfoType?.CustomAttributes + .FirstOrDefault(x => x.AttributeType == typeof(SegmentHiddenAttribute)); + + if (segmentInfoType is not null) { var segAttrType = segmentInfoType.CustomAttributes .Where(x => x.AttributeType.GetInterfaces().Contains(typeof(ISegmentAttribute))) .Select(x => x.AttributeType) .FirstOrDefault(); - var segInstance = segAttrType != null + var segInstance = segAttrType is not null ? segmentInfoType.GetCustomAttribute(segAttrType, false) as ISegmentAttribute : null; - var nameAttr = segmentInfoType.CustomAttributes - .FirstOrDefault(x => x.AttributeType == typeof(TitleAttribute)); - var descAttr = segmentInfoType.CustomAttributes - .FirstOrDefault(x => x.AttributeType == typeof(DescriptionAttribute)); - var iconAttr = segmentInfoType.CustomAttributes - .FirstOrDefault(x => x.AttributeType.IsGenericType && - x.AttributeType.GetGenericTypeDefinition() == typeof(WebIconAttribute<>)); segmentResult = segInstance?.ToPathSegment(); + segmentResult?.IsHidden = hiddenAttr is not null; name = nameAttr?.ConstructorArguments.FirstOrDefault().Value?.ToString(); description = descAttr?.ConstructorArguments.FirstOrDefault().Value?.ToString(); - icon = iconAttr != null + icon = iconAttr is not null ? Activator.CreateInstance(iconAttr.AttributeType.GenericTypeArguments.FirstOrDefault()) as IIcon : null; + hidden = hiddenAttr is not null; } return new { - Segment = segmentResult ?? new UriPathSegmentConstant(s.Segment), + Segment = segmentResult ?? new UriPathSegmentConstant(s.Segment) + { + IsHidden = segmentInfoType is null || hiddenAttr is not null + }, Name = name, Description = description, - Icon = icon + Icon = icon, + Hidden = segmentResult is null || hidden }; }); diff --git a/src/WebExpress.WebCore/WebEndpoint/EndpointRegistration.cs b/src/WebExpress.WebCore/WebEndpoint/EndpointRegistration.cs index 72b82b6..9d01d65 100644 --- a/src/WebExpress.WebCore/WebEndpoint/EndpointRegistration.cs +++ b/src/WebExpress.WebCore/WebEndpoint/EndpointRegistration.cs @@ -33,6 +33,6 @@ public class EndpointRegistration /// /// Returns or sets the function to handle requests. /// - public Func HandleRequest { get; set; } + public Func HandleRequest { get; set; } } } diff --git a/src/WebExpress.WebCore/WebEndpoint/IEndpointManager.cs b/src/WebExpress.WebCore/WebEndpoint/IEndpointManager.cs index bf11c67..780b046 100644 --- a/src/WebExpress.WebCore/WebEndpoint/IEndpointManager.cs +++ b/src/WebExpress.WebCore/WebEndpoint/IEndpointManager.cs @@ -37,21 +37,12 @@ public interface IEndpointManager : IComponentManager /// An enumeration of endpoint contexts. IEnumerable GetEndpoints(Type endpointType, IApplicationContext applicationContext = null); - ///// - ///// Creates a new instance or if caching is active, a possibly existing instance is returned. - ///// - ///// The endpoint context. - ///// The uri. - ///// The search context. - ///// The created endpoint. - //IEndpoint CreateEndpoint(IEndpointContext endpointContext, UriResource uri, SearchContext searchContext); - /// /// Handles a request and returns a response. /// /// The request to handle. /// The context of the endpoint handling the request. /// The response generated by the endpoint. - Response HandleRequest(Request request, IEndpointContext endpointContext); + IResponse HandleRequest(IRequest request, IEndpointContext endpointContext); } } diff --git a/src/WebExpress.WebCore/WebEndpoint/IRoute.cs b/src/WebExpress.WebCore/WebEndpoint/IRoute.cs index 39f8929..2130669 100644 --- a/src/WebExpress.WebCore/WebEndpoint/IRoute.cs +++ b/src/WebExpress.WebCore/WebEndpoint/IRoute.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using WebExpress.WebCore.WebMessage; +using WebExpress.WebCore.WebParameter; using WebExpress.WebCore.WebUri; namespace WebExpress.WebCore.WebEndpoint diff --git a/src/WebExpress.WebCore/WebEndpoint/RouteEndpoint.cs b/src/WebExpress.WebCore/WebEndpoint/RouteEndpoint.cs index 9666f69..a0d5c32 100644 --- a/src/WebExpress.WebCore/WebEndpoint/RouteEndpoint.cs +++ b/src/WebExpress.WebCore/WebEndpoint/RouteEndpoint.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using WebExpress.WebCore.WebMessage; +using WebExpress.WebCore.WebParameter; using WebExpress.WebCore.WebUri; namespace WebExpress.WebCore.WebEndpoint @@ -86,6 +86,19 @@ public RouteEndpoint(params IUriPathSegment[] segments) } } + /// + /// Initializes a new instance of the class. + /// + /// The path segments. + public RouteEndpoint(IEnumerable segments) + { + if (segments.Any()) + { + PathSegments = PathSegments + .Concat(segments.Where(x => !x.IsEmpty).Select(x => x.Copy())); + } + } + /// /// Initializes a new instance of the class. /// @@ -153,7 +166,7 @@ public IRoute Concat(string segment) /// A new IRoute instance representing the route after concatenation. public virtual IRoute Concat(params IUriPathSegment[] segments) { - if (segments == null || segments.Length == 0) + if (segments is null || segments.Length == 0) { return this; } @@ -161,7 +174,7 @@ public virtual IRoute Concat(params IUriPathSegment[] segments) var copy = new RouteEndpoint((IRoute)this); copy.PathSegments = copy.PathSegments .Select(x => x.Copy()) - .Concat(segments.Where(x => x != null).Where(x => !x.IsEmpty)); + .Concat(segments.Where(x => x is not null).Where(x => !x.IsEmpty)); return copy; } @@ -195,7 +208,7 @@ public virtual IRoute RemoveSegment(string segments) /// An instance of IUri representing the route as a URI. public IUri ToUri(params Parameter[] parameters) { - return new UriEndpoint(this).SetParameters(parameters); + return new UriEndpoint([.. PathSegments]).BindParameters(parameters); } /// diff --git a/src/WebExpress.WebCore/WebEvent/EventManager.cs b/src/WebExpress.WebCore/WebEvent/EventManager.cs index 04d6b87..a2630df 100644 --- a/src/WebExpress.WebCore/WebEvent/EventManager.cs +++ b/src/WebExpress.WebCore/WebEvent/EventManager.cs @@ -155,8 +155,8 @@ private void Register(IPluginContext pluginContext, IEnumerable).Name) != null + x.GetInterface(typeof(IEventHandler).Name) is not null || + x.GetInterface(typeof(IEventHandler<>).Name) is not null ) )) { @@ -260,7 +260,7 @@ internal void Remove(IPluginContext pluginContext) /// The context of the application that contains the events to remove. internal void Remove(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { return; } diff --git a/src/WebExpress.WebCore/WebEvent/Model/EventItem.cs b/src/WebExpress.WebCore/WebEvent/Model/EventItem.cs index 16c78f1..0f294a3 100644 --- a/src/WebExpress.WebCore/WebEvent/Model/EventItem.cs +++ b/src/WebExpress.WebCore/WebEvent/Model/EventItem.cs @@ -97,7 +97,7 @@ public void Process(object sender, IEventArgument eventArgument) .GetInterfaces() .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEventHandler<>)); - if (handlerType != null) + if (handlerType is not null) { var genericArgument = handlerType.GetGenericArguments().First(); var method = handlerType.GetMethod("Process"); diff --git a/src/WebExpress.WebCore/WebEx.cs b/src/WebExpress.WebCore/WebEx.cs index 31f6b7b..247ca72 100644 --- a/src/WebExpress.WebCore/WebEx.cs +++ b/src/WebExpress.WebCore/WebEx.cs @@ -65,6 +65,11 @@ public sealed class WebEx /// public static IComponentHub ComponentHub => _componentHub; + /// + /// Returns or sets the path to the favicon image used by the application. + /// + public static string Favicon { get; set; } = "webexpress.webui/assets/img/webexpress.svg"; + /// /// Running the application. /// @@ -173,7 +178,7 @@ private void OnCancel(object sender, ConsoleCancelEventArgs e) /// The configuration file. private void OnInitialization(string args, string configFile) { - // Config laden + // load configuration using var reader = new FileStream(configFile, FileMode.Open); var serializer = new XmlSerializer(typeof(HttpServerConfig)); var config = serializer.Deserialize(reader) as HttpServerConfig; @@ -231,7 +236,7 @@ private void OnInitialization(string args, string configFile) _httpServer.HttpServerContext.Log.Begin(config.Log); // log program start - _httpServer.HttpServerContext.Log.Seperator('/'); + _httpServer.HttpServerContext.Log.Separator('/'); _httpServer.HttpServerContext.Log.Info(message: I18N.Translate("webexpress.webcore:app.startup")); _httpServer.HttpServerContext.Log.Info(message: "".PadRight(80, '-')); _httpServer.HttpServerContext.Log.Info(message: I18N.Translate("webexpress.webcore:app.version"), args: Version); @@ -249,7 +254,7 @@ private void OnInitialization(string args, string configFile) _httpServer.HttpServerContext.Log.Info(message: I18N.Translate("webexpress.webcore:app.uri"), args: v.Uri); } - _httpServer.HttpServerContext.Log.Seperator('='); + _httpServer.HttpServerContext.Log.Separator('='); if (!Directory.Exists(config.PackageBase)) { @@ -293,11 +298,11 @@ private void OnExit() Exit?.Invoke(this, EventArgs.Empty); // end of program log - _httpServer.HttpServerContext.Log.Seperator('='); + _httpServer.HttpServerContext.Log.Separator('='); _httpServer.HttpServerContext.Log.Info(message: I18N.Translate("webexpress.webcore:app.errors"), args: _httpServer.HttpServerContext.Log.ErrorCount); _httpServer.HttpServerContext.Log.Info(message: I18N.Translate("webexpress.webcore:app.warnings"), args: _httpServer.HttpServerContext.Log.WarningCount); _httpServer.HttpServerContext.Log.Info(message: I18N.Translate("webexpress.webcore:app.done")); - _httpServer.HttpServerContext.Log.Seperator('/'); + _httpServer.HttpServerContext.Log.Separator('/'); // Stop running (_componentHub as ComponentHub).ShutDown(); diff --git a/src/WebExpress.WebCore/WebExpress.WebCore.csproj b/src/WebExpress.WebCore/WebExpress.WebCore.csproj index 53c7830..ea42bee 100644 --- a/src/WebExpress.WebCore/WebExpress.WebCore.csproj +++ b/src/WebExpress.WebCore/WebExpress.WebCore.csproj @@ -3,9 +3,9 @@ Library WebExpress.WebCore - 0.0.9.0 - 0.0.9.0 - net9.0 + 0.0.10.0 + 0.0.10.0 + net10.0 any https://github.com/webexpress-framework/WebExpress.git webexpress-framework@outlook.com @@ -14,7 +14,7 @@ true True Core library of the WebExpress web server. - 0.0.9-alpha + 0.0.10-alpha https://github.com/webexpress-framework/WebExpress icon.png README.md @@ -44,7 +44,7 @@ - + True \ diff --git a/src/WebExpress.WebCore/WebExpress.WebCore.sln b/src/WebExpress.WebCore/WebExpress.WebCore.sln new file mode 100644 index 0000000..00a922b --- /dev/null +++ b/src/WebExpress.WebCore/WebExpress.WebCore.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebExpress.WebCore", "WebExpress.WebCore.csproj", "{01E99C8B-0B21-46C0-93A9-6A8C2E89B619}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {01E99C8B-0B21-46C0-93A9-6A8C2E89B619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01E99C8B-0B21-46C0-93A9-6A8C2E89B619}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01E99C8B-0B21-46C0-93A9-6A8C2E89B619}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01E99C8B-0B21-46C0-93A9-6A8C2E89B619}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FD0FF9EA-12BF-4F03-B62F-F720D51C23DB} + EndGlobalSection +EndGlobal diff --git a/src/WebExpress.WebCore/WebFragment/FragmentComparer.cs b/src/WebExpress.WebCore/WebFragment/FragmentComparer.cs index 2d627ff..112bcc6 100644 --- a/src/WebExpress.WebCore/WebFragment/FragmentComparer.cs +++ b/src/WebExpress.WebCore/WebFragment/FragmentComparer.cs @@ -19,7 +19,7 @@ public class FragmentComparer : IEqualityComparer /// True if both objects are similar; false otherwise. public bool Equals(T x, T y) { - if (x == null && y == null) + if (x is null && y is null) { return true; } diff --git a/src/WebExpress.WebCore/WebFragment/FragmentConditionExtentsion.cs b/src/WebExpress.WebCore/WebFragment/FragmentConditionExtentsion.cs index e8085db..8cc85c1 100644 --- a/src/WebExpress.WebCore/WebFragment/FragmentConditionExtentsion.cs +++ b/src/WebExpress.WebCore/WebFragment/FragmentConditionExtentsion.cs @@ -15,7 +15,7 @@ public static class FragmentConditionExtentsion /// The collection of conditions to check. /// The request to evaluate the conditions against. /// True if all conditions are fulfilled; otherwise, false. - public static bool Check(this IEnumerable conditions, Request request) + public static bool Check(this IEnumerable conditions, IRequest request) { foreach (var condition in conditions) { diff --git a/src/WebExpress.WebCore/WebFragment/FragmentManager.cs b/src/WebExpress.WebCore/WebFragment/FragmentManager.cs index bf5d45b..b2b93ae 100644 --- a/src/WebExpress.WebCore/WebFragment/FragmentManager.cs +++ b/src/WebExpress.WebCore/WebFragment/FragmentManager.cs @@ -105,7 +105,7 @@ private void Register(IPluginContext pluginContext, IEnumerable x.IsClass == true && x.IsSealed && x.IsPublic) - .Where(x => x.GetInterface(typeof(IFragment<,>).Name) != null)) + .Where(x => x.GetInterface(typeof(IFragment<,>).Name) is not null)) { var id = fragmentType.FullName?.ToLower(); var scopes = new List(); @@ -236,7 +236,7 @@ private void Register(IPluginContext pluginContext, IEnumerableThe context of the plugin that contains the components to remove. internal void Remove(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return; } @@ -255,7 +255,7 @@ internal void Remove(IPluginContext pluginContext) /// The context of the application that contains the fragments to remove. internal void Remove(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { return; } @@ -352,7 +352,8 @@ public IEnumerable GetFragments(Type fragmentType) /// The fragment type. /// The application context. /// An enumeration of the filtered fragment contexts. - public IEnumerable GetFragments(IApplicationContext applicationContext) where TFragment : IFragmentBase + public IEnumerable GetFragments(IApplicationContext applicationContext) + where TFragment : IFragmentBase { return GetFragments(applicationContext, typeof(TFragment)); } diff --git a/src/WebExpress.WebCore/WebFragment/Model/FragmentDictionary.cs b/src/WebExpress.WebCore/WebFragment/Model/FragmentDictionary.cs index 1213440..51631b2 100644 --- a/src/WebExpress.WebCore/WebFragment/Model/FragmentDictionary.cs +++ b/src/WebExpress.WebCore/WebFragment/Model/FragmentDictionary.cs @@ -37,7 +37,7 @@ public bool AddFragmentItem(IPluginContext pluginContext, IApplicationContext ap { var type = fragmentItem.FragmentClass; - if (type.GetInterface(typeof(IFragment<,>).Name) == null) + if (type.GetInterface(typeof(IFragment<,>).Name) is null) { return false; } @@ -150,15 +150,15 @@ public IEnumerable GetFragmentItems(IApplicationContext applicatio public IEnumerable GetFragmentItems(IApplicationContext applicationContext, Type fragment, Type section, IEnumerable scopes) { return _dict.Values - .SelectMany(x => x) - .Where(x => x.Key == applicationContext) - .SelectMany(x => x.Value) - .Where(x => x.Key == section || section.IsAssignableFrom(x.Key)) - .SelectMany(x => x.Value) - .Where(x => scopes.Any(y => x.Key == y)) - .SelectMany(x => x.Value) - .Where(x => x.FragmentClass == fragment || fragment.IsAssignableFrom(x.FragmentClass)) - .OrderBy(x => x.Order); + .SelectMany(x => x) + .Where(x => x.Key == applicationContext) + .SelectMany(x => x.Value) + .Where(x => x.Key == section || section.IsAssignableFrom(x.Key)) + .SelectMany(x => x.Value) + .Where(x => scopes.Any(y => x.Key == y)) + .SelectMany(x => x.Value) + .Where(x => x.FragmentClass == fragment || fragment.IsAssignableFrom(x.FragmentClass)) + .OrderBy(x => x.Order); } /// diff --git a/src/WebExpress.WebCore/WebFragment/Model/FragmentItem.cs b/src/WebExpress.WebCore/WebFragment/Model/FragmentItem.cs index b2a5463..e5be93e 100644 --- a/src/WebExpress.WebCore/WebFragment/Model/FragmentItem.cs +++ b/src/WebExpress.WebCore/WebFragment/Model/FragmentItem.cs @@ -82,18 +82,18 @@ public FragmentItem(IComponentHub componentHub, IHttpServerContext httpServerCon /// Create the instance of the component. /// /// The page context. - public TFragment CreateInstance(IPageContext pageContext = null) + public TFragment CreateInstance(IPageContext pageContext = null) where TFragment : IFragmentBase { var instance = _instance; instance ??= ComponentActivator.CreateInstance ( - FragmentClass, - FragmentContext, - _httpServerContext, - _componentHub, - FragmentContext, + FragmentClass, + FragmentContext, + _httpServerContext, + _componentHub, + FragmentContext, pageContext ); @@ -157,7 +157,7 @@ public IHtmlNode Render(TRenderContext renderContex /// /// The request. /// True if the fragment is active, false otherwise. - public bool CheckConditions(Request request) + public bool CheckConditions(IRequest request) { return !FragmentContext.Conditions.Any() || FragmentContext.Conditions.All(x => x.Fulfillment(request)); } diff --git a/src/WebExpress.WebCore/WebHtml/DeterministicId.cs b/src/WebExpress.WebCore/WebHtml/DeterministicId.cs new file mode 100644 index 0000000..7764b69 --- /dev/null +++ b/src/WebExpress.WebCore/WebHtml/DeterministicId.cs @@ -0,0 +1,125 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; + +namespace WebExpress.WebCore.WebHtml +{ + /// + /// Provides methods for generating deterministic unique identifiers + /// based on the caller's file path, line number, and an optional + /// index value. + /// + /// + /// This class utilizes a caching mechanism to ensure that repeated + /// calls with the same parameters return the same identifier, + /// enhancing performance and consistency. + /// + public static class DeterministicId + { + private static readonly ConcurrentDictionary Cache = new(); + + /// + /// Generates a deterministic unique identifier based on the caller's file + /// path, line number, and an optional index value. + /// + /// + /// This method uses a caching mechanism to ensure that repeated calls with + /// the same parameters return the same identifier. The generated identifier + /// is based on a FNV-1a hash of the signature formed from the file path, + /// line number, and index. + /// + /// + /// An optional object that provides additional context for generating the + /// identifier. If specified, its hash code is included in the identifier + /// to further distinguish it. + /// + /// + /// The full path of the source file where the method is called. This + /// value is automatically supplied by the compiler. + /// + /// + /// The line number in the source file where the method is called. This + /// value is automatically supplied by the compiler. + /// + /// + /// A unique identifier string that represents the combination of the file path, + /// line number, and optional index. + /// + public static string Create + ( + object context = null, + [CallerFilePath] string file = "", + [CallerLineNumber] int line = 0 + ) + { + var stack = new StackTrace(skipFrames: 1, fNeedFileInfo: false); + var frames = stack.GetFrames(); + + var sb = new StringBuilder(128); + + sb.Append(file); + sb.Append(':'); + sb.Append(line); + + if (context is not null) + { + sb.Append(':'); + sb.Append(context.GetHashCode()); + } + + foreach (var f in frames) + { + var m = f.GetMethod(); + sb.Append(m.Name); + sb.Append(f.GetILOffset()); + } + + var signature = sb.ToString(); + + if (Cache.TryGetValue(signature, out var cached)) + { + return cached; + } + + // fnv-1a hash + var hash = Fnv1a(signature); + + var id = "id_" + hash.ToString("X"); + + Cache[signature] = id; + + return id; + } + + /// + /// Calculates the 32-bit FNV-1a hash value for the specified string. + /// + /// + /// The FNV-1a algorithm is a non-cryptographic hash function known + /// for its simplicity and speed. It is commonly used for hash tables + /// and checksums, but should not be used for cryptographic purposes. + /// + /// + /// The input string for which to compute the hash. This parameter + /// cannot be null. + /// + /// + /// A 32-bit unsigned integer representing the FNV-1a hash of the + /// input string. + /// + private static uint Fnv1a(string text) + { + unchecked + { + uint hash = 2166136261; + for (int i = 0; i < text.Length; i++) + { + hash = (hash ^ text[i]) * 16777619; + } + + return hash; + } + } + } +} diff --git a/src/WebExpress.WebCore/WebHtml/HtmlElement.cs b/src/WebExpress.WebCore/WebHtml/HtmlElement.cs index 3b9410b..e7cd6cf 100644 --- a/src/WebExpress.WebCore/WebHtml/HtmlElement.cs +++ b/src/WebExpress.WebCore/WebHtml/HtmlElement.cs @@ -278,7 +278,7 @@ protected string GetAttribute(string name) { var a = _attributes.Where(x => x.Name == name).FirstOrDefault(); - if (a != null) + if (a is not null) { return a is HtmlAttribute ? (a as HtmlAttribute).Value : string.Empty; } @@ -295,7 +295,7 @@ protected bool HasAttribute(string name) { var a = _attributes.Where(x => x.Name == name).FirstOrDefault(); - return (a != null); + return (a is not null); } /// @@ -307,7 +307,7 @@ protected void SetAttribute(string name, string value) { var a = _attributes.Where(x => x.Name == name).FirstOrDefault(); - if (a != null) + if (a is not null) { if (string.IsNullOrWhiteSpace(value)) { @@ -340,7 +340,7 @@ protected void SetAttribute(string name) var a = _attributes.Where(x => x.Name == name).FirstOrDefault(); - if (a == null) + if (a is null) { _attributes.Add(new HtmlAttributeNoneValue(name)); } @@ -354,7 +354,7 @@ protected void RemoveAttribute(string name) { var a = _attributes.Where(x => x.Name == name).FirstOrDefault(); - if (a != null) + if (a is not null) { _attributes.Remove(a); } @@ -378,7 +378,7 @@ protected HtmlElement GetElement(string name) /// The element. protected void SetElement(HtmlElement element) { - if (element != null) + if (element is not null) { var a = _elements.Where(x => x is HtmlElement && (x as HtmlElement).ElementName == element.ElementName); @@ -430,7 +430,7 @@ public virtual void ToString(StringBuilder builder, int deep) closeTag = true; var count = builder.Length; - foreach (var v in _elements.Where(x => x != null)) + foreach (var v in _elements.Where(x => x is not null)) { v.ToString(builder, deep + 1); } diff --git a/src/WebExpress.WebCore/WebHtml/HtmlElementSectionBody.cs b/src/WebExpress.WebCore/WebHtml/HtmlElementSectionBody.cs index 69f1bb6..a431b3e 100644 --- a/src/WebExpress.WebCore/WebHtml/HtmlElementSectionBody.cs +++ b/src/WebExpress.WebCore/WebHtml/HtmlElementSectionBody.cs @@ -70,7 +70,7 @@ public override void ToString(StringBuilder builder, int deep) { ToPreString(builder, deep); - foreach (var v in Elements.Where(x => x != null)) + foreach (var v in Elements.Where(x => x is not null)) { v.ToString(builder, deep + 1); } diff --git a/src/WebExpress.WebCore/WebHtml/HtmlElementTextSemanticsA.cs b/src/WebExpress.WebCore/WebHtml/HtmlElementTextSemanticsA.cs index 025bb3e..5be976d 100644 --- a/src/WebExpress.WebCore/WebHtml/HtmlElementTextSemanticsA.cs +++ b/src/WebExpress.WebCore/WebHtml/HtmlElementTextSemanticsA.cs @@ -56,7 +56,7 @@ public string Href public TypeTarget Target { get => (TypeTarget)Enum.Parse(typeof(TypeTarget), GetAttribute("target")); - set => SetAttribute("target", value.ToStringValue()); + set => SetAttribute("target", value.ToValue()); } /// diff --git a/src/WebExpress.WebCore/WebHtml/RandomId.cs b/src/WebExpress.WebCore/WebHtml/RandomId.cs new file mode 100644 index 0000000..95a09f6 --- /dev/null +++ b/src/WebExpress.WebCore/WebHtml/RandomId.cs @@ -0,0 +1,43 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace WebExpress.WebCore.WebHtml +{ + /// + /// Provides methods for generating non-deterministic unique identifiers. + /// + /// + /// Unlike , this class does not use caching + /// and does not attempt to produce stable identifiers. Each call produces + /// a new random identifier. + /// + public static class RandomId + { + private static readonly RandomNumberGenerator Rng = RandomNumberGenerator.Create(); + + /// + /// Generates a random unique identifier. The identifier is not deterministic + /// and will differ on every call. + /// + /// + /// A unique, non-deterministic identifier string. + /// + public static string Create() + { + Span buffer = stackalloc byte[16]; // 128-bit random + RandomNumberGenerator.Fill(buffer); + + // Convert to hex + var sb = new StringBuilder(35); + sb.Append("id_"); + + foreach (var b in buffer) + { + sb.Append(b.ToString("X2")); + } + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebHtml/TypeEnctype.cs b/src/WebExpress.WebCore/WebHtml/TypeEnctype.cs index 8fad32e..04d6a4c 100644 --- a/src/WebExpress.WebCore/WebHtml/TypeEnctype.cs +++ b/src/WebExpress.WebCore/WebHtml/TypeEnctype.cs @@ -6,24 +6,30 @@ public enum TypeEnctype { /// - /// All characters are encoded (spaces are conferred to "+" and special characters in the hex representation). + /// All characters are encoded (spaces are converted to "+" and special characters to hex representation). /// UrLEncoded, /// - /// No characters will be encodes. Used when transferring files. + /// Multipart form data (used for file uploads and FormData). /// - None, + Multipart, /// /// Only space characters are encoded. /// Text, + /// + /// No characters will be encoded. + /// + None, + /// /// Not assignable. /// Default + } /// @@ -38,13 +44,25 @@ public static class TypeEnctypeExtensions /// The converted encoding. public static TypeEnctype Convert(string enctype) { - return (enctype?.ToLower()) switch + if (string.IsNullOrWhiteSpace(enctype)) + { + return TypeEnctype.Default; + } + + var ct = enctype.ToLowerInvariant(); + + if (ct.StartsWith("multipart/form-data")) + { + return TypeEnctype.Multipart; + } + + return ct switch { - "multipart/form-data" => TypeEnctype.None, "text/plain" => TypeEnctype.Text, "application/x-www-form-urlencoded" => TypeEnctype.UrLEncoded, _ => TypeEnctype.Default, }; + } /// @@ -56,9 +74,11 @@ public static string Convert(this TypeEnctype enctype) { return enctype switch { - TypeEnctype.None => "multipart/form-data", + TypeEnctype.Multipart => "multipart/form-data", TypeEnctype.Text => "text/plain", - _ => "application/x-www-form-urlencoded", + TypeEnctype.UrLEncoded => "application/x-www-form-urlencoded", + TypeEnctype.None => string.Empty, + _ => string.Empty }; } } diff --git a/src/WebExpress.WebCore/WebHtml/TypeTarget.cs b/src/WebExpress.WebCore/WebHtml/TypeTarget.cs index ae56f13..cb70051 100644 --- a/src/WebExpress.WebCore/WebHtml/TypeTarget.cs +++ b/src/WebExpress.WebCore/WebHtml/TypeTarget.cs @@ -46,7 +46,7 @@ public static class TypeTargetExtensions /// /// The call target. /// The plain text of the target. - public static string ToStringValue(this TypeTarget target) + public static string ToValue(this TypeTarget target) { return target switch { diff --git a/src/WebExpress.WebCore/WebIdentity/IIdentityManager.cs b/src/WebExpress.WebCore/WebIdentity/IIdentityManager.cs index 9b4af9f..7e924ed 100644 --- a/src/WebExpress.WebCore/WebIdentity/IIdentityManager.cs +++ b/src/WebExpress.WebCore/WebIdentity/IIdentityManager.cs @@ -39,20 +39,20 @@ public interface IIdentityManager : IComponentManager /// The identity. /// The password. /// True if successful, false otherwise. - bool Login(Request request, IIdentity identity, SecureString password); + bool Login(IRequest request, IIdentity identity, SecureString password); /// /// Logout an identity. /// /// The request. - void Logout(Request request); + void Logout(IRequest request); /// /// Returns the current signed-in identity based on the provided request. /// /// The request to get the current identity for. /// The current signed-in identity. - IIdentity GetCurrentIdentity(Request request); + IIdentity GetCurrentIdentity(IRequest request); /// /// Checks if the specified identity has the given permission. diff --git a/src/WebExpress.WebCore/WebIdentity/IdentityManager.cs b/src/WebExpress.WebCore/WebIdentity/IdentityManager.cs index bf1f4ec..c368881 100644 --- a/src/WebExpress.WebCore/WebIdentity/IdentityManager.cs +++ b/src/WebExpress.WebCore/WebIdentity/IdentityManager.cs @@ -126,7 +126,7 @@ private void Register(IPluginContext pluginContext, IEnumerableThe context of the application that contains the identities to remove. internal void Remove(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { return; } @@ -383,7 +383,7 @@ private void OnAddApplication(object sender, IApplicationContext e) /// The identity. /// The password. /// True if successful, false otherwise. - public bool Login(Request request, IIdentity identity, SecureString password) + public bool Login(IRequest request, IIdentity identity, SecureString password) { if (identity?.PasswordHash == ComputeHash(password)) { @@ -405,7 +405,7 @@ public bool Login(Request request, IIdentity identity, SecureString password) /// Logout an identity. /// /// The request. - public void Logout(Request request) + public void Logout(IRequest request) { var session = _componentHub.SessionManager.GetSession(request); session.RemoveProperty(); @@ -416,7 +416,7 @@ public void Logout(Request request) /// /// The request to get the current identity for. /// The current signed-in identity. - public IIdentity GetCurrentIdentity(Request request) + public IIdentity GetCurrentIdentity(IRequest request) { var session = _componentHub.SessionManager.GetSession(request); var authentification = session.GetProperty(); diff --git a/src/WebExpress.WebCore/WebIdentity/Model/IdentityPermissionDictionary.cs b/src/WebExpress.WebCore/WebIdentity/Model/IdentityPermissionDictionary.cs index 3ddc71f..5f7ce50 100644 --- a/src/WebExpress.WebCore/WebIdentity/Model/IdentityPermissionDictionary.cs +++ b/src/WebExpress.WebCore/WebIdentity/Model/IdentityPermissionDictionary.cs @@ -68,7 +68,7 @@ public void RemovePermissionItem(IPluginContext pluginConte var permissionList = appContextDict[applicationContext]; var itemToRemove = permissionList.FirstOrDefault(x => x.PermissionClass == type); - if (itemToRemove != null) + if (itemToRemove is not null) { permissionList.Remove(itemToRemove); diff --git a/src/WebExpress.WebCore/WebIdentity/Model/IdentityPolicyDictionary.cs b/src/WebExpress.WebCore/WebIdentity/Model/IdentityPolicyDictionary.cs index d47725e..b1530e6 100644 --- a/src/WebExpress.WebCore/WebIdentity/Model/IdentityPolicyDictionary.cs +++ b/src/WebExpress.WebCore/WebIdentity/Model/IdentityPolicyDictionary.cs @@ -66,7 +66,7 @@ public void RemovePolicyItem(IPluginContext pluginContext, IApp if (appContextDict.TryGetValue(applicationContext, out var policyList)) { var itemToRemove = policyList.FirstOrDefault(x => x.PolicyClass == type); - if (itemToRemove != null) + if (itemToRemove is not null) { policyList.Remove(itemToRemove); diff --git a/src/WebExpress.WebCore/WebInclude/IncludeManager.cs b/src/WebExpress.WebCore/WebInclude/IncludeManager.cs index 864f76c..9f5067c 100644 --- a/src/WebExpress.WebCore/WebInclude/IncludeManager.cs +++ b/src/WebExpress.WebCore/WebInclude/IncludeManager.cs @@ -68,7 +68,7 @@ private IncludeManager(IComponentHub componentHub, IHttpServerContext httpServer /// The plugin context. private void Register(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return; } @@ -87,7 +87,7 @@ private void Register(IPluginContext pluginContext) /// The application context. private void Register(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { return; } @@ -112,14 +112,14 @@ private void Register(IPluginContext pluginContext, IEnumerable x.IsClass && x.IsSealed && x.IsPublic) - .Where(x => x.GetInterface(typeof(IInclude).Name) != null)) + .Where(x => x.GetInterface(typeof(IInclude).Name) is not null)) { var id = includeType.FullName?.ToLower(); var cache = false; @@ -205,7 +205,7 @@ private void Register(IPluginContext pluginContext, IEnumerableThe plugin context. internal void Remove(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return; } @@ -237,7 +237,7 @@ internal void Remove(IPluginContext pluginContext) /// The application context. internal void Remove(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { return; } @@ -250,6 +250,14 @@ internal void Remove(IApplicationContext applicationContext) { OnRemoveInclude(includeItem.IncludeContext); includeItem.Dispose(); + + _httpServerContext?.Log.Debug( + I18N.Translate( + "webexpress.webcore:includemanager.removeinclude", + includeItem.IncludeId, + includeItem.ApplicationContext.ApplicationId + ) + ); } } @@ -264,7 +272,7 @@ internal void Remove(IApplicationContext applicationContext) /// Enumerable of include contexts. public IEnumerable GetIncludes(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { return []; } @@ -284,9 +292,9 @@ public IEnumerable GetIncludes(IApplicationContext applicationC /// Enumerable of include contexts. public IEnumerable GetIncludes(IApplicationContext applicationContext, Type includeType) { - if (applicationContext == null || includeType == null) + if (applicationContext is null || includeType is null) { - return Enumerable.Empty(); + return []; } return _dictionary.Values diff --git a/src/WebExpress.WebCore/WebInclude/Model/IncludeDictionary.cs b/src/WebExpress.WebCore/WebInclude/Model/IncludeDictionary.cs index 5da0c95..b6464ec 100644 --- a/src/WebExpress.WebCore/WebInclude/Model/IncludeDictionary.cs +++ b/src/WebExpress.WebCore/WebInclude/Model/IncludeDictionary.cs @@ -19,14 +19,14 @@ internal sealed class IncludeDictionary : DictionaryTrue if the item was successfully added; otherwise, false if an item with the same key already exists in the collection. public bool AddIncludeItem(IPluginContext pluginContext, IApplicationContext applicationContext, IncludeItem includeItem) { - if (pluginContext == null || applicationContext == null || includeItem == null) + if (pluginContext is null || applicationContext is null || includeItem is null) { return false; } if (!TryGetValue(pluginContext, out var appDict)) { - appDict = new Dictionary>(); + appDict = []; Add(pluginContext, appDict); } diff --git a/src/WebExpress.WebCore/WebJob/JobManager.cs b/src/WebExpress.WebCore/WebJob/JobManager.cs index a6fe07c..9f3b27f 100644 --- a/src/WebExpress.WebCore/WebJob/JobManager.cs +++ b/src/WebExpress.WebCore/WebJob/JobManager.cs @@ -115,7 +115,7 @@ private void Register(IPluginContext pluginContext, IEnumerable x.IsClass == true && x.IsSealed && x.IsPublic && - x.GetInterface(typeof(IJob).Name) != null + x.GetInterface(typeof(IJob).Name) is not null )) { var id = job.FullName?.ToLower(); @@ -201,7 +201,7 @@ private void Register(IPluginContext pluginContext, IEnumerableThe context of the plugin that contains the jobs to remove. internal void Remove(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return; } @@ -227,7 +227,7 @@ internal void Remove(IPluginContext pluginContext) /// The context of the application that contains the jobs to remove. internal void Remove(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { return; } diff --git a/src/WebExpress.WebCore/WebLog/ILog.cs b/src/WebExpress.WebCore/WebLog/ILog.cs index 71ff833..448d8e3 100644 --- a/src/WebExpress.WebCore/WebLog/ILog.cs +++ b/src/WebExpress.WebCore/WebLog/ILog.cs @@ -72,7 +72,7 @@ public interface ILog : ILogger /// /// The default instance of the logger. /// - public static Log Current { get; } + public static ILog Current { get; } /// /// Set file name time patterns. @@ -106,13 +106,13 @@ public interface ILog : ILogger /// /// A dividing line with * characters /// - public void Seperator(); + public void Separator(); /// /// A separator with custom characters /// /// The separator. - public void Seperator(char sepChar); + public void Separator(char sepChar); /// /// Logs an info message. diff --git a/src/WebExpress.WebCore/WebLog/Log.cs b/src/WebExpress.WebCore/WebLog/Log.cs index b437925..60218d9 100644 --- a/src/WebExpress.WebCore/WebLog/Log.cs +++ b/src/WebExpress.WebCore/WebLog/Log.cs @@ -46,7 +46,7 @@ public class Log : ILog /// /// Constant that determines the further of the separator rows. /// - private const int _seperatorWidth = 260; + private const int _SeparatorWidth = 260; /// /// End worker thread lifecycle. @@ -91,7 +91,7 @@ public class Log : ILog /// /// Checks if the log has been opened for writing. /// - public bool IsOpen => _workerThread != null; + public bool IsOpen => _workerThread is not null; /// /// Returns the log mode. @@ -222,7 +222,7 @@ protected virtual void Add(LogLevel level, string message, [CallerMemberName] st break; } - Console.WriteLine(item.ToString().Length > _seperatorWidth ? string.Concat(item.ToString().AsSpan(0, _seperatorWidth - 3), "...") : item.ToString().PadRight(_width, ' ')); + Console.WriteLine(item.ToString().Length > _SeparatorWidth ? string.Concat(item.ToString().AsSpan(0, _SeparatorWidth - 3), "...") : item.ToString().PadRight(_width, ' ')); Console.ResetColor(); _queue.Enqueue(item); @@ -233,18 +233,18 @@ protected virtual void Add(LogLevel level, string message, [CallerMemberName] st /// /// A dividing line with * characters /// - public void Seperator() + public void Separator() { - Seperator('*'); + Separator('*'); } /// /// A separator with custom characters /// /// The separator. - public void Seperator(char sepChar) + public void Separator(char sepChar) { - Add(LogLevel.Seperartor, "".PadRight(_seperatorWidth, sepChar)); + Add(LogLevel.Seperartor, "".PadRight(_SeparatorWidth, sepChar)); } /// @@ -398,7 +398,9 @@ public void Exception(Exception ex, [CallerMemberName] string instance = null, [ lock (_queue) { Add(LogLevel.Exception, ex?.Message.Trim(), $"{className}.{instance}", line, file); - Add(LogLevel.Exception, ex?.StackTrace != null ? ex?.StackTrace.Trim() : ex?.Message.Trim(), $"{className}.{instance}", line, file); + Add(LogLevel.Exception, ex?.StackTrace is not null + ? ex?.StackTrace.Trim() + : ex?.Message.Trim(), $"{className}.{instance}", line, file); ExceptionCount++; ErrorCount++; diff --git a/src/WebExpress.WebCore/WebLog/LogFrame.cs b/src/WebExpress.WebCore/WebLog/LogFrame.cs index d564b12..ff6133c 100644 --- a/src/WebExpress.WebCore/WebLog/LogFrame.cs +++ b/src/WebExpress.WebCore/WebLog/LogFrame.cs @@ -31,7 +31,7 @@ public class LogFrame : IDisposable /// /// The log entry. /// - protected Log Log { get; set; } + protected ILog Log { get; set; } /// /// Initializes a new instance of the class. @@ -42,30 +42,17 @@ public class LogFrame : IDisposable /// Method that wants to log. /// The line number. /// The source file. - public LogFrame(Log log, string name, string additionalHeading = null, [CallerMemberName] string instance = null, [CallerLineNumber] int? line = null, [CallerFilePath] string file = null) + public LogFrame(ILog log, string name, string additionalHeading = null, [CallerMemberName] string instance = null, [CallerLineNumber] int? line = null, [CallerFilePath] string file = null) { Instance = instance; - Status = string.Format("{0} abgeschlossen. ", name); + Status = string.Format("{0} completed. ", name); Log = log; - Log.Seperator(); - Log.Info(string.Format("Beginne mit {0}", name) + (!string.IsNullOrWhiteSpace(additionalHeading) ? " " + additionalHeading : ""), instance, line, file); + Log.Separator(); + Log.Info(string.Format("Starting {0}", name) + (!string.IsNullOrWhiteSpace(additionalHeading) ? " " + additionalHeading : ""), instance, line, file); Log.Info("".PadRight(80, '-'), instance, line, file); } - /// - /// Initializes a new instance of the class. - /// - /// The name. - /// The additional heading or zero. - /// Method that wants to log. - /// The line number. - /// The source file. - public LogFrame(string name, string additionalHeading = null, [CallerMemberName] string instance = null, [CallerLineNumber] int? line = null, [CallerFilePath] string file = null) - : this(Log.Current, name, additionalHeading, instance, line, file) - { - } - /// /// Release unmanaged resources that were reserved during initialization. /// diff --git a/src/WebExpress.WebCore/WebMessage/HttpContext.cs b/src/WebExpress.WebCore/WebMessage/HttpContext.cs index b76dbf9..4239993 100644 --- a/src/WebExpress.WebCore/WebMessage/HttpContext.cs +++ b/src/WebExpress.WebCore/WebMessage/HttpContext.cs @@ -8,7 +8,7 @@ namespace WebExpress.WebCore.WebMessage /// /// Represents the context of an HTTP request and response. /// - public class HttpContext + public class HttpContext : IHttpContext { /// /// The context of the web server. @@ -23,7 +23,7 @@ public class HttpContext /// /// Returns the request. /// - public Request Request { get; protected set; } + public IRequest Request { get; protected set; } /// /// Gets the ip address and port number of the server to which the request is made. @@ -74,7 +74,9 @@ public HttpContext(IFeatureCollection contextFeatures, IHttpServerContext httpSe LocalEndPoint = new IPEndPoint(connectionFeature.LocalIpAddress, connectionFeature.LocalPort); RemoteEndPoint = new IPEndPoint(connectionFeature.RemoteIpAddress, connectionFeature.RemotePort); - Encoding = requestFeature.Headers.ContentEncoding.Count != 0 ? Encoding.GetEncoding(requestFeature.Headers.ContentEncoding) : Encoding.Default; + Encoding = requestFeature.Headers.ContentEncoding.Count != 0 + ? Encoding.GetEncoding(requestFeature.Headers.ContentEncoding) + : Encoding.Default; Uri = new Uri(baseUri, requestFeature.RawTarget); Request = new Request(contextFeatures, header, httpServerContext); diff --git a/src/WebExpress.WebCore/WebMessage/HttpWebSocketContext.cs b/src/WebExpress.WebCore/WebMessage/HttpWebSocketContext.cs new file mode 100644 index 0000000..cda62c8 --- /dev/null +++ b/src/WebExpress.WebCore/WebMessage/HttpWebSocketContext.cs @@ -0,0 +1,93 @@ +using Microsoft.AspNetCore.Http.Features; +using System; +using System.Net; +using System.Text; + +namespace WebExpress.WebCore.WebMessage +{ + /// + /// Represents the context for a WebSocket connection. + /// + public class HttpWebSocketContext : IHttpContext + { + /// + /// Returns the context of the web server. + /// + public IHttpServerContext HttpServerContext { get; protected set; } + + /// + /// Returns the context id. + /// + public string Id { get; protected set; } + + /// + /// Returns the request associated with this context. + /// + public IRequest Request { get; protected set; } + + /// + /// Returns the ip address and port number of the server receiving the request. + /// + public EndPoint LocalEndPoint { get; protected set; } + + /// + /// Returns the ip address and port number of the client making the request. + /// + public EndPoint RemoteEndPoint { get; protected set; } + + /// + /// Returns the set of features for this context. + /// + public IFeatureCollection Features { get; protected set; } + + /// + /// Returns the encoding used by this context. + /// + public Encoding Encoding { get; protected set; } = Encoding.Default; + + /// + /// Returns the URI associated with this context. + /// + public Uri Uri { get; internal set; } + + /// + /// Returns the WebSocket key for this context. + /// + public string WebSocketKey { get; protected set; } + + /// + /// Returns whether this WebSocket is secure (wss). + /// + public bool IsSecureWebSocket { get; protected set; } + + /// + /// Initializes a new instance of the WebSocketContext class. + /// + /// The initial set of features. + /// The context of the web server. + public HttpWebSocketContext(IFeatureCollection contextFeatures, IHttpServerContext httpServerContext) + { + var connectionFeature = contextFeatures.Get(); + var requestFeature = contextFeatures.Get(); + var header = new RequestHeaderFields(contextFeatures); + var baseUri = new UriBuilder(requestFeature.Scheme, header.Host, connectionFeature.LocalPort).Uri; + + Features = contextFeatures; + HttpServerContext = httpServerContext; + Id = connectionFeature.ConnectionId; + LocalEndPoint = new IPEndPoint(connectionFeature.LocalIpAddress, connectionFeature.LocalPort); + RemoteEndPoint = new IPEndPoint(connectionFeature.RemoteIpAddress, connectionFeature.RemotePort); + + Encoding = requestFeature.Headers.ContentEncoding.Count != 0 + ? Encoding.GetEncoding(requestFeature.Headers.ContentEncoding) + : Encoding.Default; + Uri = new Uri(baseUri, requestFeature.RawTarget); + + // always initialize as websocket-request for this context + Request = new RequestWebSocket(contextFeatures, header, httpServerContext); + + WebSocketKey = header.SecWebSocketKey; + IsSecureWebSocket = requestFeature.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebMessage/IHttpContext.cs b/src/WebExpress.WebCore/WebMessage/IHttpContext.cs new file mode 100644 index 0000000..b5fd04d --- /dev/null +++ b/src/WebExpress.WebCore/WebMessage/IHttpContext.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Http.Features; +using System; +using System.Net; +using System.Text; + +namespace WebExpress.WebCore.WebMessage +{ + /// + /// Represents the context of an HTTP request and response. + /// + public interface IHttpContext + { + /// + /// Returns the context of the web server. + /// + IHttpServerContext HttpServerContext { get; } + + /// + /// Returns the context id. + /// + string Id { get; } + + /// + /// Returns the request data. + /// + IRequest Request { get; } + + /// + /// Returns the ip address and port number of the server that receives the request. + /// + EndPoint LocalEndPoint { get; } + + /// + /// Returns the ip address and port number of the client where the request originated. + /// + EndPoint RemoteEndPoint { get; } + + /// + /// Returns the set of features. + /// + IFeatureCollection Features { get; } + + /// + /// Returns the encoding used. + /// + Encoding Encoding { get; } + + /// + /// Returns the URI. + /// + Uri Uri { get; } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebMessage/IRequest.cs b/src/WebExpress.WebCore/WebMessage/IRequest.cs new file mode 100644 index 0000000..cae0ca2 --- /dev/null +++ b/src/WebExpress.WebCore/WebMessage/IRequest.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using WebExpress.WebCore.WebParameter; +using WebExpress.WebCore.WebSession.Model; +using WebExpress.WebCore.WebUri; + +namespace WebExpress.WebCore.WebMessage +{ + /// + /// Defines the contract for accessing HTTP request information (see RFC 2616). + /// + public interface IRequest + { + /// + /// The context of the web server. + /// + IHttpServerContext HttpServerContext { get; } + + /// + /// Returns the request method (e.g. POST). + /// + RequestMethod Method { get; } + + /// + /// Returns the uri. + /// + UriEndpoint Uri { get; internal set; } + + /// + /// Returns the session. + /// + Session Session { get; } + + /// + /// Returns the http version. + /// + string Protocoll { get; } + + /// + /// Returns the options from the header. + /// + RequestHeaderFields Header { get; } + + /// + /// Returns the ip address and port number of the server to which the request is made. + /// + EndPoint LocalEndPoint { get; } + + /// + /// Returns the ip address and port number of the client from which the request originated. + /// + EndPoint RemoteEndPoint { get; } + + /// + /// Returns a boolean value that indicates whether the tcp connection used to send the request uses the secure sockets layer (ssl) protocol. + /// + bool IsSecureConnection { get; } + + /// + /// Returns the schema. This can be http or https. + /// + UriScheme Scheme { get; } + + /// + /// Returns the request identifier of the incoming http request. + /// + string RequestTraceIdentifier { get; } + + /// + /// Returns the culture. + /// + CultureInfo Culture { get; } + + /// + /// Returns the collection of parameters associated with the request. + /// + IEnumerable Parameters { get; } + + /// + /// Adds several parameters. + /// + /// The parameters. + void AddParameter(IEnumerable param); + + /// + /// Adds one parameter. + /// + /// The parameter. + void AddParameter(Parameter param); + + /// + /// Returns a parameter by name. + /// + /// The name of the parameter. + /// The value. + IParameter GetParameter(string name); + + /// + /// Returns a parameter by type. + /// + /// The parameter. + /// The value. + TParameter GetParameter() + where TParameter : IParameterStatic, new(); + + /// + /// Checks whether a parameter exists. + /// + /// The name of the parameter. + /// True if the parameter is present, false otherwise. + bool HasParameter(string name); + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebMessage/IResponse.cs b/src/WebExpress.WebCore/WebMessage/IResponse.cs new file mode 100644 index 0000000..ef63b15 --- /dev/null +++ b/src/WebExpress.WebCore/WebMessage/IResponse.cs @@ -0,0 +1,28 @@ +namespace WebExpress.WebCore.WebMessage +{ + /// + /// Defines the contract for a response according to RFC 2616 Section 6. + /// + public interface IResponse + { + /// + /// Returns the response header fields. + /// + ResponseHeaderFields Header { get; } + + /// + /// Returns or sets the response content. + /// + object Content { get; set; } + + /// + /// Returns the status code of the response. + /// + int Status { get; } + + /// + /// Returns or sets the reason phrase of the response. + /// + string Reason { get; } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebMessage/Request.cs b/src/WebExpress.WebCore/WebMessage/Request.cs index 49e54c8..1c2b422 100644 --- a/src/WebExpress.WebCore/WebMessage/Request.cs +++ b/src/WebExpress.WebCore/WebMessage/Request.cs @@ -1,16 +1,11 @@ using Microsoft.AspNetCore.Http.Features; using System; -using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; -using System.Net; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; using WebExpress.WebCore.WebHtml; -using WebExpress.WebCore.WebSession.Model; -using WebExpress.WebCore.WebUri; +using WebExpress.WebCore.WebParameter; namespace WebExpress.WebCore.WebMessage { @@ -18,89 +13,13 @@ namespace WebExpress.WebCore.WebMessage /// See RFC 2616, The Request class encapsulates and extends the /// original request of the HttpListener call. /// - public class Request + public partial class Request : RequestBase { - /// - /// The context of the web server. - /// - public IHttpServerContext HttpServerContext { get; protected set; } - - /// - /// Returns the request method (e.g. POST). - /// - public RequestMethod Method { get; private set; } - - /// - /// Returns the uri. - /// - public UriEndpoint Uri { get; internal set; } - - /// - /// Returns the parameters. - /// - private ParameterDictionary Param { get; } = []; - - /// - /// Returns the session. - /// - public Session Session { get; private set; } - - /// - /// Returns the http version. - /// - public string Protocoll { get; private set; } - - /// - /// Returns the options from the header. - /// - public RequestHeaderFields Header { get; private set; } - - /// - /// Returns the ip address and port number of the server to which the request is made. - /// - public EndPoint LocalEndPoint { get; private set; } - - /// - /// Returns the ip address and port number of the client from which the request originated. - /// - public EndPoint RemoteEndPoint { get; private set; } - - /// - /// Returns a boolean value that indicates whether the tcp connection used to send the request uses the secure sockets layer (ssl) protocol. - /// - public bool IsSecureConnection { get; private set; } + [GeneratedRegex(@"([\w-]+)=(.*)")] + private static partial Regex TextRegex(); - /// - /// Returns the shema. This can be http or https. - /// - public UriScheme Scheme { get; private set; } - - /// - /// Returns the request identifier of the incoming http request. - /// - public string RequestTraceIdentifier { get; private set; } - - /// - /// Returns the culture. - /// - public CultureInfo Culture - { - get - { - try - { - // see RFC 5646 - var languages = Header?.AcceptLanguage.FirstOrDefault(); - var language = languages?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).FirstOrDefault(); - - return new CultureInfo(language); - } - catch - { - return HttpServerContext.Culture ?? CultureInfo.CurrentCulture; - } - } - } + [GeneratedRegex(@"Content-Type:\s*(.*)", RegexOptions.IgnoreCase, "de-DE")] + private static partial Regex ContentRegex(); /// /// Returns the content. @@ -114,59 +33,13 @@ public CultureInfo Culture /// The header. /// The context of the web server. internal Request(IFeatureCollection contextFeatures, RequestHeaderFields header, IHttpServerContext httpServerContext) + : base(contextFeatures, header, httpServerContext) { - var connectionFeature = contextFeatures.Get(); var requestFeature = contextFeatures.Get(); - var requestIdentifierFeature = contextFeatures.Get(); - //var sessionFeature = contextFeatures.Get(); - - HttpServerContext = httpServerContext; - RequestTraceIdentifier = requestIdentifierFeature.TraceIdentifier; - Protocoll = requestFeature.Protocol; - - Scheme = requestFeature.Scheme.ToLower() switch - { - "http" => UriScheme.Http, - "https" => UriScheme.Https, - "ftp" => UriScheme.FTP, - "file" => UriScheme.File, - "mailto" => UriScheme.Mailto, - "ldap" => UriScheme.Ldap, - _ => UriScheme.Http - - }; - Method = requestFeature.Method.ToUpper() switch - { - "GET" => RequestMethod.GET, - "POST" => RequestMethod.POST, - "PUT" => RequestMethod.PUT, - "DELETE" => RequestMethod.DELETE, - "HEAD" => RequestMethod.HEAD, - "PATCH" => RequestMethod.PATCH, - _ => RequestMethod.GET - }; - - Header = header; - - LocalEndPoint = new IPEndPoint(connectionFeature.LocalIpAddress, connectionFeature.LocalPort); - RemoteEndPoint = new IPEndPoint(connectionFeature.RemoteIpAddress, connectionFeature.RemotePort); - - Uri = new UriEndpoint - ( - Scheme, - new UriAuthority() - { - Host = Header.Host, - Port = connectionFeature.LocalPort - }, - requestFeature.RawTarget - ); Content = GetContent(requestFeature.Body, Header.ContentLength); - ParseQueryParams(requestFeature.QueryString); ParseRequestParams(); - ParseSessionParams(); } /// @@ -189,329 +62,297 @@ internal static byte[] GetContent(Stream body, long? contentLength) } /// - /// Returns the parameters from the reuest query (for example, http://www.example.com?key=value). + /// Parse the request parameters. /// - /// The query. - private void ParseQueryParams(string query) + protected virtual void ParseRequestParams() { - query = query.TrimStart('?'); + if (string.IsNullOrWhiteSpace(Header.ContentType) || Content is null || Content.Length == 0) + { + return; + } + + // normalize content-type + var ct = Header.ContentType.Split(';') + .Select(x => x.Trim()) + .ToArray(); - Parallel.ForEach(query.Split('&'), (param) => + var mainType = ct.FirstOrDefault()?.ToLowerInvariant(); + var enctype = TypeEnctypeExtensions.Convert(mainType); + + // detect multipart/form-data even if Convert() fails + if (mainType.StartsWith("multipart/form-data")) { - if (!string.IsNullOrWhiteSpace(param)) - { - var split = param.Split('='); + enctype = TypeEnctype.Multipart; + } - if (split.Length == 1) - { - AddParameter(new Parameter(split[0], null, ParameterScope.Parameter)); - } - else if (split.Length == 2) - { - AddParameter(new Parameter(split[0], split[1], ParameterScope.Parameter)); - } - else if (split.Length > 2) - { - AddParameter(new Parameter(split[0], string.Join("=", split.Skip(1)), ParameterScope.Parameter)); - } - } - }); + switch (enctype) + { + case TypeEnctype.Multipart: + ParseMultipart(ct); + break; + + case TypeEnctype.Text: + ParseTextPlain(); + break; + + case TypeEnctype.UrLEncoded: + ParseUrlEncoded(); + break; + + default: + // unknown or unsupported content-type + break; + } } /// - /// Parse the request parameters. + /// Parses multipart form data from the provided content type parts and extracts parameters and + /// file uploads. /// - private void ParseRequestParams() + /// + /// An array of strings representing the parts of the Content-Type header. Each part may include + /// information such as the boundary used to separate multipart sections. + /// + private void ParseMultipart(string[] contentTypeParts) { - if (string.IsNullOrWhiteSpace(Header.ContentType)) + // extract boundary + var boundary = contentTypeParts + .FirstOrDefault(x => x.StartsWith("boundary=", StringComparison.OrdinalIgnoreCase)) + ?["boundary=".Length..]; + + if (string.IsNullOrWhiteSpace(boundary)) { return; } - var contentType = Header.ContentType?.Split(';'); + var boundaryBytes = Encoding.UTF8.GetBytes("--" + boundary); + var endBoundaryBytes = Encoding.UTF8.GetBytes("--" + boundary + "--"); - switch (TypeEnctypeExtensions.Convert(contentType.FirstOrDefault())) + int pos = 0; + + while (true) { - case TypeEnctype.None: - { - var boundary = Header.ContentType; - var boundaryValue = "--" + boundary?.Split('=').Skip(1)?.FirstOrDefault(); - var offset = 0; - int pos = 0; - var dispositions = new List>(); // Item1=position, Item2=size - - // determine dispositions - for (var i = 0; i < Content.Length; i++) - { - if (Content[i] == '\r') - { - var c = Encoding.UTF8.GetString(Content, offset, boundaryValue.Length).Trim(); - if (c.StartsWith(boundaryValue)) - { - if (i - boundaryValue.Length - pos > 0) - { - dispositions.Add(new Tuple(pos, i - boundaryValue.Length - pos)); - } - - pos = i + 2; - - if (c.EndsWith("--")) - { - break; - } - } - } - else if (Content[i] == '\n') - { - offset = i + 1; - } - else if (i == Content.Length - 1) - { - // at the end - var c = Encoding.UTF8.GetString(Content, offset, boundaryValue.Length).Trim(); - if (c.StartsWith(boundaryValue)) - { - dispositions.Add(new Tuple(pos, i - boundaryValue.Length - pos)); - } - } - } - - foreach (var item in dispositions) - { - var disposition = string.Empty; - var name = string.Empty; - var filename = string.Empty; - var contenttype = string.Empty; - offset = 0; - - var str = Encoding.UTF8.GetString(Content, item.Item1, item.Item2 > 256 ? 256 : item.Item2); - var match = Regex.Match(str, @"^Content-Disposition: (.*)$", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled); - if (match.Groups[1].Success) - { - offset += match.Length + 1; // + Zeilenende - var dispositionParam = match.Groups[1].ToString().Split(';'); - disposition = dispositionParam.FirstOrDefault(); - foreach (var v in dispositionParam.Skip(1)) - { - match = Regex.Match(v.Trim(), @"^name=""(.*)""$", RegexOptions.IgnoreCase | RegexOptions.Compiled); - if (match.Groups[1].Success) - { - name = match.Groups[1].ToString(); - } - - match = Regex.Match(v.Trim(), @"^filename=""(.*)""$", RegexOptions.IgnoreCase | RegexOptions.Compiled); - if (match.Groups[1].Success) - { - filename = match.Groups[1].ToString(); - } - } - } - - match = Regex.Match(str, @"^Content-Type: (.*)$", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled); - if (match.Groups[1].Success) - { - offset += match.Length + 1; // + End of line - contenttype = match.Groups[1].ToString().Trim(); - } - - if (string.IsNullOrWhiteSpace(filename)) - { - offset += 2; // + blank line - if (item.Item2 - offset - 1 >= 0) - { - var value = Encoding.UTF8.GetString(Content, item.Item1 + offset, item.Item2 - offset - 2); - - var param = new Parameter(name, value.TrimEnd(), ParameterScope.Parameter); - AddParameter(param); - } - else - { - var param = new Parameter(name, string.Empty, ParameterScope.Parameter); - AddParameter(param); - } - } - else - { - offset += 2; // + blank line - if (item.Item2 - offset - 1 >= 0) - { - var bytes = new byte[item.Item2 - offset - 2]; - Buffer.BlockCopy(Content, item.Item1 + offset, bytes, 0, item.Item2 - offset - 2); - - var param = new ParameterFile(name, filename, ParameterScope.Parameter) { ContentType = contenttype, Data = bytes }; - AddParameter(param); - } - else - { - var param = new Parameter(name, filename, ParameterScope.Parameter); - AddParameter(param); - } - } - } - - break; - } - case TypeEnctype.Text: - { - var lines = new List(); - var offset = 0; - - for (var i = 0; i < Content.Length; i++) - { - if (Content[i] == '\r') - { - lines.Add(Encoding.UTF8.GetString(Content, offset, i - offset)); - } - else if (Content[i] == '\n') - { - offset = i + 1; - } - } - - // if not all bytes have been read yet - if (offset < Content.Length) - { - lines.Add(Encoding.UTF8.GetString(Content, offset, Content.Length - offset)); - } - - var last = default(Parameter); - - foreach (var v in lines) - { - var match = Regex.Match(v, @"([\w-]*)=(.*)", RegexOptions.Compiled); - if (match.Groups[1].Success && match.Groups[2].Success) - { - last = new Parameter(match.Groups[1].ToString().Trim(), match.Groups[2].ToString().Trim(), ParameterScope.Parameter); - AddParameter(last); - } - else if (last != null) - { - last.Value += "\r\n" + v; - - } - } - - if (last != null) - { - last.Value = last.Value.TrimEnd(); - } - - break; - } - case TypeEnctype.UrLEncoded: - { - var str = Encoding.UTF8.GetString(Content, 0, Content.Length); - var param = str.Replace('+', ' '); - - foreach (var v in param.Split('&')) - { - var s = v.Split('='); - AddParameter(new Parameter - ( - s[0], - s.Length > 1 ? s[1]?.TrimEnd() : string.Empty, - ParameterScope.Parameter - )); - } - - break; - } - default: + // find next boundary + int start = IndexOf(Content, boundaryBytes, pos); + if (start < 0) + { + break; + } + + // check for end boundary + bool isFinal = StartsWith(Content, endBoundaryBytes, start); + + // move to header start + int headerStart = start + boundaryBytes.Length + 2; // skip CRLF + + // find header end (empty line) + int headerEnd = IndexOf(Content, Encoding.UTF8.GetBytes("\r\n\r\n"), headerStart); + if (headerEnd < 0) + { + break; + } + + var headerText = Encoding.UTF8.GetString(Content, headerStart, headerEnd - headerStart); + + // parse headers + var name = ExtractHeaderValue(headerText, "name"); + var filename = ExtractHeaderValue(headerText, "filename"); + var contentType = ExtractContentType(headerText); + + // content start + int dataStart = headerEnd + 4; + + // find next boundary to determine data length + int nextBoundary = IndexOf(Content, boundaryBytes, dataStart); + if (nextBoundary < 0) + { + break; + } + + int dataLength = nextBoundary - dataStart - 2; // remove trailing CRLF + + if (string.IsNullOrEmpty(filename)) + { + // normal field + var value = Encoding.UTF8.GetString(Content, dataStart, dataLength).TrimEnd(); + AddParameter(new Parameter(name, value, ParameterScope.Parameter)); + } + else + { + // file upload + var bytes = new byte[dataLength]; + Buffer.BlockCopy(Content, dataStart, bytes, 0, dataLength); + + AddParameter(new ParameterFile(name, filename, ParameterScope.Parameter) { + ContentType = contentType, + Data = bytes + }); + } + + if (isFinal) + { + break; + } - break; - } + pos = nextBoundary; } } /// - /// Parse the session parameters. + /// Parses the request content as plain text and extracts parameters from lines in + /// the format 'key=value'. /// - private void ParseSessionParams() + private void ParseTextPlain() { - Session = WebEx.ComponentHub?.SessionManager?.GetSession(this); + var text = Encoding.UTF8.GetString(Content); + var lines = text.Split('\n'); + + Parameter last = null; - var property = Session?.GetProperty(); - if (property != null && property.Params != null) + foreach (var line in lines) { - foreach (var param in property.Params) + var trimmed = line.TrimEnd('\r'); + + var match = TextRegex().Match(trimmed); + if (match.Success) + { + last = new Parameter(match.Groups[1].Value, match.Groups[2].Value, ParameterScope.Parameter); + AddParameter(last); + } + else if (last != null) { - AddParameter(new Parameter(param.Key?.ToLower(), param.Value.Value, ParameterScope.Session)); + last.Value += "\r\n" + trimmed; } } + + last?.Value = last.Value.TrimEnd(); } /// - /// Adds several parameters. + /// Parses the request content as a URL-encoded form and adds each key-value pair + /// as a parameter. /// - /// The parameters. - public void AddParameter(IEnumerable param) + private void ParseUrlEncoded() { - foreach (var p in param) + var text = Encoding.UTF8.GetString(Content); + foreach (var pair in text.Split('&')) { - AddParameter(p); + var parts = pair.Split('='); + var key = parts[0]; + var value = parts.Length > 1 ? parts[1].Replace('+', ' ') : string.Empty; + + AddParameter(new Parameter(key, value, ParameterScope.Parameter)); } } /// - /// Adds one parameter. + /// Searches for the first occurrence of a specified byte sequence within a byte array, + /// starting at a given index. /// - /// The parameter. - public void AddParameter(Parameter param) + /// + /// The search is performed using ordinal byte comparison. If needle is an empty array, + /// the method returns start. If start is greater than haystack.Length - needle.Length, + /// the method returns -1. + /// + /// + /// The byte array to search within. + /// + /// + /// The byte sequence to locate within the haystack array. + /// + /// + /// The zero-based index in the haystack array at which to begin searching. Must be + /// non-negative and less than or equal to haystack.Length. + /// + /// + /// The zero-based index of the first occurrence of needle within haystack, starting at + /// the specified index; or -1 if the sequence is not found. + /// + private static int IndexOf(byte[] haystack, byte[] needle, int start) { - var key = param.Key.ToLower(); - - if (!Param.TryAdd(key, param)) + for (int i = start; i <= haystack.Length - needle.Length; i++) { - Param[key] = param; + if (StartsWith(haystack, needle, i)) + { + return i; + } } + return -1; } /// - /// Returns a parameter by name. + /// Determines whether a specified segment of a byte array begins with the given prefix. /// - /// The name of the parameter. - /// The value. - public Parameter GetParameter(string name) + /// + /// If plus the length of exceeds the + /// length of , the method returns . + /// + /// + /// The byte array to examine. + /// + /// + /// The byte sequence to compare against the segment of . + /// + /// + /// The zero-based index in at which to begin the comparison. + /// + /// + /// if the segment of starting at + /// begins with ; otherwise, + /// . + /// + private static bool StartsWith(byte[] data, byte[] prefix, int offset) { - if (!string.IsNullOrWhiteSpace(name) && HasParameter(name)) + if (offset + prefix.Length > data.Length) { - return Param[name.ToLower()]; + return false; } - return null; + for (int i = 0; i < prefix.Length; i++) + { + if (data[offset + i] != prefix[i]) + { + return false; + } + } + return true; } /// - /// Returns a parameter by name. + /// Extracts the value associated with the specified key from a header string formatted + /// as key-value pairs. /// - /// The parameter. - /// The value. - public Parameter GetParameter() where T : Parameter + /// + /// The header string containing key-value pairs, where values are enclosed in double quotes. + /// + /// + /// The key whose associated value is to be extracted from the header. The search is case-insensitive. + /// + /// + /// The value associated with the specified key if found; otherwise, an empty string. + /// + private static string ExtractHeaderValue(string header, string key) { - var name = Parameter.GetKey(); - - if (!string.IsNullOrWhiteSpace(name) && HasParameter(name)) - { - return Param[name.ToLower()]; - } - - return null; + var match = Regex.Match(header, key + "=\"([^\"]*)\"", RegexOptions.IgnoreCase); + return match.Success ? match.Groups[1].Value : string.Empty; } /// - /// Checks whether a parameter exists. + /// Extracts the value of the Content-Type header from the specified header string. /// - /// The name of the parameter. - /// True if parameters are present, false otherwise. - public bool HasParameter(string name) + /// + /// The header string from which to extract the Content-Type value. This should contain a + /// line starting with 'Content-Type:'. + /// + /// + /// A string containing the value of the Content-Type header if found; otherwise, + /// an empty string. + /// + private static string ExtractContentType(string header) { - if (name == null) - { - return false; - } - - return Param.ContainsKey(name.ToLower()); + var match = ContentRegex().Match(header); + return match.Success ? match.Groups[1].Value.Trim() : string.Empty; } } } diff --git a/src/WebExpress.WebCore/WebMessage/RequestAuthorization.cs b/src/WebExpress.WebCore/WebMessage/RequestAuthorization.cs index 428f21d..e0cd541 100644 --- a/src/WebExpress.WebCore/WebMessage/RequestAuthorization.cs +++ b/src/WebExpress.WebCore/WebMessage/RequestAuthorization.cs @@ -37,7 +37,7 @@ public partial class RequestAuthorization /// public static RequestAuthorization Parse(string str) { - if (str == null) + if (str is null) { return null; } diff --git a/src/WebExpress.WebCore/WebMessage/RequestBase.cs b/src/WebExpress.WebCore/WebMessage/RequestBase.cs new file mode 100644 index 0000000..5536fff --- /dev/null +++ b/src/WebExpress.WebCore/WebMessage/RequestBase.cs @@ -0,0 +1,292 @@ +using Microsoft.AspNetCore.Http.Features; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using WebExpress.WebCore.WebParameter; +using WebExpress.WebCore.WebSession.Model; +using WebExpress.WebCore.WebUri; + +namespace WebExpress.WebCore.WebMessage +{ + /// + /// See RFC 2616, The Request class encapsulates and extends the + /// original request of the HttpListener call. + /// + public abstract class RequestBase : IRequest + { + private readonly ParameterDictionary _param = []; + + /// + /// The context of the web server. + /// + public IHttpServerContext HttpServerContext { get; protected set; } + + /// + /// Returns the request method (e.g. POST). + /// + public RequestMethod Method { get; private set; } + + /// + /// Returns the uri. + /// + public UriEndpoint Uri { get; set; } + + /// + /// Returns the session. + /// + public Session Session { get; private set; } + + /// + /// Returns the http version. + /// + public string Protocoll { get; private set; } + + /// + /// Returns the options from the header. + /// + public RequestHeaderFields Header { get; private set; } + + /// + /// Returns the ip address and port number of the server to which the request is made. + /// + public EndPoint LocalEndPoint { get; private set; } + + /// + /// Returns the ip address and port number of the client from which the request originated. + /// + public EndPoint RemoteEndPoint { get; private set; } + + /// + /// Returns a boolean value that indicates whether the tcp connection used to send the request uses the secure sockets layer (ssl) protocol. + /// + public bool IsSecureConnection { get; private set; } + + /// + /// Returns the shema. This can be http or https. + /// + public UriScheme Scheme { get; private set; } + + /// + /// Returns the request identifier of the incoming http request. + /// + public string RequestTraceIdentifier { get; private set; } + + /// + /// Returns the culture. + /// + public CultureInfo Culture + { + get + { + try + { + // see RFC 5646 + var languages = Header?.AcceptLanguage.FirstOrDefault(); + var language = languages?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).FirstOrDefault(); + + return new CultureInfo(language); + } + catch + { + return HttpServerContext.Culture ?? CultureInfo.CurrentCulture; + } + } + } + + /// + /// Returns the collection of parameters associated with the request. + /// + public IEnumerable Parameters => _param.Values; + + /// + /// Initializes a new instance of the class. + /// + /// Initial set of features. + /// The header. + /// The context of the web server. + internal RequestBase(IFeatureCollection contextFeatures, RequestHeaderFields header, IHttpServerContext httpServerContext) + { + var connectionFeature = contextFeatures.Get(); + var requestFeature = contextFeatures.Get(); + var requestIdentifierFeature = contextFeatures.Get(); + //var sessionFeature = contextFeatures.Get(); + + HttpServerContext = httpServerContext; + RequestTraceIdentifier = requestIdentifierFeature.TraceIdentifier; + Protocoll = requestFeature.Protocol; + + Scheme = requestFeature.Scheme.ToLower() switch + { + "http" => UriScheme.Http, + "https" => UriScheme.Https, + "ftp" => UriScheme.FTP, + "file" => UriScheme.File, + "mailto" => UriScheme.Mailto, + "ldap" => UriScheme.Ldap, + _ => UriScheme.Http + + }; + Method = requestFeature.Method.ToUpper() switch + { + "GET" => RequestMethod.GET, + "POST" => RequestMethod.POST, + "PUT" => RequestMethod.PUT, + "DELETE" => RequestMethod.DELETE, + "HEAD" => RequestMethod.HEAD, + "PATCH" => RequestMethod.PATCH, + _ => RequestMethod.GET + }; + + Header = header; + + LocalEndPoint = new IPEndPoint(connectionFeature.LocalIpAddress, connectionFeature.LocalPort); + RemoteEndPoint = new IPEndPoint(connectionFeature.RemoteIpAddress, connectionFeature.RemotePort); + + Uri = new UriEndpoint + ( + Scheme, + new UriAuthority() + { + Host = Header.Host, + Port = connectionFeature.LocalPort + }, + requestFeature.RawTarget + ); + + ParseQueryParams(requestFeature.QueryString); + ParseSessionParams(); + } + + /// + /// Returns the parameters from the reuest query (for example, http://www.example.com?key=value). + /// + /// The query. + private void ParseQueryParams(string query) + { + query = query.TrimStart('?'); + + Parallel.ForEach(query.Split('&'), (param) => + { + if (!string.IsNullOrWhiteSpace(param)) + { + var split = param.Split('='); + + if (split.Length == 1) + { + AddParameter(new Parameter(split[0], null, ParameterScope.Parameter)); + } + else if (split.Length == 2) + { + AddParameter(new Parameter(split[0], split[1], ParameterScope.Parameter)); + } + else if (split.Length > 2) + { + AddParameter(new Parameter(split[0], string.Join("=", split.Skip(1)), ParameterScope.Parameter)); + } + } + }); + } + + /// + /// Parse the session parameters. + /// + private void ParseSessionParams() + { + Session = WebEx.ComponentHub?.SessionManager?.GetSession(this); + + var property = Session?.GetProperty(); + if (property is not null && property.Params is not null) + { + foreach (var param in property.Params) + { + AddParameter(new Parameter(param.Key?.ToLower(), param.Value.Value, ParameterScope.Session)); + } + } + } + + /// + /// Adds several parameters. + /// + /// The parameters. + public void AddParameter(IEnumerable param) + { + foreach (var p in param) + { + AddParameter(p); + } + } + + /// + /// Adds one parameter. + /// + /// The parameter. + public void AddParameter(Parameter param) + { + var key = param.Key.ToLower(); + + if (!_param.TryAdd(key, param)) + { + _param[key] = param; + } + } + + /// + /// Returns a parameter by name. + /// + /// The name of the parameter. + /// The value. + public IParameter GetParameter(string name) + { + if (!string.IsNullOrWhiteSpace(name) && HasParameter(name)) + { + return _param[name.ToLower()]; + } + + return null; + } + + /// + /// Returns a parameter by name. + /// + /// The parameter. + /// The value. + public TParameter GetParameter() + where TParameter : IParameterStatic, new() + { + var key = TParameter.Key; + + if (!string.IsNullOrWhiteSpace(key) && HasParameter(key)) + { + var p = _param[key.ToLower()]; + + var parameter = new TParameter + { + Value = p.Value, + Scope = p.Scope + }; + + return parameter; + } + + return default; + } + + /// + /// Checks whether a parameter exists. + /// + /// The name of the parameter. + /// True if parameters are present, false otherwise. + public bool HasParameter(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return false; + } + + return _param.ContainsKey(name.ToLower()); + } + } +} diff --git a/src/WebExpress.WebCore/WebMessage/RequestHeaderFields.cs b/src/WebExpress.WebCore/WebMessage/RequestHeaderFields.cs index 7c1f998..d5a0cad 100644 --- a/src/WebExpress.WebCore/WebMessage/RequestHeaderFields.cs +++ b/src/WebExpress.WebCore/WebMessage/RequestHeaderFields.cs @@ -79,6 +79,26 @@ public class RequestHeaderFields /// public string Referer { get; private set; } + /// + /// Returns the upgrade header (e.g. "websocket" for WebSocket upgrades). + /// + public string Upgrade { get; private set; } + + /// + /// Returns the Sec-WebSocket-Key header value if present. + /// + public string SecWebSocketKey { get; private set; } + + /// + /// Returns the Sec-WebSocket-Protocol header value if present. + /// + public string SecWebSocketProtocol { get; private set; } + + /// + /// Returns the Sec-WebSocket-Version header value if present. + /// + public string SecWebSocketVersion { get; private set; } + /// /// Initializes a new instance of the class. /// @@ -98,19 +118,20 @@ internal RequestHeaderFields(IFeatureCollection contextFeatures) AcceptLanguage = requestFeature.Headers.AcceptLanguage.SelectMany(x => x.Split(';', StringSplitOptions.RemoveEmptyEntries)); UserAgent = requestFeature.Headers.UserAgent; Referer = requestFeature.Headers.Referer; - - var cookies = new List(); - - foreach (var cookie in requestFeature.Headers.Cookie) - { - var split = cookie.Split('='); - var key = split[0]; - var value = split[1]; - - cookies.Add(new Cookie(key, value)); - } - - Cookies = cookies; + Upgrade = requestFeature.Headers.Upgrade; + SecWebSocketKey = requestFeature.Headers.SecWebSocketKey; + SecWebSocketProtocol = requestFeature.Headers.SecWebSocketProtocol; + SecWebSocketVersion = requestFeature.Headers.SecWebSocketVersion; + + Cookies = requestFeature.Headers.Cookie + .SelectMany(c => c.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + .Select(c => + { + var eqIndex = c.IndexOf('='); + if (eqIndex < 0) { return null; } + return new Cookie(c[..eqIndex].Trim(), c[(eqIndex + 1)..].Trim()); + }) + .Where(c => c != null); Authorization = RequestAuthorization.Parse(requestFeature.Headers.Authorization); } diff --git a/src/WebExpress.WebCore/WebMessage/RequestHeaderFieldsExtensions.cs b/src/WebExpress.WebCore/WebMessage/RequestHeaderFieldsExtensions.cs new file mode 100644 index 0000000..a258ee4 --- /dev/null +++ b/src/WebExpress.WebCore/WebMessage/RequestHeaderFieldsExtensions.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace WebExpress.WebCore.WebMessage +{ + /// + /// Provides extension methods for converting strongly typed request header + /// fields to formats suitable for WebSocket handshake processing. + /// + public static class RequestHeaderFieldsExtensions + { + /// + /// Converts the strongly typed RequestHeaderFields into a dictionary + /// suitable for WebSocket handshake processing. + /// + public static Dictionary ToDictionary(this RequestHeaderFields header) + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + + void Add(string name, string value) + { + if (!string.IsNullOrEmpty(value)) + { + dict[name] = value; + } + } + + // standard http headers + Add("Host", header.Host); + Add("Connection", header.Connection); + Add("Content-Type", header.ContentType); + Add("Content-Length", header.ContentLength > 0 ? header.ContentLength.ToString() : null); + Add("Content-Language", header.ContentLanguage); + Add("Content-Encoding", header.ContentEncoding?.WebName); + Add("User-Agent", header.UserAgent); + Add("Referer", header.Referer); + + // accept headers + if (header.Accept != null) + Add("Accept", string.Join(", ", header.Accept)); + + Add("Accept-Encoding", header.AcceptEncoding); + + if (header.AcceptLanguage != null) + Add("Accept-Language", string.Join(", ", header.AcceptLanguage)); + + // cookies + if (header.Cookies != null) + { + var cookieString = string.Join("; ", header.Cookies.Select(c => $"{c.Name}={c.Value}")); + Add("Cookie", cookieString); + } + + // authorization + if (header.Authorization != null) + Add("Authorization", header.Authorization.ToString()); + + // websocket-specific headers + Add("Upgrade", header.Upgrade); + Add("Sec-WebSocket-Key", header.SecWebSocketKey); + Add("Sec-WebSocket-Protocol", header.SecWebSocketProtocol); + Add("Sec-WebSocket-Version", header.SecWebSocketVersion); + + return dict; + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebMessage/RequestWebSocket.cs b/src/WebExpress.WebCore/WebMessage/RequestWebSocket.cs new file mode 100644 index 0000000..c180845 --- /dev/null +++ b/src/WebExpress.WebCore/WebMessage/RequestWebSocket.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Http.Features; + +namespace WebExpress.WebCore.WebMessage +{ + /// + /// Represents a request for a WebSocket connection. + /// + public class RequestWebSocket : RequestBase + { + /// + /// Initializes a new instance for a WebSocket request. + /// Use this after WebSocket handshake is established. + /// + /// The feature collection from ASP.NET Core. + /// The parsed header fields of the request. + /// The context of the web server. + internal RequestWebSocket(IFeatureCollection contextFeatures, RequestHeaderFields header, IHttpServerContext httpServerContext) + : base(contextFeatures, header, httpServerContext) + { + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebMessage/Response.cs b/src/WebExpress.WebCore/WebMessage/Response.cs index f4eefb6..8774377 100644 --- a/src/WebExpress.WebCore/WebMessage/Response.cs +++ b/src/WebExpress.WebCore/WebMessage/Response.cs @@ -6,7 +6,7 @@ namespace WebExpress.WebCore.WebMessage /// /// Represents a response according to RFC 2616 Section 6. /// - public abstract class Response + public abstract class Response : IResponse { /// /// Returns the response header fields. diff --git a/src/WebExpress.WebCore/WebMessage/ResponseBadRequest.cs b/src/WebExpress.WebCore/WebMessage/ResponseBadRequest.cs index 1032f9e..b3a4c61 100644 --- a/src/WebExpress.WebCore/WebMessage/ResponseBadRequest.cs +++ b/src/WebExpress.WebCore/WebMessage/ResponseBadRequest.cs @@ -23,7 +23,7 @@ public ResponseBadRequest() /// The user defined status message or null. public ResponseBadRequest(StatusMessage message) { - var content = message?.Message ?? "404404 - Bad Request"; + var content = message?.Message ?? "400400 - Bad Request"; Reason = "Bad Request"; Header.ContentType = "text/html"; diff --git a/src/WebExpress.WebCore/WebMessage/ResponseHeaderFields.cs b/src/WebExpress.WebCore/WebMessage/ResponseHeaderFields.cs index 303ff8f..a161e5c 100644 --- a/src/WebExpress.WebCore/WebMessage/ResponseHeaderFields.cs +++ b/src/WebExpress.WebCore/WebMessage/ResponseHeaderFields.cs @@ -54,11 +54,28 @@ public class ResponseHeaderFields /// public CookieCollection Cookies { get; } = []; + /// + /// Returns or sets the Upgrade header (for protocol upgrade responses, e.g. "websocket"). + /// + public string Upgrade { get; set; } + + /// + /// Returns the connection. Keep-Alive or close. + /// + public string Connection { get; set; } + + /// + /// Returns or sets the value of the Sec-WebSocket-Accept header used in + /// the WebSocket handshake response. + /// + public string SecWebSocketAccept { get; set; } + /// /// Initializes a new instance of the class. /// public ResponseHeaderFields() { + // set defaults CustomHeader = new Dictionary(); WWWAuthenticate = false; ContentLength = -1; @@ -119,6 +136,21 @@ public override string ToString() sb.AppendLine("Location: " + Location); } + if (!string.IsNullOrWhiteSpace(Connection)) + { + sb.AppendLine("Connection: " + Connection); + } + + if (!string.IsNullOrWhiteSpace(Upgrade)) + { + sb.AppendLine("Upgrade: " + Upgrade); + } + + if (!string.IsNullOrWhiteSpace(SecWebSocketAccept)) + { + sb.AppendLine("Sec-WebSocket-Accept: " + SecWebSocketAccept); + } + foreach (var c in CustomHeader) { sb.AppendLine(c.Key + ": " + c.Value); diff --git a/src/WebExpress.WebCore/WebMessage/ResponsePayloadTooLarge.cs b/src/WebExpress.WebCore/WebMessage/ResponsePayloadTooLarge.cs new file mode 100644 index 0000000..5c911b0 --- /dev/null +++ b/src/WebExpress.WebCore/WebMessage/ResponsePayloadTooLarge.cs @@ -0,0 +1,40 @@ +using WebExpress.WebCore.WebAttribute; +using WebExpress.WebCore.WebStatusPage; + +namespace WebExpress.WebCore.WebMessage +{ + /// + /// Represents an HTTP 413 (Payload Too Large) response. + /// According to RFC 7231, section 6.5.11, this status code indicates + /// that the server is refusing to process a request because its payload + /// exceeds the size limits defined by the server. + /// + [StatusCode(413)] + public class ResponsePayloadTooLarge : Response + { + /// + /// Initializes a new instance of the class. + /// + public ResponsePayloadTooLarge() + : this(null) + { + } + + /// + /// Initializes a new instance of the class + /// with an optional user-defined status message. + /// + /// The user-defined status message, or null to use a default HTML body. + public ResponsePayloadTooLarge(StatusMessage message) + { + var content = message?.Message + ?? "413 Payload Too Large413 - Payload Too Large"; + + Reason = "Payload Too Large"; + + Header.ContentType = "text/html"; + Header.ContentLength = content.Length; + Content = content; + } + } +} diff --git a/src/WebExpress.WebCore/WebMessage/ResponseSender.cs b/src/WebExpress.WebCore/WebMessage/ResponseSender.cs new file mode 100644 index 0000000..a77b52a --- /dev/null +++ b/src/WebExpress.WebCore/WebMessage/ResponseSender.cs @@ -0,0 +1,107 @@ +using Microsoft.AspNetCore.Http.Features; +using System; +using System.Threading.Tasks; +using WebExpress.WebCore.WebHtml; + +namespace WebExpress.WebCore.WebMessage +{ + /// + /// Provides functionality for sending HTTP responses. + /// + public class ResponseSender + { + /// + /// Sends the specified response message asynchronously to the provided context. + /// + /// The context of the request. + /// The reply message. + /// Indicates whether the connection should be kept alive after sending the response. + /// A task representing the asynchronous operation. + public async Task SendAsync(IHttpContext context, IResponse response, bool keepAlive = false) + { + try + { + var responseFeature = context.Features.Get(); + var responseBodyFeature = context.Features.Get(); + + responseFeature.StatusCode = response.Status; + responseFeature.ReasonPhrase = response.Reason; + responseFeature.Headers.KeepAlive = "true"; + + if (response.Header.Location != null) + { + responseFeature.Headers.Location = response.Header.Location; + } + + if (!string.IsNullOrWhiteSpace(response.Header.CacheControl)) + { + responseFeature.Headers.CacheControl = response.Header.CacheControl; + } + + if (!string.IsNullOrWhiteSpace(response.Header.ContentType)) + { + responseFeature.Headers.ContentType = response.Header.ContentType; + } + + if (response.Header.WWWAuthenticate) + { + responseFeature.Headers.WWWAuthenticate = "Basic realm=\"Bereich\""; + } + + if (response.Header.Cookies.Count != 0) + { + responseFeature.Headers.SetCookie = string.Join(" ", response.Header.Cookies); + } + + if (!string.IsNullOrWhiteSpace(response.Header.Upgrade)) + { + responseFeature.Headers.Upgrade = response.Header.Upgrade; + } + + if (!string.IsNullOrWhiteSpace(response.Header.Connection)) + { + responseFeature.Headers.Connection = response.Header.Connection; + } + + if (!string.IsNullOrWhiteSpace(response.Header.SecWebSocketAccept)) + { + responseFeature.Headers.SecWebSocketAccept = response.Header.SecWebSocketAccept; + } + + if (response?.Content is byte[] byteContent) + { + responseFeature.Headers.ContentLength = byteContent.Length; + await responseBodyFeature.Stream.WriteAsync(byteContent); + await responseBodyFeature.Stream.FlushAsync(); + } + else if (response?.Content is string strContent) + { + var content = context.Encoding.GetBytes(strContent); + + responseFeature.Headers.ContentLength = content.Length; + await responseBodyFeature.Stream.WriteAsync(content); + await responseBodyFeature.Stream.FlushAsync(); + } + else if (response?.Content is IHtmlNode htmlContent) + { + var content = context.Encoding.GetBytes(htmlContent?.ToString()); + + responseFeature.Headers.ContentLength = content.Length; + await responseBodyFeature.Stream.WriteAsync(content); + await responseBodyFeature.Stream.FlushAsync(); + } + + if (!keepAlive) + { + responseBodyFeature.Stream.Close(); + } + } + catch (Exception ex) + { + // write error to server log + var log = WebEx.ComponentHub.LogManager.DefaultLog; + log.Error(context.RemoteEndPoint + ": " + ex.Message); + } + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebMessage/ResponseSwitchingProtocol.cs b/src/WebExpress.WebCore/WebMessage/ResponseSwitchingProtocol.cs new file mode 100644 index 0000000..e457131 --- /dev/null +++ b/src/WebExpress.WebCore/WebMessage/ResponseSwitchingProtocol.cs @@ -0,0 +1,25 @@ +using WebExpress.WebCore.WebAttribute; + +namespace WebExpress.WebCore.WebMessage +{ + /// + /// Represents a response for a protocol switch (101) according to RFC 2616 Section 6. + /// + [StatusCode(101)] + public class ResponseSwitchingProtocols : Response + { + /// + /// Initializes a new instance of the class. + /// + /// The Connection header value. + /// The Upgrade header value. + /// The Sec-WebSocket-Accept header value. + public ResponseSwitchingProtocols(string connection, string upgrade, string _secWebSocketAccept) + { + Reason = "Switching Protocols"; + Header.Upgrade = upgrade; + Header.Connection = connection; + Header.SecWebSocketAccept = _secWebSocketAccept; + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebMessage/ResponseUpgradeRequired.cs b/src/WebExpress.WebCore/WebMessage/ResponseUpgradeRequired.cs new file mode 100644 index 0000000..2b7c00d --- /dev/null +++ b/src/WebExpress.WebCore/WebMessage/ResponseUpgradeRequired.cs @@ -0,0 +1,38 @@ +using System.Text; +using WebExpress.WebCore.WebAttribute; +using WebExpress.WebCore.WebStatusPage; + +namespace WebExpress.WebCore.WebMessage +{ + /// + /// Represents a response for upgrade required (426) according to RFC 7231 section 6.5.15. + /// + [StatusCode(426)] + public class ResponseUpgradeRequired : Response + { + /// + /// Initializes a new instance of the class. + /// + public ResponseUpgradeRequired() + : this(null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The user defined status message or null. + public ResponseUpgradeRequired(StatusMessage message) + { + var content = message?.Message ?? "426426 - Upgrade Required"; + + // reason phrase for 426 (Upgrade Required) + Reason = "Upgrade Required"; + + Header.ContentType = "text/html"; + // set content length in bytes using UTF-8 encoding + Header.ContentLength = Encoding.UTF8.GetByteCount(content); + Content = content; + } + } +} diff --git a/src/WebExpress.WebCore/WebPackage/PackageManager.cs b/src/WebExpress.WebCore/WebPackage/PackageManager.cs index dc138a8..724b947 100644 --- a/src/WebExpress.WebCore/WebPackage/PackageManager.cs +++ b/src/WebExpress.WebCore/WebPackage/PackageManager.cs @@ -153,14 +153,14 @@ public void Scan() var packageFiles = Directory.GetFiles(_httpServerContext.PackagePath, "*.wxp").Select(x => Path.GetFileName(x)).ToList(); // all packages that are not yet installed - var newPackages = packageFiles.Except(Catalog.Packages.Where(x => x != null).Select(x => x.File)).ToList(); + var newPackages = packageFiles.Except(Catalog.Packages.Where(x => x is not null).Select(x => x.File)).ToList(); // all packages that are no longer available - var removePackages = Catalog.Packages.Where(x => x != null).Select(x => x.File).Except(packageFiles).ToList(); + var removePackages = Catalog.Packages.Where(x => x is not null).Select(x => x.File).Except(packageFiles).ToList(); // determine changed packages by comparing spec version and relevant metadata var changedPackages = new List(); - foreach (var existing in Catalog.Packages.Where(x => x != null)) + foreach (var existing in Catalog.Packages.Where(x => x is not null)) { var fullPath = Path.Combine(_httpServerContext.PackagePath, existing.File); if (!File.Exists(fullPath)) @@ -169,7 +169,7 @@ public void Scan() } var fromFile = LoadPackage(fullPath); - if (fromFile == null) + if (fromFile is null) { continue; } @@ -183,7 +183,7 @@ public void Scan() foreach (var package in newPackages) { var packagesFromFile = LoadPackage(Path.Combine(_httpServerContext.PackagePath, package)); - if (packagesFromFile == null) + if (packagesFromFile is null) { continue; } @@ -211,14 +211,14 @@ public void Scan() foreach (var package in changedPackages) { - var existing = Catalog.Packages.FirstOrDefault(x => x != null && x.File == package); - if (existing == null) + var existing = Catalog.Packages.FirstOrDefault(x => x is not null && x.File == package); + if (existing is null) { continue; } var fromFile = LoadPackage(Path.Combine(_httpServerContext.PackagePath, package)); - if (fromFile == null) + if (fromFile is null) { continue; } @@ -252,8 +252,8 @@ public void Scan() foreach (var package in removePackages) { - var existing = Catalog.Packages.FirstOrDefault(x => x != null && x.File == package); - if (existing == null) + var existing = Catalog.Packages.FirstOrDefault(x => x is not null && x.File == package); + if (existing is null) { continue; } @@ -305,7 +305,7 @@ private PackageCatalogItem LoadPackage(string file) using var zip = ZipFile.Open(file, ZipArchiveMode.Read); var specEntry = zip.Entries.Where(x => Path.GetExtension(x.FullName) == ".spec").FirstOrDefault(); - if (specEntry == null) + if (specEntry is null) { _httpServerContext.Log.Warning($"package spec was not found in '{file}'"); return null; @@ -559,13 +559,13 @@ private void Log() /// True if changed; otherwise false. private static bool HasPackageChanged(PackageCatalogItem existing, PackageCatalogItem fromFile) { - if (existing == null || fromFile == null) + if (existing is null || fromFile is null) { return false; } // if no metadata was present, treat as no change and let metadata be assigned on next run - if (existing.Metadata == null || fromFile.Metadata == null) + if (existing.Metadata is null || fromFile.Metadata is null) { return false; } @@ -602,7 +602,7 @@ private static bool HasPackageChanged(PackageCatalogItem existing, PackageCatalo /// The package. private void DeactivateAndUnregisterPackage(PackageCatalogItem package) { - if (package == null) + if (package is null) { return; } diff --git a/src/WebExpress.WebCore/WebPage/IPageContext.cs b/src/WebExpress.WebCore/WebPage/IPageContext.cs index 0644061..17fa145 100644 --- a/src/WebExpress.WebCore/WebPage/IPageContext.cs +++ b/src/WebExpress.WebCore/WebPage/IPageContext.cs @@ -26,5 +26,12 @@ public interface IPageContext : IEndpointContext /// determine whether content and how content should be displayed. /// IEnumerable Scopes { get; } + + /// + /// Returns the collection of domain types associated with the decorated element. + /// Domains represent logical application areas such as workspaces, modules + /// or functional segments and can be used for routing, filtering or contextual grouping. + /// + IEnumerable Domains { get; } } } diff --git a/src/WebExpress.WebCore/WebPage/IRenderContext.cs b/src/WebExpress.WebCore/WebPage/IRenderContext.cs index a9b3be0..36437f9 100644 --- a/src/WebExpress.WebCore/WebPage/IRenderContext.cs +++ b/src/WebExpress.WebCore/WebPage/IRenderContext.cs @@ -27,6 +27,6 @@ public interface IRenderContext /// /// Returns the request. /// - Request Request { get; } + IRequest Request { get; } } } diff --git a/src/WebExpress.WebCore/WebPage/IVisualTreeContext.cs b/src/WebExpress.WebCore/WebPage/IVisualTreeContext.cs index 87f0abc..4ef9186 100644 --- a/src/WebExpress.WebCore/WebPage/IVisualTreeContext.cs +++ b/src/WebExpress.WebCore/WebPage/IVisualTreeContext.cs @@ -11,12 +11,12 @@ public interface IVisualTreeContext /// /// Returns the request. /// - Request Request { get; } + IRequest Request { get; } /// /// The uri of the request. /// - UriEndpoint Uri { get; } + IUri Uri { get; } /// /// Return or sets the render context. diff --git a/src/WebExpress.WebCore/WebPage/Model/PageDictionary.cs b/src/WebExpress.WebCore/WebPage/Model/PageDictionary.cs index 2925951..8a67878 100644 --- a/src/WebExpress.WebCore/WebPage/Model/PageDictionary.cs +++ b/src/WebExpress.WebCore/WebPage/Model/PageDictionary.cs @@ -34,7 +34,7 @@ public bool AddPageItem(IPluginContext pluginContext, IApplicationContext applic { var type = pageItem.PageClass; - if (type.GetInterface(typeof(IPage<>).Name) == null) + if (type.GetInterface(typeof(IPage<>).Name) is null) { return false; } diff --git a/src/WebExpress.WebCore/WebPage/PageContext.cs b/src/WebExpress.WebCore/WebPage/PageContext.cs index c921510..a7e3a11 100644 --- a/src/WebExpress.WebCore/WebPage/PageContext.cs +++ b/src/WebExpress.WebCore/WebPage/PageContext.cs @@ -32,6 +32,13 @@ public class PageContext : IPageContext /// public IEnumerable Scopes { get; internal set; } = []; + /// + /// Returns the collection of domain types associated with the decorated element. + /// Domains represent logical application areas such as workspaces, modules + /// or functional segments and can be used for routing, filtering or contextual grouping. + /// + public IEnumerable Domains { get; internal set; } = []; + /// /// Returns the conditions that must be met for the resource to be active. /// diff --git a/src/WebExpress.WebCore/WebPage/PageManager.cs b/src/WebExpress.WebCore/WebPage/PageManager.cs index 066b472..b29285a 100644 --- a/src/WebExpress.WebCore/WebPage/PageManager.cs +++ b/src/WebExpress.WebCore/WebPage/PageManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -25,7 +26,7 @@ public class PageManager : IPageManager private readonly IComponentHub _componentHub; private readonly IHttpServerContext _httpServerContext; private readonly PageDictionary _dictionary = new(); - private static readonly Dictionary _delegateCache = []; + private static readonly ConcurrentDictionary _delegateCache = new(); /// /// An event that fires when an page is added. @@ -59,44 +60,52 @@ private PageManager(IComponentHub componentHub, IHttpServerContext httpServerCon var endpointtRegistration = new EndpointRegistration() { - EndpointResolver = (type, applicationContext) => applicationContext != null ? GetPages(type, applicationContext) : GetPages(type), + EndpointResolver = (type, applicationContext) => applicationContext is not null + ? GetPages(type, applicationContext) + : GetPages(type), EndpointsResolver = () => Pages, HandleRequest = (request, endpontContext) => { + // create or get page instance for this request var pageInstance = CreatePageInstance(endpontContext as IPageContext); var pageType = pageInstance.GetType(); var pageContext = endpontContext as IPageContext; var renderContext = new RenderContext(pageInstance, pageContext, request); var visualTreeContext = new VisualTreeContext(renderContext); - var visualTreeType = pageType.GetInterface(typeof(IPage<>).Name).GetGenericArguments()[0]; + // determine visual tree type implemented by the page + var pageInterface = pageType.GetInterface(typeof(IPage<>).Name) ?? throw new InvalidOperationException($"Page type {pageType.FullName} does not implement IPage<>."); + var visualTreeType = pageInterface.GetGenericArguments()[0]; + + // obtain or create a cached open-instance delegate safely if (!_delegateCache.TryGetValue(pageType, out var del)) { - // create and compile the expression + // create an open-instance delegate: (instance, renderContext, visualTree) => instance.Process(renderContext, visualTree) + var instanceParam = Expression.Parameter(pageType, "instance"); var renderContextParam = Expression.Parameter(typeof(IRenderContext), "renderContext"); var visualTreeParam = Expression.Parameter(visualTreeType, "visualTree"); - var processMethod = pageType.GetMethod("Process", [typeof(IRenderContext), visualTreeType]); - var callProzessMethod = Expression.Call - ( - Expression.Constant(pageInstance), - processMethod, - renderContextParam, - visualTreeParam - ); - var lambda = Expression.Lambda(callProzessMethod, renderContextParam, visualTreeParam) - .Compile(); - _delegateCache[pageType] = lambda; - del = lambda; + // find Process method matching signature Process(IRenderContext, TVisualTree) + var processMethod = pageType.GetMethod("Process", new[] { typeof(IRenderContext), visualTreeType }) ?? throw new InvalidOperationException($"Process method not found on type {pageType.FullName}"); + + // call instance.Process(renderContext, visualTree) + var callProcess = Expression.Call(instanceParam, processMethod, renderContextParam, visualTreeParam); + + // compile lambda with signature (instance, renderContext, visualTree) + var lambda = Expression.Lambda(callProcess, instanceParam, renderContextParam, visualTreeParam).Compile(); + + // add to concurrent dictionary atomically; if another thread added concurrently, use the existing one + del = _delegateCache.GetOrAdd(pageType, lambda); } // create visual tree instance - var visualTreeInstance = default(IVisualTree); + IVisualTree visualTreeInstance = null; var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var constructors = visualTreeType?.GetConstructors(flags); - if (constructors != null) + if (constructors is not null) { + // try constructors ordered by parameter count (descending) foreach (var constructor in constructors.OrderByDescending(x => x.GetParameters().Length)) { // injection @@ -107,30 +116,43 @@ private PageManager(IComponentHub componentHub, IHttpServerContext httpServerCon .FirstOrDefault(); var parameterValues = parameters.Select(parameter => - parameter.ParameterType == typeof(IComponentHub) ? componentHub : - parameter.ParameterType == typeof(IHttpServerContext) ? httpServerContext : + parameter.ParameterType == typeof(IComponentHub) ? _componentHub : + parameter.ParameterType == typeof(IHttpServerContext) ? _httpServerContext : parameter.ParameterType == typeof(IPageContext) ? pageContext : + parameter.ParameterType == typeof(IApplicationContext) ? pageContext?.ApplicationContext : parameter.ParameterType == typeof(IComponentId) ? contextIdProperty?.GetValue(pageContext) : hubProperties.Where(x => x.PropertyType == parameter.ParameterType) .FirstOrDefault()? - .GetValue(componentHub) ?? null + .GetValue(_componentHub) ?? null ).ToArray(); - if (constructor.Invoke(parameterValues) is IVisualTree visualTree) + // attempt to invoke constructor with resolved parameters + var invoked = constructor.Invoke(parameterValues); + if (invoked is IVisualTree visualTree) { visualTreeInstance = visualTree; + break; } } } else { - visualTreeInstance = Activator.CreateInstance(); + // fallback: try parameterless creation + visualTreeInstance = Activator.CreateInstance(visualTreeType) as IVisualTree; } - // execute the cached delegate - del.DynamicInvoke(renderContext, visualTreeInstance); + if (visualTreeInstance is null) + { + throw new InvalidOperationException($"Could not create visual tree instance of type {visualTreeType.FullName} for page {pageType.FullName}."); + } - return visualTreeInstance.GetResponse(visualTreeContext); + // execute the cached open-instance delegate; pass the current pageInstance + del.DynamicInvoke(pageInstance, renderContext, visualTreeInstance); + + // build response from visual tree + var response = visualTreeInstance.GetResponse(visualTreeContext); + + return response; } }; @@ -230,7 +252,7 @@ private IEndpoint CreatePageInstance(IPageContext pageContext) { var resourceItem = _dictionary.GetPageItem(pageContext); - if (resourceItem != null && resourceItem.Instance == null) + if (resourceItem is not null && resourceItem.Instance is null) { var instance = ComponentActivator.CreateInstance ( @@ -294,7 +316,7 @@ private void Register(IPluginContext pluginContext, IEnumerable x.IsClass == true && x.IsSealed && x.IsPublic) - .Where(x => x.GetInterface(typeof(IPage<>).Name) != null)) + .Where(x => x.GetInterface(typeof(IPage<>).Name) is not null)) { var id = pageType.FullName?.ToLower(); var segment = default(ISegmentAttribute); @@ -304,47 +326,107 @@ private void Register(IPluginContext pluginContext, IEnumerable(); var conditions = new List(); var cache = false; + var domains = new List(); var attributes = pageType.CustomAttributes .Where(x => !x.AttributeType.GetInterfaces().Contains(typeof(IEndpointAttribute)) && !x.AttributeType.GetInterfaces().Contains(typeof(IPageAttribute))); - foreach (var customAttribute in pageType.CustomAttributes - .Where(x => x.AttributeType.GetInterfaces().Contains(typeof(IEndpointAttribute)))) + foreach + ( + var attribute in pageType + .GetCustomAttributes(inherit: true) + .Where(x => x.GetType().GetInterfaces().Contains(typeof(IEndpointAttribute))) + ) { - if (customAttribute.AttributeType.GetInterfaces().Contains(typeof(ISegmentAttribute))) + var attributeType = attribute.GetType(); + + // segment attribute + if (attributeType.GetInterfaces().Contains(typeof(ISegmentAttribute))) { - segment = pageType.GetCustomAttributes(customAttribute.AttributeType, false).FirstOrDefault() as ISegmentAttribute; + segment = attribute as ISegmentAttribute; + continue; } - else if (customAttribute.AttributeType == typeof(IncludeSubPathsAttribute)) + + // include subpaths + if (attributeType == typeof(IncludeSubPathsAttribute)) { - includeSubPaths = Convert.ToBoolean(customAttribute.ConstructorArguments.FirstOrDefault().Value); + includeSubPaths = (attribute as IncludeSubPathsAttribute)?.IncludeSubPaths ?? false; + continue; } - else if (customAttribute.AttributeType.Name == typeof(ConditionAttribute<>).Name && customAttribute.AttributeType.Namespace == typeof(ConditionAttribute<>).Namespace) + + // condition attribute (generic) + if (attributeType.IsGenericType + && attributeType.GetGenericTypeDefinition().Name == typeof(ConditionAttribute<>).Name + && attributeType.Namespace == typeof(ConditionAttribute<>).Namespace) { - var condition = customAttribute.AttributeType.GenericTypeArguments.FirstOrDefault(); - conditions.Add(Activator.CreateInstance(condition) as ICondition); + var conditionType = attributeType.GetGenericArguments().FirstOrDefault(); + if (conditionType != null) + { + conditions.Add(Activator.CreateInstance(conditionType) as ICondition); + } + continue; } - else if (customAttribute.AttributeType == typeof(CacheAttribute)) + + // cache attribute + if (attributeType == typeof(CacheAttribute)) { cache = true; + continue; } } - foreach (var customAttribute in pageType.CustomAttributes - .Where(x => x.AttributeType.GetInterfaces().Contains(typeof(IPageAttribute)))) + foreach + ( + var attribute in pageType + .GetCustomAttributes(inherit: true) + .Where(x => x.GetType().GetInterfaces().Contains(typeof(IPageAttribute))) + ) { - if (customAttribute.AttributeType.IsGenericType && customAttribute.AttributeType.GetGenericTypeDefinition() == typeof(WebIconAttribute<>)) + var attributeType = attribute.GetType(); + + // web icon attribute (generic) + if (attributeType.IsGenericType && + attributeType.GetGenericTypeDefinition() == typeof(WebIconAttribute<>)) { - var type = customAttribute.AttributeType.GenericTypeArguments.FirstOrDefault(); - icon ??= Activator.CreateInstance(type) as IIcon; + var iconType = attributeType.GetGenericArguments().FirstOrDefault(); + if (iconType != null) + { + icon ??= Activator.CreateInstance(iconType) as IIcon; + } + continue; } - else if (customAttribute.AttributeType == typeof(TitleAttribute)) + + // title attribute + if (attributeType == typeof(TitleAttribute)) { - title = customAttribute.ConstructorArguments.FirstOrDefault().Value?.ToString(); + title = (attribute as TitleAttribute)?.Title; + continue; } - else if (customAttribute.AttributeType.Name == typeof(ScopeAttribute<>).Name && customAttribute.AttributeType.Namespace == typeof(ScopeAttribute<>).Namespace) + + // scope attribute (generic) + if (attributeType.IsGenericType && + attributeType.GetGenericTypeDefinition().Name == typeof(ScopeAttribute<>).Name && + attributeType.Namespace == typeof(ScopeAttribute<>).Namespace) { - scopes.Add(customAttribute.AttributeType.GenericTypeArguments.FirstOrDefault()); + var scopeType = attributeType.GetGenericArguments().FirstOrDefault(); + if (scopeType != null) + { + scopes.Add(scopeType); + } + continue; + } + + // domain attribute (generic) + if (attributeType.IsGenericType && + attributeType.GetGenericTypeDefinition().Name == typeof(DomainAttribute<>).Name && + attributeType.Namespace == typeof(DomainAttribute<>).Namespace) + { + var domainType = attributeType.GetGenericArguments().FirstOrDefault(); + if (domainType != null) + { + domains.Add(domainType); + } + continue; } } @@ -372,6 +454,7 @@ private void Register(IPluginContext pluginContext, IEnumerableThe context of the plugin that contains the pages to remove. public void Remove(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return; } @@ -435,7 +518,7 @@ public void Remove(IPluginContext pluginContext) /// The context of the application that contains the page to remove. internal void Remove(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { return; } diff --git a/src/WebExpress.WebCore/WebPage/RenderContext.cs b/src/WebExpress.WebCore/WebPage/RenderContext.cs index 13cd87f..441d8a6 100644 --- a/src/WebExpress.WebCore/WebPage/RenderContext.cs +++ b/src/WebExpress.WebCore/WebPage/RenderContext.cs @@ -18,7 +18,7 @@ public class RenderContext : IRenderContext /// /// Returns the request. /// - public Request Request { get; protected set; } + public IRequest Request { get; protected set; } /// /// The uri of the request. @@ -48,7 +48,7 @@ public RenderContext() /// The endpoint associated with the rendering context. /// The page context. /// The request associated with the rendering context. - public RenderContext(IEndpoint endpoint, IPageContext pageContext, Request request) + public RenderContext(IEndpoint endpoint, IPageContext pageContext, IRequest request) { Endpoint = endpoint; PageContext = pageContext; diff --git a/src/WebExpress.WebCore/WebPage/VisualTree.cs b/src/WebExpress.WebCore/WebPage/VisualTree.cs index 948e150..f6d0c2b 100644 --- a/src/WebExpress.WebCore/WebPage/VisualTree.cs +++ b/src/WebExpress.WebCore/WebPage/VisualTree.cs @@ -75,7 +75,7 @@ public VisualTree() /// The java script code. public virtual void AddScript(string key, string code) { - if (key == null) return; + if (key is null) return; var k = key.ToLower(); var dict = Scripts; @@ -124,8 +124,8 @@ public virtual IHtmlNode Render(IVisualTreeContext context) html.Body.Add(Content); html.Body.Scripts = [.. Scripts.Values]; - html.Head.CssLinks = CssLinks.Where(x => x != null).Select(x => x.ToString()); - html.Head.ScriptLinks = HeaderScriptLinks?.Where(x => x != null).Select(x => x.ToString()); + html.Head.CssLinks = CssLinks.Where(x => x is not null).Select(x => x.ToString()); + html.Head.ScriptLinks = HeaderScriptLinks?.Where(x => x is not null).Select(x => x.ToString()); return html; } diff --git a/src/WebExpress.WebCore/WebPage/VisualTreeContext.cs b/src/WebExpress.WebCore/WebPage/VisualTreeContext.cs index 59beca1..724f68f 100644 --- a/src/WebExpress.WebCore/WebPage/VisualTreeContext.cs +++ b/src/WebExpress.WebCore/WebPage/VisualTreeContext.cs @@ -11,12 +11,12 @@ public class VisualTreeContext : IVisualTreeContext /// /// Returns the request. /// - public Request Request => RenderContext?.Request; + public IRequest Request => RenderContext?.Request; /// /// The uri of the request. /// - public UriEndpoint Uri => RenderContext?.Request?.Uri; + public IUri Uri => RenderContext?.Request?.Uri; /// /// Return or sets the render context. diff --git a/src/WebExpress.WebCore/WebParameter/IParameter.cs b/src/WebExpress.WebCore/WebParameter/IParameter.cs new file mode 100644 index 0000000..6db9f5c --- /dev/null +++ b/src/WebExpress.WebCore/WebParameter/IParameter.cs @@ -0,0 +1,18 @@ +namespace WebExpress.WebCore.WebParameter +{ + /// + /// Represents a parameter with a key, value, and scope. + /// + public interface IParameter + { + /// + /// Returns or sets the scope of the parameter. + /// + ParameterScope Scope { get; set; } + + /// + /// Returns the value of the parameter. + /// + string Value { get; set; } + } +} diff --git a/src/WebExpress.WebCore/WebParameter/IParameterDynamic.cs b/src/WebExpress.WebCore/WebParameter/IParameterDynamic.cs new file mode 100644 index 0000000..dcb678a --- /dev/null +++ b/src/WebExpress.WebCore/WebParameter/IParameterDynamic.cs @@ -0,0 +1,13 @@ +namespace WebExpress.WebCore.WebParameter +{ + /// + /// Represents a parameter with a key, value, and scope. + /// + public interface IParameterDynamic : IParameter + { + /// + /// Returns the key of the parameter. + /// + string Key { get; } + } +} diff --git a/src/WebExpress.WebCore/WebParameter/IParameterStatic.cs b/src/WebExpress.WebCore/WebParameter/IParameterStatic.cs new file mode 100644 index 0000000..6d5aabb --- /dev/null +++ b/src/WebExpress.WebCore/WebParameter/IParameterStatic.cs @@ -0,0 +1,22 @@ +namespace WebExpress.WebCore.WebParameter +{ + /// + /// Represents a parameter with a key, value, and scope. + /// + public interface IParameterStatic : IParameter + { + /// + /// Returns the key of the parameter. + /// + static abstract string Key { get; } + + /// + /// Retrieves the unique key associated with the current instance. + /// + /// + /// A string representing the unique key. This key is used for identifying + /// the instance in various operations. + /// + string GetKey(); + } +} diff --git a/src/WebExpress.WebCore/WebMessage/Parameter.cs b/src/WebExpress.WebCore/WebParameter/Parameter.cs similarity index 89% rename from src/WebExpress.WebCore/WebMessage/Parameter.cs rename to src/WebExpress.WebCore/WebParameter/Parameter.cs index 8f1d4fd..d35e89c 100644 --- a/src/WebExpress.WebCore/WebMessage/Parameter.cs +++ b/src/WebExpress.WebCore/WebParameter/Parameter.cs @@ -2,27 +2,27 @@ using System.Collections.Generic; using System.Text; -namespace WebExpress.WebCore.WebMessage +namespace WebExpress.WebCore.WebParameter { /// /// Represents a parameter with a key, value, and scope. /// - public class Parameter + public class Parameter : IParameterDynamic { /// - /// Returns or sets the scope of the parameter. + /// Returns the key of the parameter. /// - public ParameterScope Scope { get; private set; } + public string Key { get; private set; } /// - /// Returns the key of the parameter. + /// Returns or sets the scope of the parameter. /// - public string Key { get; private set; } + public ParameterScope Scope { get; set; } /// /// Returns the value of the parameter. /// - public string Value { get; internal set; } + public string Value { get; set; } /// /// Initializes a new instance of the class. @@ -105,16 +105,6 @@ public static List Create(params Parameter[] param) return [.. param]; } - /// - /// Returns the key. - /// - /// The type. - /// The key. - public static string GetKey() where TParameter : Parameter - { - return Activator.CreateInstance()?.Key; - } - /// /// Conversion to string form. /// diff --git a/src/WebExpress.WebCore/WebParameter/ParameterApiVersion.cs b/src/WebExpress.WebCore/WebParameter/ParameterApiVersion.cs new file mode 100644 index 0000000..804e36a --- /dev/null +++ b/src/WebExpress.WebCore/WebParameter/ParameterApiVersion.cs @@ -0,0 +1,54 @@ +namespace WebExpress.WebCore.WebParameter +{ + /// + /// Represents a api version parameter with a key, value, and scope. + /// + public sealed class ParameterApiVersion : IParameterStatic + { + /// + /// Returns the key that uniquely identifies the parameter in configuration or + /// settings contexts. + /// + public static string Key => "_apiVersion"; + + /// + /// Returns or sets the scope of the parameter. + /// + public ParameterScope Scope { get; set; } + + /// + /// Returns the value of the parameter. + /// + public string Value { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public ParameterApiVersion() + { + Scope = ParameterScope.Url; + } + + /// + /// Initializes a new instance of the class with a specified value. + /// + /// The value of the parameter. + public ParameterApiVersion(string value) + : this() + { + Value = value; + } + + /// + /// Retrieves the unique key associated with the current instance. + /// + /// + /// A string representing the unique key. This key is used for identifying + /// the instance in various operations. + /// + public string GetKey() + { + return Key; + } + } +} diff --git a/src/WebExpress.WebCore/WebMessage/ParameterDictionary.cs b/src/WebExpress.WebCore/WebParameter/ParameterDictionary.cs similarity index 63% rename from src/WebExpress.WebCore/WebMessage/ParameterDictionary.cs rename to src/WebExpress.WebCore/WebParameter/ParameterDictionary.cs index cb515fe..0034368 100644 --- a/src/WebExpress.WebCore/WebMessage/ParameterDictionary.cs +++ b/src/WebExpress.WebCore/WebParameter/ParameterDictionary.cs @@ -1,11 +1,13 @@ using System.Collections.Concurrent; -namespace WebExpress.WebCore.WebMessage +namespace WebExpress.WebCore.WebParameter { /// /// Management of parameters. /// - public class ParameterDictionary : ConcurrentDictionary + /// Key: parameter name + /// Value: parameter + public class ParameterDictionary : ConcurrentDictionary { } } diff --git a/src/WebExpress.WebCore/WebMessage/ParameterFile.cs b/src/WebExpress.WebCore/WebParameter/ParameterFile.cs similarity index 97% rename from src/WebExpress.WebCore/WebMessage/ParameterFile.cs rename to src/WebExpress.WebCore/WebParameter/ParameterFile.cs index 492396b..19fd524 100644 --- a/src/WebExpress.WebCore/WebMessage/ParameterFile.cs +++ b/src/WebExpress.WebCore/WebParameter/ParameterFile.cs @@ -1,4 +1,4 @@ -namespace WebExpress.WebCore.WebMessage +namespace WebExpress.WebCore.WebParameter { /// /// Represents a file parameter with content type and data. diff --git a/src/WebExpress.WebCore/WebParameter/ParameterId.cs b/src/WebExpress.WebCore/WebParameter/ParameterId.cs new file mode 100644 index 0000000..a24552c --- /dev/null +++ b/src/WebExpress.WebCore/WebParameter/ParameterId.cs @@ -0,0 +1,66 @@ +using System; + +namespace WebExpress.WebCore.WebParameter +{ + /// + /// Represents a id parameter with a key, value, and scope. + /// + public sealed class ParameterId : IParameterStatic + { + /// + /// Returns the key that uniquely identifies the parameter in configuration or + /// settings contexts. + /// + public static string Key => "id"; + + /// + /// Returns or sets the scope of the parameter. + /// + public ParameterScope Scope { get; set; } + + /// + /// Returns the value of the parameter. + /// + public string Value { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public ParameterId() + { + Scope = ParameterScope.Url; + } + + /// + /// Initializes a new instance of the class with a specified value. + /// + /// The value of the parameter. + public ParameterId(string value) + : this() + { + Value = value; + } + + /// + /// Initializes a new instance of the class with a specified value. + /// + /// The value of the parameter. + public ParameterId(Guid value) + : this() + { + Value = value.ToString(); + } + + /// + /// Retrieves the unique key associated with the current instance. + /// + /// + /// A string representing the unique key. This key is used for identifying + /// the instance in various operations. + /// + public string GetKey() + { + return Key; + } + } +} diff --git a/src/WebExpress.WebCore/WebMessage/ParameterScope.cs b/src/WebExpress.WebCore/WebParameter/ParameterScope.cs similarity index 92% rename from src/WebExpress.WebCore/WebMessage/ParameterScope.cs rename to src/WebExpress.WebCore/WebParameter/ParameterScope.cs index c6fdd59..3f00af6 100644 --- a/src/WebExpress.WebCore/WebMessage/ParameterScope.cs +++ b/src/WebExpress.WebCore/WebParameter/ParameterScope.cs @@ -1,4 +1,4 @@ -namespace WebExpress.WebCore.WebMessage +namespace WebExpress.WebCore.WebParameter { /// /// Defines the scopes of the parameter. diff --git a/src/WebExpress.WebCore/WebPlugin/Model/PluginLoadContext.cs b/src/WebExpress.WebCore/WebPlugin/Model/PluginLoadContext.cs index 62499f8..9be058a 100644 --- a/src/WebExpress.WebCore/WebPlugin/Model/PluginLoadContext.cs +++ b/src/WebExpress.WebCore/WebPlugin/Model/PluginLoadContext.cs @@ -32,7 +32,7 @@ protected override Assembly Load(AssemblyName assemblyName) { string assemblyPath = Resolver.ResolveAssemblyToPath(assemblyName); - if (assemblyPath != null) + if (assemblyPath is not null) { return LoadFromAssemblyPath(assemblyPath); } @@ -49,7 +49,7 @@ protected override nint LoadUnmanagedDll(string unmanagedDllName) { string libraryPath = Resolver.ResolveUnmanagedDllToPath(unmanagedDllName); - if (libraryPath != null) + if (libraryPath is not null) { return LoadUnmanagedDllFromPath(libraryPath); } diff --git a/src/WebExpress.WebCore/WebPlugin/PluginManager.cs b/src/WebExpress.WebCore/WebPlugin/PluginManager.cs index e9587a9..40f798c 100644 --- a/src/WebExpress.WebCore/WebPlugin/PluginManager.cs +++ b/src/WebExpress.WebCore/WebPlugin/PluginManager.cs @@ -73,7 +73,7 @@ internal void Register() try { var assembly = Assembly.LoadFrom(assemblyFile); - if (assembly != null) + if (assembly is not null) { assemblies.Add(assembly); _httpServerContext.Log.Debug @@ -94,7 +94,9 @@ internal void Register() } // register plugin - foreach (var assembly in assemblies.OrderBy(x => x.GetCustomAttribute() != null ? 0 : 1)) + foreach (var assembly in assemblies.OrderBy(x => x.GetCustomAttribute() is not null + ? 0 + : 1)) { Register(assembly); } @@ -123,7 +125,7 @@ internal IEnumerable Register(string pluginFile) { var assembly = loadContext.LoadFromAssemblyName(AssemblyName.GetAssemblyName(pluginFile)); - if (assembly != null) + if (assembly is not null) { assemblies.Add(assembly); _httpServerContext.Log.Debug @@ -167,12 +169,14 @@ private IEnumerable Register(Assembly assembly, PluginLoadContex try { // system plugins without plugin class (e.g. webexpress.webui) - if (assembly.GetCustomAttribute() != null) + if (assembly.GetCustomAttribute() is not null) { var attributeData = assembly.CustomAttributes .FirstOrDefault(a => a.AttributeType == typeof(SystemPluginAttribute)); var dependency = attributeData.ConstructorArguments.FirstOrDefault().Value?.ToString(); - var dependencies = dependency != null ? new List([dependency]) : []; + var dependencies = dependency is not null + ? new List([dependency]) + : []; var id = new ComponentId(assembly.GetName().Name.ToLower()); var pluginContext = new PluginContext() { @@ -236,7 +240,7 @@ private IEnumerable Register(Assembly assembly, PluginLoadContex foreach (var type in assembly .GetExportedTypes() .Where(x => x.IsClass && x.IsSealed) - .Where(x => x.GetInterface(typeof(IPlugin).Name) != null)) + .Where(x => x.GetInterface(typeof(IPlugin).Name) is not null)) { var id = new ComponentId(type.Namespace); var name = type.Assembly.GetCustomAttribute()?.Title; @@ -364,7 +368,7 @@ private IEnumerable Register(Assembly assembly, PluginLoadContex /// The context of the plugin that contains the elemets to remove. public void Remove(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return; } @@ -457,7 +461,7 @@ public IPluginContext GetPlugin(string pluginId) return _dictionary.Values .Where ( - x => x.PluginContext != null && + x => x.PluginContext is not null && x.PluginContext.PluginId.ToString().Equals(pluginId) ) .Select(x => x.PluginContext) @@ -474,7 +478,7 @@ public IPluginContext GetPlugin(Type plugin) return _dictionary.Values .Where ( - x => x.PluginContext != null && + x => x.PluginContext is not null && x.PluginClass.Equals(plugin) ) .Select(x => x.PluginContext) @@ -489,7 +493,7 @@ public IPluginContext GetPlugin(Type plugin) public IEnumerable GetPlugins(IApplicationContext applicationContext) { return _dictionary.Values - .Where(x => x.ApplicationTypes != null) + .Where(x => x.ApplicationTypes is not null) .Where(x => x.ApplicationTypes.Select(x => _componentHub.ApplicationManager.GetApplications(x)) .SelectMany(x => x) .Where(x => x.ApplicationId == applicationContext.ApplicationId) @@ -506,7 +510,7 @@ public IEnumerable GetAssociatedApplications(IPluginContext { var pluginItem = GetPluginItem(pluginContext); - if (pluginItem == null) + if (pluginItem is null) { return []; } @@ -514,7 +518,7 @@ public IEnumerable GetAssociatedApplications(IPluginContext return pluginItem.ApplicationTypes? .Select(x => _componentHub.ApplicationManager.GetApplications(x)) .SelectMany(x => x) - .Where(x => x != null) ?? []; + .Where(x => x is not null) ?? []; } @@ -527,7 +531,7 @@ private PluginItem GetPluginItem(IPluginContext pluginContext) { var pluginId = pluginContext?.PluginId; - if (pluginId == null || !_dictionary.TryGetValue(pluginId, out PluginItem value)) + if (pluginId is null || !_dictionary.TryGetValue(pluginId, out PluginItem value)) { _httpServerContext.Log.Warning ( @@ -553,12 +557,12 @@ internal void Boot(IPluginContext pluginContext) var pluginItem = GetPluginItem(pluginContext); var token = pluginItem?.CancellationTokenSource.Token; - if (pluginItem == null) + if (pluginItem is null) { return; } - if (pluginItem.Plugin == null) + if (pluginItem.Plugin is null) { return; } @@ -663,7 +667,7 @@ private void Log() .Where ( x => x.Value.PluginClass.Assembly - .GetCustomAttribute() != null + .GetCustomAttribute() is not null ) .Select(x => string.Empty.PadRight(2) + I18N.Translate ( @@ -676,7 +680,7 @@ private void Log() .Where ( x => x.Value.PluginClass.Assembly - .GetCustomAttribute() == null + .GetCustomAttribute() is null ) .Select(x => string.Empty.PadRight(2) + I18N.Translate ( diff --git a/src/WebExpress.WebCore/WebResource/IResource.cs b/src/WebExpress.WebCore/WebResource/IResource.cs index 583e12d..6f3ec92 100644 --- a/src/WebExpress.WebCore/WebResource/IResource.cs +++ b/src/WebExpress.WebCore/WebResource/IResource.cs @@ -13,6 +13,6 @@ public interface IResource : IEndpoint /// /// The request. /// The response. - Response Process(Request request); + IResponse Process(IRequest request); } } diff --git a/src/WebExpress.WebCore/WebResource/Resource.cs b/src/WebExpress.WebCore/WebResource/Resource.cs index 9977312..6c56fbf 100644 --- a/src/WebExpress.WebCore/WebResource/Resource.cs +++ b/src/WebExpress.WebCore/WebResource/Resource.cs @@ -26,7 +26,7 @@ public Resource(IResourceContext resourceContext) /// /// The request. /// The response. - public abstract Response Process(Request request); + public abstract IResponse Process(IRequest request); /// /// Performs application-specific tasks related to sharing, returning, or resetting unmanaged resources. diff --git a/src/WebExpress.WebCore/WebResource/ResourceAsset.cs b/src/WebExpress.WebCore/WebResource/ResourceAsset.cs index 0bc86b9..ee4c4fe 100644 --- a/src/WebExpress.WebCore/WebResource/ResourceAsset.cs +++ b/src/WebExpress.WebCore/WebResource/ResourceAsset.cs @@ -38,7 +38,7 @@ public ResourceAsset(IResourceContext resourceContext) /// /// The request. /// The response. - public override Response Process(Request request) + public override IResponse Process(IRequest request) { lock (Gard) { @@ -51,7 +51,7 @@ public override Response Process(Request request) Data = GetData(file, assembly, resources.ToList()); - if (Data == null) + if (Data is null) { return new ResponseNotFound(); } @@ -137,7 +137,7 @@ public override Response Process(Request request) private static byte[] GetData(string file, Assembly assembly, IEnumerable resources) { var item = resources.Where(x => x.Equals(file, System.StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); - if (item == null) + if (item is null) { return null; } diff --git a/src/WebExpress.WebCore/WebResource/ResourceBinary.cs b/src/WebExpress.WebCore/WebResource/ResourceBinary.cs index 396966a..6bc5386 100644 --- a/src/WebExpress.WebCore/WebResource/ResourceBinary.cs +++ b/src/WebExpress.WebCore/WebResource/ResourceBinary.cs @@ -26,10 +26,10 @@ public ResourceBinary(IResourceContext resourceContext) /// /// The request. /// The response. - public override Response Process(Request request) + public override IResponse Process(IRequest request) { var response = new ResponseOK(); - response.Header.ContentLength = Data != null ? Data.Length : 0; + response.Header.ContentLength = Data is not null ? Data.Length : 0; response.Header.ContentType = "binary/octet-stream"; response.Content = Data; diff --git a/src/WebExpress.WebCore/WebResource/ResourceFile.cs b/src/WebExpress.WebCore/WebResource/ResourceFile.cs index c43d0cc..b405aa4 100644 --- a/src/WebExpress.WebCore/WebResource/ResourceFile.cs +++ b/src/WebExpress.WebCore/WebResource/ResourceFile.cs @@ -33,7 +33,7 @@ public ResourceFile(IResourceContext resourceContext) /// /// The request. /// The response. - public override Response Process(Request request) + public override IResponse Process(IRequest request) { lock (Gard) { diff --git a/src/WebExpress.WebCore/WebResource/ResourceManager.cs b/src/WebExpress.WebCore/WebResource/ResourceManager.cs index c6a899b..931b734 100644 --- a/src/WebExpress.WebCore/WebResource/ResourceManager.cs +++ b/src/WebExpress.WebCore/WebResource/ResourceManager.cs @@ -2,12 +2,14 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; using WebExpress.WebCore.Internationalization; using WebExpress.WebCore.WebApplication; using WebExpress.WebCore.WebAttribute; using WebExpress.WebCore.WebComponent; using WebExpress.WebCore.WebCondition; using WebExpress.WebCore.WebEndpoint; +using WebExpress.WebCore.WebMessage; using WebExpress.WebCore.WebPlugin; using WebExpress.WebCore.WebResource.Model; using WebExpress.WebCore.WebStatusPage; @@ -15,12 +17,18 @@ namespace WebExpress.WebCore.WebResource { /// - /// The resource manager manages WebExpress elements, which can be called with a URI (Uniform Resource Identifier). + /// The resource manager manages WebExpress elements, which can be called with a + /// URI (Uniform Resource Identifier). /// public sealed class ResourceManager : IResourceManager, ISystemComponent { + // synchronization root for protecting _dictionary and related mutable state + private readonly Lock _guard = new(); + private readonly IComponentHub _componentHub; private readonly IHttpServerContext _httpServerContext; + + // instantiate the dictionary; assume ResourceDictionary is a non-thread-safe collection private readonly ResourceDictionary _dictionary = []; /// @@ -36,10 +44,21 @@ public sealed class ResourceManager : IResourceManager, ISystemComponent /// /// Returns all resource contexts. /// - public IEnumerable Resources => _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .Select(x => x.ResourceContext); + public IEnumerable Resources + { + get + { + // return a snapshot to avoid enumeration during concurrent modifications + lock (_guard) + { + return _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Select(x => x.ResourceContext) + .ToList(); + } + } + } /// /// Initializes a new instance of the class. @@ -50,6 +69,7 @@ public sealed class ResourceManager : IResourceManager, ISystemComponent private ResourceManager(IComponentHub componentHub, IHttpServerContext httpServerContext) { _componentHub = componentHub; + _httpServerContext = httpServerContext; _componentHub.PluginManager.AddPlugin += OnAddPlugin; _componentHub.PluginManager.RemovePlugin += OnRemovePlugin; @@ -58,26 +78,47 @@ private ResourceManager(IComponentHub componentHub, IHttpServerContext httpServe var endpointtRegistration = new EndpointRegistration() { - EndpointResolver = (type, applicationContext) => applicationContext != null ? GetResorces(type, applicationContext) : GetResorces(type), - EndpointsResolver = () => Resources, + EndpointResolver = (type, applicationContext) => + { + // return appropriate endpoints based on applicationContext + return applicationContext is not null ? GetResorces(type, applicationContext) : GetResorces(type); + }, + EndpointsResolver = () => + { + // return snapshot of available resources + return Resources; + }, HandleRequest = (request, endpointContext) => { + // create or obtain resource instance and process request var resourceContext = endpointContext as IResourceContext; var resource = CreateResourceInstance(resourceContext); + if (resource is null) + { + // resource not found, return status page or bad request + return new ResponseBadRequest() + { + Content = I18N.Translate("webexpress.webcore:resourcemanager.resourcenotfound") + }; + } + return resource.Process(request); } }; - AddResource += (sender, e) => endpointtRegistration.AddEndpoint?.Invoke(sender, e); - RemoveResource += (sender, e) => endpointtRegistration.RemoveEndpoint?.Invoke(sender, e); + AddResource += (sender, e) => + { + endpointtRegistration.AddEndpoint?.Invoke(sender, e); + }; + RemoveResource += (sender, e) => + { + endpointtRegistration.RemoveEndpoint?.Invoke(sender, e); + }; _componentHub.EndpointManager.Register(endpointtRegistration); - _httpServerContext = httpServerContext; - - _httpServerContext.Log.Debug - ( + _httpServerContext.Log.Debug( I18N.Translate("webexpress.webcore:resourcemanager.initialization") ); } @@ -88,9 +129,12 @@ private ResourceManager(IComponentHub componentHub, IHttpServerContext httpServe /// The context of the plugin whose resources are to be associated. private void Register(IPluginContext pluginContext) { - if (_dictionary.ContainsKey(pluginContext)) + lock (_guard) { - return; + if (_dictionary.ContainsKey(pluginContext)) + { + return; + } } Register(pluginContext, _componentHub.ApplicationManager.GetApplications(pluginContext)); @@ -104,12 +148,22 @@ private void Register(IApplicationContext applicationContext) { foreach (var pluginContext in _componentHub.PluginManager.GetPlugins(applicationContext)) { - if (_dictionary.TryGetValue(pluginContext, out var appDict) && appDict.ContainsKey(applicationContext)) + bool shouldContinue = false; + + lock (_guard) + { + if (_dictionary.TryGetValue(pluginContext, out var appDict) && appDict.ContainsKey(applicationContext)) + { + shouldContinue = true; + } + } + + if (shouldContinue) { continue; } - Register(pluginContext, [applicationContext]); + Register(pluginContext, new[] { applicationContext }); } } @@ -120,12 +174,13 @@ private void Register(IApplicationContext applicationContext) /// The application context (optional). private void Register(IPluginContext pluginContext, IEnumerable applicationContexts) { + // assembly and reflection operations are per-plugin and read-only; mutations to _dictionary are synchronized var assembly = pluginContext?.Assembly; foreach (var resourceType in assembly.GetTypes() .Where(x => x.IsClass && x.IsSealed && x.IsPublic) - .Where(x => x.GetInterface(typeof(IResource).Name) != null) - .Where(x => x.GetInterface(typeof(IStatusPage).Name) == null)) + .Where(x => x.GetInterface(typeof(IResource).Name) is not null) + .Where(x => x.GetInterface(typeof(IStatusPage).Name) is null)) { var id = resourceType.FullName?.ToLower(); var segment = default(ISegmentAttribute); @@ -135,39 +190,61 @@ private void Register(IPluginContext pluginContext, IEnumerable !x.AttributeType.GetInterfaces().Contains(typeof(IEndpointAttribute)) && - !x.AttributeType.GetInterfaces().Contains(typeof(IPageAttribute))); + !x.AttributeType.GetInterfaces().Contains(typeof(IPageAttribute))); - foreach (var customAttribute in resourceType.CustomAttributes - .Where(x => x.AttributeType.GetInterfaces().Contains(typeof(IEndpointAttribute)))) + foreach + ( + var attribute in resourceType + .GetCustomAttributes(inherit: true) + .Where(x => x.GetType().GetInterfaces().Contains(typeof(IEndpointAttribute))) + ) { - if (customAttribute.AttributeType.GetInterfaces().Contains(typeof(ISegmentAttribute))) + var attributeType = attribute.GetType(); + + // segment attribute + if (attributeType.GetInterfaces().Contains(typeof(ISegmentAttribute))) { - segment = resourceType.GetCustomAttributes(customAttribute.AttributeType, false).FirstOrDefault() as ISegmentAttribute; + segment = attribute as ISegmentAttribute; + continue; } - else if (customAttribute.AttributeType == typeof(IncludeSubPathsAttribute)) + + // include subpaths + if (attributeType == typeof(IncludeSubPathsAttribute)) { - includeSubPaths = Convert.ToBoolean(customAttribute.ConstructorArguments.FirstOrDefault().Value); + includeSubPaths = (attribute as IncludeSubPathsAttribute)?.IncludeSubPaths ?? false; + continue; } - else if (customAttribute.AttributeType.Name == typeof(ConditionAttribute<>).Name && customAttribute.AttributeType.Namespace == typeof(ConditionAttribute<>).Namespace) + + // condition attribute (generic) + if (attributeType.IsGenericType + && attributeType.GetGenericTypeDefinition().Name == typeof(ConditionAttribute<>).Name + && attributeType.Namespace == typeof(ConditionAttribute<>).Namespace) { - var condition = customAttribute.AttributeType.GenericTypeArguments.FirstOrDefault(); - conditions.Add(Activator.CreateInstance(condition) as ICondition); + var conditionType = attributeType.GetGenericArguments().FirstOrDefault(); + if (conditionType != null) + { + conditions.Add(Activator.CreateInstance(conditionType) as ICondition); + } + continue; } - else if (customAttribute.AttributeType == typeof(CacheAttribute)) + + // cache attribute + if (attributeType == typeof(CacheAttribute)) { cache = true; + continue; } } // assign the resource to existing applications foreach (var applicationContext in applicationContexts) { - var prefix = applicationContext.Route.Concat - ( + var prefix = applicationContext.Route.Concat( applicationContext.PluginContext != pluginContext ? pluginContext.PluginName.ToLower() : "" ); + var routePath = EndpointManager.CreateEndpointRoute(resourceType, prefix, segment); var resourceContext = new ResourceContext() { @@ -194,9 +271,17 @@ private void Register(IPluginContext pluginContext, IEnumerable x.AttributeType) }; - if (_dictionary.AddResourceItem(pluginContext, applicationContext, resourceItem)) + bool added = false; + + lock (_guard) + { + added = _dictionary.AddResourceItem(pluginContext, applicationContext, resourceItem); + } + + if (added) { OnAddResource(resourceItem.ResourceContext); + _httpServerContext?.Log.Debug( I18N.Translate( "webexpress.webcore:resourcemanager.addresource", @@ -215,22 +300,23 @@ private void Register(IPluginContext pluginContext, IEnumerableThe context of the plugin that contains the resources to remove. internal void Remove(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return; } - // the plugin has not been registered in the manager - if (_dictionary.TryGetValue(pluginContext, out var value)) + lock (_guard) { - foreach (var resourceItem in value.Values - .SelectMany(x => x.Values)) + if (_dictionary.TryGetValue(pluginContext, out var value)) { - OnRemoveResource(resourceItem.ResourceContext); - resourceItem.Dispose(); - } + foreach (var resourceItem in value.Values.SelectMany(x => x.Values)) + { + OnRemoveResource(resourceItem.ResourceContext); + resourceItem.Dispose(); + } - _dictionary.Remove(pluginContext); + _dictionary.Remove(pluginContext); + } } } @@ -240,23 +326,27 @@ internal void Remove(IPluginContext pluginContext) /// The context of the application that contains the resources to remove. internal void Remove(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { return; } - foreach (var pluginDict in _dictionary.Values) + lock (_guard) { - foreach (var appDict in pluginDict.Where(x => x.Key == applicationContext).Select(x => x.Value)) + foreach (var pluginDict in _dictionary.Values) { - foreach (var resourceItem in appDict.Values) + foreach (var appDict in pluginDict.Where(x => x.Key == applicationContext).Select(x => x.Value)) { - OnRemoveResource(resourceItem.ResourceContext); - resourceItem.Dispose(); + foreach (var resourceItem in appDict.Values) + { + OnRemoveResource(resourceItem.ResourceContext); + resourceItem.Dispose(); + } } - } - pluginDict.Remove(applicationContext); + // remove the application mapping from the plugin dictionary + pluginDict.Remove(applicationContext); + } } } @@ -267,73 +357,88 @@ internal void Remove(IApplicationContext applicationContext) /// An enumeration of resource contexts. public IEnumerable GetResorces(IPluginContext pluginContext) { - if (_dictionary.TryGetValue(pluginContext, out var pluginResources)) + lock (_guard) { - return pluginResources - .SelectMany(x => x.Value) - .Select(x => x.Value.ResourceContext); - } + if (_dictionary.TryGetValue(pluginContext, out var pluginResources)) + { + return pluginResources + .SelectMany(x => x.Value) + .Select(x => x.Value.ResourceContext) + .ToList(); + } - return []; + return Enumerable.Empty(); + } } /// - /// Returns an enumeration of resource contextes. + /// Returns an enumeration of resource contexts. /// /// The resource type. - /// An enumeration of resource contextes. + /// An enumeration of resource contexts. public IEnumerable GetResorces() where T : IResource { return GetResorces(typeof(T)); } /// - /// Returns an enumeration of resource contextes. + /// Returns an enumeration of resource contexts. /// /// The resource type. - /// An enumeration of resource contextes. + /// An enumeration of resource contexts. public IEnumerable GetResorces(Type resourceType) { - return _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .Where(x => x.ResourceClass.Equals(resourceType)) - .Select(x => x.ResourceContext); + lock (_guard) + { + return _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.ResourceClass.Equals(resourceType)) + .Select(x => x.ResourceContext) + .ToList(); + } } /// - /// Returns an enumeration of resource contextes. + /// Returns an enumeration of resource contexts. /// /// The resource type. /// The context of the application. - /// An enumeration of resource contextes. + /// An enumeration of resource contexts. public IEnumerable GetResorces(Type resourceType, IApplicationContext applicationContext) { - return _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .Where(x => x.ResourceClass.Equals(resourceType)) - .Where(x => x.ResourceContext.ApplicationContext.Equals(applicationContext)) - .Select(x => x.ResourceContext); + lock (_guard) + { + return _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.ResourceClass.Equals(resourceType)) + .Where(x => x.ResourceContext.ApplicationContext.Equals(applicationContext)) + .Select(x => x.ResourceContext) + .ToList(); + } } /// - /// Returns an enumeration of resource contextes. + /// Returns an enumeration of resource contexts. /// /// The resource type. /// The context of the application. - /// An enumeration of resource contextes. + /// An enumeration of resource contexts. public IEnumerable GetResorces(IApplicationContext applicationContext) where T : IResource { - return _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .Where(x => x.ResourceClass.Equals(typeof(T))) - .Where(x => x.ResourceContext.ApplicationContext.Equals(applicationContext)) - .Select(x => x.ResourceContext); + lock (_guard) + { + return _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.ResourceClass.Equals(typeof(T))) + .Where(x => x.ResourceContext.ApplicationContext.Equals(applicationContext)) + .Select(x => x.ResourceContext) + .ToList(); + } } - /// /// Returns the resource context. /// @@ -342,13 +447,16 @@ public IEnumerable GetResorces(IApplicationContext applicat /// An resource context or null. public IResourceContext GetResorce(IApplicationContext applicationContext, string resourceId) { - return _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .Where(x => x.ResourceContext.ApplicationContext.Equals(applicationContext)) - .Where(x => x.ResourceContext.EndpointId.Equals(resourceId)) - .Select(x => x.ResourceContext) - .FirstOrDefault(); + lock (_guard) + { + return _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.ResourceContext.ApplicationContext.Equals(applicationContext)) + .Where(x => x.ResourceContext.EndpointId.Equals(resourceId)) + .Select(x => x.ResourceContext) + .FirstOrDefault(); + } } /// @@ -359,47 +467,78 @@ public IResourceContext GetResorce(IApplicationContext applicationContext, strin /// An resource context or null. public IResourceContext GetResorce(string applicationId, string resourceId) { - return _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .Where(x => x.ResourceContext.ApplicationContext.ApplicationId.Equals(applicationId)) - .Where(x => x.ResourceContext.EndpointId.Equals(resourceId)) - .Select(x => x.ResourceContext) - .FirstOrDefault(); + lock (_guard) + { + return _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.ResourceContext.ApplicationContext.ApplicationId.Equals(applicationId)) + .Where(x => x.ResourceContext.EndpointId.Equals(resourceId)) + .Select(x => x.ResourceContext) + .FirstOrDefault(); + } } /// /// Creates a new resource and returns it. If a resource already exists (through caching), the existing instance is returned. + /// Thread-safe: cached instance creation and assignment is protected. /// /// The context used for resource creation. /// The created or cached resource. private IResource CreateResourceInstance(IResourceContext resourceContext) { - var resourceItem = _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .FirstOrDefault(x => x.ResourceContext.Equals(resourceContext)); + if (resourceContext is null) + { + return null; + } - if (resourceItem != null && resourceItem.Instance == null) + ResourceItem resourceItem = null; + + // locate resourceItem and handle caching under lock + lock (_guard) { - var instance = ComponentActivator.CreateInstance - ( - resourceItem.ResourceClass, - resourceContext, - _httpServerContext, - _componentHub, - resourceContext.ApplicationContext - ); + resourceItem = _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .FirstOrDefault(x => x.ResourceContext.Equals(resourceContext)); - if (resourceItem.Cache) + if (resourceItem is null) { - resourceItem.Instance = instance; + return null; } - return instance; + // if instance already cached, return immediately + if (resourceItem.Instance is not null) + { + return resourceItem.Instance as IResource; + } + + // if caching is enabled, create and assign instance while holding lock to avoid double-creation + if (resourceItem.Cache) + { + var instanceCached = ComponentActivator.CreateInstance( + resourceItem.ResourceClass, + resourceContext, + _httpServerContext, + _componentHub, + resourceContext.ApplicationContext + ); + + resourceItem.Instance = instanceCached; + return instanceCached; + } } - return resourceItem?.Instance as IResource; + // if not caching, create instance outside lock (no shared state to modify) + var instanceNoCache = ComponentActivator.CreateInstance( + resourceItem.ResourceClass, + resourceContext, + _httpServerContext, + _componentHub, + resourceContext.ApplicationContext + ); + + return instanceNoCache; } /// @@ -439,6 +578,7 @@ private void OnRemovePlugin(object sender, IPluginContext e) { Remove(e); } + /// /// Raises the event when an application is removed. /// @@ -468,6 +608,8 @@ public void Dispose() _componentHub.PluginManager.RemovePlugin -= OnRemovePlugin; _componentHub.ApplicationManager.AddApplication -= OnAddApplication; _componentHub.ApplicationManager.RemoveApplication -= OnRemoveApplication; + + GC.SuppressFinalize(this); } } -} +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebRestAPI/CrudMethod.cs b/src/WebExpress.WebCore/WebRestAPI/CrudMethod.cs deleted file mode 100644 index a41d357..0000000 --- a/src/WebExpress.WebCore/WebRestAPI/CrudMethod.cs +++ /dev/null @@ -1,35 +0,0 @@ -using WebExpress.WebCore.WebMessage; - -namespace WebExpress.WebCore.WebRestApi -{ - /// - /// Represents the crud methods for a rest api. - /// - public enum CrudMethod - { - /// - /// Represents the HTTP POST method. - /// - POST = RequestMethod.POST, - - /// - /// Represents the HTTP GET method. - /// - GET = RequestMethod.GET, - - /// - /// Represents the HTTP PATCH method. - /// - PATCH = RequestMethod.PATCH, - - /// - /// Represents the HTTP PATCH method. - /// - PUT = RequestMethod.PUT, - - /// - /// Represents the HTTP DELETE method. - /// - DELETE = RequestMethod.DELETE - } -} diff --git a/src/WebExpress.WebCore/WebRestAPI/IRestApi.cs b/src/WebExpress.WebCore/WebRestAPI/IRestApi.cs deleted file mode 100644 index bc3368b..0000000 --- a/src/WebExpress.WebCore/WebRestAPI/IRestApi.cs +++ /dev/null @@ -1,39 +0,0 @@ -using WebExpress.WebCore.WebEndpoint; -using WebExpress.WebCore.WebMessage; - -namespace WebExpress.WebCore.WebRestApi -{ - /// - /// Defines the contract for a rest api resource. - /// - public interface IRestApi : IEndpoint - { - /// - /// Creates data. - /// - /// The request. - /// The response containing the result of the operation. - Response CreateData(Request request); - - /// - /// Gets data. - /// - /// The request. - /// The response containing the result of the operation. - Response GetData(Request request); - - /// - /// Updates data. - /// - /// The request. - /// The response containing the result of the operation. - Response UpdateData(Request request); - - /// - /// Deletes data. - /// - /// The request. - /// The response containing the result of the operation. - Response DeleteData(Request request); - } -} diff --git a/src/WebExpress.WebCore/WebRestAPI/RestApi.cs b/src/WebExpress.WebCore/WebRestAPI/RestApi.cs deleted file mode 100644 index 647894f..0000000 --- a/src/WebExpress.WebCore/WebRestAPI/RestApi.cs +++ /dev/null @@ -1,72 +0,0 @@ -using WebExpress.WebCore.WebMessage; -using WebExpress.WebCore.WebStatusPage; - -namespace WebExpress.WebCore.WebRestApi -{ - /// - /// The prototype of a rest api. - /// - public abstract class RestApi : IRestApi - { - /// - /// Returns the rest api context. - /// - public IRestApiContext RestApiContext { get; private set; } - - /// - /// Initializes a new instance of the class. - /// - /// The context of the rest api resource. - public RestApi(IRestApiContext restApiContext) - { - RestApiContext = restApiContext; - } - - /// - /// Creates data. - /// - /// The request. - /// The response containing the result of the operation. - public virtual Response CreateData(Request request) - { - return new ResponseBadRequest(new StatusMessage("Not implemented.")); - } - - /// - /// Gets data. - /// - /// The response containing the result of the operation. - public virtual Response GetData(Request request) - { - return new ResponseBadRequest(new StatusMessage("Not implemented.")); - } - - /// - /// Updates data. - /// - /// The request. - /// The response containing the result of the operation. - public virtual Response UpdateData(Request request) - { - return new ResponseBadRequest(new StatusMessage("Not implemented.")); - } - - /// - /// Deletes data. - /// - /// The request. - /// The response containing the result of the operation. - public virtual Response DeleteData(Request request) - { - return new ResponseBadRequest(new StatusMessage("Not implemented.")); - } - - /// - /// Performs application-specific tasks related to sharing, returning, or resetting unmanaged resources. - /// - public virtual void Dispose() - { - - } - } -} diff --git a/src/WebExpress.WebCore/WebRestAPI/RestApiManager.cs b/src/WebExpress.WebCore/WebRestAPI/RestApiManager.cs deleted file mode 100644 index ed0e7f6..0000000 --- a/src/WebExpress.WebCore/WebRestAPI/RestApiManager.cs +++ /dev/null @@ -1,531 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text.RegularExpressions; -using WebExpress.WebCore.Internationalization; -using WebExpress.WebCore.WebApplication; -using WebExpress.WebCore.WebAttribute; -using WebExpress.WebCore.WebComponent; -using WebExpress.WebCore.WebCondition; -using WebExpress.WebCore.WebEndpoint; -using WebExpress.WebCore.WebMessage; -using WebExpress.WebCore.WebPlugin; -using WebExpress.WebCore.WebRestApi.Model; -using WebExpress.WebCore.WebUri; - -namespace WebExpress.WebCore.WebRestApi -{ - /// - /// The rest api manager manages rest api resources, which can be called with a URI (Uniform page Identifier). - /// - public partial class RestApiManager : IRestApiManager - { - private readonly IComponentHub _componentHub; - private readonly IHttpServerContext _httpServerContext; - private readonly RestApiDictionary _dictionary = []; - - [GeneratedRegex(@"\.(?:_|V|v)(\d+)\.")] - private static partial Regex ApiVersionRegex(); - - /// - /// An event that fires when an rest api resource is added. - /// - public event EventHandler AddRestApi; - - /// - /// An event that fires when an rest api resource is removed. - /// - public event EventHandler RemoveRestApi; - - /// - /// Returns all rest api resource contexts. - /// - public IEnumerable RestApis => _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .Select(x => x.RestApiContext); - - /// - /// Initializes a new instance of the class. - /// - /// The component hub. - /// The reference to the context of the host. - [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Used via Reflection.")] - private RestApiManager(IComponentHub componentHub, IHttpServerContext httpServerContext) - { - _componentHub = componentHub; - - _componentHub.PluginManager.AddPlugin += OnAddPlugin; - _componentHub.PluginManager.RemovePlugin += OnRemovePlugin; - _componentHub.ApplicationManager.AddApplication += OnAddApplication; - _componentHub.ApplicationManager.RemoveApplication += OnRemoveApplication; - - var endpointtRegistration = new EndpointRegistration() - { - EndpointResolver = (type, applicationContext) => applicationContext != null ? GetRestApi(type, applicationContext) : GetRestApi(type), - EndpointsResolver = () => RestApis, - HandleRequest = (request, endpointContext) => - { - var restApiContext = endpointContext as IRestApiContext; - var restApi = CreateApiInstance(restApiContext) as IRestApi; - - if (restApiContext.Methods.Any(x => x.Equals((CrudMethod)request.Method))) - { - switch (request.Method) - { - case RequestMethod.POST: - return restApi.CreateData(request) ?? new ResponseOK(); - case RequestMethod.GET: - return restApi.GetData(request) ?? new ResponseOK(); - case RequestMethod.PATCH: - return restApi.UpdateData(request) ?? new ResponseOK(); - case RequestMethod.PUT: - return restApi.UpdateData(request) ?? new ResponseOK(); - case RequestMethod.DELETE: - return restApi.DeleteData(request) ?? new ResponseOK(); - } - } - - return new ResponseBadRequest() - { - Content = I18N.Translate("webexpress.webcore:restapimanager.methodnotsupported", request.Method.ToString()) - }; - } - }; - - AddRestApi += (sender, e) => endpointtRegistration.AddEndpoint?.Invoke(sender, e); - RemoveRestApi += (sender, e) => endpointtRegistration.RemoveEndpoint?.Invoke(sender, e); - - _componentHub.EndpointManager.Register(endpointtRegistration); - - _httpServerContext = httpServerContext; - - _httpServerContext.Log.Debug - ( - I18N.Translate("webexpress.webcore:restapimanager.initialization") - ); - } - - /// - /// Returns an enumeration of all containing page contexts of a plugin. - /// - /// A context of a plugin whose pages are to be registered. - /// An enumeration of rest api resource contexts. - public IEnumerable GetRestApi(IPluginContext pluginContext) - { - if (_dictionary.TryGetValue(pluginContext, out var pluginResources)) - { - return pluginResources - .SelectMany(x => x.Value) - .Select(x => x.Value.RestApiContext); - } - - return []; - } - - /// - /// Returns an enumeration of rest api resource contextes. - /// - /// The rest api resource type. - /// An enumeration of rest api resource contextes. - public IEnumerable GetRestApi() where T : IRestApi - { - return GetRestApi(typeof(T)); - } - - /// - /// Returns an enumeration of rest api resource contextes. - /// - /// The rest api resource type. - /// An enumeration of rest api resource contextes. - public IEnumerable GetRestApi(Type restApiType) - { - return _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .Where(x => x.RestApiClass.Equals(restApiType)) - .Select(x => x.RestApiContext); - } - - /// - /// Returns an enumeration of rest api resource contextes. - /// - /// The page type. - /// The context of the application. - /// An enumeration of page contextes. - public IEnumerable GetRestApi(Type restApiType, IApplicationContext applicationContext) - { - return _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .Where(x => x.RestApiClass.Equals(restApiType)) - .Where(x => x.RestApiContext.ApplicationContext.Equals(applicationContext)) - .Select(x => x.RestApiContext); - } - - /// - /// Returns an enumeration of rest api resource contextes. - /// - /// The rest api resource type. - /// The context of the application. - /// An enumeration of rest api resource contextes. - public IEnumerable GetRestApi(IApplicationContext applicationContext) where T : IRestApi - { - return _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .Where(x => x.RestApiClass.Equals(typeof(T))) - .Where(x => x.RestApiContext.ApplicationContext.Equals(applicationContext)) - .Select(x => x.RestApiContext); - } - - /// - /// Returns the rest api resource context. - /// - /// The context of the application. - /// The rest api resource id. - /// An rest api resource context or null. - public IRestApiContext GetRestApi(IApplicationContext applicationContext, string restApiId) - { - return _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .Where(x => x.RestApiContext.ApplicationContext.Equals(applicationContext)) - .Where(x => x.RestApiContext.EndpointId.Equals(restApiId)) - .Select(x => x.RestApiContext) - .FirstOrDefault(); - } - - /// - /// Returns the rest api resource context. - /// - /// The application id. - /// The rest api resource id. - /// An rest api resource context or null. - public IRestApiContext GetRestApi(string applicationId, string restApiId) - { - return _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .Where(x => x.RestApiContext.ApplicationContext.ApplicationId.Equals(applicationId)) - .Where(x => x.RestApiContext.EndpointId.Equals(restApiId)) - .Select(x => x.RestApiContext) - .FirstOrDefault(); - } - - /// - /// Creates a new rest api resource and returns it. If a rest api resource already exists (through caching), the existing instance is returned. - /// - /// The context used for rest api resource creation. - /// The created or cached rest api resource. - private IRestApi CreateApiInstance(IRestApiContext apiContext) - { - var resourceItem = _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .FirstOrDefault(x => x.RestApiContext.Equals(apiContext)); - - if (resourceItem != null && resourceItem.Instance == null) - { - var instance = ComponentActivator.CreateInstance - ( - resourceItem.RestApiClass, - apiContext, - _httpServerContext, - _componentHub, - apiContext.ApplicationContext - ); - - if (resourceItem.Cache) - { - resourceItem.Instance = instance; - } - - return instance; - } - - return resourceItem?.Instance as IRestApi; - } - - /// - /// Discovers and binds rest apis to an application. - /// - /// The context of the plugin whose rest apis are to be associated. - private void Register(IPluginContext pluginContext) - { - if (_dictionary.ContainsKey(pluginContext)) - { - return; - } - - Register(pluginContext, _componentHub.ApplicationManager.GetApplications(pluginContext)); - } - - /// - /// Discovers and binds rest apis to an application. - /// - /// The context of the application whose rest apis are to be associated. - private void Register(IApplicationContext applicationContext) - { - foreach (var pluginContext in _componentHub.PluginManager.GetPlugins(applicationContext)) - { - if (_dictionary.TryGetValue(pluginContext, out var appDict) && appDict.ContainsKey(applicationContext)) - { - continue; - } - - Register(pluginContext, [applicationContext]); - } - } - - /// - /// Registers rest apis for a given plugin and application context. - /// - /// The plugin context. - /// The application context (optional). - private void Register(IPluginContext pluginContext, IEnumerable applicationContexts) - { - var assembly = pluginContext?.Assembly; - - foreach (var restApiType in assembly.GetTypes() - .Where(x => x.IsClass == true && x.IsSealed && x.IsPublic) - .Where(x => x.GetInterface(typeof(IRestApi).Name) != null)) - { - var id = restApiType.FullName?.ToLower(); - var segment = default(ISegmentAttribute); - var title = restApiType.Name; - var contextPath = string.Empty; - var includeSubPaths = false; - var conditions = new List(); - var cache = false; - var methods = new List(); - var match = ApiVersionRegex().Match(id); - var versionSegment = match.Success ? match.Groups[0].Value.Replace(".", "") : ""; - var version = match.Success && uint.TryParse(match.Groups[1].Value, out var result) ? result : 1u; - var attributes = restApiType.CustomAttributes - .Where(x => !x.AttributeType.GetInterfaces().Contains(typeof(IEndpointAttribute)) && - !x.AttributeType.GetInterfaces().Contains(typeof(IPageAttribute))); - - foreach (var customAttribute in restApiType.CustomAttributes - .Where(x => x.AttributeType.GetInterfaces().Contains(typeof(IEndpointAttribute)))) - { - if (customAttribute.AttributeType.GetInterfaces().Contains(typeof(ISegmentAttribute))) - { - segment = restApiType.GetCustomAttributes(customAttribute.AttributeType, false).FirstOrDefault() as ISegmentAttribute; - } - else if (customAttribute.AttributeType == typeof(ContextPathAttribute)) - { - contextPath = customAttribute.ConstructorArguments.FirstOrDefault().Value?.ToString(); - } - else if (customAttribute.AttributeType == typeof(IncludeSubPathsAttribute)) - { - includeSubPaths = Convert.ToBoolean(customAttribute.ConstructorArguments.FirstOrDefault().Value); - } - else if (customAttribute.AttributeType.Name == typeof(ConditionAttribute<>).Name - && customAttribute.AttributeType.Namespace == typeof(ConditionAttribute<>).Namespace) - { - var condition = customAttribute.AttributeType.GenericTypeArguments.FirstOrDefault(); - conditions.Add(Activator.CreateInstance(condition) as ICondition); - } - else if (customAttribute.AttributeType.Name == typeof(MethodAttribute).Name - && customAttribute.AttributeType.Namespace == typeof(MethodAttribute).Namespace) - { - var method = (CrudMethod)customAttribute.ConstructorArguments.FirstOrDefault().Value; - methods.Add(method); - } - else if (customAttribute.AttributeType == typeof(CacheAttribute)) - { - cache = true; - } - } - - // assign the rest api to existing applications - foreach (var applicationContext in applicationContexts) - { - var prefix = applicationContext.Route.Concat - ( - applicationContext.PluginContext != pluginContext - ? pluginContext.PluginName.ToLower() - : "" - ); - - var routePath = EndpointManager.CreateEndpointRoute - ( - restApiType, - prefix, - segment, - [new UriPathSegmentConstant("api"), new UriPathSegmentVariableInt($"{version}") { VariableName = "_apiVersion" }], - ["api", "restapi", "rest"] - ).RemoveSegment(versionSegment); - - var restApiContext = new RestApiContext() - { - EndpointId = new ComponentId(id), - PluginContext = pluginContext, - ApplicationContext = applicationContext, - Route = routePath, - Cache = cache, - Conditions = conditions, - IncludeSubPaths = includeSubPaths, - Attributes = EndpointManager.GetAttributeInstances(attributes), - Version = version, - Methods = methods.Distinct() - }; - - var restApiItem = new RestApiItem(_componentHub.EndpointManager) - { - EndpointId = new ComponentId(restApiType.FullName), - PluginContext = pluginContext, - ApplicationContext = applicationContext, - RestApiContext = restApiContext, - RestApiClass = restApiType, - Methods = methods.Distinct(), - Version = version, - Cache = cache, - Conditions = conditions, - IncludeSubPaths = includeSubPaths, - Attributes = attributes.Select(x => x.AttributeType) - }; - - if (_dictionary.AddRestApiItem(pluginContext, applicationContext, restApiItem)) - { - OnAddRestApi(restApiItem.RestApiContext); - - _httpServerContext?.Log.Debug - ( - I18N.Translate - ( - "webexpress.webcore:restapimanager.addrestapi", - id, - applicationContext.ApplicationId - ) - ); - } - } - } - } - - /// - /// Removes all pages associated with the specified plugin context. - /// - /// The context of the plugin that contains the rest api resources to remove. - public void Remove(IPluginContext pluginContext) - { - if (pluginContext == null) - { - return; - } - - // the plugin has not been registered in the manager - if (_dictionary.TryGetValue(pluginContext, out var value)) - { - foreach (var resourceItem in value.Values - .SelectMany(x => x.Values)) - { - OnRemoveRestApi(resourceItem.RestApiContext); - resourceItem.Dispose(); - } - - _dictionary.Remove(pluginContext); - } - } - - /// - /// Removes all events associated with the specified application context. - /// - /// The context of the application that contains the events to remove. - internal void Remove(IApplicationContext applicationContext) - { - if (applicationContext == null) - { - return; - } - - foreach (var pluginDict in _dictionary.Values) - { - foreach (var appDict in pluginDict.Where(x => x.Key == applicationContext).Select(x => x.Value)) - { - foreach (var resourceItem in appDict.Values) - { - OnRemoveRestApi(resourceItem.RestApiContext); - resourceItem.Dispose(); - } - } - - pluginDict.Remove(applicationContext); - } - } - - /// - /// Raises the AddRestApi event. - /// - /// The rest api resource context. - private void OnAddRestApi(IRestApiContext resourceContext) - { - AddRestApi?.Invoke(this, resourceContext); - } - - /// - /// Raises the RemoveRestApi event. - /// - /// The rest api resource context. - private void OnRemoveRestApi(IRestApiContext pageContext) - { - RemoveRestApi?.Invoke(this, pageContext); - } - - /// - /// Raises the event when an plugin is added. - /// - /// The source of the event. - /// The context of the plugin being added. - private void OnAddPlugin(object sender, IPluginContext e) - { - Register(e); - } - - /// - /// Raises the event when a plugin is removed. - /// - /// The source of the event. - /// The context of the plugin being removed. - private void OnRemovePlugin(object sender, IPluginContext e) - { - Remove(e); - } - - /// - /// Raises the event when an application is removed. - /// - /// The source of the event. - /// The context of the application being removed. - private void OnRemoveApplication(object sender, IApplicationContext e) - { - Remove(e); - } - - /// - /// Raises the event when an application is added. - /// - /// The source of the event. - /// The context of the application being added. - private void OnAddApplication(object sender, IApplicationContext e) - { - Register(e); - } - - /// - /// Release of unmanaged resources reserved during use. - /// - public void Dispose() - { - _componentHub.PluginManager.AddPlugin -= OnAddPlugin; - _componentHub.PluginManager.RemovePlugin -= OnRemovePlugin; - _componentHub.ApplicationManager.AddApplication -= OnAddApplication; - _componentHub.ApplicationManager.RemoveApplication -= OnRemoveApplication; - - GC.SuppressFinalize(this); - } - } -} diff --git a/src/WebExpress.WebCore/WebRestApi/IRestApi.cs b/src/WebExpress.WebCore/WebRestApi/IRestApi.cs new file mode 100644 index 0000000..d27f081 --- /dev/null +++ b/src/WebExpress.WebCore/WebRestApi/IRestApi.cs @@ -0,0 +1,11 @@ +using WebExpress.WebCore.WebEndpoint; + +namespace WebExpress.WebCore.WebRestApi +{ + /// + /// Defines the contract for a rest api resource. + /// + public interface IRestApi : IEndpoint + { + } +} diff --git a/src/WebExpress.WebCore/WebRestAPI/IRestApiContext.cs b/src/WebExpress.WebCore/WebRestApi/IRestApiContext.cs similarity index 86% rename from src/WebExpress.WebCore/WebRestAPI/IRestApiContext.cs rename to src/WebExpress.WebCore/WebRestApi/IRestApiContext.cs index 5960b70..b37df27 100644 --- a/src/WebExpress.WebCore/WebRestAPI/IRestApiContext.cs +++ b/src/WebExpress.WebCore/WebRestApi/IRestApiContext.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using WebExpress.WebCore.WebEndpoint; +using WebExpress.WebCore.WebMessage; namespace WebExpress.WebCore.WebRestApi { @@ -11,7 +12,7 @@ public interface IRestApiContext : IEndpointContext /// /// Returns the crud methods. /// - IEnumerable Methods { get; } + IEnumerable Methods { get; } /// /// Returns the version number of the rest api. diff --git a/src/WebExpress.WebCore/WebRestAPI/IRestApiManager.cs b/src/WebExpress.WebCore/WebRestApi/IRestApiManager.cs similarity index 100% rename from src/WebExpress.WebCore/WebRestAPI/IRestApiManager.cs rename to src/WebExpress.WebCore/WebRestApi/IRestApiManager.cs diff --git a/src/WebExpress.WebCore/WebRestApi/IRestApiResult.cs b/src/WebExpress.WebCore/WebRestApi/IRestApiResult.cs index 2b80c6b..645e007 100644 --- a/src/WebExpress.WebCore/WebRestApi/IRestApiResult.cs +++ b/src/WebExpress.WebCore/WebRestApi/IRestApiResult.cs @@ -8,9 +8,9 @@ namespace WebExpress.WebCore.WebRestApi public interface IRestApiResult { /// - /// Converts the current instance into a object. + /// Converts the current instance into a object. /// /// A Response object representing the result of the conversion. - Response ToResponse(); + IResponse ToResponse(); } } \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebRestApi/IRestApiValidationResult.cs b/src/WebExpress.WebCore/WebRestApi/IRestApiValidationResult.cs new file mode 100644 index 0000000..aa1cfe3 --- /dev/null +++ b/src/WebExpress.WebCore/WebRestApi/IRestApiValidationResult.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; + +namespace WebExpress.WebCore.WebRestApi +{ + /// + /// Represents the result of a REST API validation, including any validation errors encountered. + /// + /// + /// This class provides a way to collect and inspect validation errors that occur + /// during the processing of a REST API request. It includes methods to add individual + /// or multiple errors, and properties to check the overall validity of the result. + /// + public interface IRestApiValidationResult + { + /// + /// Returns a read-only collection of errors encountered during the API operation. + /// + IEnumerable Errors { get; } + + /// + /// Returns a value indicating whether the current state is valid. + /// + /// The state is considered valid if there are no errors present. + bool IsValid { get; } + + /// + /// Adds a new error to the collection with the specified message, field, and code. + /// + /// + /// Use this method to record errors encountered during an operation, + /// optionally associating them with a specific field or error code. + /// + /// + /// The error message describing the issue. This parameter is required and + /// cannot be null or empty. + /// + /// + /// The name of the field associated with the error, if applicable. This + /// parameter is optional and can be null. + /// + /// + /// A code representing the type or category of the error, if applicable. + /// This parameter is optional and can be null. + /// + /// The current instance for method chaining. + IRestApiValidationResult Add(string message, string field = null, string code = null); + + /// + /// Adds one or more instances to the collection. + /// + /// + /// This method appends the specified errors to the existing collection. If + /// the array is empty, no changes are made. + /// + /// An array of error objects to add. + /// The current instance for method chaining. + IRestApiValidationResult Add(params RestApiError[] errors); + + /// + /// Adds one or more instances to the collection. + /// + /// + /// This method appends the specified errors to the existing collection. If + /// the array is empty, no changes are made. + /// + /// An array of error objects to add. + /// The current instance for method chaining. + IRestApiValidationResult AddRange(IEnumerable errors); + + /// + /// Converts the collection of errors to a JSON-formatted string. + /// + /// + /// Each error in the collection is serialized as an object containing the + /// properties Code, Message, and Field. The resulting + /// JSON string represents an array of these objects. + /// + /// + /// A JSON-formatted string representing the collection of errors. If + /// the collection is empty, the method returns an empty JSON array + /// ([]). + /// + string ToJson(); + } +} diff --git a/src/WebExpress.WebCore/WebRestAPI/Model/RestApiDictionary.cs b/src/WebExpress.WebCore/WebRestApi/Model/RestApiDictionary.cs similarity index 100% rename from src/WebExpress.WebCore/WebRestAPI/Model/RestApiDictionary.cs rename to src/WebExpress.WebCore/WebRestApi/Model/RestApiDictionary.cs diff --git a/src/WebExpress.WebCore/WebRestAPI/Model/RestApiItem.cs b/src/WebExpress.WebCore/WebRestApi/Model/RestApiItem.cs similarity index 77% rename from src/WebExpress.WebCore/WebRestAPI/Model/RestApiItem.cs rename to src/WebExpress.WebCore/WebRestApi/Model/RestApiItem.cs index ff43210..310ac85 100644 --- a/src/WebExpress.WebCore/WebRestAPI/Model/RestApiItem.cs +++ b/src/WebExpress.WebCore/WebRestApi/Model/RestApiItem.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Reflection; using WebExpress.WebCore.WebApplication; using WebExpress.WebCore.WebComponent; using WebExpress.WebCore.WebCondition; using WebExpress.WebCore.WebEndpoint; +using WebExpress.WebCore.WebMessage; using WebExpress.WebCore.WebPlugin; using WebExpress.WebCore.WebUri; @@ -62,7 +64,7 @@ internal class RestApiItem : IDisposable /// /// Returns the crud methods. /// - public IEnumerable Methods { get; internal set; } + public IEnumerable Methods { get; internal set; } /// /// Returns the version number of the rest api. @@ -74,6 +76,31 @@ internal class RestApiItem : IDisposable /// public bool Cache { get; internal set; } + /// + /// Returns the reflection information for the associated get method. + /// + public MethodInfo GetMethod { get; internal set; } + + /// + /// Returns the reflection information for the associated post method. + /// + public MethodInfo PostMethod { get; internal set; } + + /// + /// Returns the reflection information for the associated patch method. + /// + public MethodInfo PatchMethod { get; internal set; } + + /// + /// Returns the reflection information for the associated put method. + /// + public MethodInfo PutMethod { get; internal set; } + + /// + /// Returns the reflection information for the associated delete method. + /// + public MethodInfo DeleteMethod { get; internal set; } + /// /// Returns the attributes associated with the page. /// diff --git a/src/WebExpress.WebCore/WebRestAPI/RestApiContext.cs b/src/WebExpress.WebCore/WebRestApi/RestApiContext.cs similarity index 95% rename from src/WebExpress.WebCore/WebRestAPI/RestApiContext.cs rename to src/WebExpress.WebCore/WebRestApi/RestApiContext.cs index 9a21399..b52587b 100644 --- a/src/WebExpress.WebCore/WebRestAPI/RestApiContext.cs +++ b/src/WebExpress.WebCore/WebRestApi/RestApiContext.cs @@ -4,6 +4,7 @@ using WebExpress.WebCore.WebComponent; using WebExpress.WebCore.WebCondition; using WebExpress.WebCore.WebEndpoint; +using WebExpress.WebCore.WebMessage; using WebExpress.WebCore.WebPlugin; using WebExpress.WebCore.WebUri; @@ -32,7 +33,7 @@ public class RestApiContext : IRestApiContext /// /// Returns the crud methods. /// - public IEnumerable Methods { get; internal set; } = []; + public IEnumerable Methods { get; internal set; } = []; /// /// Returns the endpoint id. diff --git a/src/WebExpress.WebCore/WebRestApi/RestApiManager.cs b/src/WebExpress.WebCore/WebRestApi/RestApiManager.cs new file mode 100644 index 0000000..39ba764 --- /dev/null +++ b/src/WebExpress.WebCore/WebRestApi/RestApiManager.cs @@ -0,0 +1,724 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading; +using WebExpress.WebCore.Internationalization; +using WebExpress.WebCore.WebApplication; +using WebExpress.WebCore.WebAttribute; +using WebExpress.WebCore.WebComponent; +using WebExpress.WebCore.WebCondition; +using WebExpress.WebCore.WebEndpoint; +using WebExpress.WebCore.WebMessage; +using WebExpress.WebCore.WebParameter; +using WebExpress.WebCore.WebPlugin; +using WebExpress.WebCore.WebRestApi.Model; +using WebExpress.WebCore.WebUri; + +namespace WebExpress.WebCore.WebRestApi +{ + /// + /// The rest api manager manages rest api resources, which can be called with a URI (Uniform page Identifier). + /// + public partial class RestApiManager : IRestApiManager, IDisposable + { + // synchronization guard for protecting _dictionary and related mutable state + private readonly Lock _guard = new(); + + private readonly IComponentHub _componentHub; + private readonly IHttpServerContext _httpServerContext; + + // instantiate the dictionary; assume RestApiDictionary is a non-thread-safe collection + private readonly RestApiDictionary _dictionary = []; + + [GeneratedRegex(@"(?:_|[Vv])(\d+)_?")] + private static partial Regex ApiVersionRegex(); + + /// + /// An event that fires when an rest api resource is added. + /// + public event EventHandler AddRestApi; + + /// + /// An event that fires when an rest api resource is removed. + /// + public event EventHandler RemoveRestApi; + + /// + /// Returns all rest api resource contexts. + /// + public IEnumerable RestApis + { + get + { + // return a stable snapshot to avoid enumeration during concurrent modifications + lock (_guard) + { + return [.. _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Select(x => x.RestApiContext)]; + } + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The component hub. + /// The reference to the context of the host. + [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Used via Reflection.")] + private RestApiManager(IComponentHub componentHub, IHttpServerContext httpServerContext) + { + _componentHub = componentHub; + _httpServerContext = httpServerContext; + + _componentHub.PluginManager.AddPlugin += OnAddPlugin; + _componentHub.PluginManager.RemovePlugin += OnRemovePlugin; + _componentHub.ApplicationManager.AddApplication += OnAddApplication; + _componentHub.ApplicationManager.RemoveApplication += OnRemoveApplication; + + var endpointtRegistration = new EndpointRegistration() + { + EndpointResolver = (type, applicationContext) => applicationContext is not null + ? GetRestApi(type, applicationContext) + : GetRestApi(type), + EndpointsResolver = () => RestApis, + HandleRequest = (request, endpointContext) => + { + // get rest api context and create or obtain instance + var restApiContext = endpointContext as IRestApiContext; + var restApiItem = _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .FirstOrDefault(x => x.RestApiContext.Equals(restApiContext)); + + var restApi = CreateApiInstance(restApiItem); + + // if no resource found, return bad request + if (restApiContext is null || restApi is null) + { + return new ResponseBadRequest() + { + Content = I18N.Translate("webexpress.webcore:restapimanager.resourcenotfound") + }; + } + + // execute according to allowed methods + if (restApiContext.Methods.Any(x => x.Equals((RequestMethod)request.Method))) + { + switch (request.Method) + { + case RequestMethod.POST: + if (restApiItem.GetMethod is not null) + { + return (Response)(restApiItem.PostMethod + .Invoke(restApi, [request]) + ?? new ResponseOK()); + } + break; + case RequestMethod.GET: + if (restApiItem.GetMethod is not null) + { + return (Response)(restApiItem.GetMethod + .Invoke(restApi, [request]) + ?? new ResponseOK()); + } + break; + case RequestMethod.PATCH: + if (restApiItem.GetMethod is not null) + { + return (Response)(restApiItem.PatchMethod + .Invoke(restApi, [request]) + ?? new ResponseOK()); + } + break; + case RequestMethod.PUT: + if (restApiItem.GetMethod is not null) + { + return (Response)(restApiItem.PutMethod + .Invoke(restApi, [request]) + ?? new ResponseOK()); + } + break; + case RequestMethod.DELETE: + if (restApiItem.GetMethod is not null) + { + return (Response)(restApiItem.DeleteMethod + .Invoke(restApi, [request]) + ?? new ResponseOK()); + } + break; + default: + return new ResponseBadRequest() + { + Content = I18N.Translate("webexpress.webcore:restapimanager.methodnotsupported", request.Method.ToString()) + }; + } + } + + return new ResponseBadRequest() + { + Content = I18N.Translate("webexpress.webcore:restapimanager.methodnotsupported", request.Method.ToString()) + }; + } + }; + + AddRestApi += (sender, e) => endpointtRegistration.AddEndpoint?.Invoke(sender, e); + RemoveRestApi += (sender, e) => endpointtRegistration.RemoveEndpoint?.Invoke(sender, e); + + _componentHub.EndpointManager.Register(endpointtRegistration); + + _httpServerContext.Log.Debug(I18N.Translate("webexpress.webcore:restapimanager.initialization")); + } + + /// + /// Returns an enumeration of all containing rest api contexts of a plugin. + /// + /// A context of a plugin whose rest apis are to be registered. + /// An enumeration of rest api resource contexts. + public IEnumerable GetRestApi(IPluginContext pluginContext) + { + lock (_guard) + { + if (_dictionary.TryGetValue(pluginContext, out var pluginResources)) + { + // return snapshot list + return [.. pluginResources + .SelectMany(x => x.Value) + .Select(x => x.Value.RestApiContext)]; + } + + return []; + } + } + + /// + /// Returns an enumeration of rest api resource contexts. + /// + /// The rest api resource type. + /// An enumeration of rest api resource contexts. + public IEnumerable GetRestApi() where T : IRestApi + { + return GetRestApi(typeof(T)); + } + + /// + /// Returns an enumeration of rest api resource contexts. + /// + /// The rest api resource type. + /// An enumeration of rest api resource contexts. + public IEnumerable GetRestApi(Type restApiType) + { + lock (_guard) + { + return [.. _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.RestApiClass.Equals(restApiType)) + .Select(x => x.RestApiContext)]; + } + } + + /// + /// Returns an enumeration of rest api resource contexts for a given application. + /// + /// The rest api type. + /// The context of the application. + /// An enumeration of rest api resource contexts. + public IEnumerable GetRestApi(Type restApiType, IApplicationContext applicationContext) + { + lock (_guard) + { + return [.. _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.RestApiClass.Equals(restApiType)) + .Where(x => x.RestApiContext.ApplicationContext.Equals(applicationContext)) + .Select(x => x.RestApiContext)]; + } + } + + /// + /// Returns an enumeration of rest api resource contexts for a given application. + /// + /// The rest api resource type. + /// The context of the application. + /// An enumeration of rest api resource contexts. + public IEnumerable GetRestApi(IApplicationContext applicationContext) where T : IRestApi + { + lock (_guard) + { + return [.. _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.RestApiClass.Equals(typeof(T))) + .Where(x => x.RestApiContext.ApplicationContext.Equals(applicationContext)) + .Select(x => x.RestApiContext)]; + } + } + + /// + /// Returns the rest api resource context. + /// + /// The context of the application. + /// The rest api resource id. + /// An rest api resource context or null. + public IRestApiContext GetRestApi(IApplicationContext applicationContext, string restApiId) + { + lock (_guard) + { + return _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.RestApiContext.ApplicationContext.Equals(applicationContext)) + .Where(x => x.RestApiContext.EndpointId.Equals(restApiId)) + .Select(x => x.RestApiContext) + .FirstOrDefault(); + } + } + + /// + /// Returns the rest api resource context. + /// + /// The application id. + /// The rest api resource id. + /// An rest api resource context or null. + public IRestApiContext GetRestApi(string applicationId, string restApiId) + { + lock (_guard) + { + return _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.RestApiContext.ApplicationContext.ApplicationId.Equals(applicationId)) + .Where(x => x.RestApiContext.EndpointId.Equals(restApiId)) + .Select(x => x.RestApiContext) + .FirstOrDefault(); + } + } + + /// + /// Creates a new rest api resource and returns it. If a rest api resource already exists (through caching), the existing instance is returned. + /// Thread-safe: cached instance creation and assignment is protected. + /// + /// The item used for rest api resource creation. + /// The created or cached rest api resource. + private IRestApi CreateApiInstance(RestApiItem apiItem) + { + if (apiItem is null) + { + return null; + } + + // locate resourceItem inside lock to get a consistent view + lock (_guard) + { + // if instance already cached, return immediately + if (apiItem.Instance is not null) + { + return apiItem.Instance; + } + + // if caching is enabled, create and assign the instance under lock to avoid double-creation + if (apiItem.Cache) + { + // create instance while holding the lock to ensure only one creation and assignment occurs + var instanceCached = ComponentActivator.CreateInstance + ( + apiItem.RestApiClass, + apiItem.RestApiContext, + _httpServerContext, + _componentHub, + apiItem.ApplicationContext + ); + + apiItem.Instance = instanceCached; + return instanceCached; + } + } + + // if not caching, create instance outside lock (no shared state to modify) + var instanceNoCache = ComponentActivator.CreateInstance + ( + apiItem.RestApiClass, + apiItem.RestApiContext, + _httpServerContext, + _componentHub, + apiItem.ApplicationContext + ); + + return instanceNoCache; + } + + /// + /// Discovers and binds rest apis to an application. + /// + /// The context of the plugin whose rest apis are to be associated. + private void Register(IPluginContext pluginContext) + { + lock (_guard) + { + if (_dictionary.ContainsKey(pluginContext)) + { + return; + } + + Register(pluginContext, _componentHub.ApplicationManager.GetApplications(pluginContext)); + } + } + + /// + /// Discovers and binds rest apis to an application. + /// + /// The context of the application whose rest apis are to be associated. + private void Register(IApplicationContext applicationContext) + { + foreach (var pluginContext in _componentHub.PluginManager.GetPlugins(applicationContext)) + { + lock (_guard) + { + if (_dictionary.TryGetValue(pluginContext, out var appDict) && appDict.ContainsKey(applicationContext)) + { + continue; + } + } + + Register(pluginContext, new[] { applicationContext }); + } + } + + /// + /// Registers rest apis for a given plugin and application context. + /// + /// The plugin context. + /// The application context (optional). + private void Register(IPluginContext pluginContext, IEnumerable applicationContexts) + { + // assembly and reflection operations are per-plugin and read-only; mutations to _dictionary are synchronized + var assembly = pluginContext?.Assembly; + + foreach (var restApiType in assembly.GetTypes() + .Where(x => x.IsClass == true && x.IsSealed && x.IsPublic) + .Where(x => x.GetInterface(typeof(IRestApi).Name) is not null)) + { + var id = restApiType.FullName?.ToLower(); + var segment = default(ISegmentAttribute); + var title = restApiType.Name; + var contextPath = string.Empty; + var includeSubPaths = false; + var conditions = new List(); + var cache = false; + var match = ApiVersionRegex().Match(id); + var versionSegment = match.Success ? match.Groups[0].Value.Replace(".", "") : ""; + var version = match.Success && uint.TryParse(match.Groups[1].Value, out var result) ? result : 1u; + var attributes = restApiType.CustomAttributes + .Where(x => !x.AttributeType.GetInterfaces().Contains(typeof(IEndpointAttribute)) && + !x.AttributeType.GetInterfaces().Contains(typeof(IPageAttribute))); + var getMethod = restApiType + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(m => m.GetCustomAttributes(typeof(MethodAttribute), false) + .Cast() + .Any(attr => attr.RequestMethod == RequestMethod.GET)) + .FirstOrDefault(); + var postMethod = restApiType + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(m => m.GetCustomAttributes(typeof(MethodAttribute), false) + .Cast() + .Any(attr => attr.RequestMethod == RequestMethod.POST)) + .FirstOrDefault(); + var patchMethod = restApiType + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(m => m.GetCustomAttributes(typeof(MethodAttribute), false) + .Cast() + .Any(attr => attr.RequestMethod == RequestMethod.PATCH)) + .FirstOrDefault(); + var putMethod = restApiType + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(m => m.GetCustomAttributes(typeof(MethodAttribute), false) + .Cast() + .Any(attr => attr.RequestMethod == RequestMethod.PUT)) + .FirstOrDefault(); + var deleteMethod = restApiType + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(m => m.GetCustomAttributes(typeof(MethodAttribute), false) + .Cast() + .Any(attr => attr.RequestMethod == RequestMethod.DELETE)) + .FirstOrDefault(); + + var methods = new[] + { + (getMethod, RequestMethod.GET), + (postMethod, RequestMethod.POST), + (patchMethod, RequestMethod.PATCH), + (putMethod, RequestMethod.PUT), + (deleteMethod, RequestMethod.DELETE) + } + .Where(x => x.Item1 is not null) + .Select(x => x.Item2); + + foreach + ( + var attribute in restApiType + .GetCustomAttributes(inherit: true) + .Where(x => x.GetType().GetInterfaces().Contains(typeof(IEndpointAttribute)))) + { + var attributeType = attribute.GetType(); + + // segment attribute + if (attributeType.GetInterfaces().Contains(typeof(ISegmentAttribute))) + { + segment = attribute as ISegmentAttribute; + + continue; + } + + // context path + if (attributeType == typeof(ContextPathAttribute)) + { + contextPath = (attribute as ContextPathAttribute)?.ContextPath; + + continue; + } + + // include subpaths + if (attributeType == typeof(IncludeSubPathsAttribute)) + { + includeSubPaths = (attribute as IncludeSubPathsAttribute)?.IncludeSubPaths + ?? false; + + continue; + } + + // condition attribute (generic) + if (attributeType.IsGenericType + && attributeType.GetGenericTypeDefinition().Name == typeof(ConditionAttribute<>).Name + && attributeType.Namespace == typeof(ConditionAttribute<>).Namespace) + { + var conditionType = attributeType.GetGenericArguments().FirstOrDefault(); + if (conditionType != null) + { + conditions.Add(Activator.CreateInstance(conditionType) as ICondition); + } + + continue; + } + + // cache attribute + if (attributeType == typeof(CacheAttribute)) + { + cache = true; + + continue; + } + } + + // assign the rest api to existing applications + foreach (var applicationContext in applicationContexts) + { + var prefix = applicationContext.Route.Concat + ( + applicationContext.PluginContext != pluginContext + ? pluginContext.PluginName.ToLower() + : "" + ); + + var routePath = EndpointManager.CreateEndpointRoute + ( + restApiType, + prefix, + segment, + [ + new UriPathSegmentConstant("api"), + new UriPathSegmentVariableApiVersion($"{version}") + ], + ["api", "restapi", "rest"] + ).RemoveSegment(versionSegment); + + var restApiContext = new RestApiContext() + { + EndpointId = new ComponentId(id), + PluginContext = pluginContext, + ApplicationContext = applicationContext, + Route = routePath, + Cache = cache, + Conditions = conditions, + IncludeSubPaths = includeSubPaths, + Attributes = EndpointManager.GetAttributeInstances(attributes), + Version = version, + Methods = methods.Distinct() + }; + + var restApiItem = new RestApiItem(_componentHub.EndpointManager) + { + EndpointId = new ComponentId(restApiType.FullName), + PluginContext = pluginContext, + ApplicationContext = applicationContext, + RestApiContext = restApiContext, + RestApiClass = restApiType, + Methods = methods.Distinct(), + Version = version, + Cache = cache, + Conditions = conditions, + IncludeSubPaths = includeSubPaths, + Attributes = attributes.Select(x => x.AttributeType), + GetMethod = getMethod, + PostMethod = postMethod, + PatchMethod = patchMethod, + PutMethod = putMethod, + DeleteMethod = deleteMethod + }; + + // add mutation protected by lock to avoid concurrent modifications + var added = false; + lock (_guard) + { + added = _dictionary.AddRestApiItem(pluginContext, applicationContext, restApiItem); + } + + if (added) + { + OnAddRestApi(restApiItem.RestApiContext); + + _httpServerContext?.Log.Debug( + I18N.Translate( + "webexpress.webcore:restapimanager.addrestapi", + id, + applicationContext.ApplicationId + ) + ); + } + } + } + } + + /// + /// Removes all rest apis associated with the specified plugin context. + /// + /// The context of the plugin that contains the rest api resources to remove. + public void Remove(IPluginContext pluginContext) + { + if (pluginContext is null) + { + return; + } + + lock (_guard) + { + if (_dictionary.TryGetValue(pluginContext, out var value)) + { + foreach (var resourceItem in value.Values.SelectMany(x => x.Values)) + { + OnRemoveRestApi(resourceItem.RestApiContext); + resourceItem.Dispose(); + } + + _dictionary.Remove(pluginContext); + } + } + } + + /// + /// Removes all rest apis associated with the specified application context. + /// + /// The context of the application that contains the resources to remove. + internal void Remove(IApplicationContext applicationContext) + { + if (applicationContext is null) + { + return; + } + + lock (_guard) + { + foreach (var pluginDict in _dictionary.Values) + { + foreach (var appDict in pluginDict.Where(x => x.Key == applicationContext).Select(x => x.Value)) + { + foreach (var resourceItem in appDict.Values) + { + OnRemoveRestApi(resourceItem.RestApiContext); + resourceItem.Dispose(); + } + } + + // remove the application mapping from the plugin dictionary + pluginDict.Remove(applicationContext); + } + } + } + + /// + /// Raises the AddRestApi event. + /// + /// The rest api resource context. + private void OnAddRestApi(IRestApiContext resourceContext) + { + AddRestApi?.Invoke(this, resourceContext); + } + + /// + /// Raises the RemoveRestApi event. + /// + /// The rest api resource context. + private void OnRemoveRestApi(IRestApiContext pageContext) + { + RemoveRestApi?.Invoke(this, pageContext); + } + + /// + /// Raises the event when an plugin is added. + /// + /// The source of the event. + /// The context of the plugin being added. + private void OnAddPlugin(object sender, IPluginContext e) + { + Register(e); + } + + /// + /// Raises the event when a plugin is removed. + /// + /// The source of the event. + /// The context of the plugin being removed. + private void OnRemovePlugin(object sender, IPluginContext e) + { + Remove(e); + } + + /// + /// Raises the event when an application is removed. + /// + /// The source of the event. + /// The context of the application being removed. + private void OnRemoveApplication(object sender, IApplicationContext e) + { + Remove(e); + } + + /// + /// Raises the event when an application is added. + /// + /// The source of the event. + /// The context of the application being added. + private void OnAddApplication(object sender, IApplicationContext e) + { + Register(e); + } + + /// + /// Release of unmanaged resources reserved during use. + /// + public void Dispose() + { + _componentHub.PluginManager.AddPlugin -= OnAddPlugin; + _componentHub.PluginManager.RemovePlugin -= OnRemovePlugin; + _componentHub.ApplicationManager.AddApplication -= OnAddApplication; + _componentHub.ApplicationManager.RemoveApplication -= OnRemoveApplication; + + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebRestApi/RestApiValidationResult.cs b/src/WebExpress.WebCore/WebRestApi/RestApiValidationResult.cs index 7524032..ddf42b1 100644 --- a/src/WebExpress.WebCore/WebRestApi/RestApiValidationResult.cs +++ b/src/WebExpress.WebCore/WebRestApi/RestApiValidationResult.cs @@ -11,7 +11,7 @@ namespace WebExpress.WebCore.WebRestApi /// during the processing of a REST API request. It includes methods to add individual /// or multiple errors, and properties to check the overall validity of the result. /// - public class RestApiValidationResult + public class RestApiValidationResult : IRestApiValidationResult { private readonly List _errors = []; @@ -45,9 +45,12 @@ public class RestApiValidationResult /// A code representing the type or category of the error, if applicable. /// This parameter is optional and can be null. /// - public void Add(string message, string field = null, string code = null) + /// The current instance for method chaining. + public IRestApiValidationResult Add(string message, string field = null, string code = null) { _errors.Add(new RestApiError(message, code, field)); + + return this; } /// @@ -58,9 +61,12 @@ public void Add(string message, string field = null, string code = null) /// the array is empty, no changes are made. /// /// An array of error objects to add. - public void Add(params RestApiError[] errors) + /// The current instance for method chaining. + public IRestApiValidationResult Add(params RestApiError[] errors) { _errors.AddRange(errors); + + return this; } /// @@ -71,12 +77,15 @@ public void Add(params RestApiError[] errors) /// the array is empty, no changes are made. /// /// An array of error objects to add. - public void AddRange(IEnumerable errors) + /// The current instance for method chaining. + public IRestApiValidationResult AddRange(IEnumerable errors) { - if (errors != null) + if (errors is not null) { _errors.AddRange(errors); } + + return this; } /// diff --git a/src/WebExpress.WebCore/WebRestApi/RestApiValidator.cs b/src/WebExpress.WebCore/WebRestApi/RestApiValidator.cs index a354ebe..aa7d904 100644 --- a/src/WebExpress.WebCore/WebRestApi/RestApiValidator.cs +++ b/src/WebExpress.WebCore/WebRestApi/RestApiValidator.cs @@ -10,7 +10,7 @@ namespace WebExpress.WebCore.WebRestApi /// public class RestApiValidator { - private readonly Request _request; + private readonly IRequest _request; private readonly RestApiValidationResult _result = new(); private bool _currentCondition = true; @@ -28,7 +28,7 @@ public class RestApiValidator /// Initializes a new instance of the class with the specified request. /// /// The request to be validated. - public RestApiValidator(Request request) + public RestApiValidator(IRequest request) { _request = request; } @@ -42,7 +42,7 @@ public RestApiValidator(Request request) /// met; otherwise, false. /// /// The current instance, allowing for method chaining. - public RestApiValidator When(Func condition) + public RestApiValidator When(Func condition) { _currentCondition = condition(_request); return this; @@ -617,7 +617,7 @@ public RestApiValidator IsDate(string parameter, string message = null) /// Defaults to "CUSTOM" if not specified. /// /// The current instance, allowing for method chaining. - public RestApiValidator Custom(Func condition, string message, string parameter = null, string code = "CUSTOM") + public RestApiValidator Custom(Func condition, string message, string parameter = null, string code = "CUSTOM") { if (!_currentCondition) { diff --git a/src/WebExpress.WebCore/WebSession/ISessionManager.cs b/src/WebExpress.WebCore/WebSession/ISessionManager.cs index 29befb8..4e947d6 100644 --- a/src/WebExpress.WebCore/WebSession/ISessionManager.cs +++ b/src/WebExpress.WebCore/WebSession/ISessionManager.cs @@ -15,7 +15,7 @@ public interface ISessionManager : IComponentManager /// /// The request. /// The session. - Session GetSession(Request request); + Session GetSession(IRequest request); /// /// Cleans up expired sessions from the session manager based on the specified session timeout. diff --git a/src/WebExpress.WebCore/WebSession/Model/Session.cs b/src/WebExpress.WebCore/WebSession/Model/Session.cs index 709ee9b..d11339f 100644 --- a/src/WebExpress.WebCore/WebSession/Model/Session.cs +++ b/src/WebExpress.WebCore/WebSession/Model/Session.cs @@ -90,7 +90,7 @@ public T GetOrCreateProperty(params object[] parameters) where T : class, ISe var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var constructors = type.GetConstructors(flags); - if (constructors != null || parameters.Length > 0) + if (constructors is not null || parameters.Length > 0) { foreach (var constructor in constructors.OrderByDescending(x => x.GetParameters().Length)) { diff --git a/src/WebExpress.WebCore/WebSession/Model/SessionPropertyParameter.cs b/src/WebExpress.WebCore/WebSession/Model/SessionPropertyParameter.cs index e1a01d1..9ced217 100644 --- a/src/WebExpress.WebCore/WebSession/Model/SessionPropertyParameter.cs +++ b/src/WebExpress.WebCore/WebSession/Model/SessionPropertyParameter.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using WebExpress.WebCore.WebMessage; +using WebExpress.WebCore.WebParameter; namespace WebExpress.WebCore.WebSession.Model { diff --git a/src/WebExpress.WebCore/WebSession/SessionManager.cs b/src/WebExpress.WebCore/WebSession/SessionManager.cs index 465790d..0ee368d 100644 --- a/src/WebExpress.WebCore/WebSession/SessionManager.cs +++ b/src/WebExpress.WebCore/WebSession/SessionManager.cs @@ -38,11 +38,11 @@ private SessionManager(IHttpServerContext context) /// /// The request. /// The session. - public Session GetSession(Request request) + public Session GetSession(IRequest request) { var session = default(Session); - // Session ermitteln + // determine session var sessionCookie = request?.Header .Cookies?.Where(x => x.Name.Equals("session", StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(); @@ -58,7 +58,7 @@ public Session GetSession(Request request) } - if (sessionCookie != null && _dictionary.TryGetValue(guid, out Session value)) + if (sessionCookie is not null && _dictionary.TryGetValue(guid, out Session value)) { session = value; session.Updated = DateTime.Now; @@ -78,20 +78,20 @@ public Session GetSession(Request request) } /// - /// Cleans up expired sessions from the session manager based on the specified session timeout. + /// Cleans up expired sessions from the session manager based on the specified + /// session timeout. /// - /// - /// This method iterates through the sessions and removes those that have been inactive - /// for longer than the configured session timeout. It logs the removal of each expired session. - /// /// - /// The application context containing configuration settings, including the session timeout duration. + /// The application context containing configuration settings, including the session + /// timeout duration. /// /// - /// The explicit session timeout in minutes; if non-positive, the configured timeout is used. If - /// the effective timeout is non-positive, cleanup is skipped. + /// The explicit session timeout in minutes; if non-positive, the configured + /// timeout is used. If the effective timeout is non-positive, cleanup is skipped. /// - /// The current instance of the session manager, allowing for method chaining. + /// + /// The current instance of the session manager, allowing for method chaining. + /// public ISessionManager CleanUp(IApplicationContext applicationContext, int timeoutMinutes = 60 * 24 * 365) { // validate input diff --git a/src/WebExpress.WebCore/WebSettingPage/Model/SettingCategoryDictionary.cs b/src/WebExpress.WebCore/WebSettingPage/Model/SettingCategoryDictionary.cs index 9b27e21..8b76adf 100644 --- a/src/WebExpress.WebCore/WebSettingPage/Model/SettingCategoryDictionary.cs +++ b/src/WebExpress.WebCore/WebSettingPage/Model/SettingCategoryDictionary.cs @@ -61,7 +61,7 @@ public void Remove(IPluginContext pluginContext) /// An enumerable collection of setting categories contexts that were removed. public IEnumerable Remove(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { yield break; } diff --git a/src/WebExpress.WebCore/WebSettingPage/Model/SettingGroupDictionary.cs b/src/WebExpress.WebCore/WebSettingPage/Model/SettingGroupDictionary.cs index d46cb95..1920417 100644 --- a/src/WebExpress.WebCore/WebSettingPage/Model/SettingGroupDictionary.cs +++ b/src/WebExpress.WebCore/WebSettingPage/Model/SettingGroupDictionary.cs @@ -61,7 +61,7 @@ public void Remove(IPluginContext pluginContext) /// An enumerable collection of setting groups contexts that were removed. public IEnumerable Remove(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { yield break; } diff --git a/src/WebExpress.WebCore/WebSettingPage/Model/SettingPageDictionary.cs b/src/WebExpress.WebCore/WebSettingPage/Model/SettingPageDictionary.cs index cbbbcf2..923eaad 100644 --- a/src/WebExpress.WebCore/WebSettingPage/Model/SettingPageDictionary.cs +++ b/src/WebExpress.WebCore/WebSettingPage/Model/SettingPageDictionary.cs @@ -63,7 +63,7 @@ public void Remove(IPluginContext pluginContext) /// An enumerable collection of setting page contexts that were removed. public IEnumerable Remove(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { yield break; } @@ -148,7 +148,7 @@ public IEndpoint CreateSettingPageInstance(ISettingPageContext settingPageContex .SelectMany(x => x) .FirstOrDefault(x => x.SettingPageContext.Equals(settingPageContext)); - if (settingPageItem != null && settingPageItem.Instance == null) + if (settingPageItem is not null && settingPageItem.Instance is null) { var instance = ComponentActivator.CreateInstance ( diff --git a/src/WebExpress.WebCore/WebSettingPage/Model/TimeSpanConverter.cs b/src/WebExpress.WebCore/WebSettingPage/Model/TimeSpanConverter.cs index 47d58b7..4f84560 100644 --- a/src/WebExpress.WebCore/WebSettingPage/Model/TimeSpanConverter.cs +++ b/src/WebExpress.WebCore/WebSettingPage/Model/TimeSpanConverter.cs @@ -18,7 +18,7 @@ public class TimeSpanConverter /// A formatted string representing the TimeSpan. public object Convert(object value, Type targetType, object parameter, string language) { - if (value == null) + if (value is null) { return null; } diff --git a/src/WebExpress.WebCore/WebSettingPage/SettingPageManager.cs b/src/WebExpress.WebCore/WebSettingPage/SettingPageManager.cs index 81d2fd4..011beff 100644 --- a/src/WebExpress.WebCore/WebSettingPage/SettingPageManager.cs +++ b/src/WebExpress.WebCore/WebSettingPage/SettingPageManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -22,14 +23,16 @@ namespace WebExpress.WebCore.WebSettingPage /// /// Management of settings pages. /// - public sealed class SettingPageManager : ISettingPageManager + public sealed class SettingPageManager : ISettingPageManager, IDisposable { private readonly IComponentHub _componentHub; private readonly IHttpServerContext _httpServerContext; private readonly SettingCategoryDictionary _categoryDictionary = new(); private readonly SettingGroupDictionary _groupDictionary = new(); private readonly SettingPageDictionary _pageDictionary = new(); - private static readonly Dictionary _delegateCache = []; + + // use a concurrent dictionary for delegate cache to avoid concurrent write corruption + private static readonly ConcurrentDictionary _delegateCache = new ConcurrentDictionary(); /// /// An event that fires when an setting page is added. @@ -94,76 +97,100 @@ private SettingPageManager(IComponentHub componentHub, IHttpServerContext httpSe var endpointtRegistration = new EndpointRegistration() { - EndpointResolver = (type, applicationContext) => applicationContext != null ? GetSettingPages(type, applicationContext) : GetSettingPages(type), + EndpointResolver = (type, applicationContext) => applicationContext is not null + ? GetSettingPages(type, applicationContext) + : GetSettingPages(type), EndpointsResolver = () => SettingPages, HandleRequest = (request, endpontContext) => { + // create or obtain page instance for this request var pageInstance = CreateSettingPageInstance(endpontContext as ISettingPageContext); var pageType = pageInstance.GetType(); var pageContext = endpontContext as IPageContext; var renderContext = new RenderContext(pageInstance, pageContext, request); var visualTreeContext = new VisualTreeContext(renderContext); - var visualTreeType = pageType.GetInterface(typeof(ISettingPage<>).Name).GetGenericArguments()[0]; + // determine visual tree type implemented by the setting page + var pageInterface = pageType.GetInterface(typeof(ISettingPage<>).Name); + if (pageInterface is null) + { + throw new InvalidOperationException($"Page type {pageType.FullName} does not implement ISettingPage<>."); + } + + var visualTreeType = pageInterface.GetGenericArguments()[0]; + + // obtain or create a cached open-instance delegate safely if (!_delegateCache.TryGetValue(pageType, out var del)) { - // create and compile the expression + // create an open-instance delegate: (instance, renderContext, visualTree) => instance.Process(renderContext, visualTree) + var instanceParam = Expression.Parameter(pageType, "instance"); var renderContextParam = Expression.Parameter(typeof(IRenderContext), "renderContext"); var visualTreeParam = Expression.Parameter(visualTreeType, "visualTree"); - var processMethod = pageType.GetMethod("Process", [typeof(IRenderContext), visualTreeType]); - var callProzessMethod = Expression.Call - ( - Expression.Constant(pageInstance), - processMethod, - renderContextParam, - visualTreeParam - ); - var lambda = Expression.Lambda(callProzessMethod, renderContextParam, visualTreeParam) - .Compile(); - - _delegateCache[pageType] = lambda; - del = lambda; + + // find Process method matching signature Process(IRenderContext, TVisualTree) + var processMethod = pageType.GetMethod("Process", new[] { typeof(IRenderContext), visualTreeType }); + if (processMethod is null) + { + throw new InvalidOperationException($"Process method not found on type {pageType.FullName}"); + } + + // call instance.Process(renderContext, visualTree) + var callProcess = Expression.Call(instanceParam, processMethod, renderContextParam, visualTreeParam); + + // compile lambda with signature (instance, renderContext, visualTree) + var lambda = Expression.Lambda(callProcess, instanceParam, renderContextParam, visualTreeParam).Compile(); + + // add to concurrent dictionary atomically; if another thread added concurrently, use the existing one + del = _delegateCache.GetOrAdd(pageType, lambda); } // create visual tree instance - var visualTreeInstance = default(IVisualTree); + IVisualTree visualTreeInstance = null; var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var constructors = visualTreeType?.GetConstructors(flags); - if (constructors != null) + if (constructors is not null) { foreach (var constructor in constructors.OrderByDescending(x => x.GetParameters().Length)) { // injection var parameters = constructor.GetParameters(); var hubProperties = _componentHub.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); - var contextIdProperty = pageContext.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) + var contextIdProperty = pageContext?.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(x => x.PropertyType == typeof(IComponentId)) .FirstOrDefault(); var parameterValues = parameters.Select(parameter => - parameter.ParameterType == typeof(IComponentHub) ? componentHub : - parameter.ParameterType == typeof(IHttpServerContext) ? httpServerContext : + parameter.ParameterType == typeof(IComponentHub) ? _componentHub : + parameter.ParameterType == typeof(IHttpServerContext) ? _httpServerContext : parameter.ParameterType == typeof(IPageContext) ? pageContext : parameter.ParameterType == typeof(IComponentId) ? contextIdProperty?.GetValue(pageContext) : hubProperties.Where(x => x.PropertyType == parameter.ParameterType) .FirstOrDefault()? - .GetValue(componentHub) ?? null + .GetValue(_componentHub) ?? null ).ToArray(); - if (constructor.Invoke(parameterValues) is IVisualTree visualTree) + var invoked = constructor.Invoke(parameterValues); + if (invoked is IVisualTree visualTree) { visualTreeInstance = visualTree; + break; } } } else { - visualTreeInstance = Activator.CreateInstance(); + // fallback: try parameterless creation + visualTreeInstance = Activator.CreateInstance(visualTreeType) as IVisualTree; } - // execute the cached delegate - del.DynamicInvoke(renderContext, visualTreeInstance); + if (visualTreeInstance is null) + { + throw new InvalidOperationException($"Could not create visual tree instance of type {visualTreeType.FullName} for page {pageType.FullName}."); + } + + // execute the cached open-instance delegate; pass the current pageInstance + del.DynamicInvoke(pageInstance, renderContext, visualTreeInstance); return new ResponseOK() { @@ -177,7 +204,7 @@ private SettingPageManager(IComponentHub componentHub, IHttpServerContext httpSe _componentHub.EndpointManager.Register(endpointtRegistration); - _httpServerContext.Log.Debug(I18N.Translate("webexpress.webapp:settingpagemanager.initialization")); + _httpServerContext.Log.Debug(I18N.Translate("webexpress.webcore:settingpagemanager.initialization")); } /// @@ -219,9 +246,9 @@ private void Register(IApplicationContext applicationContext) foreach (var pluginContext in _componentHub.PluginManager.GetPlugins(applicationContext)) { - RegisterCategory(pluginContext, [applicationContext]); - RegisterGroup(pluginContext, [applicationContext]); - RegisterPage(pluginContext, [applicationContext]); + RegisterCategory(pluginContext, new[] { applicationContext }); + RegisterGroup(pluginContext, new[] { applicationContext }); + RegisterPage(pluginContext, new[] { applicationContext }); } } @@ -236,7 +263,7 @@ private void RegisterCategory(IPluginContext pluginContext, IEnumerable x.IsClass == true && x.IsSealed && x.IsPublic) - .Where(x => x.GetInterface(typeof(ISettingCategory).Name) != null)) + .Where(x => x.GetInterface(typeof(ISettingCategory).Name) is not null)) { var id = settingCategoryType.FullName?.ToLower(); var icon = default(IIcon); @@ -298,15 +325,7 @@ private void RegisterCategory(IPluginContext pluginContext, IEnumerable x.IsClass == true && x.IsSealed && x.IsPublic) - .Where(x => x.GetInterface(typeof(ISettingGroup).Name) != null)) + .Where(x => x.GetInterface(typeof(ISettingGroup).Name) is not null)) { var id = settingGroupType.FullName?.ToLower(); var icon = default(IIcon); @@ -361,14 +380,7 @@ private void RegisterGroup(IPluginContext pluginContext, IEnumerable x.IsClass == true && x.IsSealed && x.IsPublic) - .Where(x => x.GetInterface(typeof(ISettingPage<>).Name) != null)) + .Where(x => x.GetInterface(typeof(ISettingPage<>).Name) is not null)) { var id = settingPageType.FullName?.ToLower(); var title = settingPageType.Name; @@ -438,6 +442,7 @@ private void RegisterPage(IPluginContext pluginContext, IEnumerable(); var group = default(Type); + var domains = new List(); var section = SettingSection.Primary; var includeSubPaths = false; var hide = false; @@ -445,93 +450,132 @@ private void RegisterPage(IPluginContext pluginContext, IEnumerable !x.AttributeType.GetInterfaces().Contains(typeof(IEndpointAttribute)) && - !x.AttributeType.GetInterfaces().Contains(typeof(IPageAttribute))); + !x.AttributeType.GetInterfaces().Contains(typeof(IPageAttribute))); // determining attributes - foreach (var customAttribute in settingPageType.CustomAttributes - .Where(x => x.AttributeType.GetInterfaces().Contains(typeof(IEndpointAttribute)))) + foreach + ( + var attribute in settingPageType + .GetCustomAttributes(inherit: true) + .Where(x => x.GetType().GetInterfaces().Contains(typeof(IEndpointAttribute))) + ) { - if (customAttribute.AttributeType.GetInterfaces().Contains(typeof(ISegmentAttribute))) + var attributeType = attribute.GetType(); + + // segment attribute + if (attributeType.GetInterfaces().Contains(typeof(ISegmentAttribute))) { - segment = settingPageType.GetCustomAttributes(customAttribute.AttributeType, false).FirstOrDefault() as ISegmentAttribute; + segment = attribute as ISegmentAttribute; + continue; } - else if - ( - customAttribute.AttributeType.IsGenericType && - customAttribute.AttributeType.GetGenericTypeDefinition() == typeof(SettingGroupAttribute<>) - ) - { - group = customAttribute.AttributeType - .GenericTypeArguments - .FirstOrDefault(); + + // setting group (generic) + if (attributeType.IsGenericType && + attributeType.GetGenericTypeDefinition() == typeof(SettingGroupAttribute<>)) + { + group = attributeType.GetGenericArguments().FirstOrDefault(); + continue; } - else if (customAttribute.AttributeType == typeof(SettingSectionAttribute)) + + // setting section + if (attributeType == typeof(SettingSectionAttribute)) { - section = Enum.Parse(customAttribute.ConstructorArguments.FirstOrDefault().Value?.ToString()); + section = (attribute as SettingSectionAttribute)?.Section ?? default; + continue; } - else if (customAttribute.AttributeType == typeof(SettingHideAttribute)) + + // setting hide + if (attributeType == typeof(SettingHideAttribute)) { hide = true; + continue; } - else if (customAttribute.AttributeType.IsGenericType && customAttribute.AttributeType.GetGenericTypeDefinition() == typeof(WebIconAttribute<>)) + + // web icon attribute (generic) + if (attributeType.IsGenericType && + attributeType.GetGenericTypeDefinition() == typeof(WebIconAttribute<>)) { - var type = customAttribute.AttributeType.GenericTypeArguments.FirstOrDefault(); - icon ??= Activator.CreateInstance(type) as IIcon; + var iconType = attributeType.GetGenericArguments().FirstOrDefault(); + if (iconType != null) + { + icon ??= Activator.CreateInstance(iconType) as IIcon; + } + continue; } - else if (customAttribute.AttributeType == typeof(CacheAttribute)) + + // cache attribute + if (attributeType == typeof(CacheAttribute)) { cache = true; + continue; } - else if (customAttribute.AttributeType == typeof(IncludeSubPathsAttribute)) + + // include subpaths + if (attributeType == typeof(IncludeSubPathsAttribute)) { - includeSubPaths = Convert.ToBoolean(customAttribute.ConstructorArguments.FirstOrDefault().Value); + includeSubPaths = (attribute as IncludeSubPathsAttribute)?.IncludeSubPaths ?? false; + continue; } - else if - ( - customAttribute.AttributeType.Name == typeof(ConditionAttribute<>).Name && - customAttribute.AttributeType.Namespace == typeof(ConditionAttribute<>).Namespace - ) - { - var condition = customAttribute.AttributeType - .GenericTypeArguments - .FirstOrDefault(); - conditions.Add(Activator.CreateInstance(condition) as ICondition); + + // condition attribute (generic) + if (attributeType.IsGenericType && + attributeType.GetGenericTypeDefinition().Name == typeof(ConditionAttribute<>).Name && + attributeType.Namespace == typeof(ConditionAttribute<>).Namespace) + { + var conditionType = attributeType.GetGenericArguments().FirstOrDefault(); + if (conditionType != null) + { + conditions.Add(Activator.CreateInstance(conditionType) as ICondition); + } + continue; } } if (group == default) { - _httpServerContext?.Log.Warning - ( - I18N.Translate - ( - "webexpress.webcore:settingpagemanager.register.nogroup", - id - ) - ); + _httpServerContext?.Log.Warning(I18N.Translate("webexpress.webcore:settingpagemanager.register.nogroup", id)); } - foreach (var customAttribute in settingPageType.CustomAttributes.Where + foreach ( - x => x.AttributeType - .GetInterfaces() - .Contains(typeof(ISettingPageAttribute)) - )) + var attribute in settingPageType + .GetCustomAttributes(inherit: true) + .Where(x => x.GetType().GetInterfaces().Contains(typeof(ISettingPageAttribute))) + ) { - if (customAttribute.AttributeType == typeof(TitleAttribute)) + var attributeType = attribute.GetType(); + + // title attribute + if (attributeType == typeof(TitleAttribute)) { - title = customAttribute.ConstructorArguments - .FirstOrDefault().Value?.ToString(); + title = (attribute as TitleAttribute)?.Title; + continue; } - else if - ( - customAttribute.AttributeType.Name == typeof(ScopeAttribute<>).Name && - customAttribute.AttributeType.Namespace == typeof(ScopeAttribute<>).Namespace - ) - { - scopes.Add(customAttribute.AttributeType - .GenericTypeArguments - .FirstOrDefault()); + + // scope attribute (generic) + if (attributeType.IsGenericType && + attributeType.GetGenericTypeDefinition().Name == typeof(ScopeAttribute<>).Name && + attributeType.Namespace == typeof(ScopeAttribute<>).Namespace) + { + var scopeType = attributeType.GetGenericArguments().FirstOrDefault(); + if (scopeType != null) + { + scopes.Add(scopeType); + } + continue; + } + + // domain attribute (generic) + if (attributeType.IsGenericType && + attributeType.GetGenericTypeDefinition().Name == typeof(DomainAttribute<>).Name && + attributeType.Namespace == typeof(DomainAttribute<>).Namespace) + { + var domainType = attributeType.GetGenericArguments().FirstOrDefault(); + if (domainType != null) + { + domains.Add(domainType); + } + continue; } } @@ -543,19 +587,9 @@ private void RegisterPage(IPluginContext pluginContext, IEnumerableThe application context. /// The parameters to be considered for the uri. /// Returns the URI taking into account the context, or null if no valid URI is found. - IUri GetUri(IApplicationContext applicationContext, params Parameter[] parameters) + IUri GetUri(IApplicationContext applicationContext, params IParameter[] parameters) where TEndpoint : IEndpoint; /// @@ -48,7 +48,7 @@ IUri GetUri(IApplicationContext applicationContext, params Parameter[ /// The application context. /// The parameters to be considered for the uri. /// Returns the URI taking into account the context, or null if no valid URI is found. - IUri GetUri(Type endpointType, IApplicationContext applicationContext, params Parameter[] parameters); + IUri GetUri(Type endpointType, IApplicationContext applicationContext, params IParameter[] parameters); /// /// Returns the URI for this type based on the sitemap configuration, taking into account the specific context in which the URI is valid. diff --git a/src/WebExpress.WebCore/WebSitemap/Model/SitemapNode.cs b/src/WebExpress.WebCore/WebSitemap/Model/SitemapNode.cs index 6fa8516..55c157e 100644 --- a/src/WebExpress.WebCore/WebSitemap/Model/SitemapNode.cs +++ b/src/WebExpress.WebCore/WebSitemap/Model/SitemapNode.cs @@ -56,13 +56,13 @@ public SitemapNode Root /// Checks whether the node is the root. /// /// true if root, otherwise false. - public bool IsRoot => Parent == null; + public bool IsRoot => Parent is null; /// /// Checks whether the node is a leaf. /// /// true if a leaf, otherwise false. - public bool IsLeaf => !Children.Any(); + public bool IsLeaf => Children.Count == 0; /// /// Returns the path. @@ -78,7 +78,7 @@ public ICollection Path }; var parent = Parent; - while (parent != null) + while (parent is not null) { list.Add(parent); diff --git a/src/WebExpress.WebCore/WebSitemap/SearchContext.cs b/src/WebExpress.WebCore/WebSitemap/SearchContext.cs index 7ba3c4b..bff4d1e 100644 --- a/src/WebExpress.WebCore/WebSitemap/SearchContext.cs +++ b/src/WebExpress.WebCore/WebSitemap/SearchContext.cs @@ -16,7 +16,7 @@ public class SearchContext /// /// Returns the http context. /// - public HttpContext HttpContext { get; internal set; } + public IHttpContext HttpContext { get; internal set; } /// /// Returns the server context. diff --git a/src/WebExpress.WebCore/WebSitemap/SitemapManager.cs b/src/WebExpress.WebCore/WebSitemap/SitemapManager.cs index 7ae21c5..e6c897e 100644 --- a/src/WebExpress.WebCore/WebSitemap/SitemapManager.cs +++ b/src/WebExpress.WebCore/WebSitemap/SitemapManager.cs @@ -7,7 +7,7 @@ using WebExpress.WebCore.WebComponent; using WebExpress.WebCore.WebEndpoint; using WebExpress.WebCore.WebLog; -using WebExpress.WebCore.WebMessage; +using WebExpress.WebCore.WebParameter; using WebExpress.WebCore.WebSitemap.Model; using WebExpress.WebCore.WebUri; @@ -27,7 +27,7 @@ public sealed class SitemapManager : ISitemapManager, ISystemComponent /// Returns the side map. /// public IEnumerable SiteMap => _root.GetPreOrder() - .Where(x => x != null) + .Where(x => x is not null) .Select(x => x.EndpointContext); /// @@ -80,8 +80,8 @@ public void Refresh() } // endpoints - var resources = _componentHub.EndpointManager.Endpoints - .Where(x => x.Route != null) + var endpoints = _componentHub.EndpointManager.Endpoints + .Where(x => x.Route is not null) .Select(x => new { EndpointContext = x, @@ -89,7 +89,7 @@ public void Refresh() }) .OrderBy(x => x.PathSegments.Count()); - foreach (var item in resources) + foreach (var item in endpoints) { MergeSitemap(newSiteMapNode, CreateSiteMap ( @@ -115,12 +115,12 @@ public SearchResult SearchResource(Uri requestUri, SearchContext searchContext) var result = SearchNode ( _root, - new Queue(requestUri.Segments.Select(x => x == "/" ? x : (x.EndsWith('/') ? x[..^1] : x))), + new Queue(requestUri?.Segments.Select(x => x == "/" ? x : (x.EndsWith('/') ? x[..^1] : x))), new Queue(), searchContext ); - if (result != null && result.EndpointContext != null) + if (result is not null && result.EndpointContext is not null) { if (!result.EndpointContext.Conditions.Any() || result.EndpointContext.Conditions.All(x => x.Fulfillment(searchContext.HttpContext?.Request))) { @@ -133,14 +133,23 @@ public SearchResult SearchResource(Uri requestUri, SearchContext searchContext) } /// - /// Returns the URI for this type based on the sitemap configuration, taking into account the specific context - /// in which the URI is valid. + /// Returns the URI for this type based on the sitemap configuration, taking into account + /// the specific context in which the URI is valid. /// - /// The class from which the URI is to be determined. URI route must not have any dynamic components (such as '/a/guid/b'). - /// The application context. - /// The parameters to be considered for the uri. - /// Returns the URI taking into account the context, or null if no valid URI is found. - public IUri GetUri(IApplicationContext applicationContext, params Parameter[] parameters) + /// + /// The class from which the URI is to be determined. URI route must not have any dynamic + /// components (such as '/a/guid/b'). + /// + /// + /// The application context. + /// + /// + /// The parameters to be considered for the uri. + /// + /// + /// Returns the URI taking into account the context, or null if no valid URI is found. + /// + public IUri GetUri(IApplicationContext applicationContext, params IParameter[] parameters) where TEndpoint : IEndpoint { return GetUri(typeof(TEndpoint), applicationContext, parameters); @@ -153,23 +162,30 @@ public IUri GetUri(IApplicationContext applicationContext, params Par /// The application context. /// The parameters to be considered for the uri. /// Returns the URI taking into account the context, or null if no valid URI is found. - public IUri GetUri(Type endpointType, IApplicationContext applicationContext, params Parameter[] parameters) + public IUri GetUri(Type endpointType, IApplicationContext applicationContext, params IParameter[] parameters) { var endpointContexts = _componentHub.EndpointManager.GetEndpoints(endpointType, applicationContext); var node = _root.GetPreOrder() - .Where(x => endpointContexts.Contains(x.EndpointContext)) - .FirstOrDefault(); + .FirstOrDefault(x => endpointContexts.Contains(x.EndpointContext)); - return new UriEndpoint(_serverUri, node?.EndpointContext?.Route.PathSegments, null).SetParameters(parameters); + return new UriEndpoint(_serverUri, node?.EndpointContext?.Route.PathSegments, null).BindParameters(parameters); } /// - /// Returns the URI for this type based on the sitemap configuration, taking into account the specific context in which the URI is valid. + /// Returns the URI for this type based on the sitemap configuration, taking into account + /// the specific context in which the URI is valid. /// - /// The class from which the URI is to be determined. URI route must not have any dynamic components (such as '/a/guid/b'). - /// The endpoint context. - /// Returns the URI taking into account the context, or null if no valid URI is found. + /// + /// The class from which the URI is to be determined. URI route must not have any dynamic + /// components (such as '/a/guid/b'). + /// + /// + /// The endpoint context. + /// + /// + /// Returns the URI taking into account the context, or null if no valid URI is found. + /// public IUri GetUri(IEndpointContext endpointContext) where TEnpoint : IEndpoint { @@ -177,8 +193,13 @@ public IUri GetUri(IEndpointContext endpointContext) .Where(x => x.EndpointId.Equals(endpointContext.EndpointId)); var node = _root.GetPreOrder() - .Where(x => endpointContexts.Contains(x.EndpointContext)) - .FirstOrDefault(); + .FirstOrDefault(x => endpointContexts.Contains(x.EndpointContext)); + + if (node is null) + { + // fallback to the search by application context + return GetUri(endpointContext.ApplicationContext); + } return new UriEndpoint(_serverUri, node?.EndpointContext?.Route.PathSegments, null); } @@ -190,7 +211,7 @@ public IUri GetUri(IEndpointContext endpointContext) /// The endpoint context if found, otherwise null. public IEndpointContext GetEndpoint(IUri uri) { - if (uri == null || uri.Empty) + if (uri is null || uri.Empty) { return null; } @@ -229,7 +250,7 @@ IApplicationContext applicationContext var root = new SitemapNode() { PathSegment = new UriPathSegmentRoot() }; var next = CreateSiteMap(contextPathSegments, applicationContext, root); - if (next != null) + if (next is not null) { root.Children.Add(next); } @@ -255,7 +276,7 @@ SitemapNode parent { var pathSegment = contextPathSegments.Count != 0 ? contextPathSegments.Dequeue() : null; - if (pathSegment == null) + if (pathSegment is null) { return null; } @@ -296,7 +317,7 @@ IEndpointContext endpointContext var root = new SitemapNode() { PathSegment = new UriPathSegmentRoot() }; var next = CreateSiteMap(contextPathSegments, endpointContext, root); - if (next != null) + if (next is not null) { root.Children.Add(next); } @@ -326,7 +347,7 @@ private static SitemapNode CreateSiteMap { var pathSegment = contextPathSegments.Count != 0 ? contextPathSegments.Dequeue() : null; - if (pathSegment == null) + if (pathSegment is null) { return null; } @@ -393,17 +414,20 @@ SearchContext searchContext if (IsMatched(node, pathSegment)) { - var copy = node.PathSegment.Copy(); - if (copy is UriPathSegmentVariable variable) + + if (node.PathSegment is IUriPathSegmentVariable variable) { - variable.Value = pathSegment; + var copy = variable.Copy(pathSegment); + outPathSegments.Enqueue(copy); + } + else + { + outPathSegments.Enqueue(node.PathSegment.Copy()); } var type = node.EndpointContext?.GetType(); - outPathSegments.Enqueue(copy); - - if (nextPathSegment == null) + if (nextPathSegment is null) { return new SearchResult() { @@ -424,8 +448,8 @@ SearchContext searchContext else if ( node.IsLeaf - && nextPathSegment != null - && node.EndpointContext != null + && nextPathSegment is not null + && node.EndpointContext is not null && node.EndpointContext.IncludeSubPaths ) { @@ -464,7 +488,7 @@ SearchContext searchContext /// True if the path element matched, false otherwise. private static bool IsMatched(SitemapNode node, string pathSegement) { - if (node == null || string.IsNullOrWhiteSpace(pathSegement)) + if (node is null || string.IsNullOrWhiteSpace(pathSegement)) { return false; } diff --git a/src/WebExpress.WebCore/WebSocket/ISocket.cs b/src/WebExpress.WebCore/WebSocket/ISocket.cs new file mode 100644 index 0000000..3d208a2 --- /dev/null +++ b/src/WebExpress.WebCore/WebSocket/ISocket.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; +using WebExpress.WebCore.WebEndpoint; + +namespace WebExpress.WebCore.WebSocket +{ + /// + /// Defines the contract for WebSocket endpoints. + /// + public interface ISocket : IEndpoint, IDisposable + { + /// + /// Invoked after the websocket handshake has been accepted. + /// + /// The socket connection. + /// An asynchronous task. + Task OnConnectedAsync(ISocketConnection socketConnection); + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebSocket/ISocketConnection.cs b/src/WebExpress.WebCore/WebSocket/ISocketConnection.cs new file mode 100644 index 0000000..00b3b71 --- /dev/null +++ b/src/WebExpress.WebCore/WebSocket/ISocketConnection.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace WebExpress.WebCore.WebSocket +{ + /// + /// Defines the contract for a socket connection that supports sending and receiving both text and binary messages. + /// + public interface ISocketConnection : IDisposable + { + /// + /// Raised when a text message is received. + /// + event Action TextMessageReceived; + + /// + /// Raised when a binary message is received. + /// + event Action BinaryMessageReceived; + + /// + /// Raised when the WebSocket connection is closed or aborted. + /// + event Action Disconnected; + + /// + /// Sends a text message (UTF-8) over the socket connection. + /// + /// The text message to send. + /// Optional cancellation token. + Task SendTextAsync(string message, CancellationToken cancellation = default); + + /// + /// Sends binary data over the socket connection. + /// + /// The binary data to send. + /// Optional cancellation token. + Task SendBinaryAsync(byte[] data, CancellationToken cancellation = default); + + /// + /// Closes the socket connection gracefully. + /// + /// The reason for closing the connection. + /// Optional cancellation token. + Task CloseAsync(string reason = "closed", CancellationToken cancellation = default); + } +} diff --git a/src/WebExpress.WebCore/WebSocket/ISocketContext.cs b/src/WebExpress.WebCore/WebSocket/ISocketContext.cs new file mode 100644 index 0000000..7042eb0 --- /dev/null +++ b/src/WebExpress.WebCore/WebSocket/ISocketContext.cs @@ -0,0 +1,36 @@ +using WebExpress.WebCore.WebEndpoint; + +namespace WebExpress.WebCore.WebSocket +{ + /// + /// Defines the context for a WebSocket endpoint, providing access to configuration and metadata + /// that are relevant during the websocket lifecycle. + /// + public interface ISocketContext : IEndpointContext + { + /// + /// Returns the name of the WebSocket subprotocol that is supported by the connection. + /// + string SupportedSubProtocol { get; } + + /// + /// Returns the default WebSocket message type used by this endpoint when + /// sending data. Implementations may choose + /// for JSON or human-readable content, or + /// for binary payloads. + /// + SocketMessageType MessageType { get; } + + /// + /// Returns the maximum allowed message size in bytes, or null when the endpoint imposes no limit. + /// servers and hosts may use this to protect against excessively large frames. + /// + ulong? MaxMessageSize { get; } + + /// + /// Indicates whether this websocket endpoint requires an authenticated client. + /// the host can check this and reject upgrades when authentication is missing. + /// + bool RequiresAuthentication { get; } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebSocket/ISocketManager.cs b/src/WebExpress.WebCore/WebSocket/ISocketManager.cs new file mode 100644 index 0000000..847f076 --- /dev/null +++ b/src/WebExpress.WebCore/WebSocket/ISocketManager.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WebExpress.WebCore.WebApplication; +using WebExpress.WebCore.WebComponent; +using WebExpress.WebCore.WebMessage; +using WebExpress.WebCore.WebPlugin; + +namespace WebExpress.WebCore.WebSocket +{ + /// + /// The socket manager manages socket endpoints (see RFC 6455 – The WebSocket Protocol) which can be called with a URI. + /// + public interface ISocketManager : IComponentManager + { + /// + /// An event that fires when a websocket context is added. + /// + event EventHandler AddSocket; + + /// + /// An event that fires when a websocket context is removed. + /// + event EventHandler RemoveSocket; + + /// + /// Returns all registered websocket contexts. + /// + IEnumerable Sockets { get; } + + /// + /// Returns an enumeration of all websocket contexts provided by a plugin. + /// + /// A context of a plugin whose sockets are to be returned. + /// An enumeration of websocket contexts. + IEnumerable GetSockets(IPluginContext pluginContext); + + /// + /// Returns an enumeration of websocket contexts filtered by endpoint type. + /// + /// The websocket endpoint type. + /// An enumeration of websocket contexts. + IEnumerable GetSockets() + where T : ISocket; + + /// + /// Returns an enumeration of websocket contexts filtered by endpoint type. + /// + /// The websocket endpoint type. + /// An enumeration of websocket contexts. + IEnumerable GetSockets(Type socketType); + + /// + /// Returns an enumeration of websocket contexts filtered by endpoint type and application context. + /// + /// The websocket endpoint type. + /// The context of the application. + /// An enumeration of websocket contexts. + IEnumerable GetSockets(Type socketType, IApplicationContext applicationContext); + + /// + /// Returns an enumeration of websocket contexts filtered by endpoint type and application context. + /// + /// The websocket endpoint type. + /// The context of the application. + /// An enumeration of websocket contexts. + IEnumerable GetSockets(IApplicationContext applicationContext) where T : ISocket; + + /// + /// Returns the websocket context by application context and socket id. + /// + /// The context of the application. + /// The socket id. + /// A websocket context or null. + ISocketContext GetSocket(IApplicationContext applicationContext, string socketId); + + /// + /// Returns the websocket context by application id and socket id. + /// + /// The application id. + /// The socket id. + /// A websocket context or null. + ISocketContext GetSocket(string applicationId, string socketId); + + /// + /// Handles the complete lifecycle of a WebSocket connection for the given HTTP context. + /// Performs the server-side upgrade, invokes connection callbacks, processes incoming + /// WebSocket frames, parses messages, dispatches them to the socket handler, and triggers + /// disconnect and error callbacks as required. + /// + /// + /// The current HTTP context containing the WebSocket upgrade request and connection metadata. + /// + /// + /// Context information for the WebSocket endpoint, including supported subprotocols + /// and application-level metadata. + /// + /// + /// A task that represents the asynchronous handling of the WebSocket connection. + /// + Task HandleConnectionAsync(IHttpContext httpContext, ISocketContext socketContext); + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebSocket/Model/SocketDictionary.cs b/src/WebExpress.WebCore/WebSocket/Model/SocketDictionary.cs new file mode 100644 index 0000000..5f46087 --- /dev/null +++ b/src/WebExpress.WebCore/WebSocket/Model/SocketDictionary.cs @@ -0,0 +1,314 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WebExpress.WebCore.WebApplication; +using WebExpress.WebCore.WebPlugin; + +namespace WebExpress.WebCore.WebSocket.Model +{ + /// + /// Represents a dictionary that maps plugin contexts to application contexts, socket types, and socket items. + /// key = plugin context + /// value = application context { key = socket type, value = socket item } + /// + internal class SocketDictionary + { + private readonly Dictionary>> _dict = new Dictionary>>(); + + /// + /// Returns all socket contexts. + /// + public IEnumerable All => _dict.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Select(x => x.SocketContext); + + /// + /// Adds a socket item to the dictionary. + /// + /// The plugin context. + /// The application context. + /// The socket item. + /// + /// True if the socket item was added successfully, false if an element with the same status code already exists. + /// + public bool AddSocketItem(IPluginContext pluginContext, IApplicationContext applicationContext, SocketItem socketItem) + { + var type = socketItem.SocketClass; + + if (type.GetInterface(typeof(ISocket).Name) is null) + { + return false; + } + + if (!_dict.TryGetValue(pluginContext, out Dictionary> appContextDict)) + { + appContextDict = new Dictionary>(); + _dict[pluginContext] = appContextDict; + } + + if (!appContextDict.TryGetValue(applicationContext, out Dictionary socketDict)) + { + socketDict = new Dictionary(); + appContextDict[applicationContext] = socketDict; + } + + if (!socketDict.ContainsKey(type)) + { + socketDict[type] = socketItem; + return true; + } + + return false; + } + + /// + /// Removes all socket items from the dictionary for the given plugin context. + /// + /// The plugin context. + public IEnumerable RemoveSocket(IPluginContext pluginContext) + { + var removed = GetSocketItems(pluginContext); + + _dict.Remove(pluginContext); + + foreach (var item in removed) + { + item.Dispose(); + } + + return removed.Select(x => x.SocketContext); + } + + /// + /// Removes all socket items from the dictionary for the given application context. + /// + /// The application context. + public IEnumerable RemoveSocket(IApplicationContext applicationContext) + { + var removed = GetSocketItems(applicationContext); + + foreach (var applicationDict in _dict.Values) + { + applicationDict.Remove(applicationContext); + } + + foreach (var item in removed) + { + item.Dispose(); + } + + return removed.Select(x => x.SocketContext); + } + + /// + /// Returns the socket items associated with the specified plugin context. + /// + /// + /// The plugin context to retrieve docket items for. + /// + /// + /// An IEnumerable of associated with the specified + /// application context. + /// + public IEnumerable GetSocketItems(IPluginContext pluginContext) + { + return _dict.Where(x => x.Key.Equals(pluginContext)) + .Select(x => x.Value) + .SelectMany(x => x.Values) + .SelectMany(x => x.Values); + } + + /// + /// Returns the items associated with the specified application context. + /// + /// + /// The application context to retrieve items for. + /// + /// + /// An IEnumerable of associated with the specified + /// application context. + /// + public IEnumerable GetSocketItems(IApplicationContext applicationContext) + { + return _dict.Values + .SelectMany(x => x) + .Where(x => x.Key.Equals(applicationContext)) + .SelectMany(x => x.Value) + .Select(x => x.Value); + } + + /// + /// Returns the socket item associated with the specified socket context. + /// + /// + /// The context of the socket to retrieve. + /// + /// + /// The associated with the specified socket context, or + /// null if no such item exists. + /// + public SocketItem GetSocketItem(ISocketContext socketContext) + { + return _dict.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .FirstOrDefault(x => x.SocketContext.Equals(socketContext)); + } + + /// + /// Returns the socket items from the dictionary for a specific application + /// context and socket type. + /// + /// The application context. + /// The type of the socket. + /// An IEnumerable of socket items. + public IEnumerable GetSocketItems(IApplicationContext applicationContext, Type socketType) + { + if (!typeof(ISocket).IsAssignableFrom(socketType)) + { + return Enumerable.Empty(); + } + + if (_dict.ContainsKey(applicationContext?.PluginContext)) + { + var appContextDict = _dict[applicationContext?.PluginContext]; + + if (appContextDict.TryGetValue(applicationContext, out Dictionary socketDict)) + { + if (socketDict.TryGetValue(socketType, out SocketItem value)) + { + return [value]; + } + } + } + + return Enumerable.Empty(); + } + + /// + /// Returns an enumeration of all containing socket contexts of a plugin. + /// + /// + /// A context of a plugin whose sockets are to be registered. + /// + /// An enumeration of socket contexts. + public IEnumerable GetSockets(IPluginContext pluginContext) + { + if (_dict.TryGetValue(pluginContext, out var pluginResources)) + { + return pluginResources + .SelectMany(x => x.Value) + .Select(x => x.Value.SocketContext); + } + + return Enumerable.Empty(); + } + + /// + /// Returns an enumeration of socket contexts. + /// + /// The socket type. + /// An enumeration of socket contexts. + public IEnumerable GetSockets(Type socketType) + { + return _dict.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.SocketClass.Equals(socketType)) + .Select(x => x.SocketContext); + } + + /// + /// Returns an enumeration of socket contexts. + /// + /// The socket type. + /// The context of the application. + /// An enumeration of socket contexts. + public IEnumerable GetSockets(Type socketType, IApplicationContext applicationContext) + { + return _dict.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.SocketClass.Equals(socketType)) + .Where(x => x.SocketContext.ApplicationContext.Equals(applicationContext)) + .Select(x => x.SocketContext); + } + + /// + /// Returns an enumeration of socket contexts. + /// + /// The socket type. + /// The context of the application. + /// An enumeration of socket contexts. + public IEnumerable GetSockets(IApplicationContext applicationContext) where TSocket : ISocket + { + return _dict.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.SocketClass.Equals(typeof(TSocket))) + .Where(x => x.SocketContext.ApplicationContext.Equals(applicationContext)) + .Select(x => x.SocketContext); + } + + /// + /// Returns the socket context. + /// + /// The context of the application. + /// The socket id. + /// An socket context or null. + public ISocketContext GetSocket(IApplicationContext applicationContext, string socketId) + { + return _dict.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.SocketContext.ApplicationContext.Equals(applicationContext)) + .Where(x => x.SocketContext.EndpointId.Equals(socketId)) + .Select(x => x.SocketContext) + .FirstOrDefault(); + } + + /// + /// Returns the socket context. + /// + /// The application id. + /// The socket id. + /// An socket context or null. + public ISocketContext GetSocket(string applicationId, string socketId) + { + return _dict.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Where(x => x.SocketContext.ApplicationContext.ApplicationId.Equals(applicationId)) + .Where(x => x.SocketContext.EndpointId.Equals(socketId)) + .Select(x => x.SocketContext) + .FirstOrDefault(); + } + + /// + /// Checks if the dictionary contains the specified plugin context. + /// + /// The plugin context to check for. + /// + /// True if the plugin context exists in the dictionary, otherwise false. + /// + public bool Contains(IPluginContext pluginContext) + { + return _dict.ContainsKey(pluginContext); + } + + /// + /// Checks if the dictionary contains the specified plugin context and application context. + /// + /// The plugin context to check for. + /// The application context to check for. + /// + /// True if the plugin context and application context exist in the dictionary, + /// otherwise false. + /// + public bool Contains(IPluginContext pluginContext, IApplicationContext applicationContext) + { + return _dict.TryGetValue(pluginContext, out var appDict) && appDict.ContainsKey(applicationContext); + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebSocket/Model/SocketItem.cs b/src/WebExpress.WebCore/WebSocket/Model/SocketItem.cs new file mode 100644 index 0000000..85f5142 --- /dev/null +++ b/src/WebExpress.WebCore/WebSocket/Model/SocketItem.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using WebExpress.WebCore.WebApplication; +using WebExpress.WebCore.WebComponent; +using WebExpress.WebCore.WebCondition; +using WebExpress.WebCore.WebEndpoint; +using WebExpress.WebCore.WebPlugin; + +namespace WebExpress.WebCore.WebSocket.Model +{ + /// + /// A socket element that contains meta information about a socket endpoint. + /// + internal class SocketItem : IDisposable + { + /// + /// Returns the endpoint id. + /// + public IComponentId EndpointId { get; internal set; } + + /// + /// Returns the associated plugin context. + /// + public IPluginContext PluginContext { get; internal set; } + + /// + /// Returns the corresponding application context. + /// + public IApplicationContext ApplicationContext { get; internal set; } + + /// + /// Returns or sets the type of socket class. + /// + public Type SocketClass { get; set; } + + /// + /// Returns the name of the WebSocket subprotocol that is supported by the connection. + /// + public string SupportedSubProtocol { get; set; } + + /// + /// Returns the default WebSocket message type used by this endpoint when + /// sending data. Implementations may choose + /// for JSON or human-readable content, or + /// for binary payloads. + /// + public SocketMessageType MessageType { get; set; } + + /// + /// Returns the maximum allowed message size in bytes, or null when the endpoint imposes no limit. + /// servers and hosts may use this to protect against excessively large frames. + /// + public ulong? MaxMessageSize { get; set; } + + /// + /// Returns the conditions that must be met for the resource to be active. + /// + public IEnumerable Conditions { get; set; } + + /// + /// Returns whether the resource is created once and reused each time it is called. + /// + public bool Cache { get; set; } + + /// + /// Returns the attributes associated with the socket. + /// + public IEnumerable Attributes { get; internal set; } + + /// + /// Returns the socket context. + /// note: reuses ISocketContext to remain compatible with existing contexts; + /// replace with a dedicated socket context type if one exists. + /// + public ISocketContext SocketContext { get; internal set; } + + /// + /// Returns or sets the instance of the socket endpoint, if the endpoint is cached, otherwise null. + /// + public ISocket Instance { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The endpoint manager responsible for managing endpoints. + internal SocketItem(IEndpointManager endpointManager) + { + } + + /// + /// Performs application-specific tasks related to sharing, returning, or resetting unmanaged resources. + /// + public void Dispose() + { + } + + /// + /// Convert the resource element to a string. + /// + /// The resource element in its string representation. + public override string ToString() + { + return $"Socket: '{SocketContext?.EndpointId}'"; + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebSocket/SocketCloseInfo.cs b/src/WebExpress.WebCore/WebSocket/SocketCloseInfo.cs new file mode 100644 index 0000000..6caa85f --- /dev/null +++ b/src/WebExpress.WebCore/WebSocket/SocketCloseInfo.cs @@ -0,0 +1,38 @@ +using System.Net.WebSockets; + +namespace WebExpress.WebCore.WebSocket +{ + /// + /// Represents information about the reason a socket connection was closed, + /// including the close status and an optional description. + /// + public class SocketCloseInfo + { + /// + /// Returns the status that indicates the reason the socket was closed. + /// + public WebSocketCloseStatus Status { get; } + + /// + /// Returns the description associated with this instance. + /// + public string Description { get; } + + /// + /// Initializes a new instance of the SocketCloseInfo class with the specified + /// close status and optional description. + /// + /// + /// The status code indicating the reason the socket was closed. + /// + /// + /// An optional textual description providing additional details about the + /// socket closure. May be null. + /// + public SocketCloseInfo(WebSocketCloseStatus status, string description) + { + Status = status; + Description = description; + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebSocket/SocketConnection.cs b/src/WebExpress.WebCore/WebSocket/SocketConnection.cs new file mode 100644 index 0000000..b102d16 --- /dev/null +++ b/src/WebExpress.WebCore/WebSocket/SocketConnection.cs @@ -0,0 +1,217 @@ +using System; +using System.IO; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace WebExpress.WebCore.WebSocket +{ + /// + /// Encapsulates a System.Net.WebSockets.WebSocket instance and supports both text and + /// binary messages. + /// + internal class SocketConnection : ISocketConnection, IDisposable + { + private readonly System.Net.WebSockets.WebSocket _socket; + private readonly CancellationTokenSource _cts = new(); + private readonly int _bufferSize; + private bool _disconnectRaised; + + /// + /// Raised when a text message is received. + /// + public event Action TextMessageReceived; + + /// + /// Raised when a binary message is received. + /// + public event Action BinaryMessageReceived; + + /// + /// Raised when the WebSocket connection is closed or aborted. + /// + public event Action Disconnected; + + /// + /// Initializes a new instance using a raw network stream and the provided socket context. + /// + /// + /// The underlying network stream used to create the WebSocket instance. + /// + /// + /// Provides WebSocket configuration such as supported subprotocols. + /// + /// + /// The size of the internal receive buffer in bytes. The default is 8192. + /// + public SocketConnection(Stream networkStream, ISocketContext socketContext, int bufferSize = 8192) + { + var options = new WebSocketCreationOptions() + { + IsServer = true, + SubProtocol = socketContext.SupportedSubProtocol + }; + + _socket = System.Net.WebSockets.WebSocket.CreateFromStream(networkStream, options) + ?? throw new ArgumentNullException(nameof(networkStream)); + + _bufferSize = bufferSize; + } + + /// + /// Sends a text message (UTF-8) over the WebSocket. + /// + /// The text message to send. + /// A token used to cancel the send operation. + /// + /// A task that represents the asynchronous send operation. + /// + public async Task SendTextAsync(string message, CancellationToken cancellation = default) + { + var buffer = Encoding.UTF8.GetBytes(message); + await _socket.SendAsync(buffer, WebSocketMessageType.Text, true, cancellation); + } + + /// + /// Sends binary data over the WebSocket. + /// + /// The binary payload to send. + /// A token used to cancel the send operation. + /// + /// A task that represents the asynchronous send operation. + /// + public async Task SendBinaryAsync(byte[] data, CancellationToken cancellation = default) + { + await _socket.SendAsync(data, WebSocketMessageType.Binary, true, cancellation); + } + + /// + /// Internal loop for receiving messages. Invokes the appropriate events for each message. + /// + /// + /// A task that represents the asynchronous receive loop. + /// + internal async Task ReceiveLoopAsync() + { + var buffer = new byte[_bufferSize]; + using var builder = new MemoryStream(); + + while (_socket.State == WebSocketState.Open && !_cts.IsCancellationRequested) + { + WebSocketReceiveResult result; + + try + { + result = await _socket.ReceiveAsync(buffer, _cts.Token); + } + catch + { + RaiseDisconnected(WebSocketCloseStatus.InternalServerError, "connection aborted"); + break; + } + + if (result.MessageType == WebSocketMessageType.Close) + { + RaiseDisconnected(result.CloseStatus, result.CloseStatusDescription); + break; + } + + // accumulate fragments + builder.Write(buffer, 0, result.Count); + + if (result.EndOfMessage) + { + if (result.MessageType == WebSocketMessageType.Text) + { + var message = Encoding.UTF8.GetString(builder.ToArray()); + TextMessageReceived?.Invoke(message); + } + else if (result.MessageType == WebSocketMessageType.Binary) + { + BinaryMessageReceived?.Invoke(builder.ToArray()); + } + + builder.SetLength(0); + } + } + } + + /// + /// Asynchronously closes the underlying WebSocket connection. + /// + /// + /// A textual description sent to the client as part of the close frame. + /// + /// + /// A token used to cancel the close operation. + /// + /// + /// A task that represents the asynchronous close operation. + /// + public async Task CloseAsync(string reason = "closed", CancellationToken cancellation = default) + { + _cts.Cancel(); + + if (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.CloseReceived) + { + try + { + await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, reason, cancellation); + } + catch + { + // ignore + } + } + + RaiseDisconnected(WebSocketCloseStatus.NormalClosure, reason); + } + + /// + /// Ensures the Disconnected event is raised exactly once. + /// + /// The WebSocket close status. + /// The reason for the disconnection. + private void RaiseDisconnected(WebSocketCloseStatus? status, string reason) + { + if (_disconnectRaised) + { + return; + } + + _disconnectRaised = true; + Disconnected?.Invoke(new SocketCloseInfo(status ?? WebSocketCloseStatus.Empty, reason)); + } + + /// + /// Releases all resources used by the socket. + /// + public void Dispose() + { + _cts.Cancel(); + + try + { + if (_socket.State == WebSocketState.Open) + { + _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "disposed", CancellationToken.None) + .Wait(50); + } + } + catch + { + // ignore + } + + RaiseDisconnected(WebSocketCloseStatus.NormalClosure, "disposed"); + + if (_socket is IDisposable d) + { + d.Dispose(); + } + + _cts.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebSocket/SocketContext.cs b/src/WebExpress.WebCore/WebSocket/SocketContext.cs new file mode 100644 index 0000000..5fa2031 --- /dev/null +++ b/src/WebExpress.WebCore/WebSocket/SocketContext.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using WebExpress.WebCore.WebApplication; +using WebExpress.WebCore.WebComponent; +using WebExpress.WebCore.WebCondition; +using WebExpress.WebCore.WebEndpoint; +using WebExpress.WebCore.WebPlugin; + +namespace WebExpress.WebCore.WebSocket +{ + /// + /// Default implementation of ISocketContext. + /// this class provides a simple, mutable context object usable for websocket endpoints. + /// + public class SocketContext : ISocketContext + { + /// + /// Returns the associated plugin context. + /// + public IPluginContext PluginContext { get; internal set; } + + /// + /// Returns the corresponding application context. + /// + public IApplicationContext ApplicationContext { get; internal set; } + + /// + /// Returns the conditions that must be met for the resource to be active. + /// + public IEnumerable Conditions { get; internal set; } = []; + + /// + /// Returns the endpoint id. + /// + public IComponentId EndpointId { get; internal set; } + + /// + /// Returns the name of the WebSocket subprotocol that is supported by the connection. + /// + public string SupportedSubProtocol { get; internal set; } + + /// + /// Returns the default WebSocket message type used by this endpoint when + /// sending data. Implementations may choose + /// for JSON or human-readable content, or + /// for binary payloads. + /// + public SocketMessageType MessageType { get; set; } + + /// + /// Maximum allowed message size in bytes, or null when no limit is imposed. + /// + public ulong? MaxMessageSize { get; set; } + + /// + /// Indicates whether this websocket endpoint requires an authenticated client. + /// + public bool RequiresAuthentication { get; set; } + + /// + /// Returns whether the resource is created once and reused each time it is called. + /// + public bool Cache { get; internal set; } + + /// + /// Returns or sets whether all subpaths should be taken into sitemap. + /// + public bool IncludeSubPaths { get; internal set; } + + /// + /// Returns the internal routing path for the endpoint. + /// + public IRoute Route { get; internal set; } + + /// + /// Returns the attributes associated with the page. + /// + public IEnumerable Attributes { get; internal set; } + + /// + /// Creates a new instance of DefaultWebSocketContext. + /// + public SocketContext() + { + } + + /// + /// Returns a short diagnostic representation. + /// + /// string + public override string ToString() + { + // return a compact representation with name and supported subprotocols + return $"{EndpointId} (protocols: {SupportedSubProtocol})"; + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebSocket/SocketHandshakeException.cs b/src/WebExpress.WebCore/WebSocket/SocketHandshakeException.cs new file mode 100644 index 0000000..7718d81 --- /dev/null +++ b/src/WebExpress.WebCore/WebSocket/SocketHandshakeException.cs @@ -0,0 +1,39 @@ +using System; + +namespace WebExpress.WebCore.WebSocket +{ + /// + /// Represents an error that occurs when a WebSocket handshake request + /// is invalid or does not meet the required protocol specifications. + /// + public class SocketHandshakeException : Exception + { + /// + /// Initializes a new instance of the class + /// with the specified error message. + /// + /// + /// A descriptive message that explains the reason for the handshake failure. + /// + public SocketHandshakeException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class + /// with the specified error message. + /// + /// + /// A descriptive message that explains the reason for the handshake failure. + /// + /// + /// The exception that is the cause of the current exception, or a null + /// reference if no inner exception is specified. + /// + public SocketHandshakeException(string message, Exception exception) + : base(message, exception) + { + } + } +} diff --git a/src/WebExpress.WebCore/WebSocket/SocketManager.cs b/src/WebExpress.WebCore/WebSocket/SocketManager.cs new file mode 100644 index 0000000..72f663d --- /dev/null +++ b/src/WebExpress.WebCore/WebSocket/SocketManager.cs @@ -0,0 +1,584 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using WebExpress.WebCore.Internationalization; +using WebExpress.WebCore.WebApplication; +using WebExpress.WebCore.WebAttribute; +using WebExpress.WebCore.WebComponent; +using WebExpress.WebCore.WebCondition; +using WebExpress.WebCore.WebEndpoint; +using WebExpress.WebCore.WebMessage; +using WebExpress.WebCore.WebPlugin; +using WebExpress.WebCore.WebSocket.Model; + +namespace WebExpress.WebCore.WebSocket +{ + /// + /// The socket manager manages socket endpoints (see RFC 6455 – The WebSocket Protocol) + /// which can be called with a URI. + /// + public sealed class SocketManager : ISocketManager, ISystemComponent + { + private const string _webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + private readonly IComponentHub _componentHub; + private readonly IHttpServerContext _httpServerContext; + private readonly SocketDictionary _dictionary = new(); + + /// + /// An event that fires when a socket context is added. + /// + public event EventHandler AddSocket; + + /// + /// An event that fires when a socket context is removed. + /// + public event EventHandler RemoveSocket; + + /// + /// Returns all socket contexts. + /// + public IEnumerable Sockets => _dictionary.All; + + /// + /// Initializes a new instance of the class. + /// + /// The component hub. + /// The reference to the context of the host. + [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Used via Reflection.")] + private SocketManager(IComponentHub componentHub, IHttpServerContext httpServerContext) + { + _componentHub = componentHub; + + _componentHub.PluginManager.AddPlugin += OnAddPlugin; + _componentHub.PluginManager.RemovePlugin += OnRemovePlugin; + _componentHub.ApplicationManager.AddApplication += OnAddApplication; + _componentHub.ApplicationManager.RemoveApplication += OnRemoveApplication; + + var endpointRegistration = new EndpointRegistration() + { + EndpointResolver = (type, applicationContext) => applicationContext is not null + ? GetSockets(type, applicationContext) + : GetSockets(type), + EndpointsResolver = () => Sockets + }; + + AddSocket += (sender, e) => endpointRegistration.AddEndpoint?.Invoke(sender, e); + RemoveSocket += (sender, e) => endpointRegistration.RemoveEndpoint?.Invoke(sender, e); + + _componentHub.EndpointManager.Register(endpointRegistration); + + _httpServerContext = httpServerContext; + + _httpServerContext.Log.Debug + ( + I18N.Translate("webexpress.webcore:socketmanager.initialization") + ); + } + + /// + /// Handles the complete lifecycle of a WebSocket connection for the given HTTP context. + /// Performs the server-side upgrade, invokes connection callbacks, processes incoming + /// WebSocket frames, parses messages, dispatches them to the socket handler, and triggers + /// disconnect and error callbacks as required. + /// + /// + /// The current HTTP context containing the WebSocket upgrade request and connection metadata. + /// + /// + /// Context information for the WebSocket endpoint, including supported subprotocols + /// and application-level metadata. + /// + /// + /// A task that represents the asynchronous handling of the WebSocket connection. + /// + public async Task HandleConnectionAsync(IHttpContext httpContext, ISocketContext socketContext) + { + // WebSocket handshake + var connectionId = Guid.NewGuid(); + var secWebSocketKey = httpContext.Request.Header.SecWebSocketKey; + var secWebSocketAccept = ComputeWebSocketAcceptKey(secWebSocketKey); + + var headerFeatures = httpContext.Features.Get(); + headerFeatures.Headers.Append("Upgrade", "websocket"); + headerFeatures.Headers.Append("Connection", "Upgrade"); + headerFeatures.Headers.Append("Sec-WebSocket-Accept", secWebSocketAccept); + if (!string.IsNullOrWhiteSpace(socketContext.SupportedSubProtocol)) + { + headerFeatures.Headers.Append("Sec-WebSocket-Protocol", socketContext.SupportedSubProtocol); + } + + var upgradeFeature = httpContext.Features.Get() + ?? throw new SocketHandshakeException("Upgrade feature not supported. WebSocket handshake aborted."); + + Stream networkStream; + try + { + networkStream = await upgradeFeature.UpgradeAsync(); + } + catch (Exception ex) + { + throw new SocketHandshakeException("WebSocket upgrade failed.", ex); + } + + // create application socket instance + var instance = CreateSocketInstance(connectionId, socketContext, httpContext.Request); + var socketConnection = new SocketConnection(networkStream, socketContext); + + await instance.OnConnectedAsync(socketConnection); + + await socketConnection.ReceiveLoopAsync(); + + instance.Dispose(); + } + + /// + /// Returns an enumeration of all socket contexts provided by a plugin. + /// + /// + /// A context of a plugin whose sockets are to be returned. + /// + /// An enumeration of socket contexts. + public IEnumerable GetSockets(IPluginContext pluginContext) + { + return _dictionary.GetSockets(pluginContext); + } + + /// + /// Returns an enumeration of socket contexts filtered by endpoint type. + /// + /// The socket endpoint type. + /// An enumeration of socket contexts. + public IEnumerable GetSockets() + where TSocket : ISocket + { + return GetSockets(typeof(TSocket)); + } + + /// + /// Returns an enumeration of socket contexts filtered by endpoint type. + /// + /// The socket endpoint type. + /// An enumeration of socket contexts. + public IEnumerable GetSockets(Type socketType) + { + return _dictionary.GetSockets(socketType); + } + + /// + /// Returns an enumeration of socket contexts filtered by endpoint type + /// and application context. + /// + /// The socket endpoint type. + /// The context of the application. + /// An enumeration of socket contexts. + public IEnumerable GetSockets(Type socketType, IApplicationContext applicationContext) + { + return _dictionary.GetSockets(socketType, applicationContext); + } + + /// + /// Returns an enumeration of socket contexts filtered by endpoint type and + /// application context. + /// + /// The socket endpoint type. + /// The context of the application. + /// An enumeration of socket contexts. + public IEnumerable GetSockets(IApplicationContext applicationContext) where TSocket : ISocket + { + return _dictionary.GetSockets(applicationContext); + } + + /// + /// Returns the socket context by application context and socket id. + /// + /// The context of the application. + /// The socket id. + /// A socket context or null. + public ISocketContext GetSocket(IApplicationContext applicationContext, string socketId) + { + return _dictionary.GetSocket(applicationContext, socketId); + } + + /// + /// Returns the socket context by application id and socket id. + /// + /// The application id. + /// The socket id. + /// A socket context or null. + public ISocketContext GetSocket(string applicationId, string socketId) + { + return _dictionary.GetSocket(applicationId, socketId); + } + + /// + /// Creates a new socket endpoint instance and returns it. + /// If an instance is cached, the cached instance is returned. + /// + /// The unique connection Id. + /// The context used for socket creation. + /// The request. + /// The created or cached endpoint instance. + private ISocket CreateSocketInstance + ( + Guid connectionId, + ISocketContext socketContext, + IRequest request + ) + { + var resourceItem = _dictionary.GetSocketItem(socketContext); + + if (resourceItem is not null && resourceItem.Instance is null) + { + var instance = ComponentActivator.CreateInstance + ( + resourceItem.SocketClass, + socketContext, + _httpServerContext, + _componentHub, + socketContext.ApplicationContext, + connectionId, + request + ); + + if (resourceItem.Cache) + { + resourceItem.Instance = instance; + } + + return instance; + } + + return resourceItem?.Instance; + } + + /// + /// Discovers and binds sockets to an application. + /// + /// The context of the plugin whose sockets are to be associated. + private void Register(IPluginContext pluginContext) + { + if (_dictionary.Contains(pluginContext)) + { + return; + } + + Register(pluginContext, _componentHub.ApplicationManager.GetApplications(pluginContext)); + } + + /// + /// Discovers and binds sockets to an application. + /// + /// The context of the application whose sockets are to be associated. + private void Register(IApplicationContext applicationContext) + { + foreach (var pluginContext in _componentHub.PluginManager.GetPlugins(applicationContext)) + { + if (_dictionary.Contains(pluginContext, applicationContext)) + { + continue; + } + + Register(pluginContext, [applicationContext]); + } + } + + /// + /// Registers sockets for a given plugin and application contexts. + /// + /// The plugin context. + /// The application context(s). + private void Register(IPluginContext pluginContext, IEnumerable applicationContexts) + { + var assembly = pluginContext?.Assembly; + + foreach (var socketType in assembly.GetTypes() + .Where(x => x.IsClass == true && x.IsSealed && x.IsPublic) + .Where(x => x.GetInterfaces().Where(i => i == typeof(ISocket)).Any())) + { + var id = socketType.FullName?.ToLower(); + var includeSubPaths = false; + var conditions = new List(); + var cache = false; + var subProtocol = ""; + var messageType = SocketMessageType.Text; + var maxMessageSize = (ulong?)null; + var attributes = socketType.CustomAttributes + .Where(x => !x.AttributeType.GetInterfaces().Contains(typeof(IEndpointAttribute))); + + foreach + ( + var attribute in socketType + .GetCustomAttributes(inherit: true) + .Where(x => x.GetType().GetInterfaces().Contains(typeof(IEndpointAttribute))) + ) + { + var attributeType = attribute.GetType(); + + // include subpaths + if (attributeType == typeof(IncludeSubPathsAttribute)) + { + includeSubPaths = (attribute as IncludeSubPathsAttribute)?.IncludeSubPaths + ?? false; + continue; + } + + // condition attribute (generic) + if (attributeType.IsGenericType && + attributeType.GetGenericTypeDefinition().Name == typeof(ConditionAttribute<>).Name && + attributeType.Namespace == typeof(ConditionAttribute<>).Namespace) + { + var conditionType = attributeType.GetGenericArguments().FirstOrDefault(); + if (conditionType != null) + { + conditions.Add(Activator.CreateInstance(conditionType) as ICondition); + } + continue; + } + + // cache attribute + if (attributeType == typeof(CacheAttribute)) + { + cache = true; + continue; + } + } + + foreach + ( + var attribute in socketType + .GetCustomAttributes(inherit: true) + .Where(x => x.GetType().GetInterfaces().Contains(typeof(ISocketAttribute))) + ) + { + var attributeType = attribute.GetType(); + + // MESSAGE TYPE + if (attributeType == typeof(MessageTypeAttribute)) + { + messageType = (attribute as MessageTypeAttribute)?.MessageType ?? default; + continue; + } + + // SUB PROTOCOL + if (attributeType == typeof(SubProtocolAttribute)) + { + subProtocol = (attribute as SubProtocolAttribute)?.SubProtocol; + continue; + } + + // MAX MESSAGE SIZE + if (attributeType == typeof(MaxMessageSizeAttribute)) + { + maxMessageSize = (attribute as MaxMessageSizeAttribute)?.MaxMessageSize; + continue; + } + } + + // assign the socket to existing applications + foreach (var applicationContext in applicationContexts) + { + var prefix = applicationContext.Route.Concat + ( + applicationContext.PluginContext != pluginContext + ? pluginContext.PluginName.ToLower() + : "" + ); + var routePath = EndpointManager.CreateEndpointRoute(socketType, prefix, null); + var socketContext = new SocketContext() + { + EndpointId = new ComponentId(id), + PluginContext = pluginContext, + ApplicationContext = applicationContext, + Route = routePath, + MessageType = messageType, + SupportedSubProtocol = subProtocol, + MaxMessageSize = maxMessageSize, + Cache = cache, + Conditions = conditions, + IncludeSubPaths = includeSubPaths, + Attributes = EndpointManager.GetAttributeInstances(attributes) + }; + + var socketItem = new SocketItem(_componentHub.EndpointManager) + { + EndpointId = new ComponentId(id), + PluginContext = pluginContext, + ApplicationContext = applicationContext, + SocketContext = socketContext, + SocketClass = socketType, + MessageType = messageType, + SupportedSubProtocol = subProtocol, + MaxMessageSize = maxMessageSize, + Cache = cache, + Conditions = conditions, + Attributes = attributes.Select(x => x.AttributeType) + }; + + if (_dictionary.AddSocketItem(pluginContext, applicationContext, socketItem)) + { + OnAddSocket(socketItem.SocketContext); + + _httpServerContext?.Log.Debug + ( + I18N.Translate + ( + "webexpress.webcore:socketmanager.addsocket", + id, + applicationContext.ApplicationId + ) + ); + } + } + } + } + + /// + /// Removes all sockets associated with the specified plugin context. + /// + /// The context of the plugin that contains the sockets to remove. + public void Remove(IPluginContext pluginContext) + { + if (pluginContext is null) + { + return; + } + + // deregister all sockets associated with this plugin context + foreach (var socketContext in _dictionary.RemoveSocket(pluginContext)) + { + OnRemoveSocket(socketContext); + + _httpServerContext?.Log.Debug + ( + I18N.Translate + ( + "webexpress.webcore:socketmanager.removesocket", + socketContext.EndpointId, + socketContext.ApplicationContext.ApplicationId + ) + ); + } + } + + /// + /// Removes all sockets associated with the specified application context. + /// + /// + /// The context of the application that contains the sockets to remove. + /// + internal void Remove(IApplicationContext applicationContext) + { + if (applicationContext is null) + { + return; + } + + // deregister all sockets associated with this application context + foreach (var socketContext in _dictionary.RemoveSocket(applicationContext)) + { + OnRemoveSocket(socketContext); + + _httpServerContext?.Log.Debug + ( + I18N.Translate + ( + "webexpress.webcore:socketmanager.removesocket", + socketContext.EndpointId, + socketContext.ApplicationContext.ApplicationId + ) + ); + } + } + + /// + /// Raises the AddSocket event. + /// + /// The socket context. + private void OnAddSocket(ISocketContext socketContext) + { + AddSocket?.Invoke(this, socketContext); + } + + /// + /// Raises the RemoveSocket event. + /// + /// The socket context. + private void OnRemoveSocket(ISocketContext socketContext) + { + RemoveSocket?.Invoke(this, socketContext); + } + + /// + /// Raises the event when a plugin is added. + /// + /// The source of the event. + /// The context of the plugin being added. + private void OnAddPlugin(object sender, IPluginContext pluginContext) + { + Register(pluginContext); + } + + /// + /// Raises the event when a plugin is removed. + /// + /// The source of the event. + /// The context of the plugin being removed. + private void OnRemovePlugin(object sender, IPluginContext pluginContext) + { + Remove(pluginContext); + } + + /// + /// Raises the event when an application is added. + /// + /// The source of the event. + /// The context of the application being added. + private void OnAddApplication(object sender, IApplicationContext applicationContext) + { + Register(applicationContext); + } + + /// + /// Raises the event when an application is removed. + /// + /// The source of the event. + /// The application context. + private void OnRemoveApplication(object sender, IApplicationContext applicationContext) + { + Remove(applicationContext); + } + + /// + /// Computes the Sec-WebSocket-Accept header value from the client key. + /// + /// The Sec-WebSocket-Key from the client. + /// The computed Sec-WebSocket-Accept value. + private static string ComputeWebSocketAcceptKey(string key) + { + var combined = key + _webSocketGuid; + var hash = SHA1.HashData(Encoding.UTF8.GetBytes(combined)); + + return Convert.ToBase64String(hash); + } + + /// + /// Release of unmanaged resources reserved during use. + /// + public void Dispose() + { + _componentHub.PluginManager.AddPlugin -= OnAddPlugin; + _componentHub.PluginManager.RemovePlugin -= OnRemovePlugin; + _componentHub.ApplicationManager.AddApplication -= OnAddApplication; + _componentHub.ApplicationManager.RemoveApplication -= OnRemoveApplication; + + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebSocket/SocketMessageTooLargeException.cs b/src/WebExpress.WebCore/WebSocket/SocketMessageTooLargeException.cs new file mode 100644 index 0000000..010488a --- /dev/null +++ b/src/WebExpress.WebCore/WebSocket/SocketMessageTooLargeException.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.Serialization; + +namespace WebExpress.WebCore.WebSocket +{ + /// + /// Represents an error that occurs when an incoming WebSocket message + /// exceeds the maximum allowed message size configured for the endpoint. + /// + public class SocketMessageTooLargeException : Exception + { + /// + /// Returns the total number of bytes received for the message. + /// + public ulong ActualSize { get; } + + /// + /// Returns the maximum allowed message size in bytes. + /// + public ulong MaxSize { get; } + + /// + /// Initializes a new instance of the class + /// with the specified actual and maximum message sizes. + /// + /// The number of bytes received. + /// The maximum allowed message size in bytes. + public SocketMessageTooLargeException(ulong actualSize, ulong maxSize) + : base($"WebSocket message size {actualSize} bytes exceeds the maximum allowed size of {maxSize} bytes.") + { + ActualSize = actualSize; + MaxSize = maxSize; + } + + /// + /// Initializes a new instance of the class + /// with a custom error message and the specified actual and maximum message sizes. + /// + /// The custom exception message. + /// The number of bytes received. + /// The maximum allowed message size in bytes. + public SocketMessageTooLargeException(string message, ulong actualSize, ulong maxSize) + : base(message) + { + ActualSize = actualSize; + MaxSize = maxSize; + } + + /// + /// Initializes a new instance of the class + /// with a custom error message, an inner exception, and the specified size values. + /// + /// The custom exception message. + /// The inner exception. + /// The number of bytes received. + /// The maximum allowed message size in bytes. + public SocketMessageTooLargeException(string message, Exception innerException, ulong actualSize, ulong maxSize) + : base(message, innerException) + { + ActualSize = actualSize; + MaxSize = maxSize; + } + } +} diff --git a/src/WebExpress.WebCore/WebSocket/SocketMessageType.cs b/src/WebExpress.WebCore/WebSocket/SocketMessageType.cs new file mode 100644 index 0000000..b94cdbf --- /dev/null +++ b/src/WebExpress.WebCore/WebSocket/SocketMessageType.cs @@ -0,0 +1,19 @@ +namespace WebExpress.WebCore.WebSocket +{ + /// + /// Defines the type of WebSocket message represented or sent + /// in the native WebExpress WebSocket protocol. + /// + public enum SocketMessageType + { + /// + /// Specifies that the data is UTF‑8 encoded text. + /// + Text, + + /// + /// Specifies that the data is binary. + /// + Binary + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebStatusPage/IStatusPageManager.cs b/src/WebExpress.WebCore/WebStatusPage/IStatusPageManager.cs index d1c9b05..58436e3 100644 --- a/src/WebExpress.WebCore/WebStatusPage/IStatusPageManager.cs +++ b/src/WebExpress.WebCore/WebStatusPage/IStatusPageManager.cs @@ -42,6 +42,6 @@ public interface IStatusPageManager : IComponentManager /// The application context where the status pages are located or null for an undefined page (may be from another application) that matches the status code. /// The request. /// The response or null. - Response CreateStatusResponse(string message, int status, IApplicationContext applicationContext, Request request); + Response CreateStatusResponse(string message, int status, IApplicationContext applicationContext, IRequest request); } } diff --git a/src/WebExpress.WebCore/WebStatusPage/Model/StatusPageDictionary.cs b/src/WebExpress.WebCore/WebStatusPage/Model/StatusPageDictionary.cs index 7ed442d..dd73a21 100644 --- a/src/WebExpress.WebCore/WebStatusPage/Model/StatusPageDictionary.cs +++ b/src/WebExpress.WebCore/WebStatusPage/Model/StatusPageDictionary.cs @@ -87,7 +87,7 @@ public void RemoveStatusPageItem(IPluginContext pluginContext, IApplicationConte /// The status page item if found, otherwise null. public StatusPageItem GetStatusPageItem(IApplicationContext applicationContext, int statusCode) { - if (applicationContext != null && ContainsKey(applicationContext?.PluginContext)) + if (applicationContext is not null && ContainsKey(applicationContext?.PluginContext)) { var appContextDict = this[applicationContext?.PluginContext]; diff --git a/src/WebExpress.WebCore/WebStatusPage/StatusPageManager.cs b/src/WebExpress.WebCore/WebStatusPage/StatusPageManager.cs index 4e83e14..244f894 100644 --- a/src/WebExpress.WebCore/WebStatusPage/StatusPageManager.cs +++ b/src/WebExpress.WebCore/WebStatusPage/StatusPageManager.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Threading; using WebExpress.WebCore.Internationalization; using WebExpress.WebCore.WebApplication; using WebExpress.WebCore.WebAttribute; @@ -23,11 +25,18 @@ namespace WebExpress.WebCore.WebStatusPage /// public class StatusPageManager : IStatusPageManager, ISystemComponent { + // synchronization guard for protecting _dictionary and related mutable state + private readonly Lock _guard = new(); + private readonly IComponentHub _componentHub; private readonly IHttpServerContext _httpServerContext; - private readonly StatusPageDictionary _dictionary = []; - private readonly Dictionary _defaults = []; - private static readonly Dictionary _delegateCache = []; + private readonly StatusPageDictionary _dictionary = new StatusPageDictionary(); + + // use a concurrent dictionary for defaults to make default lookups/updates thread-safe + private readonly ConcurrentDictionary _defaults = new(); + + // change delegate cache to concurrent dictionary (open-instance delegates are cached) + private static readonly ConcurrentDictionary _delegateCache = new(); /// /// An event that fires when an status page is added. @@ -42,10 +51,21 @@ public class StatusPageManager : IStatusPageManager, ISystemComponent /// /// Returns all status pages. /// - public IEnumerable StatusPages => _dictionary.Values - .SelectMany(x => x.Values) - .SelectMany(x => x.Values) - .Select(x => x.StatusPageContext); + public IEnumerable StatusPages + { + get + { + // return a stable snapshot to avoid enumeration during concurrent modifications + lock (_guard) + { + return _dictionary.Values + .SelectMany(x => x.Values) + .SelectMany(x => x.Values) + .Select(x => x.StatusPageContext) + .ToList(); + } + } + } /// /// Initializes a new instance of the class. @@ -76,9 +96,12 @@ private StatusPageManager(IComponentHub componentHub, IHttpServerContext httpSer /// The context of the plugin whose status pages are to be associated. private void Register(IPluginContext pluginContext) { - if (_dictionary.ContainsKey(pluginContext)) + lock (_guard) { - return; + if (_dictionary.ContainsKey(pluginContext)) + { + return; + } } Register(pluginContext, _componentHub.ApplicationManager.GetApplications(pluginContext)); @@ -92,12 +115,18 @@ private void Register(IApplicationContext applicationContext) { foreach (var pluginContext in _componentHub.PluginManager.GetPlugins(applicationContext)) { - if (_dictionary.TryGetValue(pluginContext, out var appDict) && appDict.ContainsKey(applicationContext)) + bool already; + lock (_guard) + { + already = _dictionary.TryGetValue(pluginContext, out var appDict) && appDict.ContainsKey(applicationContext); + } + + if (already) { continue; } - Register(pluginContext, [applicationContext]); + Register(pluginContext, new[] { applicationContext }); } } @@ -112,7 +141,7 @@ private void Register(IPluginContext pluginContext, IEnumerable x.IsClass == true && x.IsSealed && x.IsPublic) - .Where(x => x.GetInterface(typeof(IStatusPage<>).Name) != null)) + .Where(x => x.GetInterface(typeof(IStatusPage<>).Name) is not null)) { var id = new ComponentId(resource.FullName); var statusResponse = typeof(ResponseInternalServerError); @@ -149,7 +178,9 @@ private void Register(IPluginContext pluginContext, IEnumerable()?.StatusCode == null) + // validate that a StatusCodeAttribute is present; if not, log and skip registration for this resource + var statusCodeAttr = statusResponse?.GetCustomAttribute(); + if (statusCodeAttr is null || statusCodeAttr.StatusCode == 0) { _httpServerContext.Log.Debug ( @@ -160,10 +191,13 @@ private void Register(IPluginContext pluginContext, IEnumerable().StatusCode; + var statusCode = statusCodeAttr.StatusCode; var statusPageContext = new StatusPageContext() { StatusPageId = id, @@ -175,14 +209,21 @@ private void Register(IPluginContext pluginContext, IEnumerable new StatusPageItem() + { + StatusPageContext = new StatusPageContext() + { + StatusPageId = id, + PluginContext = pluginContext, + ApplicationContext = applicationContext, + StatusCode = statusCode, + StatusTitle = title, + StatusIcon = stausIcon + }, + StatusPageClass = resource, + StatusResponse = statusResponse, + PluginContext = pluginContext, + }, + (_, __) => new StatusPageItem() + { + StatusPageContext = new StatusPageContext() + { + StatusPageId = id, + PluginContext = pluginContext, + ApplicationContext = applicationContext, + StatusCode = statusCode, + StatusTitle = title, + StatusIcon = stausIcon + }, + StatusPageClass = resource, + StatusResponse = statusResponse, + PluginContext = pluginContext, + }); + } } } } @@ -261,9 +318,11 @@ private void Register(IPluginContext pluginContext, IEnumerableThe context of the status page or null. public IStatusPageContext GetStatusPage(IApplicationContext applicationContext, Type statusPageClass) { - var item = _dictionary.GetStatusPageItem(applicationContext, statusPageClass); - - return item?.StatusPageContext; + lock (_guard) + { + var item = _dictionary.GetStatusPageItem(applicationContext, statusPageClass); + return item?.StatusPageContext; + } } /// @@ -274,16 +333,22 @@ public IStatusPageContext GetStatusPage(IApplicationContext applicationContext, /// The application context where the status pages are located or null for an undefined page (may be from another application) that matches the status code. /// The request. /// The response or null. - public Response CreateStatusResponse(string message, int status, IApplicationContext applicationContext, Request request) + public Response CreateStatusResponse(string message, int status, IApplicationContext applicationContext, IRequest request) { - var statusPageItem = _dictionary.GetStatusPageItem(applicationContext, status); + StatusPageItem statusPageItem = null; - if (statusPageItem == null && _defaults.TryGetValue(status, out StatusPageItem value)) + // access dictionary safely + lock (_guard) + { + statusPageItem = _dictionary.GetStatusPageItem(applicationContext, status); + } + + if (statusPageItem is null && _defaults.TryGetValue(status, out StatusPageItem value)) { statusPageItem = value; } - if (statusPageItem == null) + if (statusPageItem is null) { return status switch { @@ -307,30 +372,36 @@ public Response CreateStatusResponse(string message, int status, IApplicationCon var pageContext = new PageContext() { ApplicationContext = applicationContext, - Scopes = [typeof(IScopeStatusPage)] + Scopes = new[] { typeof(IScopeStatusPage) } }; var renderContext = new RenderContext(pageInstance as IEndpoint, pageContext, request); var visualTreeContext = new VisualTreeContext(renderContext); var visualTreeType = pageType.GetInterface(typeof(IStatusPage<>).Name).GetGenericArguments()[0]; + + // obtain or create a cached open-instance delegate safely if (!_delegateCache.TryGetValue(pageType, out var del)) { - // create and compile the expression + // create an open-instance delegate: (instance, renderContext, visualTree) => instance.Process(renderContext, visualTree) + var instanceParam = Expression.Parameter(pageType, "instance"); var renderContextParam = Expression.Parameter(typeof(IRenderContext), "renderContext"); var visualTreeParam = Expression.Parameter(visualTreeType, "visualTree"); - var processMethod = pageType.GetMethod("Process", [typeof(IRenderContext), visualTreeType]); - var callProzessMethod = Expression.Call - ( - Expression.Constant(pageInstance), - processMethod, - renderContextParam, - visualTreeParam - ); - var lambda = Expression.Lambda(callProzessMethod, renderContextParam, visualTreeParam) - .Compile(); - _delegateCache[pageType] = lambda; - del = lambda; + var processMethod = pageType.GetMethod("Process", new[] { typeof(IRenderContext), visualTreeType }); + + if (processMethod is null) + { + throw new InvalidOperationException($"Process method not found on type {pageType.FullName}"); + } + + // call instance.Process(renderContext, visualTree) + var callProcess = Expression.Call(instanceParam, processMethod, renderContextParam, visualTreeParam); + + // create a lambda with signature (instance, renderContext, visualTree) and compile it + var lambda = Expression.Lambda(callProcess, instanceParam, renderContextParam, visualTreeParam).Compile(); + + // try to add to concurrent dictionary atomically; if another thread added concurrently, use the existing one + del = _delegateCache.GetOrAdd(pageType, lambda); } // create visual tree instance @@ -338,7 +409,7 @@ public Response CreateStatusResponse(string message, int status, IApplicationCon var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var constructors = visualTreeType?.GetConstructors(flags); - if (constructors != null) + if (constructors is not null) { foreach (var constructor in constructors.OrderByDescending(x => x.GetParameters().Length)) { @@ -363,6 +434,7 @@ public Response CreateStatusResponse(string message, int status, IApplicationCon if (constructor.Invoke(parameterValues) is IVisualTree visualTree) { visualTreeInstance = visualTree; + break; } } } @@ -371,8 +443,13 @@ public Response CreateStatusResponse(string message, int status, IApplicationCon visualTreeInstance = Activator.CreateInstance(visualTreeType) as IVisualTree; } - // execute the cached delegate - del.DynamicInvoke(renderContext, visualTreeInstance); + if (visualTreeInstance is null) + { + throw new InvalidOperationException($"Could not create visual tree instance of type {visualTreeType.FullName} for page {pageType.FullName}."); + } + + // execute the cached open-instance delegate; pass the current pageInstance + del.DynamicInvoke(pageInstance, renderContext, visualTreeInstance); var response = ComponentActivator.CreateInstance(statusPageItem.StatusResponse, _httpServerContext, _componentHub, new StatusMessage(message)); var content = visualTreeInstance.Render(new VisualTreeContext(renderContext))?.ToString(); @@ -389,18 +466,20 @@ public Response CreateStatusResponse(string message, int status, IApplicationCon /// The context of the plugin that contains the status pages to remove. internal void Remove(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return; } - // the plugin has not been registered in the manager - if (!_dictionary.ContainsKey(pluginContext)) + lock (_guard) { - return; - } + if (!_dictionary.ContainsKey(pluginContext)) + { + return; + } - _dictionary.Remove(pluginContext); + _dictionary.Remove(pluginContext); + } } /// @@ -409,23 +488,26 @@ internal void Remove(IPluginContext pluginContext) /// The context of the application that contains the status pages to remove. internal void Remove(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { return; } - foreach (var pluginDict in _dictionary.Values) + lock (_guard) { - foreach (var appDict in pluginDict.Where(x => x.Key == applicationContext).Select(x => x.Value)) + foreach (var pluginDict in _dictionary.Values) { - foreach (var resourceItem in appDict.Values) + foreach (var appDict in pluginDict.Where(x => x.Key == applicationContext).Select(x => x.Value)) { - OnRemoveStatusPage(resourceItem.StatusPageContext); - resourceItem.Dispose(); + foreach (var resourceItem in appDict.Values) + { + OnRemoveStatusPage(resourceItem.StatusPageContext); + resourceItem.Dispose(); + } } - } - pluginDict.Remove(applicationContext); + pluginDict.Remove(applicationContext); + } } } @@ -491,6 +573,7 @@ private void OnAddApplication(object sender, IApplicationContext e) /// private void Log() { + // use snapshot StatusPages property (which is already locked) to avoid concurrent enumeration exceptions if (!StatusPages.Any()) { return; @@ -526,4 +609,4 @@ public void Dispose() GC.SuppressFinalize(this); } } -} +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebTask/ITaskManager.cs b/src/WebExpress.WebCore/WebTask/ITaskManager.cs index 105ce16..0607e15 100644 --- a/src/WebExpress.WebCore/WebTask/ITaskManager.cs +++ b/src/WebExpress.WebCore/WebTask/ITaskManager.cs @@ -9,6 +9,11 @@ namespace WebExpress.WebCore.WebTask /// public interface ITaskManager : IComponentManager { + /// + /// Event is triggered when a task's changes. + /// + event EventHandler TaskChanged; + /// /// Returns the collection of tasks. /// diff --git a/src/WebExpress.WebCore/WebTask/Task.cs b/src/WebExpress.WebCore/WebTask/Task.cs index 17cf6e9..67432da 100644 --- a/src/WebExpress.WebCore/WebTask/Task.cs +++ b/src/WebExpress.WebCore/WebTask/Task.cs @@ -11,6 +11,7 @@ namespace WebExpress.WebCore.WebTask public class Task : ITask { private int _progress; + private string _message; /// /// Event is triggered when the task is executed. @@ -22,6 +23,16 @@ public class Task : ITask /// public event EventHandler Finish; + /// + /// Event is triggered when the progress changes. + /// + public event EventHandler ProgressChanged; + + /// + /// Event is triggered when the message changes. + /// + public event EventHandler MessageChanged; + /// /// Returns the id of the task. /// @@ -47,14 +58,35 @@ public class Task : ITask /// public int Progress { - get => _progress; - set => _progress = Math.Min(value, 100); + get { return _progress; } + set + { + var newValue = Math.Min(value, 100); + if (_progress != newValue) + { + _progress = newValue; + // trigger progress changed event + OnProgressChanged(); + } + } } /// /// Returns or sets a message that provides information about the processing. /// - public string Message { get; set; } + public string Message + { + get { return _message; } + set + { + if (_message != value) + { + _message = value; + // trigger message changed event + OnMessageChanged(); + } + } + } /// /// Initializes a new instance of the class. @@ -72,7 +104,7 @@ public Task(string id, params object[] args) /// protected virtual void OnProcess() { - Process?.Invoke(this, new TaskEventArgs()); + Process?.Invoke(this, new TaskEventArgs(this, 0)); } /// @@ -80,7 +112,23 @@ protected virtual void OnProcess() /// protected virtual void OnFinish() { - Finish?.Invoke(this, new TaskEventArgs()); + Finish?.Invoke(this, new TaskEventArgs(this, 100)); + } + + /// + /// Triggered when the progress changes. + /// + protected virtual void OnProgressChanged() + { + ProgressChanged?.Invoke(this, new TaskEventArgs(this, Progress)); + } + + /// + /// Triggered when the message changes. + /// + protected virtual void OnMessageChanged() + { + MessageChanged?.Invoke(this, new TaskEventArgs(this, Progress, Message)); } /// diff --git a/src/WebExpress.WebCore/WebTask/TaskEventArgs.cs b/src/WebExpress.WebCore/WebTask/TaskEventArgs.cs index 6d9fd3a..6347705 100644 --- a/src/WebExpress.WebCore/WebTask/TaskEventArgs.cs +++ b/src/WebExpress.WebCore/WebTask/TaskEventArgs.cs @@ -7,5 +7,32 @@ namespace WebExpress.WebCore.WebTask /// public class TaskEventArgs : EventArgs { + /// + /// Returns the related task. + /// + public ITask Task { get; } + + /// + /// Returns the current progress (if relevant). + /// + public int Progress { get; } + + /// + /// Returns the current or new message (if relevant). + /// + public string Message { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The related task. + /// The current progress. + /// The current or new message. + public TaskEventArgs(ITask task, int progress = 0, string message = null) + { + Task = task; + Progress = progress; + Message = message; + } } } diff --git a/src/WebExpress.WebCore/WebTask/TaskManager.cs b/src/WebExpress.WebCore/WebTask/TaskManager.cs index 50c2065..5543401 100644 --- a/src/WebExpress.WebCore/WebTask/TaskManager.cs +++ b/src/WebExpress.WebCore/WebTask/TaskManager.cs @@ -16,6 +16,11 @@ public class TaskManager : ITaskManager, ISystemComponent private readonly IHttpServerContext _httpServerContext; private readonly TaskDictionary _dictionary = []; + /// + /// Event is triggered when a task's changes. + /// + public event EventHandler TaskChanged; + /// /// Returns the collection of tasks. /// @@ -73,12 +78,16 @@ public ITask CreateTask(string id, params object[] args) { var key = id?.ToLower(); - if (_dictionary.TryGetValue(id, out var value)) + if (_dictionary.TryGetValue(key, out var value)) { return value; } var task = ComponentActivator.CreateInstance(_httpServerContext, _componentHub, [id, args]); + + // register events for the newly created task + task.ProgressChanged += OnTaskChanged; + _dictionary.Add(key, task); return task; @@ -109,12 +118,16 @@ public ITask CreateTask(string id, EventHandler handler, p { var key = id?.ToLower(); - if (_dictionary.TryGetValue(id, out var value)) + if (_dictionary.TryGetValue(key, out var value)) { return value; } var task = ComponentActivator.CreateInstance(_httpServerContext, _componentHub, [id, args]); + + // register events for the newly created task + task.ProgressChanged += OnTaskChanged; + _dictionary.Add(key, task); task.Process += handler; @@ -128,7 +141,18 @@ public ITask CreateTask(string id, EventHandler handler, p /// The task. public void RemoveTask(ITask task) { - var key = task?.Id.ToLower(); + if (task?.Id is null) + { + return; + } + + var key = task.Id.ToLower(); + + if (_dictionary.TryGetValue(key, out var storedTask) && storedTask is Task t) + { + // unregister events to prevent memory leaks + t.ProgressChanged -= OnTaskChanged; + } _dictionary.Remove(key); } @@ -140,5 +164,15 @@ public void Dispose() { GC.SuppressFinalize(this); } + + /// + /// Handles the changed event from a task. + /// + /// The sender. + /// The event arguments. + private void OnTaskChanged(object sender, TaskEventArgs e) + { + TaskChanged?.Invoke(sender, e); + } } } diff --git a/src/WebExpress.WebCore/WebTheme/Model/ThemeItemDictionary.cs b/src/WebExpress.WebCore/WebTheme/Model/ThemeItemDictionary.cs index bfc38df..6679d5d 100644 --- a/src/WebExpress.WebCore/WebTheme/Model/ThemeItemDictionary.cs +++ b/src/WebExpress.WebCore/WebTheme/Model/ThemeItemDictionary.cs @@ -63,7 +63,7 @@ public bool AddThemeItem(IPluginContext pluginContext, IApplicationContext appli /// An enumeration of theme contexts that were removed. public IEnumerable Remove(IPluginContext pluginContext) { - if (pluginContext == null) + if (pluginContext is null) { return []; } @@ -95,7 +95,7 @@ public IEnumerable Remove(IPluginContext pluginContext) /// An enumeration of theme contexts that were removed. internal IEnumerable Remove(IApplicationContext applicationContext) { - if (applicationContext == null) + if (applicationContext is null) { return []; } diff --git a/src/WebExpress.WebCore/WebTheme/ThemeManager.cs b/src/WebExpress.WebCore/WebTheme/ThemeManager.cs index 4e8c830..02ac097 100644 --- a/src/WebExpress.WebCore/WebTheme/ThemeManager.cs +++ b/src/WebExpress.WebCore/WebTheme/ThemeManager.cs @@ -16,7 +16,7 @@ namespace WebExpress.WebCore.WebTheme /// /// Manages themes for the web application. /// - public class ThemeManager : IThemeManager + public class ThemeManager : IThemeManager, ISystemComponent { private readonly IComponentHub _componentHub; private readonly IHttpServerContext _httpServerContext; @@ -144,7 +144,7 @@ private void Register(IPluginContext pluginContext, IEnumerable - /// An Uri represents a complete, fully qualified Uniform Resource Identifier (URI) that uniquely identifies a endpoint. - /// - /// This interface encapsulates all components of a typical URI, such as the scheme (e.g., "http", "https"), - /// the authority (e.g., "example.com"), path segments, query parameters, and fragment. It provides the external - /// address used for resource identification and linking (e.g., "http://example.com/users/123"). + /// An Uri represents a complete, fully qualified Uniform Resource + /// Identifier (URI) that uniquely identifies a endpoint. + /// This interface encapsulates all components of a typical URI, such as + /// the scheme (e.g., "http", "https"), the authority (e.g., "example.com"), + /// path segments, query parameters, and fragment. It provides the external + /// address used for resource identification and linking + /// (e.g., "http://example.com/users/123"). /// public interface IUri { @@ -40,18 +46,13 @@ public interface IUri /// /// The query part (e.g. ?title=Uniform_Resource_Identifier). /// - IEnumerable Query { get; } + IEnumerable Query { get; } /// /// References a position within a resource (e.g. #Anchor). /// string Fragment { get; } - /// - /// Returns the display string of the Uri - /// - string Display { get; } - /// /// Determines if the uri is empty. /// @@ -72,6 +73,18 @@ public interface IUri /// IDictionary Parameters { get; } + /// + /// Appends one or more query parameters to the current URI and returns a new instance with + /// the updated query + /// string. + /// + /// An array of objects representing the query parameters to add. Each + /// parameter must not be null. + /// + /// An uri instance containing the original URI with the specified query parameters appended. + /// + IUri Add(params IUriQuery[] query); + /// /// Concatenates the given path segment to the current URI and returns a new instance of IUri with the updated path. /// @@ -86,6 +99,19 @@ public interface IUri /// A new IUri instance representing the URI after concatenation. IUri Concat(params IUriPathSegment[] segments); + /// + /// Appends one or more query segments and returns a new URI instance with the + /// combined query parameters. + /// + /// + /// An array representing the query segments to append. The order of segments + /// determines their position in the resulting query string. + /// + /// + /// A new uri instance containing the original URI with the specified query segments appended. + /// + IUri Concat(params IUriQuery[] query); + /// /// Return a shortened uri containing n-elements. /// count greater than 0 count elements are included @@ -124,5 +150,62 @@ public interface IUri /// /// A new IUri instance with the updated fragment. The original URI remains unchanged. IUri SetFragment(string fragment); + + /// + /// Returns a string that represents the display text for the current instance. + /// + /// The render context. + /// + /// A string containing the display text associated with the instance. The + /// value may be empty if no display text is available. + /// + string GetDisplayText(IRenderContext renderContext); + + /// + /// Returns an icon that visually represents the parameter within + /// the given render context. + /// + /// + /// The rendering context that provides information required to + /// determine the appropriate icon. + /// + /// + /// An icon associated with the current instance. The value may be + /// null or empty if no icon is available. + /// + IIcon GetIcon(IRenderContext renderContext); + + /// + /// Creates a new endpoint uri and fills it with the given parameters. + /// + /// + /// The parameters that fill in the variable parts of the uri. + /// + /// + /// A new endpoint uri with the populated parameters. + /// + IUri BindParameters(params IParameter[] parameters); + + /// + /// Creates a new endpoint uri and fills it with the given parameters. + /// + /// + /// The parameters that fill in the variable parts of the uri. + /// + /// + /// A new endpoint uri with the populated parameters. + /// + IUri BindParameters(IEnumerable parameters); + + /// + /// Binds the parameters from the specified request to a URI instance. + /// + /// + /// The request object containing the parameters to be bound to the URI. Cannot be null. + /// + /// + /// An new IUri instance that represents the URI with parameters bound from the request. + /// + IUri BindParameters(IRequest request); } } diff --git a/src/WebExpress.WebCore/WebUri/IUriPathSegment.cs b/src/WebExpress.WebCore/WebUri/IUriPathSegment.cs index 9cd674c..4877693 100644 --- a/src/WebExpress.WebCore/WebUri/IUriPathSegment.cs +++ b/src/WebExpress.WebCore/WebUri/IUriPathSegment.cs @@ -1,5 +1,3 @@ -using System.Globalization; - namespace WebExpress.WebCore.WebUri { /// @@ -10,18 +8,13 @@ public interface IUriPathSegment /// /// Returns or sets the id. /// - internal string Id { get; } + string Id { get; } /// /// Returns the value. /// string Value { get; } - /// - /// Returns or sets the display text. - /// - string Display { get; set; } - /// /// Returns the tag. /// @@ -32,6 +25,20 @@ public interface IUriPathSegment /// bool IsEmpty { get; } + /// + /// Returns a value indicating whether the item is hidden. + /// + /// + /// This property can be used to determine if the item should be displayed in user + /// interfaces or lists. + /// + bool IsHidden { get; set; } + + /// + /// Returns the URI to which the user is redirected. + /// + IUri Uri { get; set; } + /// /// Checks whether the node matches the path element. /// @@ -51,11 +58,5 @@ public interface IUriPathSegment /// The comparison object. /// true if equals, false otherwise bool Equals(IUriPathSegment obj); - - /// - /// Returns or sets the display text. - /// - /// The culture. - string GetDisplay(CultureInfo culture); } } \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebUri/IUriPathSegmentConstant.cs b/src/WebExpress.WebCore/WebUri/IUriPathSegmentConstant.cs index c3aab3b..bdb6ef8 100644 --- a/src/WebExpress.WebCore/WebUri/IUriPathSegmentConstant.cs +++ b/src/WebExpress.WebCore/WebUri/IUriPathSegmentConstant.cs @@ -5,6 +5,5 @@ namespace WebExpress.WebCore.WebUri /// public interface IUriPathSegmentConstant : IUriPathSegment { - } } \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebUri/IUriPathSegmentVariable.cs b/src/WebExpress.WebCore/WebUri/IUriPathSegmentVariable.cs index fc3c2ce..a0b98c4 100644 --- a/src/WebExpress.WebCore/WebUri/IUriPathSegmentVariable.cs +++ b/src/WebExpress.WebCore/WebUri/IUriPathSegmentVariable.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using WebExpress.WebCore.WebIcon; +using WebExpress.WebCore.WebPage; namespace WebExpress.WebCore.WebUri { @@ -23,10 +24,36 @@ public interface IUriPathSegmentVariable : IUriPathSegment string Expression { get; } /// - /// Returns the variable. + /// Creates a deep copy of the current path segment and assigns the specified value. /// - /// The value. - /// The variable value pair. - IDictionary GetVariable(string value); + /// + /// The string value to assign to the copied segment. + /// + /// + /// A new instance representing the copied segment with the assigned value. + /// + IUriPathSegment Copy(string value); + + /// + /// Returns a string that represents the display text for the current instance. + /// + /// The render context. + /// + /// A string containing the display text associated with the instance. The + /// value may be empty if no display text is available. + /// + string GetDisplayText(IRenderContext renderContext); + + /// + /// Returns an icon that visually represents the parameter within the given render context. + /// + /// + /// The rendering context that provides information required to determine the appropriate icon. + /// + /// + /// An icon associated with the current instance. The value may be null or empty + /// if no icon is available. + /// + IIcon GetIcon(IRenderContext renderContext); } } \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebUri/IUriQuery.cs b/src/WebExpress.WebCore/WebUri/IUriQuery.cs new file mode 100644 index 0000000..0951bc0 --- /dev/null +++ b/src/WebExpress.WebCore/WebUri/IUriQuery.cs @@ -0,0 +1,18 @@ +namespace WebExpress.WebCore.WebUri +{ + /// + /// The query part (e.g. ?title=Uniform_Resource_Identifier). + /// + public interface IUriQuery + { + /// + /// Returns the key. + /// + string Key { get; } + + /// + /// Returns the value. + /// + string Value { get; } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebUri/UriEndpoint.cs b/src/WebExpress.WebCore/WebUri/UriEndpoint.cs index e98845d..3654142 100644 --- a/src/WebExpress.WebCore/WebUri/UriEndpoint.cs +++ b/src/WebExpress.WebCore/WebUri/UriEndpoint.cs @@ -1,16 +1,22 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using WebExpress.WebCore.WebIcon; +using WebExpress.WebCore.WebMessage; +using WebExpress.WebCore.WebPage; +using WebExpress.WebCore.WebParameter; namespace WebExpress.WebCore.WebUri { /// - /// An Uri represents a complete, fully qualified Uniform Resource Identifier (URI) that uniquely - /// identifies a endpoint (see RFC 3986). - /// This interface encapsulates all components of a typical URI, such as the scheme (e.g., "http", "https"), - /// the authority (e.g., "example.com"), path segments, query parameters, and fragment. It provides the external - /// address used for resource identification and linking (e.g., "http://example.com/users/123"). + /// An Uri represents a complete, fully qualified Uniform Resource + /// Identifier (URI) that uniquely identifies a endpoint (see RFC 3986). + /// This interface encapsulates all components of a typical URI, such as + /// the scheme (e.g., "http", "https"), the authority (e.g., "example.com"), + /// path segments, query parameters, and fragment. It provides the external + /// address used for resource identification and linking + /// (e.g., "http://example.com/users/123"). /// public partial class UriEndpoint : IUri { @@ -57,37 +63,13 @@ public partial class UriEndpoint : IUri /// /// The query part (e.g. ?title=Uniform_Resource_Identifier). /// - public IEnumerable Query { get; } = []; + public IEnumerable Query { get; private set; } = []; /// /// References a position within a resource (e.g. #Anchor). /// public string Fragment { get; set; } - /// - /// Returns the display string of the Uri - /// - public virtual string Display - { - get - { - if (PathSegments.LastOrDefault() is IUriPathSegment last) - { - return last?.Display; - } - - return null; - } - - set - { - if (PathSegments.LastOrDefault() is IUriPathSegment last) - { - last.Display = value; - } - } - } - /// /// Determines if the uri is empty. /// @@ -101,7 +83,7 @@ public virtual string Display /// /// Checks if it is a relative uri. /// - public bool IsRelative => Authority == null; + public bool IsRelative => Authority is null; /// /// Retrieves a collection of variables represented as key-value pairs. @@ -244,7 +226,9 @@ public UriEndpoint(IUri uri, IEnumerable segments) /// The path segments. /// Other segments. public UriEndpoint(IUri uri, IEnumerable segments, IEnumerable extendedSegments) - : this(uri.Scheme, uri.Authority, uri.Fragment, uri.Query, extendedSegments != null ? segments.Union(extendedSegments) : segments) + : this(uri.Scheme, uri.Authority, uri.Fragment, uri.Query, extendedSegments is not null + ? segments.Union(extendedSegments) + : segments) { } @@ -256,7 +240,7 @@ public UriEndpoint(IUri uri, IEnumerable segments, IEnumerable< /// References a position within a resource (e.g. #Anchor). /// The query part (e.g. ?title=Uniform_Resource_Identifier). /// The path segments. - public UriEndpoint(UriScheme scheme, UriAuthority authority, string fragment, IEnumerable query, IEnumerable segments) + public UriEndpoint(UriScheme scheme, UriAuthority authority, string fragment, IEnumerable query, IEnumerable segments) { Scheme = scheme; Authority = authority; @@ -265,6 +249,21 @@ public UriEndpoint(UriScheme scheme, UriAuthority authority, string fragment, IE Fragment = fragment; } + /// + /// Appends one or more query parameters to the current URI and returns a new instance with + /// the updated query + /// string. + /// + /// An array of objects representing the query parameters to add. Each + /// parameter must not be null. + /// The current instance for method chaining. + public virtual IUri Add(params IUriQuery[] query) + { + Query = Query.Concat(query.Where(x => x is not null)); + + return this; + } + /// /// Concatenates the given path segment to the current URI and returns a new instance of IUri with the updated path. /// @@ -304,6 +303,25 @@ public virtual IUri Concat(params IUriPathSegment[] segments) return copy; } + /// + /// Appends one or more query segments and returns a new URI instance with the + /// combined query parameters. + /// + /// + /// An array representing the query segments to append. The order of segments + /// determines their position in the resulting query string. + /// + /// + /// A new uri instance containing the original URI with the specified query segments appended. + /// + public IUri Concat(params IUriQuery[] query) + { + var copy = new UriEndpoint((IUri)this); + copy.Query = copy.Query.Concat(query.Where(x => x is not null)); + + return copy; + } + /// /// Return a shortened uri containing n-elements. /// count greater than 0 count elements are included @@ -338,6 +356,46 @@ public virtual IUri Take(int count) return copy; } + /// + /// Returns a new URI containing the last path segments. + /// + /// + /// The number of trailing path segments to include. + /// + /// + /// • If is 0, the full URI is returned. + /// • If is positive, the last segments are returned. + /// • If exceeds the number of segments, the full URI is returned. + /// • Negative values are not allowed and result in null. + /// + /// + /// + /// A new URI containing the selected trailing segments, or null if + /// is negative. + /// + public virtual IUri TakeLast(int count) + { + var copy = new UriEndpoint((IUri)this); + var path = copy.PathSegments.ToList(); + + // negative values → return full URI + if (count < 0) + { + return copy; + } + + // 0 or count >= total → full URI + if (count is 0 || count >= path.Count) + { + return copy; + } + + // take last n segments + copy.PathSegments = [.. path.Skip(path.Count - count)]; + + return copy; + } + /// /// Return a shortened uri by not including the first n elements. /// count greater than 0 count elements are skipped @@ -381,24 +439,52 @@ public virtual bool Contains(string segment) /// true if part of the uri, false otherwise. public bool StartsWith(IUri uri) { - return ToString().StartsWith(uri.ToString()); + var a = uri.PathSegments; + var b = PathSegments; + + if (a.Count() > b.Count()) + { + return false; + } + + for (int i = 0; i < a.Count(); i++) + { + if (!a.ElementAt(i).Value.Equals(b.ElementAt(i).Value, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + + return true; + } /// /// Creates a new endpoint uri and fills it with the given parameters. /// - /// The parameters that fill in the variable parts of the uri. - /// A new endpoint uri with the populated parameters. - public IUri SetParameters(params WebMessage.Parameter[] parameters) + /// + /// The parameters that fill in the variable parts of the uri. + /// + /// + /// A new endpoint uri with the populated parameters. + /// + public virtual IUri BindParameters(params IParameter[] parameters) { var pathSegments = PathSegments.AsEnumerable(); - foreach (var parameter in parameters) + foreach (var parameter in parameters ?? []) { + var key = parameter switch + { + IParameterStatic staticParam => staticParam.GetKey(), + IParameterDynamic dynamicParam => dynamicParam.Key, + _ => null + }; + pathSegments = pathSegments.Select(x => { if (x is IUriPathSegmentVariable variable && - variable.VariableName.Equals(parameter?.Key, StringComparison.OrdinalIgnoreCase)) + variable.VariableName.Equals(key, StringComparison.OrdinalIgnoreCase)) { var copy = variable.Copy() as IUriPathSegmentVariable; copy.Value = parameter.Value; @@ -410,7 +496,67 @@ public IUri SetParameters(params WebMessage.Parameter[] parameters) }); } - return new UriEndpoint(this, pathSegments); + // copy query collection and bind matching parameters by query keys + var boundQuery = new List(); + foreach (var query in Query) + { + var parameter = parameters + .Select(x => + { + var key = x switch + { + IParameterStatic staticParam => staticParam.GetKey(), + IParameterDynamic dynamicParam => dynamicParam.Key, + _ => null + }; + return (key, x.Value); + }) + .FirstOrDefault(x => x.key.Equals(query?.Key, StringComparison.InvariantCultureIgnoreCase)); + + if (!string.IsNullOrWhiteSpace(parameter.key)) + { + // if parameter found for query key, set value accordingly + boundQuery.Add(new UriQuery(parameter.key, parameter.Value)); + } + else + { + // otherwise keep unchanged + boundQuery.Add(new UriQuery(query.Key, query.Value)); + } + } + + return new UriEndpoint(this, pathSegments) + { + Query = boundQuery + }; + } + + /// + /// Creates a new endpoint uri and fills it with the given parameters. + /// + /// + /// The parameters that fill in the variable parts of the uri. + /// + /// + /// A new endpoint uri with the populated parameters. + /// + public virtual IUri BindParameters(IEnumerable parameters) + { + return BindParameters([.. parameters]); + } + + /// + /// Binds the parameters from the specified request to a URI instance. + /// + /// + /// The request object containing the parameters to be bound to the URI. Cannot be null. + /// + /// + /// An new IUri instance that represents the URI with parameters bound from the request. + /// + public virtual IUri BindParameters(IRequest request) + { + return BindParameters([.. request?.Parameters]); } /// @@ -480,6 +626,48 @@ public static implicit operator string(UriEndpoint uri) return uri?.ToString(); } + /// + /// Returns a string that represents the display text for the current instance. + /// + /// The render context. + /// + /// A string containing the display text associated with the instance. The + /// value may be empty if no display text is available. + /// + public virtual string GetDisplayText(IRenderContext renderContext) + { + var last = PathSegments.LastOrDefault(); + + if (last is IUriPathSegmentVariable variable) + { + return variable.GetDisplayText(renderContext); + } + + return null; + } + + /// + /// Returns an icon that visually represents the parameter within the given render context. + /// + /// + /// The rendering context that provides information required to determine the appropriate icon. + /// + /// + /// An icon associated with the current instance. The value may be null or empty + /// if no icon is available. + /// + public virtual IIcon GetIcon(IRenderContext renderContext) + { + var last = PathSegments.LastOrDefault(); + + if (last is IUriPathSegmentVariable variable) + { + return variable.GetIcon(renderContext); + } + + return null; + } + /// /// Converts the uri to a string. /// diff --git a/src/WebExpress.WebCore/WebUri/UriPathSegmentConstant.cs b/src/WebExpress.WebCore/WebUri/UriPathSegmentConstant.cs index 9abbc45..cc1eee4 100644 --- a/src/WebExpress.WebCore/WebUri/UriPathSegmentConstant.cs +++ b/src/WebExpress.WebCore/WebUri/UriPathSegmentConstant.cs @@ -1,6 +1,5 @@ using System; -using System.Globalization; -using WebExpress.WebCore.Internationalization; +using WebExpress.WebCore.WebPage; namespace WebExpress.WebCore.WebUri { @@ -19,11 +18,6 @@ public class UriPathSegmentConstant : IUriPathSegmentConstant /// public string Value { get; set; } - /// - /// Returns or sets the display text. - /// - public string Display { get; set; } - /// /// Returns or sets the tag. /// @@ -35,25 +29,27 @@ public class UriPathSegmentConstant : IUriPathSegmentConstant public bool IsEmpty => string.IsNullOrWhiteSpace(Value) || Value.Equals("/"); /// - /// Initializes a new instance of the class. + /// Returns or sets a value indicating whether the item is hidden. /// - /// The name. - /// The tag or null - public UriPathSegmentConstant(string value, object tag = null) - : this(value, null, tag) - { - } + /// + /// This property can be used to determine if the item should be displayed in user + /// interfaces or lists. + /// + public bool IsHidden { get; set; } + + /// + /// Returns or sets the URI to which the user is redirected. + /// + public IUri Uri { get; set; } /// /// Initializes a new instance of the class. /// /// The name. - /// The display text. /// The tag or null - public UriPathSegmentConstant(string value, string display, object tag = null) + public UriPathSegmentConstant(string value, object tag = null) { - Value = value ?? string.Empty; - Display = display; + Value = value; Tag = tag; } @@ -79,7 +75,11 @@ public bool IsMatched(string value) /// The copy. public virtual IUriPathSegment Copy() { - return new UriPathSegmentConstant(Value, Display, Tag); + return new UriPathSegmentConstant(Value, Tag) + { + IsHidden = IsHidden, + Uri = Uri + }; } /// @@ -89,7 +89,7 @@ public virtual IUriPathSegment Copy() /// true if equals, false otherwise public virtual bool Equals(IUriPathSegment obj) { - if (obj == null) + if (obj is null) { return false; } @@ -102,12 +102,16 @@ public virtual bool Equals(IUriPathSegment obj) } /// - /// Returns or sets the display text. + /// Returns a string that represents the display text for the current instance. /// - /// The culture. - public virtual string GetDisplay(CultureInfo culture) + /// The render context. + /// + /// A string containing the display text associated with the instance. The + /// value may be empty if no display text is available. + /// + public virtual string GetDisplayText(IRenderContext renderContext) { - return I18N.Translate(culture, Display); + return null; } /// diff --git a/src/WebExpress.WebCore/WebUri/UriPathSegmentRoot.cs b/src/WebExpress.WebCore/WebUri/UriPathSegmentRoot.cs index a27075c..02fedff 100644 --- a/src/WebExpress.WebCore/WebUri/UriPathSegmentRoot.cs +++ b/src/WebExpress.WebCore/WebUri/UriPathSegmentRoot.cs @@ -1,6 +1,6 @@ using System; -using System.Globalization; using WebExpress.WebCore.Internationalization; +using WebExpress.WebCore.WebPage; namespace WebExpress.WebCore.WebUri { @@ -34,6 +34,20 @@ public class UriPathSegmentRoot : IUriPathSegment /// public bool IsEmpty => false; + /// + /// Returns or sets a value indicating whether the item is hidden. + /// + /// + /// This property can be used to determine if the item should be displayed in user + /// interfaces or lists. + /// + public bool IsHidden { get; set; } + + /// + /// Returns or sets the URI to which the user is redirected. + /// + public IUri Uri { get; set; } + /// /// Initializes a new instance of the class. /// @@ -68,7 +82,11 @@ public bool IsMatched(string value) /// A copy of the current segment. public virtual IUriPathSegment Copy() { - return new UriPathSegmentRoot(Display, Tag); + return new UriPathSegmentRoot(Display, Tag) + { + IsHidden = IsHidden, + Uri = Uri + }; } /// @@ -78,7 +96,7 @@ public virtual IUriPathSegment Copy() /// True if the objects are equal, false otherwise. public virtual bool Equals(IUriPathSegment obj) { - if (obj == null) + if (obj is null) { return false; } @@ -87,13 +105,16 @@ public virtual bool Equals(IUriPathSegment obj) } /// - /// Returns the display text for the specified culture. + /// Returns a string that represents the display text for the current instance. /// - /// The culture. - /// The display text for the specified culture. - public virtual string GetDisplay(CultureInfo culture) + /// The render context. + /// + /// A string containing the display text associated with the instance. The + /// value may be empty if no display text is available. + /// + public virtual string GetDisplayText(IRenderContext renderContext) { - return I18N.Translate(culture, Display); + return I18N.Translate(renderContext, Display); } /// diff --git a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariable.cs b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariable.cs index 88325b1..626f6a6 100644 --- a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariable.cs +++ b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariable.cs @@ -1,15 +1,18 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Text.RegularExpressions; -using WebExpress.WebCore.Internationalization; +using WebExpress.WebCore.WebIcon; +using WebExpress.WebCore.WebPage; +using WebExpress.WebCore.WebParameter; namespace WebExpress.WebCore.WebUri { /// /// Variable path segment. /// - public abstract class UriPathSegmentVariable : IUriPathSegmentVariable + /// The parameter type. + public abstract class UriPathSegmentVariable : IUriPathSegmentVariable + where TParameter : IParameterStatic, new() { /// /// Returns or sets the id. @@ -26,11 +29,6 @@ public abstract class UriPathSegmentVariable : IUriPathSegmentVariable /// public string Value { get; set; } - /// - /// Returns or sets the display text. - /// - public string Display { get; set; } - /// /// Returns or sets the regex expression. /// @@ -47,25 +45,26 @@ public abstract class UriPathSegmentVariable : IUriPathSegmentVariable public bool IsEmpty => string.IsNullOrWhiteSpace(VariableName) || VariableName.Equals("/"); /// - /// Initializes a new instance of the class. + /// Returns or sets a value indicating whether the item is hidden. /// - /// The name. - /// The tag or null - public UriPathSegmentVariable(string name, object tag = null) - : this(name, null, tag) - { - } + /// + /// This property can be used to determine if the item should be displayed in user + /// interfaces or lists. + /// + public bool IsHidden { get; set; } + + /// + /// Returns or sets the URI to which the user is redirected. + /// + public IUri Uri { get; set; } /// /// Initializes a new instance of the class. /// - /// The name. - /// The display text. /// The tag or null - public UriPathSegmentVariable(string name, string display, object tag = null) + public UriPathSegmentVariable(object tag = null) { - VariableName = name; - Display = display; + VariableName = TParameter.Key; Tag = tag; } @@ -81,7 +80,7 @@ public UriPathSegmentVariable(string name, string display, object tag = null) /// /// The value to check. /// True if the path element matched, false otherwise. - public bool IsMatched(string value) + public virtual bool IsMatched(string value) { if (string.IsNullOrWhiteSpace(value)) { @@ -112,26 +111,68 @@ public bool IsMatched(string value) /// true if equals, false otherwise public virtual bool Equals(IUriPathSegment obj) { - if (obj == null) + if (obj is null) { return false; } - else if (obj is UriPathSegmentVariable segment) + else if (obj is UriPathSegmentVariable segment) { - return VariableName.Equals(segment.VariableName, StringComparison.OrdinalIgnoreCase) && - Expression.Equals(segment.Expression); + return VariableName.Equals(segment.VariableName, StringComparison.OrdinalIgnoreCase) + && ( + (Expression is null && segment.Expression is null) + || Expression.Equals(segment.Expression) + ); } return false; } /// - /// Returns or sets the display text. + /// Creates a deep copy of the current path segment and assigns the specified value. + /// + /// + /// The string value to assign to the copied segment. + /// + /// + /// A new instance representing the copied segment with the assigned value. + /// + public IUriPathSegment Copy(string value) + { + var copy = Copy(); + if (copy is UriPathSegmentVariable segment) + { + segment.Value = value; + } + + return copy; + } + + /// + /// Returns a string that represents the display text for the current instance. + /// + /// The render context. + /// + /// A string containing the display text associated with the instance. The + /// value may be empty if no display text is available. + /// + public virtual string GetDisplayText(IRenderContext renderContext) + { + return Value; + } + + /// + /// Returns an icon that visually represents the parameter within the given render context. /// - /// The culture. - public virtual string GetDisplay(CultureInfo culture) + /// + /// The rendering context that provides information required to determine the appropriate icon. + /// + /// + /// An icon associated with the current instance. The value may be null or empty + /// if no icon is available. + /// + public virtual IIcon GetIcon(IRenderContext renderContext) { - return string.Format(I18N.Translate(culture, Display), Value); + return null; } /// diff --git a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableApiVersion.cs b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableApiVersion.cs new file mode 100644 index 0000000..0be147e --- /dev/null +++ b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableApiVersion.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using WebExpress.WebCore.WebParameter; + +namespace WebExpress.WebCore.WebUri +{ + /// + /// A variable path segment for the api version (e.g., /api/1/...). + /// + /// The parameter type. + internal class UriPathSegmentVariableApiVersion : UriPathSegmentVariable + where TParameter : IParameterStatic, new() + { + /// + /// Initializes a new instance of the class. + /// + /// The value. + public UriPathSegmentVariableApiVersion(string value) + : base() + { + Value = value; + } + + /// + /// Returns the variable. + /// + /// The value. + /// The variable value pair. + public override IDictionary GetVariable(string value) + { + return new Dictionary(); + } + + /// + /// Checks whether the node matches the path element. + /// + /// The value to check. + /// True if the path element matched, false otherwise. + public override bool IsMatched(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + else if (value.Equals(Value)) + { + return true; + } + + return false; + } + + /// + /// Make a deep copy. + /// + /// The copy. + public override IUriPathSegment Copy() + { + return new UriPathSegmentVariableApiVersion(Value) + { + IsHidden = IsHidden, + Uri = Uri + }; + } + + /// + /// Compare the object. + /// + /// The comparison object. + /// true if equals, false otherwise + public override bool Equals(IUriPathSegment obj) + { + if (obj is null) + { + return false; + } + else if (obj is UriPathSegmentVariable segment) + { + return VariableName.Equals(segment.VariableName, StringComparison.OrdinalIgnoreCase) + && Value.Equals(segment.Value); + } + + return false; + } + + /// + /// Converts the segment to a string. + /// + /// A string that represents the current segment. + public override string ToString() + { + return base.ToString(); + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableDouble.cs b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableDouble.cs index d165294..7d4f32d 100644 --- a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableDouble.cs +++ b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableDouble.cs @@ -1,39 +1,22 @@ using System.Collections.Generic; +using WebExpress.WebCore.WebParameter; namespace WebExpress.WebCore.WebUri { /// /// Variable path segment. /// - public class UriPathSegmentVariableDouble : UriPathSegmentVariable + /// The parameter type. + public class UriPathSegmentVariableDouble : UriPathSegmentVariable + where TParameter : IParameterStatic, new() { /// /// Initializes a new instance of the class. /// - /// The name. /// The tag or null - public UriPathSegmentVariableDouble(string name, object tag = null) - : base(name, tag) + public UriPathSegmentVariableDouble(object tag = null) + : base(tag) { - VariableName = name; - Value = name; - Display = name; - Expression = @"^[+-]?(\d*,\d+|\d+(,\d*)?)( +[eE][+-]?\d+)?$"; - Tag = tag; - } - - /// - /// Initializes a new instance of the class. - /// - /// The name. - /// The display text. - /// The tag or null - public UriPathSegmentVariableDouble(string name, string display, object tag = null) - : base(name, tag) - { - VariableName = name; - Value = name; - Display = display; Expression = @"^[+-]?(\d*,\d+|\d+(,\d*)?)( +[eE][+-]?\d+)?$"; Tag = tag; } @@ -42,8 +25,8 @@ public UriPathSegmentVariableDouble(string name, string display, object tag = nu /// Initializes a new instance of the class. /// /// The path segment to copy. - public UriPathSegmentVariableDouble(UriPathSegmentVariableDouble segment) - : base(segment.VariableName, segment.Display, segment.Tag) + public UriPathSegmentVariableDouble(UriPathSegmentVariableDouble segment) + : base(segment.Tag) { Expression = segment.Expression; } @@ -64,7 +47,12 @@ public override IDictionary GetVariable(string value) /// The copy. public override IUriPathSegment Copy() { - return new UriPathSegmentVariableDouble(this) { Value = Value }; + return new UriPathSegmentVariableDouble(this) + { + Value = Value, + IsHidden = IsHidden, + Uri = Uri + }; } /// diff --git a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableGuid.cs b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableGuid.cs index 0b536f9..5f207a5 100644 --- a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableGuid.cs +++ b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableGuid.cs @@ -1,14 +1,17 @@ using System.Collections.Generic; -using System.Globalization; using System.Text.RegularExpressions; using WebExpress.WebCore.Internationalization; +using WebExpress.WebCore.WebPage; +using WebExpress.WebCore.WebParameter; namespace WebExpress.WebCore.WebUri { /// /// Represents a URI path segment variable for GUIDs. /// - public class UriPathSegmentVariableGuid : UriPathSegmentVariable + /// The parameter type. + public class UriPathSegmentVariableGuid : UriPathSegmentVariable + where TParameter : IParameterStatic, new() { /// /// The display formats of the guid. @@ -34,50 +37,24 @@ public enum Format /// /// Initializes a new instance of the class. /// - /// The path text. /// The tag or null - public UriPathSegmentVariableGuid(string name, object tag = null) - : this(name, null, tag) + public UriPathSegmentVariableGuid(object tag = null) + : this(Format.Simple, tag) { } /// /// Initializes a new instance of the class. /// - /// The name. - /// The display text. - /// The tag or null - public UriPathSegmentVariableGuid(string name, string display, object tag = null) - : this(name, display, Format.Full, tag) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The name. - /// The display text. /// The display format. /// The tag or null - public UriPathSegmentVariableGuid(string name, string display, Format displayFormat, object tag = null) - : base(name, display, tag) + public UriPathSegmentVariableGuid(Format displayFormat, object tag = null) + : base(tag) { - VariableName = name; DisplayFormat = displayFormat; Expression = @"^(\{){0,1}(([0-9a-fA-F]{8})\-([0-9a-fA-F]{4})\-([0-9a-fA-F]{4})\-([0-9a-fA-F]{4})\-([0-9a-fA-F]{12}))(\}){0,1}$"; } - /// - /// Initializes a new instance of the class. - /// - /// The path segment to copy. - public UriPathSegmentVariableGuid(UriPathSegmentVariableGuid segment) - : base(segment.VariableName, segment.Display, segment.Tag) - { - DisplayFormat = segment.DisplayFormat; - Expression = segment.Expression; - } - /// /// Returns the variable. /// @@ -106,26 +83,41 @@ public override IDictionary GetVariable(string value) /// The copy. public override IUriPathSegment Copy() { - return new UriPathSegmentVariableGuid(this) { Value = Value }; + return new UriPathSegmentVariableGuid(DisplayFormat) + { + Expression = Expression, + Value = Value, + IsHidden = IsHidden, + Uri = Uri + }; } /// - /// Returns or sets the display text. + /// Returns a string that represents the display text for the current instance. /// - /// The culture. - public override string GetDisplay(CultureInfo culture) + /// The render context. + /// + /// A string containing the display text associated with the instance. The + /// value may be empty if no display text is available. + /// + public override string GetDisplayText(IRenderContext renderContext) { + if (Value is null) + { + return base.GetDisplayText(renderContext); + } + var match = Regex.Match(Value, Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled); var guid = DisplayFormat == Format.Simple ? match.Groups[7].ToString() : match.Groups[2].ToString(); - if (string.IsNullOrWhiteSpace(Display) || !Display.Contains("{0}")) + if (string.IsNullOrWhiteSpace(Value) || !Value.Contains("{0}")) { return guid; } return string.Format ( - I18N.Translate(culture, Display), + I18N.Translate(renderContext, Value), guid ); } diff --git a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableInt.cs b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableInt.cs index a2a3b04..f44bed3 100644 --- a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableInt.cs +++ b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableInt.cs @@ -1,39 +1,22 @@ using System.Collections.Generic; +using WebExpress.WebCore.WebParameter; namespace WebExpress.WebCore.WebUri { /// - /// Variable path segment. + /// Int variable path segment. /// - public class UriPathSegmentVariableInt : UriPathSegmentVariable + /// The parameter type. + public class UriPathSegmentVariableInt : UriPathSegmentVariable + where TParameter : IParameterStatic, new() { /// /// Initializes a new instance of the class. /// - /// The path text. /// The tag or null - public UriPathSegmentVariableInt(string name, object tag = null) - : base(name, tag) + public UriPathSegmentVariableInt(object tag = null) + : base(tag) { - VariableName = name; - Value = name; - Display = name; - Expression = @"^[+-]*\d$"; - Tag = tag; - } - - /// - /// Initializes a new instance of the class. - /// - /// The name. - /// The display text. - /// The tag or null - public UriPathSegmentVariableInt(string name, string display, object tag = null) - : base(name, tag) - { - VariableName = name; - Value = name; - Display = display; Expression = @"^[+-]*\d$"; Tag = tag; } @@ -42,8 +25,8 @@ public UriPathSegmentVariableInt(string name, string display, object tag = null) /// Initializes a new instance of the class. /// /// The path segment to copy. - public UriPathSegmentVariableInt(UriPathSegmentVariableInt segment) - : base(segment.VariableName, segment.Display, segment.Tag) + public UriPathSegmentVariableInt(UriPathSegmentVariableInt segment) + : base(segment.Tag) { Expression = segment.Expression; } @@ -64,7 +47,12 @@ public override IDictionary GetVariable(string value) /// The copy. public override IUriPathSegment Copy() { - return new UriPathSegmentVariableInt(this) { Value = Value }; + return new UriPathSegmentVariableInt(this) + { + Value = Value, + IsHidden = IsHidden, + Uri = Uri + }; } /// diff --git a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableRegex.cs b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableRegex.cs new file mode 100644 index 0000000..f584bbc --- /dev/null +++ b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableRegex.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using WebExpress.WebCore.WebParameter; + +namespace WebExpress.WebCore.WebUri +{ + /// + /// Variable path segment. + /// + /// The parameter type. + public class UriPathSegmentVariableRegex : UriPathSegmentVariable + where TParameter : IParameterStatic, new() + { + /// + /// Initializes a new instance of the class. + /// + /// The regular expression. + /// The tag or null + public UriPathSegmentVariableRegex(string regex, object tag = null) + : base(tag) + { + Expression = regex; + Tag = tag; + } + + /// + /// Initializes a new instance of the class. + /// + /// The path segment to copy. + public UriPathSegmentVariableRegex(UriPathSegmentVariableRegex segment) + : base(segment.Tag) + { + Expression = segment.Expression; + } + + /// + /// Returns the variable. + /// + /// The value. + /// The variable value pair. + public override IDictionary GetVariable(string value) + { + return new Dictionary(); + } + + /// + /// Make a deep copy. + /// + /// The copy. + public override IUriPathSegment Copy() + { + return new UriPathSegmentVariableRegex(this) + { + Value = Value, + IsHidden = IsHidden, + Uri = Uri + }; + } + + /// + /// Converts the segment to a string. + /// + /// A string that represents the current segment. + public override string ToString() + { + return base.ToString(); + } + } +} \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableString.cs b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableString.cs index ba56c3f..b7fd3ba 100644 --- a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableString.cs +++ b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableString.cs @@ -1,39 +1,22 @@ using System.Collections.Generic; +using WebExpress.WebCore.WebParameter; namespace WebExpress.WebCore.WebUri { /// - /// Variable path segment. + /// String variable path segment. /// - public class UriPathSegmentVariableString : UriPathSegmentVariable + /// The parameter type. + public class UriPathSegmentVariableString : UriPathSegmentVariable + where TParameter : IParameterStatic, new() { /// /// Initializes a new instance of the class. /// - /// The name. /// The tag or null - public UriPathSegmentVariableString(string name, object tag = null) - : base(name, tag) + public UriPathSegmentVariableString(object tag = null) + : base(tag) { - VariableName = name; - Value = name; - Display = name; - Expression = "^[^\"]*$"; - Tag = tag; - } - - /// - /// Initializes a new instance of the class. - /// - /// The name. - /// The display text. - /// The tag or null - public UriPathSegmentVariableString(string name, string display, object tag = null) - : base(name, tag) - { - VariableName = name; - Value = name; - Display = display; Expression = "^[^\"]*$"; Tag = tag; } @@ -42,8 +25,8 @@ public UriPathSegmentVariableString(string name, string display, object tag = nu /// Initializes a new instance of the class. /// /// The path segment to copy. - public UriPathSegmentVariableString(UriPathSegmentVariableString segment) - : base(segment.VariableName, segment.Display, segment.Tag) + public UriPathSegmentVariableString(UriPathSegmentVariableString segment) + : base(segment.Tag) { Expression = segment.Expression; } @@ -64,7 +47,12 @@ public override IDictionary GetVariable(string value) /// The copy. public override IUriPathSegment Copy() { - return new UriPathSegmentVariableString(this) { Value = Value }; + return new UriPathSegmentVariableString(this) + { + Value = Value, + IsHidden = IsHidden, + Uri = Uri + }; } /// diff --git a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableUInt.cs b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableUInt.cs index b7ad4c7..fdde726 100644 --- a/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableUInt.cs +++ b/src/WebExpress.WebCore/WebUri/UriPathSegmentVariableUInt.cs @@ -1,39 +1,22 @@ using System.Collections.Generic; +using WebExpress.WebCore.WebParameter; namespace WebExpress.WebCore.WebUri { /// - /// Variable path segment. + /// UInt variable path segment. /// - public class UriPathSegmentVariableUInt : UriPathSegmentVariable + /// The parameter type. + public class UriPathSegmentVariableUInt : UriPathSegmentVariable + where TParameter : IParameterStatic, new() { /// /// Initializes a new instance of the class. /// - /// The name. /// The tag or null - public UriPathSegmentVariableUInt(string name, object tag = null) - : base(name, tag) + public UriPathSegmentVariableUInt(object tag = null) + : base(tag) { - VariableName = name; - Value = name; - Display = name; - Expression = @"^\d$"; - Tag = tag; - } - - /// - /// Initializes a new instance of the class. - /// - /// The name. - /// The display text. - /// The tag or null - public UriPathSegmentVariableUInt(string name, string display, object tag = null) - : base(name, display, tag) - { - VariableName = name; - Value = name; - Display = display; Expression = @"^\d$"; Tag = tag; } @@ -42,8 +25,8 @@ public UriPathSegmentVariableUInt(string name, string display, object tag = null /// Initializes a new instance of the class. /// /// The path segment to copy. - public UriPathSegmentVariableUInt(UriPathSegmentVariableUInt segment) - : base(segment.VariableName, segment.Display, segment.Tag) + public UriPathSegmentVariableUInt(UriPathSegmentVariableUInt segment) + : base(segment.Tag) { Expression = segment.Expression; } @@ -64,7 +47,12 @@ public override IDictionary GetVariable(string value) /// The copy. public override IUriPathSegment Copy() { - return new UriPathSegmentVariableUInt(this) { Value = Value }; + return new UriPathSegmentVariableUInt(this) + { + Value = Value, + IsHidden = IsHidden, + Uri = Uri + }; } /// diff --git a/src/WebExpress.WebCore/WebUri/UriQuery.cs b/src/WebExpress.WebCore/WebUri/UriQuery.cs index 54f6982..21e3bb2 100644 --- a/src/WebExpress.WebCore/WebUri/UriQuery.cs +++ b/src/WebExpress.WebCore/WebUri/UriQuery.cs @@ -1,9 +1,11 @@ +using WebExpress.WebCore.WebParameter; + namespace WebExpress.WebCore.WebUri { /// /// The query part (e.g. ?title=Uniform_Resource_Identifier). /// - public class UriQuery + public class UriQuery : IUriQuery { /// /// Returns the key. @@ -35,4 +37,44 @@ public override string ToString() return $"{Key}={Value}"; } } + + /// + /// Represents a key-value pair used as a query parameter in a URI. + /// + /// + /// The type that implements the IParameterStatic interface and defines the structure + /// of the query parameter. + /// + public class UriQuery : IUriQuery + where TParameter : IParameterStatic + { + /// + /// Returns the key. + /// + public string Key { get; protected set; } + + /// + /// Returns the value. + /// + public string Value { get; protected set; } + + /// + /// Initializes a new instance of the class. + /// + /// The value. + public UriQuery(string value = null) + { + Key = TParameter.Key; + Value = value; + } + + /// + /// Converts the query to a string. + /// + /// A string that represents the current query. + public override string ToString() + { + return $"{Key}={Value}"; + } + } } \ No newline at end of file diff --git a/src/WebExpress.WebCore/WebUri/UriScheme.cs b/src/WebExpress.WebCore/WebUri/UriScheme.cs index 2b11f04..4c616a3 100644 --- a/src/WebExpress.WebCore/WebUri/UriScheme.cs +++ b/src/WebExpress.WebCore/WebUri/UriScheme.cs @@ -38,7 +38,18 @@ public enum UriScheme /// /// The Mailto URI scheme. /// - Mailto + Mailto, + + /// + /// Represents a web service or related functionality. + /// + Ws, + + /// + /// Specifies the WebSocket Secure (WSS) protocol, which provides encrypted + /// communication over WebSockets using TLS. + /// + Wss } /// @@ -62,6 +73,8 @@ public static string ToString(this UriScheme scheme) UriScheme.Ldap => "ldap", UriScheme.Ldaps => "ldaps", UriScheme.Mailto => "mailto", + UriScheme.Ws => "ws", + UriScheme.Wss => "wss", _ => "http" }; } diff --git a/src/WebExpress.WebCore/ufo.ico b/src/WebExpress.WebCore/ufo.ico new file mode 100644 index 0000000..e4d2fef Binary files /dev/null and b/src/WebExpress.WebCore/ufo.ico differ