@if (SummaryTemplate is not null)
{
- @SummaryTemplate
+ @SummaryTemplate(Value)
+ }
+ else if (SummaryFormat is not null)
+ {
+ @SummaryFormat(Value)
}
else
{
@@ -14,14 +18,44 @@
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/Pagination/BitDataGridPaginator.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/Pagination/BitDataGridPaginator.razor.cs
index 57f036b321..140c9a2847 100644
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/Pagination/BitDataGridPaginator.razor.cs
+++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/Pagination/BitDataGridPaginator.razor.cs
@@ -8,14 +8,49 @@ public partial class BitDataGridPaginator : IDisposable
private readonly EventCallbackSubscriber _totalItemCountChanged;
///
- /// Specifies the associated . This parameter is required.
+ /// The title of the go to first page button.
///
- [Parameter, EditorRequired] public BitDataGridPaginationState Value { get; set; } = default!;
+ [Parameter] public string GoToFirstButtonTitle { get; set; } = "Go to first page";
+
+ ///
+ /// The title of the go to previous page button.
+ ///
+ [Parameter] public string GoToPrevButtonTitle { get; set; } = "Go to previous page";
+
+ ///
+ /// The title of the go to next page button.
+ ///
+ [Parameter] public string GoToNextButtonTitle { get; set; } = "Go to next page";
+
+ ///
+ /// The title of the go to last page button.
+ ///
+ [Parameter] public string GoToLastButtonTitle { get; set; } = "Go to last page";
+
+ ///
+ /// Optionally supplies a format for rendering the page count summary.
+ ///
+ [Parameter] public Func? SummaryFormat { get; set; }
///
/// Optionally supplies a template for rendering the page count summary.
///
- [Parameter] public RenderFragment? SummaryTemplate { get; set; }
+ [Parameter] public RenderFragment? SummaryTemplate { get; set; }
+
+ ///
+ /// The optional custom format for the main text of the paginator in the middle of it.
+ ///
+ [Parameter] public Func? TextFormat { get; set; }
+
+ ///
+ /// The optional custom template for the main text of the paginator in the middle of it.
+ ///
+ [Parameter] public RenderFragment? TextTemplate { get; set; }
+
+ ///
+ /// Specifies the associated . This parameter is required.
+ ///
+ [Parameter, EditorRequired] public BitDataGridPaginationState Value { get; set; } = default!;
///
/// Constructs an instance of .
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/Pagination/BitDataGridPaginator.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/Pagination/BitDataGridPaginator.scss
index 04cda8f09b..04010c2c1a 100644
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/Pagination/BitDataGridPaginator.scss
+++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/Pagination/BitDataGridPaginator.scss
@@ -57,3 +57,15 @@
transform: scaleX(-1);
}
}
+
+[dir=rtl] {
+ .bitdatagrid-paginator {
+ .go-next, .go-last {
+ transform: scaleX(1);
+ }
+
+ .go-previous, .go-first {
+ transform: scaleX(-1);
+ }
+ }
+}
diff --git a/src/BlazorUI/Bit.BlazorUI.Icons/Bit.BlazorUI.Icons.csproj b/src/BlazorUI/Bit.BlazorUI.Icons/Bit.BlazorUI.Icons.csproj
index 358bb5f7dd..d6d4fa27b7 100644
--- a/src/BlazorUI/Bit.BlazorUI.Icons/Bit.BlazorUI.Icons.csproj
+++ b/src/BlazorUI/Bit.BlazorUI.Icons/Bit.BlazorUI.Icons.csproj
@@ -3,10 +3,10 @@
- net8.0;net7.0;net6.0
+ net9.0;net8.0Bit.BlazorUI0
-
+
BeforeBuildTasks;
$(ResolveStaticWebAssetsInputsDependsOn)
@@ -26,7 +26,7 @@
-
+
diff --git a/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj b/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj
index 70cf41fb96..f204e95372 100644
--- a/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj
+++ b/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj
@@ -1,7 +1,7 @@
- net8.0;net7.0;net6.0
+ net9.0;net8.0false11.0
@@ -13,10 +13,10 @@
-
+
-
-
+
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Inputs/Dropdown/BitDropdownTests.cs b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Inputs/Dropdown/BitDropdownTests.cs
index 84b5f9207c..96a5108ba0 100644
--- a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Inputs/Dropdown/BitDropdownTests.cs
+++ b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Inputs/Dropdown/BitDropdownTests.cs
@@ -903,6 +903,12 @@ public void BitDropdownVirtualizeTest(bool virtualize, int? itemSize, int? overs
//https://bunit.dev/docs/test-doubles/emulating-ijsruntime.html#-jsinterop-emulation
const double viewportHeight = 1_000_000_000;
var items = GetRangeDropdownItems(500);
+
+ // To ensure a consistent display structure in the Virtualize component across .NET 9 and .NET 8,
+ // we've set the default value of MaxItemCount to 100. This means that even if a higher value is specified,
+ // only a maximum of 100 items will be rendered by default.
+ AppContext.SetData("Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize.MaxItemCount", 100);
+
var component = RenderComponent, string>>(parameters =>
{
parameters.Add(p => p.IsEnabled, true);
@@ -931,6 +937,7 @@ public void BitDropdownVirtualizeTest(bool virtualize, int? itemSize, int? overs
{
//When virtualize is true, number of rendered items is greater than number of items show in the list + 2 * overScanCount.
var expectedRenderedItemCount = Math.Ceiling((decimal)(viewportHeight / component.Instance.ItemSize)) + (2 * component.Instance.OverscanCount);
+ expectedRenderedItemCount = Math.Min(expectedRenderedItemCount, 100);
//When actualRenderedItemCount is smaller than expectedRenderedItemCount, so show all items in viewport then actualRenderedItemCount equals total items count
if (actualRenderedItemCount < expectedRenderedItemCount)
@@ -1051,9 +1058,9 @@ private void HandleValuesChanged(IEnumerable values)
new() { Text = "Banana", Value = "f-ban" },
new() { Text = "Broccoli", Value = "v-bro" }
};
-
- private static ICollection> GetRangeDropdownItems(int count) =>
+
+ private static ICollection> GetRangeDropdownItems(int count) =>
Enumerable.Range(1, count).Select(item => new BitDropdownItem
{
ItemType = BitDropdownItemType.Normal,
diff --git a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Inputs/FileUpload/BitFileUploadTests.cs b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Inputs/FileUpload/BitFileUploadTests.cs
index 2d724f6ef3..d90edf040d 100644
--- a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Inputs/FileUpload/BitFileUploadTests.cs
+++ b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Inputs/FileUpload/BitFileUploadTests.cs
@@ -27,15 +27,15 @@ public void BitUploadFileHasBasicClass(bool isEnabled)
DataRow(true),
DataRow(false)
]
- public void BitFileUploadMultipleAttributeTest(bool isMultiSelect)
+ public void BitFileUploadMultipleAttributeTest(bool isMultiple)
{
var com = RenderComponent(parameters =>
{
- parameters.Add(p => p.MultiSelect, isMultiSelect);
+ parameters.Add(p => p.Multiple, isMultiple);
});
var bitFileUpload = com.Find(".bit-upl-fi");
- Assert.AreEqual(isMultiSelect, bitFileUpload.HasAttribute("multiple"));
+ Assert.AreEqual(isMultiple, bitFileUpload.HasAttribute("multiple"));
}
[TestMethod]
diff --git a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Lists/BasicList/BitBasicListTests.cs b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Lists/BasicList/BitBasicListTests.cs
index 36b72a53c3..b1266f6fd0 100644
--- a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Lists/BasicList/BitBasicListTests.cs
+++ b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Lists/BasicList/BitBasicListTests.cs
@@ -26,6 +26,11 @@ public void BitBasicListShouldRenderExpectedChildElements(bool virtualize, int?
Context.JSInterop.Mode = JSRuntimeMode.Loose;
+ // To ensure a consistent display structure in the Virtualize component across .NET 9 and .NET 8,
+ // we've set the default value of MaxItemCount to 100. This means that even if a higher value is specified,
+ // only a maximum of 100 items will be rendered by default.
+ AppContext.SetData("Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize.MaxItemCount", 100);
+
var component = RenderComponent(parameters =>
{
parameters.Add(p => p.Virtualize, virtualize);
@@ -42,6 +47,8 @@ public void BitBasicListShouldRenderExpectedChildElements(bool virtualize, int?
{
//When virtualize is true, number of rendered items is greater than number of items show in the list + 2 * overScanCount.
var expectedRenderedItemCount = Math.Ceiling((decimal)(viewportHeight / component.Instance.ItemSize)) + (2 * component.Instance.OverscanCount);
+ expectedRenderedItemCount = Math.Min(expectedRenderedItemCount, 100);
+
var actualRenderedItemCount = bitList.GetElementsByClassName("list-item").Length;
//When actualRenderedItemCount is smaller than expectedRenderedItemCount, so show all items in viewport then actualRenderedItemCount equals total items count
diff --git a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Utilities/Link/BitLinkTests.cs b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Utilities/Link/BitLinkTests.cs
index 905bbe19d5..38583c7bad 100644
--- a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Utilities/Link/BitLinkTests.cs
+++ b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Utilities/Link/BitLinkTests.cs
@@ -603,16 +603,16 @@ public void BitLinkShouldRespectHtmlAttributes(string href)
[DataTestMethod,
DataRow(null, null),
- DataRow(null, BitAnchorRel.Bookmark),
- DataRow(null, BitAnchorRel.Bookmark | BitAnchorRel.Alternate),
+ DataRow(null, BitLinkRel.Bookmark),
+ DataRow(null, BitLinkRel.Bookmark | BitLinkRel.Alternate),
DataRow("https://bitplatform.dev", null),
- DataRow("https://bitplatform.dev", BitAnchorRel.Bookmark),
- DataRow("https://bitplatform.dev", BitAnchorRel.Bookmark | BitAnchorRel.Alternate),
+ DataRow("https://bitplatform.dev", BitLinkRel.Bookmark),
+ DataRow("https://bitplatform.dev", BitLinkRel.Bookmark | BitLinkRel.Alternate),
DataRow("#go-to-section", null),
- DataRow("#go-to-section", BitAnchorRel.Bookmark),
- DataRow("#go-to-section", BitAnchorRel.Bookmark | BitAnchorRel.Alternate)
+ DataRow("#go-to-section", BitLinkRel.Bookmark),
+ DataRow("#go-to-section", BitLinkRel.Bookmark | BitLinkRel.Alternate)
]
- public void BitLinkShouldRespectTarget(string href, BitAnchorRel? rel)
+ public void BitLinkShouldRespectTarget(string href, BitLinkRel? rel)
{
var component = RenderComponent(parameters =>
{
@@ -630,7 +630,7 @@ public void BitLinkShouldRespectTarget(string href, BitAnchorRel? rel)
{
if (rel.HasValue)
{
- var rels = string.Join(" ", Enum.GetValues(typeof(BitAnchorRel)).Cast().Where(r => rel.Value.HasFlag(r)).Select(r => r.ToString().ToLower()));
+ var rels = string.Join(" ", Enum.GetValues(typeof(BitLinkRel)).Cast().Where(r => rel.Value.HasFlag(r)).Select(r => r.ToString().ToLower()));
component.MarkupMatches(@$"");
}
diff --git a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Utilities/Stack/BitStackTests.cs b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Utilities/Stack/BitStackTests.cs
index 5a08dba411..ab98936f24 100644
--- a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Utilities/Stack/BitStackTests.cs
+++ b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Utilities/Stack/BitStackTests.cs
@@ -420,6 +420,52 @@ public void BitStackShouldRespectHorizontalChangingAfterRender()
component.MarkupMatches(@$"");
}
+ [DataTestMethod,
+ DataRow(null),
+ DataRow(BitAlignment.Start),
+ DataRow(BitAlignment.End),
+ DataRow(BitAlignment.Center),
+ DataRow(BitAlignment.SpaceBetween),
+ DataRow(BitAlignment.SpaceAround),
+ DataRow(BitAlignment.SpaceEvenly),
+ DataRow(BitAlignment.Baseline),
+ DataRow(BitAlignment.Stretch)
+ ]
+ public void BitStackShouldRespectAlignment(BitAlignment? alignment)
+ {
+ var component = RenderComponent(parameters =>
+ {
+ parameters.Add(p => p.Alignment, alignment);
+ });
+
+ if (alignment.HasValue)
+ {
+ var jc = _AlignmentMap[alignment.Value];
+ var ai = _AlignmentMap[alignment.Value];
+
+ component.MarkupMatches(@$"");
+ }
+ else
+ {
+ component.MarkupMatches(@$"");
+ }
+ }
+
+ [DataTestMethod]
+ public void BitStackShouldRespectAlignmentChangingAfterRender()
+ {
+ var component = RenderComponent();
+
+ component.MarkupMatches(@$"");
+
+ component.SetParametersAndRender(parameters =>
+ {
+ parameters.Add(p => p.Alignment, BitAlignment.SpaceBetween);
+ });
+
+ component.MarkupMatches(@$"");
+ }
+
[DataTestMethod,
DataRow(BitAlignment.Start),
DataRow(BitAlignment.End),
@@ -498,7 +544,7 @@ public void BitStackShouldRespectReversedChangingAfterRender()
DataRow(BitAlignment.Baseline),
DataRow(BitAlignment.Stretch)
]
- public void BitStackShouldRespectBitAlignment(BitAlignment verticalAlign)
+ public void BitStackShouldRespectVerticalAlign(BitAlignment verticalAlign)
{
var component = RenderComponent(parameters =>
{
@@ -626,20 +672,20 @@ public void BitStackShouldRespectHorizontalAndReversedAndHorizontalAlignAndVerti
DataRow(true),
DataRow(false)
]
- public void BitStackShouldRespectFull(bool full)
+ public void BitStackShouldRespectAutoSize(bool autoSize)
{
var component = RenderComponent(parameters =>
{
- parameters.Add(p => p.AutoSize, full);
+ parameters.Add(p => p.AutoSize, autoSize);
});
- var style = full ? "width:auto;height:auto;" : null;
+ var style = autoSize ? "width:auto;height:auto;" : null;
component.MarkupMatches(@$"");
}
[DataTestMethod]
- public void BitStackShouldRespectFullChangingAfterRender()
+ public void BitStackShouldRespectAutoSizeChangingAfterRender()
{
var component = RenderComponent();
@@ -657,7 +703,7 @@ public void BitStackShouldRespectFullChangingAfterRender()
DataRow(true),
DataRow(false)
]
- public void BitStackShouldRespectFullWidth(bool autoWidth)
+ public void BitStackShouldRespectAutoWidth(bool autoWidth)
{
var component = RenderComponent(parameters =>
{
@@ -670,7 +716,7 @@ public void BitStackShouldRespectFullWidth(bool autoWidth)
}
[DataTestMethod]
- public void BitStackShouldRespectFullWidthChangingAfterRender()
+ public void BitStackShouldRespectAutoWidthChangingAfterRender()
{
var component = RenderComponent();
@@ -688,7 +734,7 @@ public void BitStackShouldRespectFullWidthChangingAfterRender()
DataRow(true),
DataRow(false)
]
- public void BitStackShouldRespectFullHeight(bool autoHeight)
+ public void BitStackShouldRespectAutoHeight(bool autoHeight)
{
var component = RenderComponent(parameters =>
{
@@ -701,7 +747,7 @@ public void BitStackShouldRespectFullHeight(bool autoHeight)
}
[DataTestMethod]
- public void BitStackShouldRespectFullHeightChangingAfterRender()
+ public void BitStackShouldRespectAutoHeightChangingAfterRender()
{
var component = RenderComponent();
@@ -725,7 +771,7 @@ public void BitStackShouldRespectFullHeightChangingAfterRender()
DataRow(false, false, true),
DataRow(false, false, false)
]
- public void BitStackShouldRespectFullAndFullWidthAndFullHeight(bool autoSize, bool autoWidth, bool autoHeight)
+ public void BitStackShouldRespectAutoSizeAndAutoWidthAndAutoHeight(bool autoSize, bool autoWidth, bool autoHeight)
{
var component = RenderComponent(parameters =>
{
@@ -748,4 +794,97 @@ public void BitStackShouldRespectFullAndFullWidthAndFullHeight(bool autoSize, bo
component.MarkupMatches(@$"");
}
+
+ [DataTestMethod,
+ DataRow(true),
+ DataRow(false)
+ ]
+ public void BitStackShouldRespectFitHeight(bool fitHeight)
+ {
+ var component = RenderComponent(parameters =>
+ {
+ parameters.Add(p => p.FitHeight, fitHeight);
+ });
+
+ var style = fitHeight ? "height:fit-content;" : null;
+
+ component.MarkupMatches(@$"");
+ }
+
+ [DataTestMethod]
+ public void BitStackShouldRespectFitHeightChangingAfterRender()
+ {
+ var component = RenderComponent();
+
+ component.MarkupMatches(@$"");
+
+ component.SetParametersAndRender(parameters =>
+ {
+ parameters.Add(p => p.FitHeight, true);
+ });
+
+ component.MarkupMatches(@$"");
+ }
+
+ [DataTestMethod,
+ DataRow(true),
+ DataRow(false)
+ ]
+ public void BitStackShouldRespectFitWidth(bool fitWidth)
+ {
+ var component = RenderComponent(parameters =>
+ {
+ parameters.Add(p => p.FitWidth, fitWidth);
+ });
+
+ var style = fitWidth ? "width:fit-content;" : null;
+
+ component.MarkupMatches(@$"");
+ }
+
+ [DataTestMethod]
+ public void BitStackShouldRespectFitWidthChangingAfterRender()
+ {
+ var component = RenderComponent();
+
+ component.MarkupMatches(@$"");
+
+ component.SetParametersAndRender(parameters =>
+ {
+ parameters.Add(p => p.FitWidth, true);
+ });
+
+ component.MarkupMatches(@$"");
+ }
+
+ [DataTestMethod,
+ DataRow(true),
+ DataRow(false)
+ ]
+ public void BitStackShouldRespectFitSize(bool fitSize)
+ {
+ var component = RenderComponent(parameters =>
+ {
+ parameters.Add(p => p.FitSize, fitSize);
+ });
+
+ var style = fitSize ? "width:fit-content;height:fit-content;" : null;
+
+ component.MarkupMatches(@$"");
+ }
+
+ [DataTestMethod]
+ public void BitStackShouldRespectFitSizeChangingAfterRender()
+ {
+ var component = RenderComponent();
+
+ component.MarkupMatches(@$"");
+
+ component.SetParametersAndRender(parameters =>
+ {
+ parameters.Add(p => p.FitSize, true);
+ });
+
+ component.MarkupMatches(@$"");
+ }
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Bit.BlazorUI.csproj b/src/BlazorUI/Bit.BlazorUI/Bit.BlazorUI.csproj
index 9f440dbd75..9438d817fd 100644
--- a/src/BlazorUI/Bit.BlazorUI/Bit.BlazorUI.csproj
+++ b/src/BlazorUI/Bit.BlazorUI/Bit.BlazorUI.csproj
@@ -3,10 +3,10 @@
- net8.0;net7.0;net6.0
+ net9.0;net8.0trueenable
-
+
BeforeBuildTasks;
$(ResolveStaticWebAssetsInputsDependsOn)
@@ -19,9 +19,8 @@
-
-
+
@@ -52,7 +51,7 @@
-
+
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/BitAnchorRel.cs b/src/BlazorUI/Bit.BlazorUI/Components/BitLinkRel.cs
similarity index 92%
rename from src/BlazorUI/Bit.BlazorUI/Components/BitAnchorRel.cs
rename to src/BlazorUI/Bit.BlazorUI/Components/BitLinkRel.cs
index 96bd668b78..08b9b02045 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/BitAnchorRel.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/BitLinkRel.cs
@@ -1,7 +1,10 @@
namespace Bit.BlazorUI;
+///
+/// The rel attribute defines the relationship between a linked resource and the current document.
+///
[Flags]
-public enum BitAnchorRel
+public enum BitLinkRel
{
///
/// Provides a link to an alternate representation of the document. (i.e. print page, translated or mirror)
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.razor b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.razor
index 8ee398dd36..1b488d8975 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.razor
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.razor
@@ -78,6 +78,7 @@ else
{
+ /// Expand the button width to 100% of the available width.
+ ///
+ [Parameter, ResetClassBuilder]
+ public bool FullWidth { get; set; }
+
///
/// The value of the href attribute of the link rendered by the button. If provided, the component will be rendered as an anchor tag instead of button.
///
- [Parameter] public string? Href { get; set; }
+ [Parameter]
+ [CallOnSet(nameof(OnSetHrefAndRel))]
+ public string? Href { get; set; }
///
/// The name of the icon to render inside the button.
@@ -117,6 +126,13 @@ public partial class BitButton : BitComponentBase
[Parameter, ResetClassBuilder]
public bool ReversedIcon { get; set; }
+ ///
+ /// If Href provided, specifies the relationship between the current document and the linked document.
+ ///
+ [Parameter]
+ [CallOnSet(nameof(OnSetHrefAndRel))]
+ public BitLinkRel? Rel { get; set; }
+
///
/// The text of the secondary section of the button.
///
@@ -211,6 +227,8 @@ protected override void RegisterCssClasses()
ClassBuilder.Register(() => ReversedIcon ? "bit-btn-rvi" : string.Empty);
ClassBuilder.Register(() => FixedColor ? "bit-btn-ftc" : string.Empty);
+
+ ClassBuilder.Register(() => FullWidth ? "bit-btn-flw" : string.Empty);
}
protected override void RegisterCssStyles()
@@ -255,4 +273,15 @@ private async Task HandleOnClick(MouseEventArgs e)
await AssignIsLoading(false);
}
+
+ private void OnSetHrefAndRel()
+ {
+ if (Rel.HasValue is false || Href.HasNoValue() || Href!.StartsWith('#'))
+ {
+ _rel = null;
+ return;
+ }
+
+ _rel = BitLinkRelUtils.GetRels(Rel.Value);
+ }
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss
index 677347da00..abbf42f882 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss
@@ -155,6 +155,9 @@
color: var(--bit-btn-clr-txt);
}
+.bit-btn-flw {
+ width: 100%;
+}
.bit-btn-pri {
--bit-btn-clr: #{$clr-pri};
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor
index a5284de16c..b9c24df77b 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor
@@ -90,7 +90,7 @@
aria-disabled="@nextDisabled">
+ class="bit-icon bit-icon--Up bit-ico-r180 @Classes?.NextMonthNavIcon" />
@if (ShowTimePicker && ShowTimePickerAsOverlay && (ShowMonthPicker is false || ShowMonthPickerAsOverlay))
{
@@ -247,7 +247,7 @@
class="bit-cal-nbt @Classes?.NextYearNavButton">
+ class="bit-icon bit-icon--Up bit-ico-r180 @Classes?.NextYearNavIcon" />
@if (ShowTimePickerAsOverlay && ShowTimePicker)
{
@@ -363,7 +363,7 @@
class="bit-cal-nbt @Classes?.NextYearRangeNavButton">
+ class="bit-icon bit-icon--Up bit-ico-r180 @Classes?.NextYearRangeNavIcon" />
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUpload.razor b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUpload.razor
index 77b3d2e2c6..c78fa0e0f8 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUpload.razor
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUpload.razor
@@ -9,24 +9,20 @@
@if (LabelTemplate is not null)
{
-
+ @LabelTemplate
}
else if (Label.HasValue())
{
-
+
}
@if (Files is not null)
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUpload.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUpload.razor.cs
index b5f0335b08..f7bee17cbb 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUpload.razor.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUpload.razor.cs
@@ -42,13 +42,18 @@ public partial class BitFileUpload : BitComponentBase, IAsyncDisposable
///
[Parameter] public bool AutoChunkSize { get; set; }
+ ///
+ /// Automatically resets the file-upload before starting to browse for files.
+ ///
+ [Parameter] public bool AutoReset { get; set; }
+
///
/// Automatically starts the upload file(s) process immediately after selecting the file(s).
///
[Parameter] public bool AutoUpload { get; set; }
///
- /// Enables or disables the chunked upload feature.
+ /// Enables the chunked upload.
///
[Parameter] public bool ChunkedUpload { get; set; }
@@ -70,9 +75,9 @@ public partial class BitFileUpload : BitComponentBase, IAsyncDisposable
[Parameter] public string FailedRemoveMessage { get; set; } = "File remove failed";
///
- /// Enables multi-file select and upload.
+ /// Enables multi-file selection.
///
- [Parameter] public bool MultiSelect { get; set; }
+ [Parameter] public bool Multiple { get; set; }
///
/// The text of select file button.
@@ -192,12 +197,12 @@ public partial class BitFileUpload : BitComponentBase, IAsyncDisposable
public IReadOnlyList? Files { get; private set; }
///
- /// General upload status.
+ /// The general status of the upload.
///
public BitFileUploadStatus UploadStatus { get; private set; }
///
- /// File input id.
+ /// The id of the file input element.
///
public string? InputId { get; private set; }
@@ -207,7 +212,7 @@ public partial class BitFileUpload : BitComponentBase, IAsyncDisposable
public bool IsRemoving { get; private set; }
///
- /// Starts Uploading the file(s).
+ /// Starts uploading the file(s).
///
public async Task Upload(BitFileInfo? fileInfo = null, string? uploadUrl = null)
{
@@ -234,12 +239,11 @@ public async Task Upload(BitFileInfo? fileInfo = null, string? uploadUrl = null)
}
///
- /// Pause upload.
+ /// Pauses the upload.
///
///
- /// null => all files | else => specific file
+ /// null (default) => all files | else => specific file
///
- ///
public void PauseUpload(BitFileInfo? fileInfo = null)
{
if (Files is null) return;
@@ -258,12 +262,11 @@ public void PauseUpload(BitFileInfo? fileInfo = null)
}
///
- /// Cancel upload.
+ /// Cancels the upload.
///
///
- /// null => all files | else => specific file
+ /// null (default) => all files | else => specific file
///
- ///
public void CancelUpload(BitFileInfo? fileInfo = null)
{
if (Files is null) return;
@@ -287,7 +290,6 @@ public void CancelUpload(BitFileInfo? fileInfo = null)
///
/// null => all files | else => specific file
///
- ///
public async Task RemoveFile(BitFileInfo? fileInfo = null)
{
if (Files is null) return;
@@ -311,16 +313,29 @@ public async Task RemoveFile(BitFileInfo? fileInfo = null)
}
///
- /// Open a file selection dialog
+ /// Opens a file selection dialog.
///
- ///
public async Task Browse()
{
if (IsEnabled is false) return;
+ if (AutoReset)
+ {
+ await Reset();
+ }
+
await _js.BitFileUploadBrowse(_inputRef);
}
+ ///
+ /// Resets the file-upload.
+ ///
+ public async Task Reset()
+ {
+ Files = [];
+ await _js.BitFileUploadReset(UniqueId, _inputRef);
+ }
+
///
@@ -417,7 +432,7 @@ private async Task HandleOnChange()
{
var url = AddQueryString(UploadUrl, UploadRequestQueryStrings);
- Files = await _js.BitFileUploadReset(UniqueId, _dotnetObj, _inputRef, url, UploadRequestHttpHeaders);
+ Files = await _js.BitFileUploadSetup(UniqueId, _dotnetObj, _inputRef, url, UploadRequestHttpHeaders);
if (Files is null) return;
@@ -706,7 +721,7 @@ protected virtual async ValueTask DisposeAsync(bool disposing)
try
{
- await _js.BitFileUploadDispose(UniqueId);
+ await _js.BitFileUploadClear(UniqueId);
}
catch (JSDisconnectedException) { } // we can ignore this exception here
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUpload.ts b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUpload.ts
index 3f3aa4bcd2..83eac375f8 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUpload.ts
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUpload.ts
@@ -2,14 +2,14 @@
export class FileUpload {
private static _fileUploaders: BitFileUploader[] = [];
- public static reset(
+ public static setup(
id: string,
dotnetReference: DotNetObject,
inputElement: HTMLInputElement,
uploadEndpointUrl: string,
headers: Record) {
- FileUpload._fileUploaders = FileUpload._fileUploaders.filter(u => u.id !== id);
+ FileUpload.clear(id);
const files = Array.from(inputElement.files!).map((file, index) => ({
name: file.name,
@@ -50,7 +50,7 @@
}
}
- public static setupDragDrop(dropZoneElement: HTMLElement, inputFile: HTMLInputElement) {
+ public static setupDragDrop(dropZoneElement: HTMLElement, inputElement: HTMLInputElement) {
function onDragHover(e: DragEvent) {
e.preventDefault();
@@ -62,15 +62,15 @@
function onDrop(e: DragEvent) {
e.preventDefault();
- inputFile.files = e.dataTransfer!.files;
+ inputElement.files = e.dataTransfer!.files;
const event = new Event('change', { bubbles: true });
- inputFile.dispatchEvent(event);
+ inputElement.dispatchEvent(event);
}
function onPaste(e: ClipboardEvent) {
- inputFile.files = e.clipboardData!.files;
+ inputElement.files = e.clipboardData!.files;
const event = new Event('change', { bubbles: true });
- inputFile.dispatchEvent(event);
+ inputElement.dispatchEvent(event);
}
dropZoneElement.addEventListener("dragenter", onDragHover);
@@ -91,13 +91,18 @@
}
- public static browse(inputFile: HTMLInputElement) {
- inputFile.click();
+ public static browse(inputElement: HTMLInputElement) {
+ inputElement.click();
}
- public static dispose(id: string) {
+ public static clear(id: string) {
FileUpload._fileUploaders = FileUpload._fileUploaders.filter(u => u.id !== id);
}
+
+ public static reset(id: string, inputElement: HTMLInputElement,) {
+ FileUpload.clear(id);
+ inputElement.value = '';
+ }
}
class BitFileUploader {
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUploadJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUploadJsRuntimeExtensions.cs
index 497ee7c0cd..b247ed68c7 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUploadJsRuntimeExtensions.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileUpload/BitFileUploadJsRuntimeExtensions.cs
@@ -5,22 +5,22 @@ namespace Bit.BlazorUI;
internal static class BitFileUploadJsRuntimeExtensions
{
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitFileInfo))]
- internal static ValueTask BitFileUploadReset(this IJSRuntime jsRuntime,
+ internal static ValueTask BitFileUploadSetup(this IJSRuntime jsRuntime,
string id,
DotNetObjectReference? dotnetObjectReference,
ElementReference element,
string uploadAddress,
IReadOnlyDictionary uploadRequestHttpHeaders)
{
- return jsRuntime.Invoke("BitBlazorUI.FileUpload.reset", id, dotnetObjectReference, element, uploadAddress, uploadRequestHttpHeaders);
+ return jsRuntime.Invoke("BitBlazorUI.FileUpload.setup", id, dotnetObjectReference, element, uploadAddress, uploadRequestHttpHeaders);
}
- internal static ValueTask BitFileUploadUpload(this IJSRuntime jsRuntime,
- string id,
- long from,
- long to,
- int index,
- string? uploadUrl,
+ internal static ValueTask BitFileUploadUpload(this IJSRuntime jsRuntime,
+ string id,
+ long from,
+ long to,
+ int index,
+ string? uploadUrl,
IReadOnlyDictionary? httpHeaders)
{
return (httpHeaders is null ? jsRuntime.InvokeVoid("BitBlazorUI.FileUpload.upload", id, from, to, index, uploadUrl)
@@ -32,8 +32,8 @@ internal static ValueTask BitFileUploadPause(this IJSRuntime jsRuntime, string i
return jsRuntime.InvokeVoid("BitBlazorUI.FileUpload.pause", id, index);
}
- internal static ValueTask BitFileUploadSetupDragDrop(this IJSRuntime jsRuntime,
- ElementReference dragDropZoneElement,
+ internal static ValueTask BitFileUploadSetupDragDrop(this IJSRuntime jsRuntime,
+ ElementReference dragDropZoneElement,
ElementReference inputFileElement)
{
return jsRuntime.Invoke("BitBlazorUI.FileUpload.setupDragDrop", dragDropZoneElement, inputFileElement);
@@ -44,8 +44,13 @@ internal static ValueTask BitFileUploadBrowse(this IJSRuntime jsRuntime, Element
return jsRuntime.InvokeVoid("BitBlazorUI.FileUpload.browse", inputFileElement);
}
- internal static ValueTask BitFileUploadDispose(this IJSRuntime jsRuntime, string id)
+ internal static ValueTask BitFileUploadClear(this IJSRuntime jsRuntime, string id)
{
- return jsRuntime.InvokeVoid("BitBlazorUI.FileUpload.dispose", id);
+ return jsRuntime.InvokeVoid("BitBlazorUI.FileUpload.clear", id);
+ }
+
+ internal static ValueTask BitFileUploadReset(this IJSRuntime jsRuntime, string id, ElementReference inputFileElement)
+ {
+ return jsRuntime.InvokeVoid("BitBlazorUI.FileUpload.reset", id, inputFileElement);
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.razor b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.razor
index 5f53830704..a4efe15cb9 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.razor
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.razor
@@ -173,7 +173,7 @@
aria-disabled="@nextDisabled">
+ class="bit-icon bit-icon--Up bit-ico-r180 @Classes?.NextMonthNavIcon" />
@if (ShowCloseButton && Standalone is false)
{
@@ -346,7 +346,7 @@
class="bit-dtp-nbt @Classes?.NextYearNavButton">
+ class="bit-icon bit-icon--Up bit-ico-r180 @Classes?.NextYearNavIcon" />
@if (_showTimePickerAsOverlayInternal && ShowTimePicker)
{
@@ -464,7 +464,7 @@
class="bit-dtp-nbt @Classes?.NextYearRangeNavButton">
+ class="bit-icon bit-icon--Up bit-ico-r180 @Classes?.NextYearRangeNavIcon" />
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DateRangePicker/BitDateRangePicker.razor b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DateRangePicker/BitDateRangePicker.razor
index 6d5297b4b5..80d4233f48 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DateRangePicker/BitDateRangePicker.razor
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DateRangePicker/BitDateRangePicker.razor
@@ -170,7 +170,7 @@
class="bit-dtrp-nbt @Classes?.NextMonthNavButton">
+ class="bit-icon bit-icon--Up bit-ico-r180 @Classes?.NextMonthNavIcon" />
@if (ShowCloseButton && Standalone is false)
{
@@ -344,7 +344,7 @@
class="bit-dtrp-nbt @Classes?.NextYearNavButton">
+ class="bit-icon bit-icon--Up bit-ico-r180 @Classes?.NextYearNavIcon" />
@if (_showTimePickerAsOverlayInternal && ShowTimePicker)
{
@@ -462,7 +462,7 @@
class="bit-dtrp-nbt @Classes?.NextYearRangeNavButton">
+ class="bit-icon bit-icon--Up bit-ico-r180 @Classes?.NextYearRangeNavIcon" />
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/_BitBasicListVirtualize.cs b/src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/_BitBasicListVirtualize.cs
index 210d33d3a4..6517912358 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/_BitBasicListVirtualize.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/_BitBasicListVirtualize.cs
@@ -44,9 +44,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
builder.AddAttribute(seq++, nameof(Virtualize.ItemContent),
(RenderFragment)(item => b => b.AddContent(seq++, (ItemContent ?? ChildContent)?.Invoke(item))));
-#if NET8_0_OR_GREATER
builder.AddAttribute(seq++, nameof(Virtualize.EmptyContent), (RenderFragment)(b => b.AddContent(seq++, EmptyContent)));
-#endif
builder.AddComponentReferenceCapture(seq++, v => _virtualizeRef = (Virtualize)v);
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Message/BitMessage.razor b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Message/BitMessage.razor
index 53beeaa963..6d14fcece6 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Message/BitMessage.razor
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Message/BitMessage.razor
@@ -32,14 +32,14 @@
@if (Truncate && Multiline is false)
{
}
@if (OnDismiss.HasDelegate)
{
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Message/BitMessage.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Message/BitMessage.razor.cs
index 6b3e455d6b..f7c63a9a4d 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Message/BitMessage.razor.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Message/BitMessage.razor.cs
@@ -28,9 +28,9 @@ public partial class BitMessage : BitComponentBase
[Parameter] public BitMessageClassStyles? Classes { get; set; }
///
- /// Custom Fabric icon name for the collapse icon in Truncate mode. If unset, default will be the Fabric DoubleChevronUp icon.
+ /// Custom Fabric icon name for the collapse icon in Truncate mode.
///
- [Parameter] public string CollapseIcon { get; set; } = "DoubleChevronUp";
+ [Parameter] public string? CollapseIcon { get; set; }
///
/// The general color of the message.
@@ -46,7 +46,7 @@ public partial class BitMessage : BitComponentBase
///
/// Custom Fabric icon name to replace the dismiss icon. If unset, default will be the Fabric Cancel icon.
///
- [Parameter] public string DismissIcon { get; set; } = "Cancel";
+ [Parameter] public string? DismissIcon { get; set; }
///
/// Determines the elevation of the message, a scale from 1 to 24.
@@ -55,9 +55,9 @@ public partial class BitMessage : BitComponentBase
public int? Elevation { get; set; }
///
- /// Custom Fabric icon name for the expand icon in Truncate mode. If unset, default will be the Fabric DoubleChevronDown icon.
+ /// Custom Fabric icon name for the expand icon in Truncate mode.
///
- [Parameter] public string ExpandIcon { get; set; } = "DoubleChevronDown";
+ [Parameter] public string? ExpandIcon { get; set; }
///
/// Prevents rendering the icon of the message.
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.razor.cs
index 952d7ee7c7..3ffb63375f 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.razor.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.razor.cs
@@ -38,7 +38,7 @@ public partial class BitLink : BitComponentBase
///
[Parameter]
[CallOnSet(nameof(OnSetHrefAndRel))]
- public BitAnchorRel? Rel { get; set; }
+ public BitLinkRel? Rel { get; set; }
///
/// If Href provided, specifies how to open the link.
@@ -84,6 +84,6 @@ private void OnSetHrefAndRel()
return;
}
- _rel = string.Join(" ", Enum.GetValues(typeof(BitAnchorRel)).Cast().Where(r => Rel.Value.HasFlag(r)).Select(r => r.ToString().ToLower()));
+ _rel = BitLinkRelUtils.GetRels(Rel.Value);
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts b/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts
index bc021a8c6d..19a2b5482e 100644
--- a/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts
+++ b/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts
@@ -1,4 +1,4 @@
-(BitBlazorUI as any).version = (window as any)['bit-blazorui version'] = '8.12.0';
+(BitBlazorUI as any).version = (window as any)['bit-blazorui version'] = '9.0.0';
interface DotNetObject {
invokeMethod(methodIdentifier: string, ...args: any[]): T;
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/fabric.mdl2.bit.blazoui.scss b/src/BlazorUI/Bit.BlazorUI/Styles/fabric.mdl2.bit.blazoui.scss
index 7320c60d99..4e54544c60 100644
--- a/src/BlazorUI/Bit.BlazorUI/Styles/fabric.mdl2.bit.blazoui.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/fabric.mdl2.bit.blazoui.scss
@@ -17,17 +17,13 @@
.bit-icon--Add:before { content: "\E710"; }
.bit-icon--CalendarMirrored:before { content: "\ED28"; }
.bit-icon--Cancel:before { content: "\E711"; }
-.bit-icon--ChevronDown:before { content: "\E70D"; }
.bit-icon--ChevronDownSmall:before { content: "\E96E"; }
.bit-icon--ChevronRight:before { content: "\E76C"; }
-.bit-icon--ChevronUpSmall:before { content: "\E96D"; }
.bit-icon--ChromeBackMirrored:before { content: "\EA47"; }
.bit-icon--Clock:before { content: "\E917"; }
.bit-icon--Completed:before { content: "\E930"; }
.bit-icon--Delete:before { content: "\E74D"; }
-.bit-icon--DoubleChevronDown:before { content: "\EE04"; }
.bit-icon--DoubleChevronUp:before { content: "\EDBD"; }
-.bit-icon--Down:before { content: "\E74B"; }
.bit-icon--ErrorBadge:before { content: "\EA39"; }
.bit-icon--FavoriteStar:before { content: "\E734"; }
.bit-icon--FavoriteStarFill:before { content: "\E735"; }
diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Enums/BitLinkRelUtils.cs b/src/BlazorUI/Bit.BlazorUI/Utils/Enums/BitLinkRelUtils.cs
new file mode 100644
index 0000000000..775aaaaf88
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI/Utils/Enums/BitLinkRelUtils.cs
@@ -0,0 +1,11 @@
+namespace Bit.BlazorUI;
+
+internal class BitLinkRelUtils
+{
+ internal static BitLinkRel[] AllRels = Enum.GetValues();
+
+ internal static string GetRels(BitLinkRel rel)
+ {
+ return string.Join(" ", AllRels.Where(r => rel.HasFlag(r)).Select(r => r.ToString().ToLower()));
+ }
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/wwwroot/fonts/FabMDL2.4.66.bit.BlazorUI.woff2 b/src/BlazorUI/Bit.BlazorUI/wwwroot/fonts/FabMDL2.4.66.bit.BlazorUI.woff2
index 45daf4f8ab..0cdfa16d83 100644
Binary files a/src/BlazorUI/Bit.BlazorUI/wwwroot/fonts/FabMDL2.4.66.bit.BlazorUI.woff2 and b/src/BlazorUI/Bit.BlazorUI/wwwroot/fonts/FabMDL2.4.66.bit.BlazorUI.woff2 differ
diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj
index df65f045da..04d65f1029 100644
--- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj
+++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj
@@ -1,27 +1,27 @@
- net8.0
+ net9.0
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+
-
+
diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Components/App.razor b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Components/App.razor
index 6858b979df..8d494028ae 100644
--- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Components/App.razor
+++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Components/App.razor
@@ -52,7 +52,6 @@
-
@if (AppRenderMode.PrerenderEnabled is false || noPrerender)
@@ -63,7 +62,7 @@
@if (HttpContext.Request.IsCrawlerClient() is false)
{
-
+
@if (AppRenderMode.PwaEnabled)
{
diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Startup/Middlewares.cs b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Startup/Middlewares.cs
index 907a30c9fd..bc579d3bdd 100644
--- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Startup/Middlewares.cs
+++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Startup/Middlewares.cs
@@ -85,6 +85,7 @@ public static void Use(WebApplication app, IWebHostEnvironment env, IConfigurati
UseSiteMap(app);
// Handle the rest of requests with blazor
+ app.MapStaticAssets();
app.MapRazorComponents()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj
index 1959d70b2f..7413a7365b 100644
--- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj
+++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj
@@ -1,22 +1,22 @@
- net8.0
+ net9.0
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
+
+
+
+ compile; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj
index 1c25d95b4c..342f95da8f 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net9.0
BeforeBuildTasks;
$(ResolveStaticWebAssetsInputsDependsOn)
@@ -16,17 +16,17 @@
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor
index dd127e7bcb..e9acf05018 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor
@@ -193,7 +193,23 @@
-
+
+
+
Use BitButton as a hyperlink to external URLs, with a rel attribute.
+
+
+
+ Open bitplatform.dev with a rel attribute (nofollow)
+
+
+
+ Open bitplatform.dev with a rel attribute (nofollow & noreferrer)
+
+
+
+
+
+
BitButton supports three different types, 'Submit' for sending form data, 'Reset' to clear form inputs, and 'Button' to provide flexibility for different interaction purposes.
@@ -225,7 +241,7 @@
-
+
Here are some examples of customizing the button content.
Varying sizes for buttons tailored to meet diverse design needs, ensuring flexibility and visual hierarchy within your interface.
@@ -329,7 +345,17 @@
-
+
+
+
Setting the FullWidth makes the button occupy 100% of its container's width.
+
+
+ Full Width Button
+
+
+
+
+
Offering a range of specialized color variants with Primary being the default, providing visual cues for specific actions or states within your application.
@@ -443,7 +469,7 @@
-
+
Empower customization by overriding default styles and classes, allowing tailored design modifications to suit specific UI requirements.
@@ -486,7 +512,7 @@
-
+
Use BitButton in right-to-left (RTL).
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.cs
index 3b8b054bd2..5bc909c0d0 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.cs
@@ -74,6 +74,13 @@ public partial class BitButtonDemo
Description = "Preserves the foreground color of the button through hover and focus.",
},
new()
+ {
+ Name = "FullWidth",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Expand the button width to 100% of the available width.",
+ },
+ new()
{
Name = "Href",
Type = "string?",
@@ -146,6 +153,15 @@ public partial class BitButtonDemo
Description = "Reverses the positions of the icon and the main content of the button.",
},
new()
+ {
+ Name = "Rel",
+ Type = "BitLinkRel?",
+ DefaultValue = "null",
+ Description = "If Href provided, specifies the relationship between the current document and the linked document.",
+ LinkType = LinkType.Link,
+ Href = "#button-rel",
+ },
+ new()
{
Name = "SecondaryText",
Type = "string?",
@@ -496,6 +512,93 @@ public partial class BitButtonDemo
Value="3",
},
]
+ },
+ new()
+ {
+ Id = "button-rel",
+ Name = "BitLinkRel",
+ Description = "",
+ Items =
+ [
+ new()
+ {
+ Name = "Alternate",
+ Value = "1",
+ Description = "Provides a link to an alternate representation of the document. (i.e. print page, translated or mirror)"
+ },
+ new()
+ {
+ Name = "Author",
+ Value = "2",
+ Description = "Provides a link to the author of the document."
+ },
+ new()
+ {
+ Name = "Bookmark",
+ Value = "4",
+ Description = "Permanent URL used for bookmarking."
+ },
+ new()
+ {
+ Name = "External",
+ Value = "8",
+ Description = "Indicates that the referenced document is not part of the same site as the current document."
+ },
+ new()
+ {
+ Name = "Help",
+ Value = "16",
+ Description = "Provides a link to a help document."
+ },
+ new()
+ {
+ Name = "License",
+ Value = "32",
+ Description = "Provides a link to licensing information for the document."
+ },
+ new()
+ {
+ Name = "Next",
+ Value = "64",
+ Description = "Provides a link to the next document in the series."
+ },
+ new()
+ {
+ Name = "NoFollow",
+ Value = "128",
+ Description = @"Links to an unendorsed document, like a paid link. (""NoFollow"" is used by Google, to specify that the Google search spider should not follow that link)"
+ },
+ new()
+ {
+ Name = "NoOpener",
+ Value = "256",
+ Description = "Requires that any browsing context created by following the hyperlink must not have an opener browsing context."
+ },
+ new()
+ {
+ Name = "NoReferrer",
+ Value = "512",
+ Description = "Makes the referrer unknown. No referrer header will be included when the user clicks the hyperlink."
+ },
+ new()
+ {
+ Name = "Prev",
+ Value = "1024",
+ Description = "The previous document in a selection."
+ },
+ new()
+ {
+ Name = "Search",
+ Value = "2048",
+ Description = "Links to a search tool for the document."
+ },
+ new()
+ {
+ Name = "Tag",
+ Value = "4096",
+ Description = "A tag (keyword) for the current document."
+ }
+ ]
}
];
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.samples.cs
index 9641719c35..7d34795322 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.samples.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.samples.cs
@@ -152,6 +152,15 @@ Go to bitplatform GitHub
";
private readonly string example8RazorCode = @"
+
+ Open bitplatform.dev with a rel attribute (nofollow)
+
+
+
+ Open bitplatform.dev with a rel attribute (nofollow & noreferrer)
+";
+
+ private readonly string example9RazorCode = @"
@@ -163,7 +172,7 @@ Go to bitplatform GitHub
Button
";
- private readonly string example8CsharpCode = @"
+ private readonly string example9CsharpCode = @"
public class ButtonValidationModel
{
[Required]
@@ -182,7 +191,7 @@ private async Task HandleValidSubmit()
StateHasChanged();
}";
- private readonly string example9RazorCode = @"
+ private readonly string example10RazorCode = @"
- { "".jpeg"", "".jpg"", "".png"", "".bpm"" })"">
+
@if (FileUploadIsEmpty())
{
@@ -752,7 +742,7 @@ Browse file
Upload";
- private readonly string example9CsharpCode = @"
+ private readonly string example11CsharpCode = @"
[Inject] public IJSRuntime JSRuntime { get; set; } = default!;
private string UploadUrl => ""/Upload"";
@@ -823,14 +813,14 @@ private bool IsFileTypeNotAllowed(BitFileInfo file)
return bitFileUpload.AllowedExtensions.Count > 0 && bitFileUpload.AllowedExtensions.All(ext => ext != ""*"") && bitFileUpload.AllowedExtensions.All(ext => ext != extension);
}";
- private readonly string example10RazorCode = @"
+ private readonly string example12RazorCode = @"
Browse file";
- private readonly string example10CsharpCode = @"
+ private readonly string example12CsharpCode = @"
private string UploadUrl = ""/Upload"";
private string RemoveUrl = ""/Remove"";
private BitFileUpload bitFileUploadWithBrowseFile;
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Message/BitMessageDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Message/BitMessageDemo.razor.cs
index d841de31cd..03a4472870 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Message/BitMessageDemo.razor.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Message/BitMessageDemo.razor.cs
@@ -39,9 +39,9 @@ public partial class BitMessageDemo
new()
{
Name = "CollapseIcon",
- Type = "string",
- DefaultValue = "DoubleChevronUp",
- Description = "Custom Fabric icon name for the collapse icon in Truncate mode. If unset, default will be the Fabric DoubleChevronUp icon.",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom Fabric icon name for the collapse icon in Truncate mode.",
},
new()
{
@@ -62,8 +62,8 @@ public partial class BitMessageDemo
new()
{
Name = "DismissIcon",
- Type = "string",
- DefaultValue = "Cancel",
+ Type = "string?",
+ DefaultValue = "null",
Description = "Custom Fabric icon name to replace the dismiss icon. If unset, default will be the Fabric Cancel icon.",
},
new()
@@ -76,9 +76,9 @@ public partial class BitMessageDemo
new()
{
Name = "ExpandIcon",
- Type = "string",
- DefaultValue = "DoubleChevronDown",
- Description = "Custom Fabric icon name for the expand icon in Truncate mode. If unset, default will be the Fabric DoubleChevronDown icon.",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom Fabric icon name for the expand icon in Truncate mode.",
},
new()
{
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor
index 29f5b72545..3d0544709f 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/Link/BitLinkDemo.razor
@@ -98,9 +98,9 @@
- Link with a rel attribute (nofollow)
+ Link with a rel attribute (nofollow)
-
+
+ @Localizer[nameof(AppStrings.ResetPasswordButtonText)]
+
+ }
+
+
+ }
+ else
+ {
+
+ @Localizer[nameof(AppStrings.ResetPasswordSuccessTitle), model.PhoneNumber!]
+
-
- @Localizer[nameof(AppStrings.ResetPasswordButtonText)]
-
- }
-
-
- }
- else
- {
-
- @Localizer[nameof(AppStrings.ResetPasswordSuccessTitle), model.PhoneNumber!]
-
+
+ @Localizer[nameof(AppStrings.ResetPasswordSuccessBody)]
+
+ }
+
+
+ @Localizer[nameof(AppStrings.SignIn)]
+ @Localizer[nameof(AppStrings.Or)]
+ @Localizer[nameof(AppStrings.SignUp)]
+
-
- @Localizer[nameof(AppStrings.ResetPasswordSuccessBody)]
-
- }
-
-
- @Localizer[nameof(AppStrings.SignIn)]
- @Localizer[nameof(AppStrings.Or)]
- @Localizer[nameof(AppStrings.SignUp)]
-
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ResetPasswordPage.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ResetPasswordPage.razor.scss
index 0fb1d9708a..ac4f4f5252 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ResetPasswordPage.razor.scss
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ResetPasswordPage.razor.scss
@@ -3,10 +3,4 @@
section {
width: 100%;
height: 100%;
-}
-
-::deep {
- form {
- width: 304px;
- }
-}
+}
\ No newline at end of file
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor
index 0536df2943..0abf13e725 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor
@@ -7,13 +7,13 @@
-
+
@if (requiresTwoFactor is false)
{
- @if (isOtpSent is false)
+ @if (isOtpRequested is false)
{
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs
index 0cc06ee91e..a70d77046e 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs
@@ -1,7 +1,6 @@
//+:cnd:noEmit
-using Boilerplate.Shared.Controllers.Identity;
using Boilerplate.Shared.Dtos.Identity;
-using Microsoft.AspNetCore.Components.Forms;
+using Boilerplate.Shared.Controllers.Identity;
namespace Boilerplate.Client.Core.Components.Pages.Identity.SignIn;
@@ -33,7 +32,7 @@ public partial class SignInPage : IDisposable
private bool isWaiting;
- private bool isOtpSent;
+ private bool isOtpRequested;
private bool requiresTwoFactor;
private readonly SignInRequestDto model = new();
private Action unsubscribeIdentityHeaderBackLinkClicked = default!;
@@ -76,7 +75,7 @@ protected override async Task OnInitAsync()
if (source == OtpPayload)
{
- isOtpSent = false;
+ isOtpRequested = false;
model.Otp = null;
}
@@ -112,7 +111,7 @@ private async Task SocialSignIn(string provider)
private async Task DoSignIn()
{
if (isWaiting) return;
- if (isOtpSent && string.IsNullOrWhiteSpace(model.Otp)) return;
+ if (isOtpRequested && string.IsNullOrWhiteSpace(model.Otp)) return;
isWaiting = true;
@@ -162,14 +161,14 @@ private async Task SendOtp(bool resend)
var request = new IdentityRequestDto { UserName = model.UserName, Email = model.Email, PhoneNumber = model.PhoneNumber };
- _ = identityController.SendOtp(request, ReturnUrlQueryString, CurrentCancellationToken);
-
if (resend is false)
{
- isOtpSent = true;
+ isOtpRequested = true;
PubSubService.Publish(ClientPubSubMessages.UPDATE_IDENTITY_HEADER_BACK_LINK, OtpPayload);
}
+
+ await identityController.SendOtp(request, ReturnUrlQueryString, CurrentCancellationToken);
}
private async Task SendTfaToken()
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.scss
index c1250519ac..265a370b27 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.scss
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.scss
@@ -4,20 +4,3 @@ section {
width: 100%;
height: 100%;
}
-
-.form-forgot-password {
- font-size: 14px;
- line-height: 24px;
- margin-bottom: 20px;
-}
-
-.tfa-otp-container {
- gap: 4px;
-}
-
-
-::deep {
- form {
- max-width: 460px;
- }
-}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor
index af6bc32736..d6fbcea999 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor
@@ -7,25 +7,26 @@
-
- @Localizer[nameof(AppStrings.SignUpPanelTitle)]
+
+
+ @Localizer[nameof(AppStrings.SignUpPanelTitle)]
-
- @Localizer[nameof(AppStrings.SignUpPanelSubtitle)]
-
-
+
+ @Localizer[nameof(AppStrings.SignUpPanelSubtitle)]
+
+
-
+
- @Localizer[AppStrings.Or]
- @Localizer[AppStrings.Or]
+ @Localizer[AppStrings.Or]
+ @Localizer[AppStrings.Or]
-
-
+
+
-
-
-
+
+
+
@* *@
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
@*#if (captcha == "reCaptcha")*@
-
+
@*#endif*@
-
- @Localizer[nameof(AppStrings.SignUp)]
-
-
-
-
-
- @Localizer[nameof(AppStrings.SignInMessageInSignUp)]
- @Localizer[nameof(AppStrings.SignIn)]
- @Localizer[nameof(AppStrings.Or)]
-
- @Localizer[nameof(AppStrings.Confirm)]
-
-
-
- By signing up, you agree to our @Localizer[nameof(AppStrings.Terms)]
-
+
+ @Localizer[nameof(AppStrings.SignUp)]
+
+
+
+
+
+ @Localizer[nameof(AppStrings.SignInMessageInSignUp)]
+ @Localizer[nameof(AppStrings.SignIn)]
+ @Localizer[nameof(AppStrings.Or)]
+
+ @Localizer[nameof(AppStrings.Confirm)]
+
+
+
+ By signing up, you agree to our @Localizer[nameof(AppStrings.Terms)]
+
+
\ No newline at end of file
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor.cs
index 9d356140e4..9d3068a43d 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor.cs
@@ -46,7 +46,7 @@ private async Task DoSignUp()
queryParams.Add("phoneNumber", signUpModel.PhoneNumber);
}
var confirmUrl = NavigationManager.GetUriWithQueryParameters(Urls.ConfirmPage, queryParams);
- NavigationManager.NavigateTo(confirmUrl);
+ NavigationManager.NavigateTo(confirmUrl, replace: true);
}
catch (KnownException e)
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor
index 2f0047c280..658228b47a 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor
@@ -4,18 +4,27 @@
@Localizer[nameof(AppStrings.NotAuthorizedPageTitle)]
-
-
-
-
-
-
- @Localizer[nameof(AppStrings.ForbiddenException)]
-
+@if (isRefreshingToken)
+{
+
+}
+else
+{
+
+
+
+
+
- @Localizer[nameof(AppStrings.YouAreSignInAs)] @user.GetDisplayName()
+
+ @Localizer[nameof(AppStrings.ForbiddenException)]
+
- @Localizer[nameof(AppStrings.SignInAsDifferentUser)]
-
-
-
\ No newline at end of file
+ @Localizer[nameof(AppStrings.YouAreSignInAs)] @user.GetDisplayName()
+
+ @Localizer[nameof(AppStrings.SignInAsDifferentUser)]
+
+
+
+
+}
\ No newline at end of file
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs
index 9d76ecf3ec..b53a0b4c07 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs
@@ -1,15 +1,12 @@
-using Microsoft.Extensions.Logging;
-
-namespace Boilerplate.Client.Core.Components.Pages;
+namespace Boilerplate.Client.Core.Components.Pages;
public partial class NotAuthorizedPage
{
+ private bool isRefreshingToken;
private ClaimsPrincipal user = default!;
[SupplyParameterFromQuery(Name = "return-url"), Parameter] public string? ReturnUrl { get; set; }
- [AutoInject] private ILogger logger = default!;
-
protected override async Task OnParamsSetAsync()
{
user = (await AuthenticationStateTask).User;
@@ -19,46 +16,44 @@ protected override async Task OnParamsSetAsync()
protected override async Task OnAfterFirstRenderAsync()
{
- string? refresh_token = await StorageService.GetItem("refresh_token");
+ var refresh_token = await StorageService.GetItem("refresh_token");
// Let's update the access token by refreshing it when a refresh token is available.
// Following this procedure, the newly acquired access token may now include the necessary roles or claims.
- // To prevent infinitie redirect loop, let's append try_refreshing_token=false to the url, so we only redirect in case no try_refreshing_token=false is present
+ // To prevent infinities redirect loop, let's append try_refreshing_token=false to the url, so we only redirect in case no try_refreshing_token=false is present
if (string.IsNullOrEmpty(refresh_token) is false && ReturnUrl?.Contains("try_refreshing_token=false", StringComparison.InvariantCulture) is null or false)
{
- await AuthenticationManager.RefreshToken();
-
- logger.LogInformation("Refreshing access token.");
-
- if ((await AuthenticationStateTask).User.IsAuthenticated())
+ isRefreshingToken = true;
+ StateHasChanged();
+ try
{
- if (ReturnUrl is not null)
+ var access_token = await AuthenticationManager.RefreshToken(requestedBy: nameof(NotAuthorizedPage), CurrentCancellationToken);
+ if (string.IsNullOrEmpty(access_token) is false && ReturnUrl is not null)
{
var @char = ReturnUrl.Contains('?') ? '&' : '?'; // The RedirectUrl may already include a query string.
- NavigationManager.NavigateTo($"{ReturnUrl}{@char}try_refreshing_token=false");
+ NavigationManager.NavigateTo($"{ReturnUrl}{@char}try_refreshing_token=false", replace: true);
+ return;
}
}
+ finally
+ {
+ isRefreshingToken = false;
+ StateHasChanged();
+ }
}
if ((await AuthenticationStateTask).User.IsAuthenticated() is false)
{
- // If neither the refresh_token nor the access_token is present, proceed to the sign-in page.
- RedirectToSignInPage();
+ await SignOut();
}
await base.OnAfterFirstRenderAsync();
}
- private async Task SignIn()
+ private async Task SignOut()
{
await AuthenticationManager.SignOut(CurrentCancellationToken);
-
- RedirectToSignInPage();
- }
-
- private void RedirectToSignInPage()
- {
var returnUrl = ReturnUrl ?? NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
NavigationManager.NavigateTo(Urls.SignInPage + (string.IsNullOrEmpty(returnUrl) ? string.Empty : $"?return-url={returnUrl}"));
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Parameters.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Parameters.cs
index 8c0f5a830e..dd7464d93b 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Parameters.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Parameters.cs
@@ -1,4 +1,6 @@
-namespace Boilerplate.Client.Core.Components;
+using Boilerplate.Client.Core.Services.HttpMessageHandlers;
+
+namespace Boilerplate.Client.Core.Components;
public class Parameters
{
@@ -9,15 +11,17 @@ public class Parameters
public const string CurrentRouteData = nameof(CurrentRouteData);
///
- /// If the current page is part of the cross-layout pages that are rendered in multiple layouts.
+ /// The cross-layout pages are the pages that are getting rendered in multiple layouts (authenticated and unauthenticated).
+ /// The Terms and Home pages are examples of cross-layout pages that.
///
public const string IsCrossLayoutPage = nameof(IsCrossLayoutPage);
///
- /// Determines the connection status, with default behavior based on SignalR connection status.
- /// If SignalR is not added to the project during initial project creation, this value will always be null by default.
- /// Alternatively, you can implement custom logic to control this value.
- /// true => Online, false => Offline, null => Unknown
+ /// Indicates the connection status, with default behavior tied to the SignalR connection status.
+ /// For projects without SignalR, allows this value to be updated based on server responses:
+ /// - When the first response is received from the server, this value becomes true (Online).
+ /// - When a server connection exception occurs, it becomes false (Offline).
+ /// By default, this value is null (Unknown).
///
public const string IsOnline = nameof(IsOnline);
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/CompiledModel/OfflineDbContextAssemblyAttributes.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/CompiledModel/OfflineDbContextAssemblyAttributes.cs
new file mode 100644
index 0000000000..491acb9707
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/CompiledModel/OfflineDbContextAssemblyAttributes.cs
@@ -0,0 +1,8 @@
+//
+using Boilerplate.Client.Core.Data;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+#pragma warning disable 219, 612, 618
+#nullable disable
+
+[assembly: DbContextModel(typeof(OfflineDbContext), typeof(OfflineDbContextModel))]
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/CompiledModel/OfflineDbContextModel.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/CompiledModel/OfflineDbContextModel.cs
new file mode 100644
index 0000000000..5b072f4673
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/CompiledModel/OfflineDbContextModel.cs
@@ -0,0 +1,47 @@
+//
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+
+#pragma warning disable 219, 612, 618
+#nullable disable
+
+namespace Boilerplate.Client.Core.Data
+{
+ [DbContext(typeof(OfflineDbContext))]
+ public partial class OfflineDbContextModel : RuntimeModel
+ {
+ private static readonly bool _useOldBehavior31751 =
+ System.AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue31751", out var enabled31751) && enabled31751;
+
+ static OfflineDbContextModel()
+ {
+ var model = new OfflineDbContextModel();
+
+ if (_useOldBehavior31751)
+ {
+ model.Initialize();
+ }
+ else
+ {
+ var thread = new System.Threading.Thread(RunInitialization, 10 * 1024 * 1024);
+ thread.Start();
+ thread.Join();
+
+ void RunInitialization()
+ {
+ model.Initialize();
+ }
+ }
+
+ model.Customize();
+ _instance = (OfflineDbContextModel)model.FinalizeModel();
+ }
+
+ private static OfflineDbContextModel _instance;
+ public static IModel Instance => _instance;
+
+ partial void Initialize();
+
+ partial void Customize();
+ }
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/CompiledModel/OfflineDbContextModelBuilder.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/CompiledModel/OfflineDbContextModelBuilder.cs
new file mode 100644
index 0000000000..b2282d4809
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/CompiledModel/OfflineDbContextModelBuilder.cs
@@ -0,0 +1,27 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+
+#pragma warning disable 219, 612, 618
+#nullable disable
+
+namespace Boilerplate.Client.Core.Data
+{
+ public partial class OfflineDbContextModel
+ {
+ private OfflineDbContextModel()
+ : base(skipDetectChanges: false, modelId: new Guid("b97b95bd-89b9-4be0-a574-d2035391c0c8"), entityTypeCount: 1)
+ {
+ }
+
+ partial void Initialize()
+ {
+ var userDto = UserDtoEntityType.Create(this);
+
+ UserDtoEntityType.CreateAnnotations(userDto);
+
+ AddAnnotation("ProductVersion", "9.0.0");
+ }
+ }
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/CompiledModel/UserDtoEntityType.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/CompiledModel/UserDtoEntityType.cs
new file mode 100644
index 0000000000..a898c0d728
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/CompiledModel/UserDtoEntityType.cs
@@ -0,0 +1,111 @@
+//
+using System;
+using System.Reflection;
+using Boilerplate.Shared.Dtos.Identity;
+using Boilerplate.Shared.Enums;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#pragma warning disable 219, 612, 618
+#nullable disable
+
+namespace Boilerplate.Client.Core.Data
+{
+ [EntityFrameworkInternal]
+ public partial class UserDtoEntityType
+ {
+ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
+ {
+ var runtimeEntityType = model.AddEntityType(
+ "Boilerplate.Shared.Dtos.Identity.UserDto",
+ typeof(UserDto),
+ baseEntityType,
+ propertyCount: 9,
+ keyCount: 1);
+
+ var id = runtimeEntityType.AddProperty(
+ "Id",
+ typeof(Guid),
+ propertyInfo: typeof(UserDto).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ fieldInfo: typeof(UserDto).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ valueGenerated: ValueGenerated.OnAdd,
+ afterSaveBehavior: PropertySaveBehavior.Throw,
+ sentinel: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ var birthDate = runtimeEntityType.AddProperty(
+ "BirthDate",
+ typeof(DateTimeOffset?),
+ propertyInfo: typeof(UserDto).GetProperty("BirthDate", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ fieldInfo: typeof(UserDto).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ nullable: true,
+ valueConverter: new DateTimeOffsetToBinaryConverter());
+
+ var email = runtimeEntityType.AddProperty(
+ "Email",
+ typeof(string),
+ propertyInfo: typeof(UserDto).GetProperty("Email", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ fieldInfo: typeof(UserDto).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ nullable: true);
+
+ var fullName = runtimeEntityType.AddProperty(
+ "FullName",
+ typeof(string),
+ propertyInfo: typeof(UserDto).GetProperty("FullName", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ fieldInfo: typeof(UserDto).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
+
+ var gender = runtimeEntityType.AddProperty(
+ "Gender",
+ typeof(Gender?),
+ propertyInfo: typeof(UserDto).GetProperty("Gender", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ fieldInfo: typeof(UserDto).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ nullable: true);
+
+ var password = runtimeEntityType.AddProperty(
+ "Password",
+ typeof(string),
+ propertyInfo: typeof(UserDto).GetProperty("Password", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ fieldInfo: typeof(UserDto).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
+
+ var phoneNumber = runtimeEntityType.AddProperty(
+ "PhoneNumber",
+ typeof(string),
+ propertyInfo: typeof(UserDto).GetProperty("PhoneNumber", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ fieldInfo: typeof(UserDto).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ nullable: true);
+
+ var profileImageName = runtimeEntityType.AddProperty(
+ "ProfileImageName",
+ typeof(string),
+ propertyInfo: typeof(UserDto).GetProperty("ProfileImageName", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ fieldInfo: typeof(UserDto).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ nullable: true);
+
+ var userName = runtimeEntityType.AddProperty(
+ "UserName",
+ typeof(string),
+ propertyInfo: typeof(UserDto).GetProperty("UserName", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
+ fieldInfo: typeof(UserDto).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
+
+ var key = runtimeEntityType.AddKey(
+ new[] { id });
+ runtimeEntityType.SetPrimaryKey(key);
+
+ return runtimeEntityType;
+ }
+
+ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
+ {
+ runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
+ runtimeEntityType.AddAnnotation("Relational:Schema", null);
+ runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
+ runtimeEntityType.AddAnnotation("Relational:TableName", "Users");
+ runtimeEntityType.AddAnnotation("Relational:ViewName", null);
+ runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
+
+ Customize(runtimeEntityType);
+ }
+
+ static partial void Customize(RuntimeEntityType runtimeEntityType);
+ }
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/Migrations/20241030140343_InitialMigration.Designer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/Migrations/20241030140343_InitialMigration.Designer.cs
index 046b42efe0..27ff3c7e8a 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/Migrations/20241030140343_InitialMigration.Designer.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/Migrations/20241030140343_InitialMigration.Designer.cs
@@ -15,7 +15,7 @@ partial class InitialMigration
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.Entity("Boilerplate.Shared.Dtos.Identity.UserDto", b =>
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/Migrations/OfflineDbContextModelSnapshot.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/Migrations/OfflineDbContextModelSnapshot.cs
index 13cca205c7..f277e593fa 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/Migrations/OfflineDbContextModelSnapshot.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/Migrations/OfflineDbContextModelSnapshot.cs
@@ -12,7 +12,7 @@ partial class OfflineDbContextModelSnapshot : ModelSnapshot
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.Entity("Boilerplate.Shared.Dtos.Identity.UserDto", b =>
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/OfflineDbContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/OfflineDbContext.cs
index e19d864706..c88dc5e81c 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/OfflineDbContext.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/OfflineDbContext.cs
@@ -4,16 +4,8 @@
namespace Boilerplate.Client.Core.Data;
-public partial class OfflineDbContext(DbContextOptions options) : DbContext(options)
+public partial class OfflineDbContext : DbContext
{
- static OfflineDbContext()
- {
- if (AppPlatform.IsBrowser)
- {
- AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue31751", true);
- }
- }
-
public virtual DbSet Users { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
@@ -40,4 +32,31 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura
configurationBuilder.Properties().HaveConversion();
configurationBuilder.Properties().HaveConversion();
}
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ var isRunningInsideDocker = Directory.Exists("/container_volume"); // Blazor Server - Docker (It's supposed to be a mounted volume named /container_volume)
+ var dirPath = isRunningInsideDocker ? "/container_volume"
+ : AppPlatform.IsBlazorHybridOrBrowser ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AC87AA5B-4B37-4E52-8468-2D5DF24AF256")
+ : Directory.GetCurrentDirectory(); // Blazor server (Non docker Linux, macOS or Windows)
+
+ dirPath = Path.Combine(dirPath, "App_Data");
+
+ Directory.CreateDirectory(dirPath);
+
+ var dbPath = Path.Combine(dirPath, "Offline.db");
+
+ optionsBuilder
+ .UseSqlite($"Data Source={dbPath}");
+
+ if (AppEnvironment.IsProd())
+ {
+ optionsBuilder.UseModel(OfflineDbContextModel.Instance);
+ }
+
+ optionsBuilder.EnableSensitiveDataLogging(AppEnvironment.IsDev())
+ .EnableDetailedErrors(AppEnvironment.IsDev());
+
+ base.OnConfiguring(optionsBuilder);
+ }
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/README.md b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/README.md
index b10ecd210f..660082f762 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/README.md
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/README.md
@@ -1,11 +1,9 @@
-## bit entity framework core sqlite (bit Besql)
+## bit entity framework core sqlite (bit Besql)
How to use `Bit.Besql`:
The usage of `Bit.Besql` is exactly the same as the regular usage of `Microsoft.EntityFrameworkCore.Sqlite` with [IDbContextFactory](https://learn.microsoft.com/en-us/aspnet/core/blazor/blazor-ef-core?view=aspnetcore-8.0#new-dbcontext-instances).
-To get start, simply install `Bit.Besql` and use `services.AddBesqlDbContextFactory` instead of `services.AddDbContextFactory`.
-
In order to download sqlite db file from browser cache storage in blazor WebAssembly run the followings in browser console:
```js
const cache = await caches.open('Bit-Besql');
@@ -16,11 +14,11 @@ URL.createObjectURL(blob);
**Migration**
-Set `Server` as the Startup Project in solution explorer and set `Client.Core` it as the Default Project in Package Manager Console and run the following commands:
+Set `Server.Web` as the Startup Project in solution explorer and set `Client.Core` it as the Default Project in Package Manager Console and run the following commands:
```powershell
Add-Migration InitialMigration -OutputDir Data\Migrations -Context OfflineDbContext
```
-Or open a terminal in your Server project directory and run followings:
+Or open a terminal in your Server.Web project directory and run followings:
```bash
dotnet ef migrations add InitialMigration --context OfflineDbContext --output-dir Data/Migrations --project ../Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj
```
@@ -35,7 +33,7 @@ To enhance the performance of your models, consider compiling them using EF Core
To implement this optimization, follow these steps in the Package Manager Console:
-1. Make sure `Boilerplate.Server` is set as the default startup project, and `Boilerplate.Client.Core` is the default project in the Package Manager Console.
+1. Make sure `Server.Web` is set as the default startup project, and `Client.Core` is the default project in the Package Manager Console.
2. Run the following command:
@@ -43,10 +41,4 @@ To implement this optimization, follow these steps in the Package Manager Consol
Optimize-DbContext -Context OfflineDbContext -OutputDir Data/CompiledModel -Namespace Boilerplate.Client.Core.Data
```
-3. Uncomment the following line in the `Boilerplate.Client.Core/Extensions/IServiceCollectionExtensions.cs` file:
-
- ```csharp
- .UseModel(OfflineDbContextModel.Instance)
- ```
-
By adhering to these steps, you leverage EF Core compiled models to boost the performance of your application, ensuring an optimized and efficient data access method.
\ No newline at end of file
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs
index b0b74618df..a1dda1fb46 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs
@@ -1,7 +1,6 @@
//+:cnd:noEmit
//#if (offlineDb == true)
using Boilerplate.Client.Core.Data;
-using Microsoft.EntityFrameworkCore;
//#endif
//#if (appInsights == true)
using BlazorApplicationInsights;
@@ -10,7 +9,6 @@
using Boilerplate.Client.Core;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
-using Boilerplate.Client.Core.Components;
using Boilerplate.Client.Core.Services.HttpMessageHandlers;
namespace Microsoft.Extensions.DependencyInjection;
@@ -30,6 +28,7 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped(sp => new() { GetAddress = () => sp.GetRequiredService().BaseAddress! /* Read AbsoluteServerAddressProvider's comments for more info. */ });
// The following services must be unique to each app session.
// Defining them as singletons would result in them being shared across all users in Blazor Server and during pre-rendering.
@@ -58,14 +57,16 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle
// handlers, such as ASP.NET Core's `HttpMessageHandler` from the Test Host, which is useful for integration tests.
services.AddScoped(serviceProvider => transportHandler =>
{
- var constructedHttpMessageHandler = ActivatorUtilities.CreateInstance(serviceProvider,
+ var constructedHttpMessageHandler = ActivatorUtilities.CreateInstance(serviceProvider,
+ [ActivatorUtilities.CreateInstance(serviceProvider,
[ActivatorUtilities.CreateInstance(serviceProvider,
[ActivatorUtilities.CreateInstance(serviceProvider,
- [ActivatorUtilities.CreateInstance(serviceProvider, [transportHandler])])])]);
+ [ActivatorUtilities.CreateInstance(serviceProvider, [transportHandler])])])])]);
return constructedHttpMessageHandler;
});
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped(serviceProvider =>
@@ -76,26 +77,11 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle
});
//#if (offlineDb == true)
- services.AddBesqlDbContextFactory(options =>
+ if (AppPlatform.IsBrowser)
{
- var isRunningInsideDocker = Directory.Exists("/container_volume"); // Blazor Server - Docker (It's supposed to be a mounted volume named /container_volume)
- var dirPath = isRunningInsideDocker ? "/container_volume"
- : AppPlatform.IsBlazorHybridOrBrowser ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AC87AA5B-4B37-4E52-8468-2D5DF24AF256")
- : Directory.GetCurrentDirectory(); // Blazor server (Non docker Linux, macOS or Windows)
-
- dirPath = Path.Combine(dirPath, "App_Data");
-
- Directory.CreateDirectory(dirPath);
-
- var dbPath = Path.Combine(dirPath, "Offline.db");
-
- options
- // .UseModel(OfflineDbContextModel.Instance)
- .UseSqlite($"Data Source={dbPath}");
-
- options.EnableSensitiveDataLogging(AppEnvironment.IsDev())
- .EnableDetailedErrors(AppEnvironment.IsDev());
- });
+ AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue31751", true);
+ }
+ services.AddBesqlDbContextFactory();
//#endif
//#if (appInsights == true)
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs
index 474e6d82fa..aa9b0d9a84 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs
@@ -13,18 +13,14 @@ public static ILoggingBuilder AddDiagnosticLogger(this ILoggingBuilder builder)
public static ILoggingBuilder ConfigureLoggers(this ILoggingBuilder loggingBuilder)
{
- loggingBuilder.ClearProviders();
-
if (AppEnvironment.IsDev())
{
loggingBuilder.AddDebug();
}
- if (!AppPlatform.IsBrowser)
+ if (!AppPlatform.IsBrowser) // Browser has its own WebAssemblyConsoleLoggerProvider.
{
- loggingBuilder.AddConsole();
- // DiagnosticLogger is already logging in browser's console.
- // But Console logger is still useful in Visual Studio's Device Log (Android, iOS) or BrowserStack etc.
+ loggingBuilder.AddConsole(); // Device Log / logcat
}
loggingBuilder.AddDiagnosticLogger();
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AbsoluteServerAddressProvider.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AbsoluteServerAddressProvider.cs
new file mode 100644
index 0000000000..230e7daf9e
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AbsoluteServerAddressProvider.cs
@@ -0,0 +1,28 @@
+namespace Boilerplate.Client.Core.Services;
+
+///
+/// The `ServerAddress` setting in `Client.Core/appsettings.json` can be either relative or absolute.
+/// If the server address is relative, we prepend it with `builder.HostEnvironment.BaseAddress` in Blazor WebAssembly
+/// or with the request URL from `IHttpContextAccessor.HttpContext.Request` in Blazor Server.
+/// The resulting server address is useful in various scenarios, such as binding an image's `src` attribute to a server API,
+/// like retrieving the current user's profile image.
+///
+public class AbsoluteServerAddressProvider
+{
+ public required Func GetAddress { get; init; }
+
+ public static implicit operator string(AbsoluteServerAddressProvider provider)
+ {
+ return provider.GetAddress().ToString();
+ }
+
+ public static implicit operator Uri(AbsoluteServerAddressProvider provider)
+ {
+ return provider.GetAddress();
+ }
+
+ public override string ToString()
+ {
+ return this;
+ }
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppInsightsJsSdkService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppInsightsJsSdkService.cs
index 516158ac21..5562b4111d 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppInsightsJsSdkService.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppInsightsJsSdkService.cs
@@ -4,6 +4,9 @@
namespace Boilerplate.Client.Core.Services;
+///
+/// A Blazor Hybrid / Blazor Server compatible version of
+///
public partial class AppInsightsJsSdkService : IApplicationInsights
{
private TaskCompletionSource? telemetryInitializerIsAddedTcs;
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppTelemetryContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppTelemetryContext.cs
index b7b071834c..18e241c1fa 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppTelemetryContext.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppTelemetryContext.cs
@@ -17,6 +17,8 @@ public class AppTelemetryContext : ITelemetryContext
public virtual string? WebView { get; set; }
+ public virtual string? PageUrl { get; set; }
+
public virtual string? UserAgent { get; set; }
public string? TimeZone { get; set; }
@@ -25,5 +27,5 @@ public class AppTelemetryContext : ITelemetryContext
public string? Environment { get; set; } = AppEnvironment.Current;
- public bool IsOnline { get; set; }
+ public bool? IsOnline { get; set; }
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs
index 897886353e..524c4088c2 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs
@@ -17,7 +17,9 @@ public partial class AuthenticationManager : AuthenticationStateProvider
[AutoInject] private IAuthTokenProvider tokenProvider = default!;
[AutoInject] private IPrerenderStateService prerenderStateService;
[AutoInject] private IExceptionHandler exceptionHandler = default!;
+ [AutoInject] private IStringLocalizer localizer = default!;
[AutoInject] private IIdentityController identityController = default!;
+ [AutoInject] private ILogger authLogger = default!;
///
/// Sign in and return whether the user requires two-factor authentication.
@@ -29,95 +31,111 @@ public async Task SignIn(SignInRequestDto request, CancellationToken cance
if (response.RequiresTwoFactor) return true;
- await OnNewToken(response!, request.RememberMe);
+ await StoreTokens(response!, request.RememberMe);
return false;
}
- public async Task OnNewToken(TokenResponseDto response, bool? rememberMe = null)
+ public async Task StoreTokens(TokenResponseDto response, bool? rememberMe = null)
{
- await StoreTokens(response, rememberMe);
+ if (rememberMe is null)
+ {
+ rememberMe = await storageService.IsPersistent("refresh_token");
+ }
+
+ await storageService.SetItem("access_token", response!.AccessToken, rememberMe is true);
+ await storageService.SetItem("refresh_token", response!.RefreshToken, rememberMe is true);
- var state = await GetAuthenticationStateAsync();
+ if (AppPlatform.IsBlazorHybrid is false && jsRuntime.IsInitialized())
+ {
+ await cookie.Set(new()
+ {
+ Name = "access_token",
+ Value = response.AccessToken,
+ MaxAge = rememberMe is true ? response.ExpiresIn : null, // to create a session cookie
+ Path = "/",
+ SameSite = SameSite.Strict,
+ Secure = AppEnvironment.IsDev() is false
+ });
+ }
- NotifyAuthenticationStateChanged(Task.FromResult(state));
+ NotifyAuthenticationStateChanged(Task.FromResult(await GetAuthenticationStateAsync()));
}
public async Task SignOut(CancellationToken cancellationToken)
{
try
{
- if (await storageService.GetItem("refresh_token") is not null)
- {
- await userController.SignOut(cancellationToken);
- }
+ await userController.SignOut(cancellationToken);
}
- finally
+ catch (Exception exp) when (exp is ServerConnectionException or UnauthorizedException)
{
- await storageService.RemoveItem("access_token");
- await storageService.RemoveItem("refresh_token");
- if (AppPlatform.IsBlazorHybrid is false)
- {
- await cookie.Remove("access_token");
- }
- NotifyAuthenticationStateChanged(Task.FromResult(await GetAuthenticationStateAsync()));
+ // The user might sign out while the app is offline, making token refresh attempts fail.
+ // These exceptions are intentionally ignored in this case.
}
- }
-
- public async Task RefreshToken()
- {
- if (AppPlatform.IsBlazorHybrid is false)
+ finally
{
- await cookie.Remove("access_token");
+ await ClearTokens();
}
- await storageService.RemoveItem("access_token");
- NotifyAuthenticationStateChanged(Task.FromResult(await GetAuthenticationStateAsync()));
}
- public override async Task GetAuthenticationStateAsync()
+ public async Task RefreshToken(string requestedBy, CancellationToken cancellationToken)
{
try
{
- var access_token = await prerenderStateService.GetValue(() => tokenProvider.GetAccessToken());
-
- bool inPrerenderSession = AppPlatform.IsBlazorHybrid is false && jsRuntime.IsInitialized() is false;
+ var access_token_BeforeLockValue = await tokenProvider.GetAccessToken();
+ await semaphore.WaitAsync();
+ var access_token_AfterLockValue = await tokenProvider.GetAccessToken();
+ if (access_token_BeforeLockValue != access_token_AfterLockValue)
+ return access_token_AfterLockValue; // It was renewed by a concurrent refresh token request.
+ authLogger.LogInformation("Refreshing access token requested by {RequestedBy}", requestedBy);
+ try
+ {
+ string? refresh_token = await storageService.GetItem("refresh_token");
+ if (string.IsNullOrEmpty(refresh_token))
+ throw new UnauthorizedException(localizer[nameof(AppStrings.YouNeedToSignIn)]);
- if (string.IsNullOrEmpty(access_token) && inPrerenderSession is false)
+ var refreshTokenResponse = await identityController.Refresh(new() { RefreshToken = refresh_token }, cancellationToken);
+ await StoreTokens(refreshTokenResponse);
+ return refreshTokenResponse.AccessToken!;
+ }
+ catch (Exception exp)
{
- try
+ if (exp is UnauthorizedException)
{
- await semaphore.WaitAsync();
- access_token = await tokenProvider.GetAccessToken();
- if (string.IsNullOrEmpty(access_token)) // Check again after acquiring the lock.
- {
- string? refresh_token = await storageService.GetItem("refresh_token");
-
- if (string.IsNullOrEmpty(refresh_token) is false)
- {
- // We refresh the access_token to ensure a seamless user experience, preventing unnecessary 'NotAuthorized' page redirects and improving overall UX.
- // This method is triggered after 401 and 403 server responses in AuthDelegationHandler,
- // as well as when accessing pages without the required permissions in NotAuthorizedPage, ensuring that any recent claims granted to the user are promptly reflected.
-
- try
- {
- var refreshTokenResponse = await identityController.Refresh(new() { RefreshToken = refresh_token }, CancellationToken.None);
- await StoreTokens(refreshTokenResponse!);
- access_token = refreshTokenResponse!.AccessToken;
- }
- catch (UnauthorizedException) // refresh_token is either invalid or expired.
- {
- await storageService.RemoveItem("refresh_token");
- }
- }
- }
+ // refresh_token is either invalid or expired.
+ await ClearTokens();
}
- finally
+ exceptionHandler.Handle(exp, new()
{
- semaphore.Release();
- }
+ { "AdditionalData", "Refreshing access token failed." },
+ { "RefreshTokenRequestedBy", requestedBy }
+ });
+ return null;
}
+ }
+ finally
+ {
+ semaphore.Release();
+ }
+ }
- return new AuthenticationState(tokenProvider.ParseAccessToken(access_token, validateExpiry: false /* For better UX in order to minimize Routes.razor's Authorizing loading duration. */));
+ ///
+ /// Handles the process of determining the user's authentication state based on the availability of access and refresh tokens.
+ ///
+ /// - If no access / refresh token exists, an anonymous user object is returned to Blazor.
+ /// - If an access token exists, a ClaimsPrincipal is created from it regardless of its expiration status. This ensures:
+ /// - Users can access anonymous-allowed pages without unnecessary delays caused by token refresh attempts **during app startup**.
+ /// - For protected pages, it is typical for these pages to make HTTP requests to secured APIs. In such cases, the `AuthDelegatingHandler.cs`
+ /// validates the access token and refreshes it if necessary, keeping Blazor updated with the latest authentication state.
+ ///
+ public override async Task GetAuthenticationStateAsync()
+ {
+ try
+ {
+ var access_token = await prerenderStateService.GetValue(() => tokenProvider.GetAccessToken());
+
+ return new AuthenticationState(tokenProvider.ParseAccessToken(access_token, validateExpiry: false));
}
catch (Exception exp)
{
@@ -126,27 +144,14 @@ public override async Task GetAuthenticationStateAsync()
}
}
- private async Task StoreTokens(TokenResponseDto response, bool? rememberMe = null)
+ private async Task ClearTokens()
{
- if (rememberMe is null)
- {
- rememberMe = await storageService.IsPersistent("refresh_token");
- }
-
- await storageService.SetItem("access_token", response!.AccessToken, rememberMe is true);
- await storageService.SetItem("refresh_token", response!.RefreshToken, rememberMe is true);
-
- if (AppPlatform.IsBlazorHybrid is false && jsRuntime.IsInitialized())
+ await storageService.RemoveItem("access_token");
+ await storageService.RemoveItem("refresh_token");
+ if (AppPlatform.IsBlazorHybrid is false)
{
- await cookie.Set(new()
- {
- Name = "access_token",
- Value = response.AccessToken,
- MaxAge = rememberMe is true ? response.ExpiresIn : null, // to create a session cookie
- Path = "/",
- SameSite = SameSite.Strict,
- Secure = AppEnvironment.IsDev() is false
- });
+ await cookie.Remove("access_token");
}
+ NotifyAuthenticationStateChanged(Task.FromResult(await GetAuthenticationStateAsync()));
}
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ExceptionHandlerBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientExceptionHandlerBase.cs
similarity index 50%
rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ExceptionHandlerBase.cs
rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientExceptionHandlerBase.cs
index 6c137be287..87795acfa1 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ExceptionHandlerBase.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientExceptionHandlerBase.cs
@@ -1,18 +1,15 @@
//+:cnd:noEmit
-using System.Reflection;
using System.Diagnostics;
-using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;
namespace Boilerplate.Client.Core.Services;
-public abstract partial class ExceptionHandlerBase : IExceptionHandler
+public abstract partial class ClientExceptionHandlerBase : SharedExceptionHandler, IExceptionHandler
{
[AutoInject] protected Bit.Butil.Console Console = default!;
[AutoInject] protected ITelemetryContext TelemetryContext = default!;
- [AutoInject] protected ILogger Logger = default!;
+ [AutoInject] protected ILogger Logger = default!;
[AutoInject] protected readonly MessageBoxService MessageBoxService = default!;
- [AutoInject] protected readonly IStringLocalizer Localizer = default!;
public void Handle(Exception exception,
Dictionary? parameters = null,
@@ -35,14 +32,7 @@ protected virtual void Handle(Exception exception, Dictionary pa
using (var scope = Logger.BeginScope(parameters.ToDictionary(i => i.Key, i => i.Value ?? string.Empty)))
{
- var exceptionMessageToLog = exception.Message;
- var innerException = exception.InnerException;
-
- while (innerException is not null)
- {
- exceptionMessageToLog += $"{Environment.NewLine}{innerException.Message}";
- innerException = innerException.InnerException;
- }
+ var exceptionMessageToLog = GetExceptionMessageToLog(exception);
if (exception is KnownException)
{
@@ -54,8 +44,7 @@ protected virtual void Handle(Exception exception, Dictionary pa
}
}
- string exceptionMessageToShow = (exception as KnownException)?.Message ??
- (isDevEnv ? exception.ToString() : Localizer[nameof(AppStrings.UnknownException)]);
+ string exceptionMessageToShow = GetExceptionMessageToShow(exception);
MessageBoxService.Show(exceptionMessageToShow, Localizer[nameof(AppStrings.Error)]);
@@ -64,29 +53,4 @@ protected virtual void Handle(Exception exception, Dictionary pa
Debugger.Break();
}
}
-
- protected Exception UnWrapException(Exception exception)
- {
- if (exception is AggregateException aggregateException)
- {
- return aggregateException.Flatten().InnerException ?? aggregateException;
- }
- else if (exception is TargetInvocationException)
- {
- return exception.InnerException ?? exception;
- }
-
- return exception;
- }
-
- protected bool IgnoreException(Exception exception)
- {
- if (exception is KnownException)
- return false;
-
- return exception is TaskCanceledException ||
- exception is OperationCanceledException ||
- exception is TimeoutException ||
- (exception.InnerException is not null && IgnoreException(exception.InnerException));
- }
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/ILocalHttpServer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/ILocalHttpServer.cs
index a5a0b7cf5c..dfcf4ba11f 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/ILocalHttpServer.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/ILocalHttpServer.cs
@@ -1,5 +1,9 @@
namespace Boilerplate.Client.Core.Services.Contracts;
+///
+/// Social sign-in functions seamlessly on web browsers and on Android and iOS via universal app links.
+/// However, for Windows and macOS, a local HTTP server is needed to ensure a smooth social sign-in experience.
+///
public interface ILocalHttpServer
{
int Start(CancellationToken cancellationToken);
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/ITelemetryContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/ITelemetryContext.cs
index a1b5962c34..99c74936bd 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/ITelemetryContext.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/ITelemetryContext.cs
@@ -37,6 +37,8 @@ public static ITelemetryContext? Current
public string? AppVersion { get; set; }
public string? WebView { get; set; }
+ public string? PageUrl { get; set; }
+
public string? UserAgent { get; set; }
public string? TimeZone { get; set; }
@@ -44,7 +46,7 @@ public static ITelemetryContext? Current
public string? Environment { get; set; }
- public bool IsOnline { get; set; }
+ public bool? IsOnline { get; set; }
public Dictionary ToDictionary(Dictionary? additionalParameters = null)
{
@@ -55,8 +57,10 @@ public static ITelemetryContext? Current
{ nameof(AppSessionId), AppSessionId },
{ nameof(OS), OS },
{ nameof(AppVersion), AppVersion },
+ { nameof(PageUrl), PageUrl },
{ nameof(UserAgent), UserAgent },
{ nameof(TimeZone), TimeZone },
+ { "ClientDateTime", DateTimeOffset.UtcNow.ToString("u") },
{ nameof(Culture), Culture },
{ nameof(Environment), Environment },
{ nameof(IsOnline), IsOnline }
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/CultureService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/CultureService.cs
index 2722fdf4c7..9221921744 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/CultureService.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/CultureService.cs
@@ -20,10 +20,12 @@ public async Task ChangeCulture(string? cultureName)
{
await cookie.Set(new()
{
- MaxAge = 30 * 24 * 3600,
Name = ".AspNetCore.Culture",
- Secure = AppEnvironment.IsDev() is false,
Value = Uri.EscapeDataString($"c={cultureName}|uic={cultureName}"),
+ MaxAge = 30 * 24 * 3600,
+ Path = "/",
+ SameSite = SameSite.Strict,
+ Secure = AppEnvironment.IsDev() is false
});
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLog.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLog.cs
index a3b76bbb0a..e11d4ae726 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLog.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLog.cs
@@ -1,6 +1,4 @@
-using Microsoft.Extensions.Logging;
-
-namespace Boilerplate.Client.Core.Services.DiagnosticLog;
+namespace Boilerplate.Client.Core.Services.DiagnosticLog;
public class DiagnosticLog
{
@@ -10,6 +8,8 @@ public class DiagnosticLog
public string? Message { get; set; }
+ public string? Category { get; set; }
+
public Exception? Exception { get; set; }
public IDictionary? State { get; set; }
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLogger.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLogger.cs
index 9f25214154..e88cc29cd1 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLogger.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLogger.cs
@@ -1,6 +1,4 @@
-using Microsoft.Extensions.Logging;
-
-namespace Boilerplate.Client.Core.Services.DiagnosticLog;
+namespace Boilerplate.Client.Core.Services.DiagnosticLog;
public partial class DiagnosticLogger : ILogger, IDisposable
{
@@ -8,7 +6,7 @@ public partial class DiagnosticLogger : ILogger, IDisposable
private IDictionary? currentState;
- public string? CategoryName { get; set; }
+ public string? Category { get; set; }
public IDisposable? BeginScope(TState state)
where TState : notnull
@@ -32,7 +30,15 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except
var message = formatter(state, exception);
- Store.Add(new() { CreatedOn = DateTimeOffset.Now, Level = logLevel, Message = message, Exception = exception, State = currentState?.ToDictionary(i => i.Key, i => i.Value?.ToString()) });
+ Store.Add(new()
+ {
+ CreatedOn = DateTimeOffset.Now,
+ Level = logLevel,
+ Message = message,
+ Exception = exception,
+ Category = Category,
+ State = currentState?.ToDictionary(i => i.Key, i => i.Value?.ToString())
+ });
}
public void Dispose()
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLoggerProvider.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLoggerProvider.cs
index 74b2cccbd9..8848fc049e 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLoggerProvider.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLoggerProvider.cs
@@ -1,13 +1,12 @@
//+:cnd:noEmit
-using Microsoft.Extensions.Logging;
namespace Boilerplate.Client.Core.Services.DiagnosticLog;
// https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/developer-tools
///
-/// Provides a custom logger that outputs log messages to the browser's console and allows for selective display of logs
-/// within the application UI for enhanced diagnostics.
+/// Provides a custom logger that outputs log messages to the in memory store and allows for selective display of logs
+/// within the application UI for enhanced diagnostics using
///
[ProviderAlias("DiagnosticLogger")]
public partial class DiagnosticLoggerProvider : ILoggerProvider
@@ -16,7 +15,7 @@ public ILogger CreateLogger(string categoryName)
{
return new DiagnosticLogger()
{
- CategoryName = categoryName
+ Category = categoryName
};
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs
index c796951373..3c35813586 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs
@@ -1,61 +1,81 @@
-using System.Net.Http.Headers;
+using System.Reflection;
+using System.Net.Http.Headers;
+using Boilerplate.Shared.Controllers;
using Boilerplate.Shared.Controllers.Identity;
namespace Boilerplate.Client.Core.Services.HttpMessageHandlers;
-public partial class AuthDelegatingHandler(IAuthTokenProvider tokenProvider,
- IJSRuntime jsRuntime,
- IServiceProvider serviceProvider,
- IStorageService storageService,
- HttpMessageHandler handler)
- : DelegatingHandler(handler)
+public partial class AuthDelegatingHandler(IJSRuntime jsRuntime,
+ IStorageService storageService,
+ IServiceProvider serviceProvider,
+ IAuthTokenProvider tokenProvider,
+ IStringLocalizer localizer,
+ AbsoluteServerAddressProvider absoluteServerAddress,
+ HttpMessageHandler handler) : DelegatingHandler(handler)
{
+
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
- var isRefreshTokenRequest = request.RequestUri?.LocalPath?.Contains(IIdentityController.RefreshUri, StringComparison.InvariantCultureIgnoreCase) is true;
+ var isInternalRequest = request.RequestUri!.ToString().StartsWith(absoluteServerAddress, StringComparison.InvariantCultureIgnoreCase);
try
{
- if (request.Headers.Authorization is null && isRefreshTokenRequest is false)
+ if (isInternalRequest && /* We will restrict sending the access token to our own server only. */
+ request.Headers.Authorization is null)
{
var access_token = await tokenProvider.GetAccessToken();
- if (access_token is not null)
+ if (string.IsNullOrEmpty(access_token) is false && HasAuthorizedApiAttribute(request))
{
if (tokenProvider.ParseAccessToken(access_token, validateExpiry: true).IsAuthenticated() is false)
- throw new UnauthorizedException(nameof(AppStrings.YouNeedToSignIn));
-
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token);
+ throw new UnauthorizedException(localizer[nameof(AppStrings.YouNeedToSignIn)]);
}
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token);
}
return await base.SendAsync(request, cancellationToken);
}
catch (KnownException _) when (_ is ForbiddenException or UnauthorizedException)
{
+ // Notes about ForbiddenException (403):
// Let's update the access token by refreshing it when a refresh token is available.
// Following this procedure, the newly acquired access token may now include the necessary roles or claims.
if (AppPlatform.IsBlazorHybrid is false && jsRuntime.IsInitialized() is false)
throw; // We don't have access to refresh_token during pre-rendering.
+ var isRefreshTokenRequest = request.RequestUri?.LocalPath?.Contains(IIdentityController.RefreshUri, StringComparison.InvariantCultureIgnoreCase) is true;
+
if (isRefreshTokenRequest)
throw; // To prevent refresh token loop
var refresh_token = await storageService.GetItem("refresh_token");
- if (refresh_token is null) throw;
+ if (string.IsNullOrEmpty(refresh_token)) throw;
var authManager = serviceProvider.GetRequiredService();
// In the AuthenticationStateProvider, the access_token is refreshed using the refresh_token (if available).
- await authManager.RefreshToken();
-
- var access_token = await tokenProvider.GetAccessToken();
+ var access_token = await authManager.RefreshToken(requestedBy: nameof(AuthDelegatingHandler), cancellationToken);
- if (string.IsNullOrEmpty(access_token)) throw;
+ if (string.IsNullOrEmpty(access_token))
+ throw;
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token);
return await base.SendAsync(request, cancellationToken);
}
}
+
+ ///
+ ///
+ ///
+ private static bool HasAuthorizedApiAttribute(HttpRequestMessage request)
+ {
+ if (request.Options.TryGetValue(new(RequestOptionNames.IControllerType), out Type? controllerType) is false)
+ return false;
+
+ var parameterTypes = ((Dictionary)request.Options.GetValueOrDefault(RequestOptionNames.ActionParametersInfo)!).Select(p => p.Value).ToArray();
+ var method = controllerType!.GetMethod((string)request.Options.GetValueOrDefault(RequestOptionNames.ActionName)!, parameterTypes)!;
+ return controllerType.GetCustomAttribute(inherit: true) is not null ||
+ method.GetCustomAttribute() is not null;
+ }
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs
index 2ee7fec9cf..1a7bf35e3d 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs
@@ -4,16 +4,17 @@
namespace Boilerplate.Client.Core.Services.HttpMessageHandlers;
public partial class ExceptionDelegatingHandler(IStringLocalizer localizer,
- //#if (signalR != true)
- PubSubService pubSubService,
- //#endif
- JsonSerializerOptions jsonSerializerOptions,
- HttpMessageHandler handler)
- : DelegatingHandler(handler)
+ //#if (signalR != true)
+ PubSubService pubSubService,
+ //#endif
+ JsonSerializerOptions jsonSerializerOptions,
+ AbsoluteServerAddressProvider absoluteServerAddress,
+ HttpMessageHandler handler) : DelegatingHandler(handler)
{
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
bool serverCommunicationSuccess = false;
+ var isInternalRequest = request.RequestUri!.ToString().StartsWith(absoluteServerAddress, StringComparison.InvariantCultureIgnoreCase);
try
{
@@ -21,25 +22,24 @@ protected override async Task SendAsync(HttpRequestMessage
serverCommunicationSuccess = true;
- if (response.IsSuccessStatusCode is false && response.Content.Headers.ContentType?.MediaType?.Contains("application/json", StringComparison.InvariantCultureIgnoreCase) is true)
+ if (isInternalRequest && /* The following exception handling mechanism applies exclusively to responses from our own server. */
+ response.IsSuccessStatusCode is false &&
+ response.Content.Headers.ContentType?.MediaType?.Contains("application/json", StringComparison.InvariantCultureIgnoreCase) is true)
{
- if (response.Headers.TryGetValues("Request-Id", out IEnumerable? values) && values is not null && values.Any())
- {
- RestErrorInfo restError = (await response!.Content.ReadFromJsonAsync(jsonSerializerOptions.GetTypeInfo(), cancellationToken))!;
+ RestErrorInfo restError = (await response!.Content.ReadFromJsonAsync(jsonSerializerOptions.GetTypeInfo(), cancellationToken))!;
- Type exceptionType = typeof(RestErrorInfo).Assembly.GetType(restError.ExceptionType!) ?? typeof(UnknownException);
+ Type exceptionType = typeof(RestErrorInfo).Assembly.GetType(restError.ExceptionType!) ?? typeof(UnknownException);
- var args = new List
-public partial class MauiExceptionHandler : ExceptionHandlerBase
+public partial class MauiExceptionHandler : ClientExceptionHandlerBase
{
protected override void Handle(Exception exception, Dictionary parameters)
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs
index 79885729bc..31babcf5dc 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs
@@ -1,16 +1,18 @@
-using System.Net;
+using EmbedIO;
+using System.Net;
using System.Net.Sockets;
-using EmbedIO;
using EmbedIO.Actions;
using Boilerplate.Client.Core.Components;
-using Microsoft.Extensions.Logging;
namespace Boilerplate.Client.Maui.Services;
+///
+///
+///
public partial class MauiLocalHttpServer : ILocalHttpServer
{
- [AutoInject] private IConfiguration configuration;
[AutoInject] private IExceptionHandler exceptionHandler;
+ [AutoInject] private AbsoluteServerAddressProvider absoluteServerAddress;
private WebServer? localHttpServer;
@@ -25,7 +27,7 @@ public int Start(CancellationToken cancellationToken)
{
try
{
- var url = $"{configuration.GetServerAddress()}/api/Identity/SocialSignedIn?culture={CultureInfo.CurrentUICulture.Name}";
+ var url = new Uri(absoluteServerAddress, $"/api/Identity/SocialSignedIn?culture={CultureInfo.CurrentUICulture.Name}").ToString();
ctx.Redirect(url);
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Boilerplate.Client.Web.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Boilerplate.Client.Web.csproj
index 57509f1fc4..ffc21acad0 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Boilerplate.Client.Web.csproj
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Boilerplate.Client.Web.csproj
@@ -1,19 +1,18 @@
- net8.0
+ net9.0truefalsetruetrue
-
- falseservice-worker-assets.jsfalseDefault
+ falsetruetrue
@@ -24,6 +23,7 @@
+
@@ -44,7 +44,7 @@
-
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Program.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Program.cs
index fea7c32e43..97b2bc2bfe 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Program.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Program.cs
@@ -1,4 +1,4 @@
-//-:cnd:noEmit
+//+:cnd:noEmit
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
@@ -12,6 +12,19 @@ public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
+ //#if (advancedTests == true)
+ //Pass configuration to BlazorWebAssembly
+ //More info: https://stackoverflow.com/questions/60831359/how-are-string-args-passed-to-program-main-in-a-blazor-webassembly-app
+ try
+ {
+ var js = (IJSInProcessRuntime)builder.Services.BuildServiceProvider().GetRequiredService();
+ var startupParams = js.Invoke("startupParams");
+ var configData = startupParams.Select(p => p.Split('=')).ToDictionary(p => p[0], p => p[1]);
+ builder.Configuration.AddInMemoryCollection(configData!);
+ }
+ catch { }
+ //#endif
+
AppEnvironment.Set(builder.HostEnvironment.Environment);
builder.Configuration.AddClientConfigurations(clientEntryAssemblyName: "Boilerplate.Client.Web");
@@ -21,11 +34,9 @@ public static async Task Main(string[] args)
// By default, App.razor adds Routes and HeadOutlet.
// The following is only required for blazor webassembly standalone.
builder.RootComponents.Add("head::after");
- //+:cnd:noEmit
//#if (appInsights == true)
builder.RootComponents.Add(selector: "head::after");
//#endif
- //-:cnd:noEmit
builder.RootComponents.Add("#app-container");
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebExceptionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebExceptionHandler.cs
index 2b6f05695e..8e7dd2c635 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebExceptionHandler.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebExceptionHandler.cs
@@ -1,6 +1,6 @@
namespace Boilerplate.Client.Web.Services;
-public partial class WebExceptionHandler : ExceptionHandlerBase
+public partial class WebExceptionHandler : ClientExceptionHandlerBase
{
protected override void Handle(Exception exception, Dictionary parameters)
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebPrerenderStateService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebPrerenderStateService.cs
index 0232dc1144..b93bcb17ad 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebPrerenderStateService.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebPrerenderStateService.cs
@@ -7,7 +7,7 @@
namespace Boilerplate.Client.Web.Services;
///
-/// For more information docs.
+///
///
public partial class WebPrerenderStateService : IPrerenderStateService, IAsyncDisposable
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json
index 5fe62e7f93..d2235cccdd 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json
@@ -1,6 +1,7 @@
{
"WebAppRender": {
"PrerenderEnabled": false,
+ "PrerenderEnabled_Comment": "for apps with Prerender enabled, follow the instructions in the service-worker.published.js file",
"BlazorMode": "BlazorServer",
"BlazorMode_Comment": "BlazorServer, BlazorWebAssembly and BlazorAuto. Default value of Client.Core/appsettings.Production.json is BlazorAuto"
},
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/.well-known/apple-app-site-association b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/.well-known/apple-app-site-association
index 479c377c60..979b6f7d64 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/.well-known/apple-app-site-association
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/.well-known/apple-app-site-association
@@ -1,4 +1,4 @@
-{
+{
"activitycontinuation": {
"apps": [ "76WD644YU8.com.bitplatform.AdminPanel.Template", "76WD644YU8.com.bitplatform.Todo.Template" ]
},
@@ -18,6 +18,7 @@
"NOT /swagger/*",
"NOT /signin/*",
"NOT /.well-known/*",
+ "NOT /sitemap.xml",
"*"
]
},
@@ -34,6 +35,7 @@
"NOT /swagger/*",
"NOT /signin/*",
"NOT /.well-known/*",
+ "NOT /sitemap.xml",
"*"
]
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js
index e275214ce2..da582b35c7 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js
@@ -1,5 +1,5 @@
-//+:cnd:noEmit
-// bit version: 8.12.0
+//+:cnd:noEmit
+// bit version: 9.0.0
// https://github.com/bitfoundation/bitplatform/tree/develop/src/Bswup
//#if (notification == true)
@@ -31,7 +31,17 @@ self.externalAssets = [
"url": "/"
},
{
- url: "_framework/blazor.web.js?ver=8.0.404"
+ //#if (framework == "net9.0")
+ url: "_framework/blazor.web.js?ver=9.0.0"
+ //#else
+ //#if (IsInsideProjectTemplate == true)
+ /*
+ //#endif
+ url: "_framework/blazor.web.js?ver=8.0.11"
+ //#if (IsInsideProjectTemplate == true)
+ */
+ //#endif
+ //#endif
},
{
"url": "Boilerplate.Server.Web.styles.css"
@@ -52,9 +62,15 @@ self.serverHandledUrls = [
];
self.defaultUrl = "/";
-self.caseInsensitiveUrl = true;
-self.noPrerenderQuery = 'no-prerender=true';
-self.isPassive = self.disablePassiveFirstBoot = true;
+self.isPassive = true;
self.errorTolerance = 'lax';
+self.caseInsensitiveUrl = true;
+
+
+// on apps with Prerendering enabled, to have the best experience for the end user un-comment the following two lines.
+// more info: https://bitplatform.dev/bswup/service-worker
+// self.noPrerenderQuery = 'no-prerender=true';
+// self.disablePassiveFirstBoot = true;
+
self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js');
\ No newline at end of file
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/.config/dotnet-tools.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/.config/dotnet-tools.json
index f8cf15add4..461b027bf4 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/.config/dotnet-tools.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/.config/dotnet-tools.json
@@ -1,9 +1,9 @@
-{
+{
"version": 1,
"isRoot": true,
"tools": {
"vpk": {
- "version": "0.0.869",
+ "version": "0.0.942",
"commands": [
"vpk"
]
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/App.xaml.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/App.xaml.cs
index f3a417e7b8..acc1f9a615 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/App.xaml.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/App.xaml.cs
@@ -1,9 +1,11 @@
-using System.IO.IsolatedStorage;
+//+:cnd:noEmit
using System.IO;
using System.Windows;
-using System.Collections;
using Microsoft.Win32;
+using System.Collections;
using System.Windows.Media;
+using System.Windows.Threading;
+using System.IO.IsolatedStorage;
using Boilerplate.Client.Core.Styles;
namespace Boilerplate.Client.Windows;
@@ -17,14 +19,24 @@ public App()
var splash = new SplashScreen(typeof(App).Assembly, @"Resources\SplashScreen.png");
splash.Show(autoClose: true, topMost: true);
- Resources["PrimaryBgColor"] = new BrushConverter().ConvertFrom(IsDarkTheme() ? ThemeColors.PrimaryDarkBgColor : ThemeColors.PrimaryLightBgColor);
+ ConfigureAppTheme();
}
- private static bool IsDarkTheme()
+ private void ConfigureAppTheme()
{
+ //#if (framework == 'net9.0')
+ ThemeMode = ThemeMode.System;
+ Resources.MergedDictionaries.Add(new ResourceDictionary
+ {
+ Source = new Uri("pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.xaml", UriKind.Absolute)
+ });
+ //#endif
+
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
var value = key?.GetValue("AppsUseLightTheme");
- return value is int i && i == 0;
+ var isDark = value is int i && i == 0;
+
+ Resources["PrimaryBgColor"] = new BrushConverter().ConvertFrom(isDark ? ThemeColors.PrimaryDarkBgColor : ThemeColors.PrimaryLightBgColor);
}
const string WindowsStorageFilename = "windows.storage.json";
@@ -53,7 +65,7 @@ private void App_Exit(object sender, ExitEventArgs e)
writer.Write(JsonSerializer.Serialize(Properties));
}
- private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
+ private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
try
{
@@ -62,8 +74,8 @@ private void App_DispatcherUnhandledException(object sender, System.Windows.Thre
catch
{
var errorMessage = e.Exception.ToString();
- System.Windows.Clipboard.SetText(errorMessage);
- System.Windows.Forms.MessageBox.Show(errorMessage, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ Clipboard.SetText(errorMessage);
+ System.Windows.MessageBox.Show(errorMessage, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
e.Handled = true;
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Boilerplate.Client.Windows.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Boilerplate.Client.Windows.csproj
index f0d7956536..d137dc0ad5 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Boilerplate.Client.Windows.csproj
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Boilerplate.Client.Windows.csproj
@@ -2,11 +2,10 @@
WinExe
- net8.0-windows
+ net9.0-windowsenableenabletrue
- truetrueBoilerplate.Client.WindowsBoilerplate.Client.Windows.Program
@@ -15,6 +14,7 @@
BeforeBuildTasks;
$(ResolveStaticWebAssetsInputsDependsOn)
+ $(NoWarn);WPF0001
@@ -26,6 +26,8 @@
+
+
@@ -33,9 +35,6 @@
-
-
-
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml.cs
index b171fb6cbf..4a05bf130d 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml.cs
@@ -1,6 +1,8 @@
//+:cnd:noEmit
using Microsoft.Web.WebView2.Core;
using Microsoft.AspNetCore.Components.WebView;
+using System.Drawing;
+using System.Diagnostics;
namespace Boilerplate.Client.Windows;
@@ -30,8 +32,9 @@ public MainWindow()
}
AppWebView.Services.GetRequiredService().Subscribe(ClientPubSubMessages.CULTURE_CHANGED, async culture =>
{
+ string executablePath = Environment.ProcessPath!;
+ Process.Start(executablePath);
App.Current.Shutdown();
- Application.Restart();
});
AppWebView.Loaded += async delegate
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs
index 0a9b133850..b55365d779 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs
@@ -42,16 +42,7 @@ public static void AddClientWindowsProjectServices(this IServiceCollection servi
loggingBuilder.AddConfiguration(configuration.GetSection("Logging"));
loggingBuilder.AddEventSourceLogger();
- if (AppPlatform.IsWindows)
- {
- loggingBuilder.AddEventLog();
- }
- //#if (appCenter == true)
- if (Microsoft.AppCenter.AppCenter.Configured)
- {
- loggingBuilder.AddAppCenter(options => options.IncludeScopes = true);
- }
- //#endif
+ loggingBuilder.AddEventLog();
//#if (appInsights == true)
loggingBuilder.AddApplicationInsights(config =>
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs
index f29d6a50d7..9e30c106a8 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs
@@ -8,16 +8,6 @@ public partial class Program
[STAThread]
public static void Main(string[] args)
{
- //+:cnd:noEmit
- //#if (appCenter == true)
- string? appCenterSecret = null;
- if (appCenterSecret is not null)
- {
- Microsoft.AppCenter.AppCenter.Start(appCenterSecret, typeof(Microsoft.AppCenter.Crashes.Crashes), typeof(Microsoft.AppCenter.Analytics.Analytics));
- }
- //#endif
- //-:cnd:noEmit
-
AppPlatform.IsBlazorHybrid = true;
ITelemetryContext.Current = new WindowsTelemetryContext();
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsAppInsightsTelemetryInitializer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsAppInsightsTelemetryInitializer.cs
index 3589b903d5..b6d9ac626c 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsAppInsightsTelemetryInitializer.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsAppInsightsTelemetryInitializer.cs
@@ -20,7 +20,7 @@ public void Initialize(ITelemetry telemetry)
telemetry.Context.GlobalProperties[nameof(ITelemetryContext.UserAgent)] = ITelemetryContext.Current.UserAgent;
telemetry.Context.GlobalProperties[nameof(ITelemetryContext.TimeZone)] = ITelemetryContext.Current.TimeZone;
telemetry.Context.GlobalProperties[nameof(ITelemetryContext.Culture)] = ITelemetryContext.Current.Culture;
- telemetry.Context.GlobalProperties[nameof(ITelemetryContext.IsOnline)] = ITelemetryContext.Current.IsOnline.ToString().ToLowerInvariant();
+ telemetry.Context.GlobalProperties[nameof(ITelemetryContext.IsOnline)] = ITelemetryContext.Current.IsOnline?.ToString().ToLowerInvariant();
}
}
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsDeviceCoordinator.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsDeviceCoordinator.cs
index 6dbbf26ea7..86f542899e 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsDeviceCoordinator.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsDeviceCoordinator.cs
@@ -1,3 +1,12 @@
-namespace Boilerplate.Client.Windows.Services;
+//+:cnd:noEmit
+namespace Boilerplate.Client.Windows.Services;
-public partial class WindowsDeviceCoordinator : IBitDeviceCoordinator { }
+public partial class WindowsDeviceCoordinator : IBitDeviceCoordinator
+{
+ //#if (framework == 'net9.0')
+ public async Task ApplyTheme(bool isDark)
+ {
+ App.Current.ThemeMode = isDark ? System.Windows.ThemeMode.Dark : System.Windows.ThemeMode.Light;
+ }
+ //#endif
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsExceptionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsExceptionHandler.cs
index 03c98a0444..ded179e349 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsExceptionHandler.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsExceptionHandler.cs
@@ -2,11 +2,11 @@
///
/// Instead of Client.Core, install AppCenter, Firebase etc in Client.Windows, so the web version of the app won't download unnecessary packages.
-/// You can call their APIs such as Crashes.TrackError in WindowsExceptionHandler to monitor all exceptions across Android, iOS, Windows, and macOS.
+/// You can call their APIs such as Crashes.TrackError inside WindowsExceptionHandler to monitor all exceptions.
/// Employing Microsoft.Extensions.Logging implementations (like Sentry.Extensions.Logging) will result in
-/// automatic exception logging due to the logger.LogError method call within the ExceptionHandlerBase class.
+/// automatic exception logging due to the logger.LogError method call within the class.
///
-public partial class WindowsExceptionHandler : ExceptionHandlerBase
+public partial class WindowsExceptionHandler : ClientExceptionHandlerBase
{
protected override void Handle(Exception exception, Dictionary parameters)
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs
index 5b5c46bb18..c10bb849e8 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs
@@ -1,18 +1,19 @@
-using System.Net;
+using EmbedIO;
+using System.Net;
using System.Net.Http;
using System.Net.Sockets;
-using EmbedIO;
using EmbedIO.Actions;
using Boilerplate.Client.Core.Components;
-using Microsoft.Extensions.Logging;
namespace Boilerplate.Client.Windows.Services;
+///
+///
+///
public partial class WindowsLocalHttpServer : ILocalHttpServer
{
- [AutoInject] private IConfiguration configuration;
[AutoInject] private IExceptionHandler exceptionHandler;
- [AutoInject] private ILogger logger = default!;
+ [AutoInject] private AbsoluteServerAddressProvider absoluteServerAddress;
private WebServer? localHttpServer;
@@ -27,7 +28,7 @@ public int Start(CancellationToken cancellationToken)
{
try
{
- var url = $"{configuration.GetServerAddress()}/api/Identity/SocialSignedIn?culture={CultureInfo.CurrentUICulture.Name}";
+ var url = new Uri(absoluteServerAddress, $"/api/Identity/SocialSignedIn?culture={CultureInfo.CurrentUICulture.Name}").ToString();
ctx.Redirect(url);
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props
index fb789b23f0..31777cc84c 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props
@@ -1,4 +1,4 @@
-
+
@@ -21,12 +21,6 @@
DevelopmentProduction
- 14.0
- 14.0
- 26.0
- 10.0.17763.0
- 10.0.17763.0
- 6.5$(DefineConstants);Android$(DefineConstants);iOS$(DefineConstants);Windows
@@ -37,6 +31,28 @@
+
+
+ 14.0
+ 14.0
+ 26.0
+ 10.0.17763.0
+ 10.0.17763.0
+ 6.5
+
+
+
+
+
+ 16.4
+ 16.4
+ 26.0
+ 10.0.17763.0
+ 10.0.17763.0
+ 6.5
+
+
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props
index 3e970cb07a..999ded2dca 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props
@@ -1,32 +1,32 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
@@ -42,19 +42,19 @@
-
-
+
+
-
-
-
-
-
+
+
+
+
+
@@ -66,25 +66,25 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props
new file mode 100644
index 0000000000..6c9ba3edaa
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/.config/dotnet-tools.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/.config/dotnet-tools.json
index 40d4373bd4..8f16bf4c3a 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/.config/dotnet-tools.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/.config/dotnet-tools.json
@@ -3,7 +3,11 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "8.0.10",
+ //#if (framework == 'net9.0')
+ "version": "9.0.0",
+ //#else
+ "version": "8.0.11",
+ //#endif
"commands": [
"dotnet-ef"
]
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj
index f729be6517..54e6d35083 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net9.0AC87AA5B-4B37-4E52-8468-2D5DF24AF256PrepareResources;$(CompileDependsOn)
@@ -18,6 +18,7 @@
+
@@ -33,7 +34,7 @@
-
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs
index e2f39ea182..536fe678c6 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs
@@ -6,7 +6,7 @@
using Boilerplate.Shared.Dtos.Categories;
using Boilerplate.Shared.Controllers.Categories;
-namespace Boilerplate.Server.Api.Controllers;
+namespace Boilerplate.Server.Api.Controllers.Categories;
[ApiController, Route("api/[controller]/[action]")]
public partial class CategoryController : AppControllerBase, ICategoryController
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Dashboard/DashboardController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Dashboard/DashboardController.cs
index 7a2ac7701f..7895f782bc 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Dashboard/DashboardController.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Dashboard/DashboardController.cs
@@ -1,7 +1,7 @@
using Boilerplate.Shared.Dtos.Dashboard;
using Boilerplate.Shared.Controllers.Dashboard;
-namespace Boilerplate.Server.Api.Controllers;
+namespace Boilerplate.Server.Api.Controllers.Dashboard;
[ApiController, Route("api/[controller]/[action]")]
public partial class DashboardController : AppControllerBase, IDashboardController
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs
index 88a97b1f5f..5abd60e54d 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs
@@ -159,7 +159,7 @@ public async Task SignIn(SignInRequestDto request, CancellationToken cancellatio
}
///
- /// Creates a user session and adds its ID to the access and refresh tokens, but only if the sign-in is successful.
+ /// Creates a user session and adds its ID to the access and refresh tokens, but only if the sign-in is successful
///
private UserSession CreateUserSession(string? device)
{
@@ -218,14 +218,12 @@ public async Task> Refresh(RefreshRequestDto requ
}
finally
{
- try
+ if (user is not null)
{
- if (user is not null)
- {
- await userManager.UpdateAsync(user);
- }
+ var result = await userManager.UpdateAsync(user);
+ if (!result.Succeeded)
+ throw new ResourceValidationException(result.Errors.Select(err => new LocalizedString(err.Code, err.Description)).ToArray());
}
- catch (ConflictException) { /* When access_token gets expired and user navigates to the page that sends multiple requests in parallel, multiple concurrent refresh token api call happens and this will results into concurrency exception during updating session's renewed on. */ }
}
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs
index be48987b41..b455070c9b 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs
@@ -4,9 +4,9 @@
using Microsoft.AspNetCore.SignalR;
//#endif
using Boilerplate.Shared.Dtos.Products;
-using Boilerplate.Shared.Controllers.Product;
+using Boilerplate.Shared.Controllers.Products;
-namespace Boilerplate.Server.Api.Controllers;
+namespace Boilerplate.Server.Api.Controllers.Products;
[ApiController, Route("api/[controller]/[action]")]
public partial class ProductController : AppControllerBase, IProductController
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Statistics/StatisticsController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Statistics/StatisticsController.cs
new file mode 100644
index 0000000000..09b4d02182
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Statistics/StatisticsController.cs
@@ -0,0 +1,18 @@
+using Boilerplate.Server.Api.Services;
+using Boilerplate.Shared.Dtos.Statistics;
+using Boilerplate.Shared.Controllers.Statistics;
+
+namespace Boilerplate.Server.Api.Controllers.Statistics;
+
+[ApiController, Route("api/[controller]/[action]")]
+public partial class StatisticsController : AppControllerBase, IStatisticsController
+{
+ [AutoInject] private NugetStatisticsHttpClient nugetHttpClient = default!;
+
+ [AllowAnonymous]
+ [HttpGet("{packageId}")]
+ public async Task GetNugetStats(string packageId, CancellationToken cancellationToken)
+ {
+ return await nugetHttpClient.GetPackageStats(packageId, cancellationToken);
+ }
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/AppDbContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/AppDbContext.cs
index 081ce477e8..39f2c1e9e3 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/AppDbContext.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/AppDbContext.cs
@@ -207,7 +207,7 @@ private void ConcurrencyStamp(ModelBuilder modelBuilder)
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var property in entityType.GetProperties()
- .Where(p => p.Name is "ConcurrencyStamp"))
+ .Where(p => p.Name is "ConcurrencyStamp" && p.PropertyInfo?.PropertyType == typeof(byte[])))
{
var builder = new PropertyBuilder(property);
builder.IsConcurrencyToken()
@@ -218,12 +218,9 @@ private void ConcurrencyStamp(ModelBuilder modelBuilder)
{
//#endif
//#if (database == "PostgreSQL")
- if (property.ClrType == typeof(byte[]))
- {
- builder.HasConversion(new ValueConverter(
- v => BitConverter.ToUInt32(v, 0),
- v => BitConverter.GetBytes(v)));
- }
+ builder.HasConversion(new ValueConverter(
+ v => BitConverter.ToUInt32(v, 0),
+ v => BitConverter.GetBytes(v)));
//#endif
//#if (IsInsideProjectTemplate == true)
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241107182721_InitialMigration.Designer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241107182721_InitialMigration.Designer.cs
index d7123ec841..6d697cb8a1 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241107182721_InitialMigration.Designer.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241107182721_InitialMigration.Designer.cs
@@ -13,7 +13,7 @@ partial class InitialMigration
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.Entity("Boilerplate.Server.Api.Models.Categories.Category", b =>
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs
index 03c510345a..f864c7220d 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs
@@ -11,7 +11,7 @@ partial class AppDbContextModelSnapshot : ModelSnapshot
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.Entity("Boilerplate.Server.Api.Models.Categories.Category", b =>
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Extensions/SignInManagerExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Extensions/SignInManagerExtensions.cs
index 8a63f0b23f..20f6b832e9 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Extensions/SignInManagerExtensions.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Extensions/SignInManagerExtensions.cs
@@ -12,14 +12,13 @@ public static partial class SignInManagerExtensions
///
/// 1. When the user opts to sign in using a 6-digit code received via SMS.
/// 2. When the user chooses to sign in using a 6-digit code sent via email, typically within a magic link.
- /// 3. After a successful email confirmation post sign-up, to automatically sign in the confirmed user for an improved user experience.
- /// 4. After a successful phone number confirmation post sign-up, to automatically sign in the confirmed user for a smoother user experience.
+ /// 3. After a successful email confirmation after sign-up, to automatically sign in the confirmed user for an improved user experience.
+ /// 4. After a successful phone number confirmation after sign-up, to automatically sign in the confirmed user for a smoother user experience.
/// 5. When the browser is redirected to a magic link created after a social sign-in, to automatically authenticate the user.
/// 6. When the user opts to sign in using a 6-digit code delivered via native push notification, web push or SignalR message (if configured).
///
/// It's important to clarify the authentication method (e.g., Social, SMS, Email, or Push Notification)
- /// to avoid sending a second step to the same communication channel.
- /// For successful two-step authentication, the user must use a different method for the second step.
+ /// to avoid sending a second step to the same communication channel: For successful two-step authentication, the user must use a different method for the second step.
///
public static async Task<(SignInResult signInResult, string? authenticationMethod)> OtpSignInAsync(this SignInManager signInManager, User user, string otp)
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/Readme.md b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/Readme.md
index fad9970be8..6ebaa3422d 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/Readme.md
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/Readme.md
@@ -1,14 +1,14 @@
-When you have an IQueryable of an Entity or Model classes from EntityFrameworkCore,
+When you have an IQueryable of an Entity or Model classes from EntityFrameworkCore,
you ultimately need to convert it into an IQueryable of DTO classes and return it to the client.
The client can also implement pagination during the API call by sending values for $top, $skip and sort by $orderby in request query string.
-Ultimately, the query is executed by aspnetcore and the data gets streamed from the database to the client, which is the most optimal case.
-For this, you need to write a `Project` for each Entity you intend to return a query of DTO class from.
+Ultimately, the query is executed by aspnetcore and the data gets `streamed` from the database to the client, which is the most optimal case.
+For this, you need to write a `Project` method for each Entity you intend to return a query of DTO class from.
You also write a `Map` for when a DTO is sent to the server for create or update api,
so you can convert it to an Entity and save it in the database using ef core.
You also need a Map method to convert DTO to Entity and vice versa.
-You also need a `Patch` method for the update scenario, to perform update operation, first get the Entity from the database with its Id,
+You might also need a `Patch` method for the update scenario, to perform update operation, first get the Entity from the database with its Id,
then patch the latest changes from the DTO sent by the client, and finally save it.
These methods (`Project`, `Map` and `Patch`) are gets called in controllers.
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
index 72b4b9ed1e..c2165f0c70 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
@@ -131,11 +131,11 @@ public static void AddServerApiProjectServices(this WebApplicationBuilder builde
services.AddAntiforgery();
- services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.AddRange([AppJsonContext.Default, IdentityJsonContext.Default]));
+ services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.AddRange([AppJsonContext.Default, IdentityJsonContext.Default, ServerJsonContext.Default]));
services
.AddControllers()
- .AddJsonOptions(options => options.JsonSerializerOptions.TypeInfoResolverChain.AddRange([AppJsonContext.Default, IdentityJsonContext.Default]))
+ .AddJsonOptions(options => options.JsonSerializerOptions.TypeInfoResolverChain.AddRange([AppJsonContext.Default, IdentityJsonContext.Default, ServerJsonContext.Default]))
//#if (api == "Integrated")
.AddApplicationPart(typeof(AppControllerBase).Assembly)
//#endif
@@ -189,10 +189,16 @@ void AddDbContext(DbContextOptionsBuilder options)
});
//#elif (database == "MySql")
+ //#if (IsInsideProjectTemplate == true)
+ /*
+ //#endif
options.UseMySql(configuration.GetConnectionString("MySqlSQLConnectionString"), ServerVersion.AutoDetect(configuration.GetConnectionString("MySqlSQLConnectionString")), dbOptions =>
{
});
+ //#if (IsInsideProjectTemplate == true)
+ */
+ //#endif
//#elif (database == "Other")
throw new NotImplementedException("Install and configure any database supported by ef core (https://learn.microsoft.com/en-us/ef/core/providers)");
//#endif
@@ -254,6 +260,11 @@ void AddDbContext(DbContextOptionsBuilder options)
c.BaseAddress = new Uri("https://www.google.com/recaptcha/");
});
//#endif
+
+ services.AddHttpClient(c =>
+ {
+ c.BaseAddress = new Uri("https://azuresearch-usnc.nuget.org");
+ });
}
private static void AddIdentity(WebApplicationBuilder builder)
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/NugetStatisticsHttpClient.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/NugetStatisticsHttpClient.cs
new file mode 100644
index 0000000000..22f870b34c
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/NugetStatisticsHttpClient.cs
@@ -0,0 +1,18 @@
+using Boilerplate.Shared.Dtos.Statistics;
+
+namespace Boilerplate.Server.Api.Services;
+
+public partial class NugetStatisticsHttpClient
+{
+ [AutoInject] protected HttpClient httpClient = default!;
+
+ public virtual async ValueTask GetPackageStats(string packageId, CancellationToken cancellationToken)
+ {
+ var url = $"/query?q=packageid:{packageId}";
+
+ var response = await httpClient.GetFromJsonAsync(url, ServerJsonContext.Default.Options.GetTypeInfo(), cancellationToken)
+ ?? throw new ResourceNotFoundException(packageId);
+
+ return response;
+ }
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs
index 0b49fd0eae..6df512bf92 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs
@@ -96,7 +96,7 @@ public async Task RequestPush(string? title = null, string? message = null, stri
}
catch (Exception exp)
{
- logger.LogError(exp, "Failed to send push notification.");
+ logger.LogWarning(exp, "Failed to send push notification.");
}
}
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerExceptionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerExceptionHandler.cs
index 4cbea7ccfd..f0d31bcd1c 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerExceptionHandler.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerExceptionHandler.cs
@@ -1,15 +1,13 @@
using System.Net;
-using System.Reflection;
using System.Diagnostics;
using Microsoft.Net.Http.Headers;
using Microsoft.AspNetCore.Diagnostics;
namespace Boilerplate.Server.Api.Services;
-public partial class ServerExceptionHandler : IExceptionHandler
+public partial class ServerExceptionHandler : SharedExceptionHandler, IExceptionHandler
{
[AutoInject] private IWebHostEnvironment webHostEnvironment = default!;
- [AutoInject] private IStringLocalizer localizer = default!;
[AutoInject] private JsonSerializerOptions jsonSerializerOptions = default!;
public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e, CancellationToken cancellationToken)
@@ -22,13 +20,13 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
// The details of all of the exceptions are returned only in dev mode. in any other modes like production, only the details of the known exceptions are returned.
var key = knownException?.Key ?? nameof(UnknownException);
- var message = knownException?.Message ?? (webHostEnvironment.IsDevelopment() ? exception.Message : localizer[nameof(UnknownException)]);
+ var message = GetExceptionMessageToShow(exception);
var statusCode = (int)(exception is RestException restExp ? restExp.StatusCode : HttpStatusCode.InternalServerError);
if (exception is KnownException && message == key)
{
- message = localizer[message];
+ message = Localizer[message];
}
var restExceptionPayload = new RestErrorInfo
@@ -49,18 +47,4 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
return true;
}
-
- private Exception UnWrapException(Exception exception)
- {
- if (exception is AggregateException aggregateException)
- {
- return aggregateException.Flatten().InnerException ?? aggregateException;
- }
- else if (exception is TargetInvocationException)
- {
- return exception.InnerException ?? exception;
- }
-
- return exception;
- }
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerJsonContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerJsonContext.cs
index 801a336c29..6b02767d20 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerJsonContext.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerJsonContext.cs
@@ -1,11 +1,13 @@
//+:cnd:noEmit
+using Boilerplate.Shared.Dtos.Statistics;
+
namespace Boilerplate.Server.Api.Services;
///
/// https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator/
///
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
-[JsonSerializable(typeof(Dictionary))]
+[JsonSerializable(typeof(NugetStatsDto))]
//#if (captcha == "reCaptcha")
[JsonSerializable(typeof(GoogleRecaptchaVerificationResponse))]
//#endif
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/.config/dotnet-tools.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/.config/dotnet-tools.json
index 40d4373bd4..8f16bf4c3a 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/.config/dotnet-tools.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/.config/dotnet-tools.json
@@ -3,7 +3,11 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "8.0.10",
+ //#if (framework == 'net9.0')
+ "version": "9.0.0",
+ //#else
+ "version": "8.0.11",
+ //#endif
"commands": [
"dotnet-ef"
]
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Boilerplate.Server.Web.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Boilerplate.Server.Web.csproj
index 813308ddd4..4a7e29850d 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Boilerplate.Server.Web.csproj
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Boilerplate.Server.Web.csproj
@@ -1,12 +1,13 @@
- net8.0
+ net9.0AC87AA5B-4B37-4E52-8468-2D5DF24AF256false
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor
index 5204de1499..0e360e84d5 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor
@@ -1,9 +1,18 @@
@using Boilerplate.Client.Web.Components
+@using Microsoft.AspNetCore.Components.Routing
+@attribute [StreamRendering(enabled: true)]
@*+:cnd:noEmit*@
@{
var noPrerender = HttpContext.Request.Query["no-prerender"].Count > 0;
var renderMode = noPrerender ? noPrerenderBlazorWebAssembly : serverWebSettings.WebAppRender.RenderMode;
+ @*#if (framework == "net9.0")*@
+ if (HttpContext.AcceptsInteractiveRouting() is false)
+ {
+ // https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-9.0#add-static-server-side-rendering-ssr-pages-to-a-globally-interactive-blazor-web-app
+ renderMode = null;
+ }
+ @*#endif*@
}
@@ -54,14 +63,24 @@
@if (renderMode != null && (serverWebSettings.WebAppRender.PrerenderEnabled is false || noPrerender))
{
-
+
}
@if (HttpContext.Request.IsCrawlerClient() is false)
{
-
+ @*#if (framework == "net9.0")*@
+
+ @*#else*@
+ @*#if (IsInsideProjectTemplate == true)*@
+ /*
+ @*#endif)*@
+
+ @*#if (IsInsideProjectTemplate == true)*@
+ */
+ @*#endif)*@
+ @*#endif)*@
@if (serverWebSettings.WebAppRender.PwaEnabled)
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor.cs
index 1d08359c5d..e745ec44e9 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor.cs
@@ -4,7 +4,6 @@
namespace Boilerplate.Server.Web.Components;
-[StreamRendering(enabled: true)]
public partial class App
{
private static readonly IComponentRenderMode noPrerenderBlazorWebAssembly = new InteractiveWebAssemblyRenderMode(prerender: false);
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs
index 8f2c0376ff..46409dcf14 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs
@@ -132,6 +132,9 @@ public static void ConfigureMiddlewares(this WebApplication app)
app.UseSiteMap();
// Handle the rest of requests with blazor
+ //#if (framework == 'net9.0')
+ app.MapStaticAssets();
+ //#endif
var blazorApp = app.MapRazorComponents()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
@@ -212,7 +215,7 @@ private static void Configure_401_403_404_Pages(WebApplication app)
var qs = HttpUtility.ParseQueryString(httpContext.Request.QueryString.Value ?? string.Empty);
qs.Remove("try_refreshing_token");
- var returnUrl = UriHelper.BuildRelative(httpContext.Request.PathBase, httpContext.Request.Path, new QueryString(qs.ToString()));
+ var returnUrl = UriHelper.BuildRelative(httpContext.Request.PathBase, httpContext.Request.Path, new QueryString($"?{qs}"));
httpContext.Response.Redirect($"{Urls.NotAuthorizedPage}?return-url={returnUrl}&isForbidden={(is403 ? "true" : "false")}");
}
else if (httpContext.Response.StatusCode is 404 &&
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Boilerplate.Shared.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Boilerplate.Shared.csproj
index d91b299f5b..a38fb10801 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Boilerplate.Shared.csproj
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Boilerplate.Shared.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net9.0PrepareResources;$(CompileDependsOn)
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Attributes.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Attributes.cs
index 55a18795a7..b8a2b5b43e 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Attributes.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Attributes.cs
@@ -38,10 +38,23 @@ internal partial class HttpPatchAttribute(string? template = null) : Attribute
///
/// Avoid retrying the request upon failure.
-///
///
-[AttributeUsage(AttributeTargets.Method)]
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)]
public partial class NoRetryPolicyAttribute : Attribute
{
}
+
+///
+/// This `optional` attribute enables `client-side` validation of access token expiry before making any requests to the server.
+/// By checking the token's validity locally, unnecessary server requests that would result in a 401 response are avoided,
+/// and the refresh token can be invoked immediately if the access token is expired.
+/// Since access tokens are intentionally set to expire frequently for security and other benefits, this scenario is expected to occur often.
+/// Performing this validation on the client improves overall application performance by reducing redundant network calls.
+/// Note: This attribute requires the client’s date and time settings to be accurate, as incorrect settings may cause validation issues.
+///
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)]
+public partial class AuthorizedApiAttribute : Attribute
+{
+
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Categories/ICategoryController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Categories/ICategoryController.cs
index e05c46bb5a..2f3e35b1f3 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Categories/ICategoryController.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Categories/ICategoryController.cs
@@ -2,7 +2,7 @@
namespace Boilerplate.Shared.Controllers.Categories;
-[Route("api/[controller]/[action]/")]
+[Route("api/[controller]/[action]/"), AuthorizedApi]
public interface ICategoryController : IAppController
{
[HttpGet("{id}")]
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Dashboard/IDashboardController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Dashboard/IDashboardController.cs
index a238327a30..836079c966 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Dashboard/IDashboardController.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Dashboard/IDashboardController.cs
@@ -2,7 +2,7 @@
namespace Boilerplate.Shared.Controllers.Dashboard;
-[Route("api/[controller]/[action]/")]
+[Route("api/[controller]/[action]/"), AuthorizedApi]
public interface IDashboardController : IAppController
{
[HttpGet]
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs
index 0d915c8e2b..2e72e977a8 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs
@@ -2,7 +2,7 @@
namespace Boilerplate.Shared.Controllers.Identity;
-[Route("api/[controller]/[action]/")]
+[Route("api/[controller]/[action]/"), AuthorizedApi]
public interface IUserController : IAppController
{
[HttpGet]
@@ -11,7 +11,7 @@ public interface IUserController : IAppController
[HttpGet]
Task> GetUserSessions(CancellationToken cancellationToken);
- [HttpPost]
+ [HttpPost, NoRetryPolicy]
Task SignOut(CancellationToken cancellationToken);
[HttpPost("{id}")]
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Products/IProductController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Products/IProductController.cs
index 669f71e376..1656a8a3e7 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Products/IProductController.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Products/IProductController.cs
@@ -1,8 +1,8 @@
using Boilerplate.Shared.Dtos.Products;
-namespace Boilerplate.Shared.Controllers.Product;
+namespace Boilerplate.Shared.Controllers.Products;
-[Route("api/[controller]/[action]/")]
+[Route("api/[controller]/[action]/"), AuthorizedApi]
public interface IProductController : IAppController
{
[HttpGet("{id}")]
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Readme.md b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Readme.md
index cb5eda9f51..eed5b93cf9 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Readme.md
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Readme.md
@@ -1,8 +1,8 @@
-**Bit.SourceGenerator interface based HttpClient proxy generator**
+**Bit.SourceGenerator interface based HttpClient proxy generator**
**Introduction:**
When defining server-side APIs, there are no restrictions. You can leverage features such as Api versioning, OData, ASP.NET Core Minimal API,
-Middleware, and more. On the client side, use HttpClient and optionally employ Bit.SourceGenerator.
+Middleware, and more. On the client side, use HttpClient and optionally employ Bit.SourceGenerators.
Getting started:
@@ -15,7 +15,8 @@ public interface ICategoryController : IAppController
2- Simply inject these interfaces into your classes, and you're all set!
-3- (Optional) implement that interface in server project.
+3- `Optionaly` implement that interface in server project.
+
```csharp
public class CategoryController : AppControllerBase, ICategoryController
```
@@ -27,7 +28,7 @@ that methods seen by the client in `ICategoryController` are present in `Categor
**Note:** Server-side methods may have conditions that make direct definition in the client-side interface challenging.
For example, an `Upload` method in `AttachmentController` has `IFormFile`,
and `Refresh` method of `IdentityController` returns `ActionResult` and these types are not present in client side.
-In this case you can still use `Bit.SourceGenerator`, but in order to prevent C# compiler's build error, write the followings:
+In this case you can still use `Bit.SourceGenerators`, but in order to prevent C# compiler's build error, write the followings:
```csharp
[HttpPost]
Task Refresh(RefreshRequestDto body) => default!;
@@ -52,5 +53,7 @@ Explore `IMinimalApiController` for example of ASP.NET Core Minimal API that has
3- Example of Query String.
+4- Direct GitHub API call at client-side in IStatisticsController's GetGitHubStats.
+
**Note:** We supprt [RFC6570](https://datatracker.ietf.org/doc/html/rfc6570) for request url templates thanks to [DoLess.UriTemplates](https://github.com/letsar/DoLess.UriTemplates?tab=readme-ov-file#examples)!
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Statistics/IStatisticsController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Statistics/IStatisticsController.cs
new file mode 100644
index 0000000000..c4196ebc04
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Statistics/IStatisticsController.cs
@@ -0,0 +1,13 @@
+using Boilerplate.Shared.Dtos.Statistics;
+
+namespace Boilerplate.Shared.Controllers.Statistics;
+
+[Route("api/[controller]/[action]/")]
+public interface IStatisticsController : IAppController
+{
+ [HttpGet("{packageId}")]
+ Task GetNugetStats(string packageId, CancellationToken cancellationToken);
+
+ [HttpGet, Route("https://api.github.com/repos/bitfoundation/bitplatform")]
+ Task GetGitHubStats(CancellationToken cancellationToken) => default!;
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Todo/ITodoItemController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Todo/ITodoItemController.cs
index a93719b9c0..00eac962fa 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Todo/ITodoItemController.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Todo/ITodoItemController.cs
@@ -2,7 +2,7 @@
namespace Boilerplate.Shared.Controllers.Todo;
-[Route("api/[controller]/[action]/")]
+[Route("api/[controller]/[action]/"), AuthorizedApi]
public interface ITodoItemController : IAppController
{
[HttpGet("{id}")]
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs
index 6ab5f7d91c..39d4b3a7ad 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs
@@ -9,6 +9,7 @@
//#if (notification == true)
using Boilerplate.Shared.Dtos.PushNotification;
//#endif
+using Boilerplate.Shared.Dtos.Statistics;
namespace Boilerplate.Shared.Dtos;
@@ -19,6 +20,8 @@ namespace Boilerplate.Shared.Dtos;
[JsonSerializable(typeof(Dictionary))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(RestErrorInfo))]
+[JsonSerializable(typeof(GitHubStats))]
+[JsonSerializable(typeof(NugetStatsDto))]
//#if (notification == true)
[JsonSerializable(typeof(DeviceInstallationDto))]
//#endif
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/GitHubStats.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/GitHubStats.cs
new file mode 100644
index 0000000000..27903db9f0
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/GitHubStats.cs
@@ -0,0 +1,18 @@
+namespace Boilerplate.Shared.Dtos.Statistics;
+
+public record GitHubStats(
+ [property: JsonPropertyName("id")] int Id,
+ [property: JsonPropertyName("name")] string Name,
+ [property: JsonPropertyName("full_name")] string FullName,
+ [property: JsonPropertyName("description")] string Description,
+ [property: JsonPropertyName("homepage")] string Homepage,
+ [property: JsonPropertyName("size")] int Size,
+ [property: JsonPropertyName("stargazers_count")] int StargazersCount,
+ [property: JsonPropertyName("watchers_count")] int WatchersCount,
+ [property: JsonPropertyName("language")] string Language,
+ [property: JsonPropertyName("forks_count")] int ForksCount,
+ [property: JsonPropertyName("open_issues_count")] int OpenIssuesCount,
+ [property: JsonPropertyName("default_branch")] string DefaultBranch,
+ [property: JsonPropertyName("network_count")] int NetworkCount,
+ [property: JsonPropertyName("subscribers_count")] int SubscribersCount
+);
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/NugetStatsDto.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/NugetStatsDto.cs
new file mode 100644
index 0000000000..04d74ac268
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/NugetStatsDto.cs
@@ -0,0 +1,15 @@
+namespace Boilerplate.Shared.Dtos.Statistics;
+
+public record NugetStatsDto(
+ [property: JsonPropertyName("data")] IReadOnlyList Data
+);
+
+public record Datum(
+ [property: JsonPropertyName("id")] string Id,
+ [property: JsonPropertyName("version")] string Version,
+ [property: JsonPropertyName("description")] string Description,
+ [property: JsonPropertyName("title")] string Title,
+ [property: JsonPropertyName("projectUrl")] string ProjectUrl,
+ [property: JsonPropertyName("totalDownloads")] int TotalDownloads
+);
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/TupleExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/TupleExtensions.cs
index a81cd2fc1c..e84fb45b37 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/TupleExtensions.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/TupleExtensions.cs
@@ -33,6 +33,8 @@ namespace System.Threading.Tasks;
/// // The total time is 150ms 👍
/// }
/// }
+///
+/// See HomePage.razor.cs's OnInitAsync example.
///
public static partial class TupleExtensions
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Mapper.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Mapper.cs
index c798f4904b..6f803dcfb9 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Mapper.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Mapper.cs
@@ -9,7 +9,7 @@ namespace Boilerplate.Shared;
///
/// Patching methods help you patch the DTO you have received from the server (for example, after calling an Update api)
-/// onto the DTO you have bound to the UI. This way, the UI gets updated with the latest saved changes,
+/// onto the DTO you have bound to the UI. This way, the UI gets updated with the latest stored changes,
/// and there's no need to re-fetch that specific data from the server.
/// For complete end to end sample you can check ProfilePage.razor.cs
/// You can add as many as Patch methods you want for other DTO classes here.
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx
index 84e99323b0..1f2165350a 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx
@@ -277,6 +277,9 @@
ویرایش پروفایل آفلاین
+
+
+ فرم ویرایش آفلاین مبتنیبر EF Core / Sqlite قابل استفاده در برنامه های وب / موبایل / دسکتاپ شما!
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx
index 3873a39d58..89588e6038 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx
@@ -274,6 +274,9 @@
Offline Profiel bewerken
+
+
+ EF Core/Sqlite-aangedreven offline bewerkingsformulieren direct in uw web-/mobiele/desktop-apps!
@@ -1067,7 +1070,7 @@
Voer de productnaam in
-
+
Aantal categorieën met product
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx
index 3c21d04baa..f3b7ac1621 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx
@@ -277,10 +277,13 @@
Offline Edit profile
+
+
+ EF Core / Sqlite powered offline edit form right into your web/mobile/desktop apps!
- FullName
+ Full nameTerms
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AppEnvironment.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AppEnvironment.cs
index 638ef21103..c0b217ada9 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AppEnvironment.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AppEnvironment.cs
@@ -5,8 +5,7 @@ namespace Boilerplate.Shared.Services;
/// Unlike ASP.NET Core, which allows environment configuration via environment variables,
/// Android, iOS, Windows, and macOS do not support the exact same concept.
/// To maintain consistency, we introduced .
-/// The environment name is synchronized with ASP.NET Core environment's name in the API,
-/// Blazor Server, and Blazor WebAssembly (WASM).
+/// The environment name is synchronized with ASP.NET Core environment's name in the API, Blazor Server, and Blazor WebAssembly (WASM).
/// Additionally, in Blazor Hybrid, it stays in sync with the build configuration (Debug, Release).
///
public static partial class AppEnvironment
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/Contracts/IPrerenderStateService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/Contracts/IPrerenderStateService.cs
index 3333d1ea2d..84c4acb4ec 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/Contracts/IPrerenderStateService.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/Contracts/IPrerenderStateService.cs
@@ -3,17 +3,24 @@
namespace Boilerplate.Shared.Services.Contracts;
///
-/// This service simplifies the process of persisting application state in Pre-Rendering mode
-/// (explained in this documentation: https://docs.microsoft.com/en-us/aspnet/core/blazor/components/prerendering-and-integration#persist-prerendered-state).
-/// If your project does not require prerendering to be enabled, you can completely remove this service and its usages from your project.
+/// The Client.Core codebase is designed to support various Blazor hosting models, including Hybrid and WebAssembly,
+/// which may or may not enable pre-rendering. To ensure consistent behavior across all scenarios,
+/// the `IPrerenderStateService` interface is introduced. This service provides the `GetValue` method for data retrieval,
+/// such as during the `OnInitAsync` method in components like UserMenu.
+///
+/// The `WebPrerenderStateService` implementation of `IPrerenderStateService` facilitates pre-rendering by using
+/// PersistentComponentState to persist data across renders, simplifies the process of managing application state in pre-rendering scenarios outlined in
+/// the documentation: https://docs.microsoft.com/en-us/aspnet/core/blazor/components/prerendering-and-integration#persist-prerendered-state.
+///
+/// For cases where pre-rendering is unnecessary, such as in Blazor Hybrid, a `NoopPrerenderStateService` is provided. This implementation simply executes the provided
+/// function and returns its result without persisting any state.
+///
+/// If pre-rendering is not required for your project, this service and its related dependencies can be safely removed.
///
public interface IPrerenderStateService : IAsyncDisposable
{
///
- /// Instead of using ApplicationState.TryTakeFromJson, ApplicationState.RegisterOnPersisting,
- /// and ApplicationState.PersistAsJson (explained here: https://docs.microsoft.com/en-us/aspnet/core/blazor/components/prerendering-and-integration#persist-prerendered-state),
- /// one can easily use the following method () in the OnInit lifecycle method of the Blazor components or pages
- /// to retrieve everything that requires an async-await (like current user's info).
+ ///
///
Task GetValue(Func> factory,
[CallerLineNumber] int lineNumber = 0,
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/CultureInfoManager.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/CultureInfoManager.cs
index e249529b1d..982ae71684 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/CultureInfoManager.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/CultureInfoManager.cs
@@ -39,7 +39,7 @@ public static CultureInfo CreateCultureInfo(string name)
public void SetCurrentCulture(string cultureName)
{
- var cultureInfo = SupportedCultures.FirstOrDefault(sc => sc.Culture.Name == cultureName).Culture ?? DefaultCulture;
+ var cultureInfo = SupportedCultures.FirstOrDefault(sc => string.Equals(sc.Culture.Name, cultureName, StringComparison.InvariantCultureIgnoreCase)).Culture ?? DefaultCulture;
CultureInfo.CurrentCulture = CultureInfo.DefaultThreadCurrentCulture = Thread.CurrentThread.CurrentCulture = cultureInfo;
CultureInfo.CurrentUICulture = CultureInfo.DefaultThreadCurrentUICulture = Thread.CurrentThread.CurrentUICulture = cultureInfo;
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedExceptionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedExceptionHandler.cs
new file mode 100644
index 0000000000..6a54ab1abb
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedExceptionHandler.cs
@@ -0,0 +1,58 @@
+using System.Reflection;
+
+namespace Boilerplate.Shared.Services;
+
+public partial class SharedExceptionHandler
+{
+ [AutoInject] protected IStringLocalizer Localizer { get; set; } = default!;
+
+ protected string GetExceptionMessageToShow(Exception exception)
+ {
+ if (exception is KnownException)
+ return exception.Message;
+
+ if (AppEnvironment.IsDev())
+ return exception.ToString();
+
+ return Localizer[nameof(AppStrings.UnknownException)];
+ }
+
+ protected string GetExceptionMessageToLog(Exception exception)
+ {
+ var exceptionMessageToLog = exception.Message;
+ var innerException = exception.InnerException;
+
+ while (innerException is not null)
+ {
+ exceptionMessageToLog += $"{Environment.NewLine}{innerException.Message}";
+ innerException = innerException.InnerException;
+ }
+
+ return exceptionMessageToLog;
+ }
+
+ protected Exception UnWrapException(Exception exception)
+ {
+ if (exception is AggregateException aggregateException)
+ {
+ return aggregateException.Flatten().InnerException ?? aggregateException;
+ }
+ else if (exception is TargetInvocationException)
+ {
+ return exception.InnerException ?? exception;
+ }
+
+ return exception;
+ }
+
+ protected bool IgnoreException(Exception exception)
+ {
+ if (exception is KnownException)
+ return false;
+
+ return exception is TaskCanceledException ||
+ exception is OperationCanceledException ||
+ exception is TimeoutException ||
+ (exception.InnerException is not null && IgnoreException(exception.InnerException));
+ }
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/SharedSettings.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/SharedSettings.cs
index f2f9c80e7c..2cc3ddc864 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/SharedSettings.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/SharedSettings.cs
@@ -4,7 +4,7 @@ namespace Boilerplate.Shared;
public partial class SharedSettings : IValidatableObject
{
///
- /// If you are hosting the API and web client on different URLs (e.g., api.company.com and app.company.com), you must set `WebClientUrl` to your web client's address.
+ /// If you are hosting the API and web client on different URLs (e.g., adminpanel-api.bitplatform.dev and adminpanel.bitplatform.dev), you must set `WebClientUrl` to your web client's address.
/// This ensures that the API server redirects to the correct URL after social sign-ins and other similar actions.
///
public string? WebClientUrl { get; set; }
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.Development.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.Development.json
index d2a86e4696..d2b0879764 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.Development.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.Development.json
@@ -1,65 +1,81 @@
-{
+{
"Logging": {
- "Console": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore*": "Warning",
+ "System.Net.Http.HttpClient": "Warning",
+ "System.Net.Http.HttpClient.*.LogicalHandler": "Warning"
+ },
+ //#if (appInsights == true)
+ "ApplicationInsights": {
"LogLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning"
+ "Microsoft.AspNetCore*": "Warning",
+ "System.Net.Http.HttpClient.*.LogicalHandler": "Warning"
},
"IncludeScopes": true
},
- "EventLog": {
+ "ApplicationInsightsLoggerProvider": {
"LogLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning"
+ "Microsoft.AspNetCore*": "Warning",
+ "System.Net.Http.HttpClient": "Warning",
+ "System.Net.Http.HttpClient.*.LogicalHandler": "Warning"
},
"IncludeScopes": true
},
- "EventSource": {
+ //#endif
+ //#if (appCenter == true)
+ "AppCenterLoggerProvider": {
"LogLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning"
+ "Microsoft.AspNetCore*": "Warning",
+ "System.Net.Http.HttpClient.*.LogicalHandler": "Warning"
},
"IncludeScopes": true
},
- "DiagnosticLogger": {
+ //#endif
+ "Console": {
"LogLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning"
+ "Microsoft.AspNetCore*": "Warning",
+ "System.Net.Http.HttpClient.*.LogicalHandler": "Warning"
},
"IncludeScopes": true
},
- "Debug": {
+ "WebAssemblyConsoleLoggerProvider": {
"LogLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning"
+ "Microsoft.AspNetCore*": "Warning",
+ "System.Net.Http.HttpClient": "Warning",
+ "System.Net.Http.HttpClient.*.LogicalHandler": "Warning"
},
"IncludeScopes": true
},
- //#if (appInsights == true)
- "ApplicationInsights": {
+ "EventLog": {
"LogLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning"
+ "Microsoft.AspNetCore*": "Warning",
+ "System.Net.Http.HttpClient.*.LogicalHandler": "Warning"
},
- "IncludeScopes": false
+ "IncludeScopes": true
},
- "ApplicationInsightsLoggerProvider": {
+ "EventSource": {
"LogLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning"
+ "Microsoft.AspNetCore*": "Warning",
+ "System.Net.Http.HttpClient.*.LogicalHandler": "Warning"
},
- "IncludeScopes": false
+ "IncludeScopes": true
},
- //#endif
- //#if (appCenter == true)
- "AppCenterLoggerProvider": {
+ "Debug": {
"LogLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning"
+ "Microsoft.AspNetCore*": "Warning",
+ "System.Net.Http.HttpClient.*.LogicalHandler": "Warning"
},
- "IncludeScopes": false
+ "IncludeScopes": true
}
- //#endif
},
"$schema": "https://json.schemastore.org/appsettings.json"
}
\ No newline at end of file
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json
index f7313af1be..bc4b2d2dfe 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json
@@ -1,4 +1,4 @@
-{
+{
"WebClientUrl": null,
"WebClientUrl_Comment": "If you are hosting the API and web client on different URLs (e.g., api.company.com and app.company.com), you must set `WebClientUrl` to your web client's address. This ensures that the API server redirects to the correct URL after social sign-ins and other similar actions.",
//#if (appInsights == true)
@@ -7,20 +7,22 @@
},
//#endif
"Logging": {
+ "LogLevel": {
+ "Default": "Warning",
+ "Microsoft.EntityFrameworkCore.Database.Command": "Information"
+ },
//#if (appInsights == true)
"ApplicationInsights": {
"LogLevel": {
"Default": "Warning",
- "Microsoft.EntityFrameworkCore.Database.Command": "Information",
- "Boilerplate.Client.Core.Services.AuthenticationManager": "Information"
+ "Microsoft.EntityFrameworkCore.Database.Command": "Information"
},
"IncludeScopes": true
},
"ApplicationInsightsLoggerProvider": {
"LogLevel": {
"Default": "Warning",
- "Microsoft.EntityFrameworkCore.Database.Command": "Information",
- "Boilerplate.Client.Core.Services.AuthenticationManager": "Information"
+ "Microsoft.EntityFrameworkCore.Database.Command": "Information"
},
"IncludeScopes": true
},
@@ -42,6 +44,13 @@
},
"IncludeScopes": true
},
+ "WebAssemblyConsoleLoggerProvider": {
+ "LogLevel": {
+ "Default": "Warning",
+ "Microsoft.EntityFrameworkCore.Database.Command": "Information"
+ },
+ "IncludeScopes": true
+ },
"EventLog": {
"LogLevel": {
"Default": "Warning",
@@ -59,7 +68,8 @@
"DiagnosticLogger": {
"LogLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning"
+ "Microsoft.AspNetCore*": "Warning",
+ "System.Net.Http.HttpClient.*.LogicalHandler": "Warning"
},
"IncludeScopes": true
},
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Boilerplate.Tests.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Boilerplate.Tests.csproj
index 2c299476b7..25ea8e7f1a 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Boilerplate.Tests.csproj
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Boilerplate.Tests.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net9.0enabletrue$(MSBuildProjectDirectory)\.runsettings
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/BrowserContextExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/BrowserContextExtensions.cs
new file mode 100644
index 0000000000..f1ad37ad74
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/BrowserContextExtensions.cs
@@ -0,0 +1,18 @@
+namespace Boilerplate.Tests.Extensions;
+
+public static class PlaywrightInitialScriptExtensions
+{
+ public static async Task SetBlazorWebAssemblyServerAddress(this IBrowserContext context, string serverAddress)
+ {
+ await context.SetBlazorWebAssemblyConfiguration(new() { ["ServerAddress"] = serverAddress });
+ }
+
+ public static async Task SetBlazorWebAssemblyConfiguration(this IBrowserContext context, Dictionary configs)
+ {
+ //Pass configuration to BlazorWebAssembly
+ //More info: https://stackoverflow.com/questions/60831359/how-are-string-args-passed-to-program-main-in-a-blazor-webassembly-app
+
+ var arrayString = string.Join(',', configs.Select(p => $"'{p.Key}={p.Value}'"));
+ await context.AddInitScriptAsync($"window.startupParams = function() {{ return [ {arrayString} ]; }};");
+ }
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightCacheExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightAssetCachingExtensions.cs
similarity index 63%
rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightCacheExtensions.cs
rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightAssetCachingExtensions.cs
index 2d42b91d73..86ee67b098 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightCacheExtensions.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightAssetCachingExtensions.cs
@@ -3,9 +3,9 @@
namespace Boilerplate.Tests.Extensions;
-public static partial class PlaywrightCacheExtensions
+public static partial class PlaywrightAssetCachingExtensions
{
- private static readonly ConcurrentDictionary Headers)> cachedResponses = [];
+ public static ConcurrentDictionary Headers)> CachedResponses { get; } = [];
public static Task EnableBlazorWasmCaching(this IPage page) => page.EnableAssetCaching(BlazorWasmRegex());
@@ -27,12 +27,12 @@ private static async Task CacheHandler(IRoute route)
{
var url = new Uri(route.Request.Url).PathAndQuery;
- if (cachedResponses.TryGetValue(url, out var cachedResponse) is false)
+ if (CachedResponses.TryGetValue(url, out var cachedResponse) is false)
{
var response = await route.FetchAsync();
var body = await response.BodyAsync();
cachedResponse = (body, response.Headers);
- cachedResponses[url] = cachedResponse;
+ CachedResponses[url] = cachedResponse;
}
await route.FulfillAsync(new RouteFulfillOptions
@@ -43,10 +43,19 @@ await route.FulfillAsync(new RouteFulfillOptions
});
}
- public static void ClearCache() => cachedResponses.Clear();
+ public static bool ContainsAsset(Regex regex) => CachedResponses.Keys.Any(regex.IsMatch);
- public static bool ContainsAsset(Regex regex) => cachedResponses.Keys.Any(regex.IsMatch);
+ public static bool ContainsAsset(string url) => CachedResponses.Keys.Any(url.Contains);
+ public static void ClearBlazorWasmCache() => ClearCache(BlazorWasmRegex());
+
+ public static void ClearCache() => CachedResponses.Clear();
+
+ public static void ClearCache(Regex regex) => CachedResponses.Where(x => regex.IsMatch(x.Key)).ToList().ForEach(key => CachedResponses.TryRemove(key));
+
+ public static void ClearCache(string url) => CachedResponses.Where(x => url.Contains(x.Key)).ToList().ForEach(key => CachedResponses.TryRemove(key));
+
+ //Glob pattern: /_framework/*.{wasm|pdb|dat}?v=sha256-*
[GeneratedRegex(@"\/_framework\/[\w\.]+\.((wasm)|(pdb)|(dat))\?v=sha256-.+")]
- private static partial Regex BlazorWasmRegex();
+ public static partial Regex BlazorWasmRegex();
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightCacheStorageExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightCacheStorageExtensions.cs
new file mode 100644
index 0000000000..2c3aecbeed
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightCacheStorageExtensions.cs
@@ -0,0 +1,81 @@
+using System.Text.RegularExpressions;
+
+namespace Boilerplate.Tests.Extensions;
+
+public static class PlaywrightCacheStorageExtensions
+{
+ public static async Task DeleteCacheStorage(this IPage page, CacheId? cacheId = null)
+ {
+ // Chrome DevTools Protocol
+ // https://chromedevtools.github.io/devtools-protocol/tot/CacheStorage/
+
+ cacheId ??= new();
+ await using var client = await page.Context.NewCDPSessionAsync(page);
+ await client.SendAsync("CacheStorage.deleteCache", new() { ["cacheId"] = cacheId.Value });
+ }
+
+ public static async Task GetCacheStorageEntries(this IPage page, CacheId? cacheId = null)
+ {
+ cacheId ??= new();
+ await using var client = await page.Context.NewCDPSessionAsync(page);
+ var json = await client.SendAsync("CacheStorage.requestEntries", new() { ["cacheId"] = cacheId.Value });
+ return json.Value.Deserialize()!;
+ }
+}
+
+public record CacheId(string Origin = "http://localhost:5000/", string CacheName = "dotnet-resources-/")
+{
+ public string Value { get; init; } = $"{Origin}|{CacheName}";
+
+ public override string ToString() => Value;
+}
+
+public class CacheEntries
+{
+ [JsonPropertyName("cacheDataEntries")]
+ public List CacheDataEntries { get; set; } = [];
+
+ public List GetCacheEntries(Regex regex) => CacheDataEntries.Where(x => regex.IsMatch(x.RequestURL)).ToList();
+
+ public List GetCacheEntries(string url) => CacheDataEntries.Where(x => url.Contains(x.RequestURL)).ToList();
+
+ public bool ContainsCacheEntry(Regex regex) => CacheDataEntries.Exists(x => regex.IsMatch(x.RequestURL));
+
+ public bool ContainsCacheEntry(string url) => CacheDataEntries.Exists(x => url.Contains(x.RequestURL));
+}
+
+public class CacheEntry
+{
+ [JsonPropertyName("requestURL")]
+ public string RequestURL { get; set; }
+
+ [JsonPropertyName("requestMethod")]
+ public string RequestMethod { get; set; }
+
+ [JsonPropertyName("responseTime")]
+ public double ResponseTime { get; set; }
+
+ [JsonPropertyName("responseStatus")]
+ public int ResponseStatus { get; set; }
+
+ [JsonPropertyName("responseStatusText")]
+ public string ResponseStatusText { get; set; }
+
+ [JsonPropertyName("responseType")]
+ public string ResponseType { get; set; }
+
+ [JsonPropertyName("requestHeaders")]
+ public List RequestHeaders { get; set; }
+
+ [JsonPropertyName("responseHeaders")]
+ public List ResponseHeaders { get; set; }
+}
+
+public class RequestResponseHeader
+{
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ [JsonPropertyName("value")]
+ public string Value { get; set; }
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightHydrationExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightHydrationExtensions.cs
new file mode 100644
index 0000000000..6f02accab3
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightHydrationExtensions.cs
@@ -0,0 +1,43 @@
+using System.Text.RegularExpressions;
+
+namespace Boilerplate.Tests.Extensions;
+public static partial class PlaywrightHydrationExtensions
+{
+ public static async Task WaitForHydrationToComplete(this IPage page)
+ {
+ await page.WaitForFunctionAsync("document.title != 'Before-Hydration'");
+ //await Assertions.Expect(page.Locator("title")).Not.ToHaveTextAsync("Before-Hydration");
+ }
+
+ public static Task EnableHydrationCheck(this IBrowserContext context) => context.RouteAsync("**/*", ChangeTitleHandler);
+
+ public static Task EnableHydrationCheck(this IPage page) => page.RouteAsync("**/*", ChangeTitleHandler);
+
+ private static async Task ChangeTitleHandler(IRoute route)
+ {
+ var request = route.Request;
+ if (request.ResourceType == "document" && request.Method == HttpMethods.Get)
+ {
+ var response = await route.FetchAsync();
+
+ // Change page title to Before-Hydration to check if hydration is completed
+ // It will be set to original title once after hydration is completed
+ var body = await response.TextAsync();
+ body = TitleRegex().Replace(body, "Before-Hydration");
+
+ await route.FulfillAsync(new()
+ {
+ Response = response,
+ Body = body,
+ });
+ }
+ else
+ {
+ await route.ContinueAsync();
+ }
+ }
+
+ [GeneratedRegex(@"(.*?)<\/title>")]
+ private static partial Regex TitleRegex();
+}
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightNetworkExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightNetworkExtensions.cs
new file mode 100644
index 0000000000..a9f9285cdd
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightNetworkExtensions.cs
@@ -0,0 +1,320 @@
+using System.Text.RegularExpressions;
+
+namespace Boilerplate.Tests.Extensions;
+
+public static class PlaywrightNetworkExtensions
+{
+ public static async Task OpenNetworkSession(this IPage page)
+ {
+ // Enable CDP session to access network details
+ var client = await page.Context.NewCDPSessionAsync(page);
+ var networkSession = new PlaywrightNetworkSession(client);
+ await networkSession.Enable();
+ return networkSession;
+ }
+}
+
+public class PlaywrightNetworkSession : IAsyncDisposable
+{
+ private readonly ICDPSession client;
+ private readonly ICDPSessionEvent responseReceived;
+ private readonly ICDPSessionEvent loadingFinished;
+
+ public List DownloadedResponses { get; } = [];
+ public int TotalDownloaded => DownloadedResponses.Sum(x => x.EncodedDataLength);
+
+ public PlaywrightNetworkSession(ICDPSession client)
+ {
+ this.client = client;
+
+ // Chrome DevTools Protocol
+ // https://chromedevtools.github.io/devtools-protocol/1-3/Network/
+
+ // Listen to the 'Network.responseReceived' events
+ responseReceived = client.Event("Network.responseReceived");
+ responseReceived.OnEvent += OnResponseReceived;
+
+ // Listen to the 'Network.loadingFinished' events
+ loadingFinished = client.Event("Network.loadingFinished");
+ loadingFinished.OnEvent += OnLoadingFinished;
+ }
+
+ ///
+ /// Enables network tracking, network events will now be delivered to the client.
+ ///
+ public async Task Enable() => client.SendAsync("Network.enable");
+
+ ///
+ /// Disables network tracking, prevents network events from being sent to the client.
+ ///
+ public async Task Disable() => client.SendAsync("Network.disable");
+
+ private async void OnResponseReceived(object? sender, JsonElement? arg)
+ {
+ try
+ {
+ var data = arg!.Value.Deserialize()!;
+ data.Response.RequestId = data.RequestId;
+ DownloadedResponses.Add(data.Response);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Failed to retrieve response data: {ex.Message}");
+ }
+ }
+
+ private async void OnLoadingFinished(object? sender, JsonElement? arg)
+ {
+ try
+ {
+ var data = arg!.Value.Deserialize()!;
+ var response = DownloadedResponses.Find(x => x.RequestId == data.RequestId)!;
+ response.EncodedDataLength = data.EncodedDataLength;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Failed to retrieve encoded size: {ex.Message}");
+ }
+ }
+
+ #region Other Commands
+
+ public Task ClearBrowserCache() => client.SendAsync("Network.clearBrowserCache");
+
+ public Task ClearBrowserCookies() => client.SendAsync("Network.clearBrowserCookies");
+
+ ///
+ /// Toggles ignoring cache for each request. If true, cache will not be used.
+ ///
+ public Task SetCacheDisabled(bool disabled) => client.SendAsync("Network.setCacheDisabled", new() { ["cacheDisabled"] = disabled });
+
+ ///
+ /// Activates emulation of network conditions.
+ ///
+ public Task EmulateNetworkConditions(NetworkCondition networkCondition) =>
+ client.SendAsync("Network.emulateNetworkConditions", new()
+ {
+ ["offline"] = networkCondition.Offline,
+ ["latency"] = networkCondition.Latency,
+ ["downloadThroughput"] = networkCondition.DownloadThroughput,
+ ["uploadThroughput"] = networkCondition.UploadThroughput,
+ ["connectionType"] = networkCondition.ConnectionType
+ });
+
+ ///
+ /// Blocks URLs from loading.
+ ///
+ /// URL patterns to block. Wildcards ('*') are allowed.
+ public Task SetBlockedUrls(string[] urls) => client.SendAsync("Network.setBlockedURLs", new() { ["urls"] = urls });
+
+ #endregion
+
+ public List GetResponses(Regex regex) => DownloadedResponses.Where(x => regex.IsMatch(x.Url)).ToList();
+
+ public List GetResponses(string url) => DownloadedResponses.Where(x => url.Contains(x.Url)).ToList();
+
+ public bool ContainsResponse(Regex regex) => DownloadedResponses.Exists(x => regex.IsMatch(x.Url));
+
+ public bool ContainsResponse(string url) => DownloadedResponses.Exists(x => url.Contains(x.Url));
+
+ public async ValueTask DisposeAsync()
+ {
+ responseReceived.OnEvent -= OnResponseReceived;
+ loadingFinished.OnEvent -= OnLoadingFinished;
+ await client.DisposeAsync();
+ }
+
+ private class LoadingFinishedData
+ {
+ [JsonPropertyName("requestId")]
+ public string RequestId { get; set; }
+
+ [JsonPropertyName("timestamp")]
+ public double Timestamp { get; set; }
+
+ [JsonPropertyName("encodedDataLength")]
+ public int EncodedDataLength { get; set; }
+ }
+
+ private class ResponseReceivedData
+ {
+ [JsonPropertyName("requestId")]
+ public string RequestId { get; set; }
+
+ [JsonPropertyName("loaderId")]
+ public string LoaderId { get; set; }
+
+ [JsonPropertyName("timestamp")]
+ public double Timestamp { get; set; }
+
+ [JsonPropertyName("type")]
+ public string Type { get; set; }
+
+ [JsonPropertyName("response")]
+ public DownloadedResponse Response { get; set; }
+ }
+}
+
+public class DownloadedResponse : IEquatable
+{
+ public string RequestId { get; set; }
+
+ [JsonPropertyName("url")]
+ public string Url { get; set; }
+
+ [JsonPropertyName("status")]
+ public int Status { get; set; }
+
+ [JsonPropertyName("statusText")]
+ public string StatusText { get; set; }
+
+ [JsonPropertyName("headers")]
+ public Dictionary Headers { get; set; }
+
+ [JsonPropertyName("mimeType")]
+ public string MimeType { get; set; }
+
+ [JsonPropertyName("charset")]
+ public string Charset { get; set; }
+
+ [JsonPropertyName("remoteIPAddress")]
+ public string RemoteIPAddress { get; set; }
+
+ [JsonPropertyName("remotePort")]
+ public int RemotePort { get; set; }
+
+ [JsonPropertyName("fromDiskCache")]
+ public bool FromDiskCache { get; set; }
+
+ [JsonPropertyName("fromServiceWorker")]
+ public bool FromServiceWorker { get; set; }
+
+ [JsonPropertyName("fromPrefetchCache")]
+ public bool FromPrefetchCache { get; set; }
+
+ [JsonPropertyName("encodedDataLength")]
+ public int EncodedDataLength { get; set; }
+
+ [JsonPropertyName("responseTime")]
+ public double ResponseTime { get; set; }
+
+ [JsonPropertyName("protocol")]
+ public string Protocol { get; set; }
+
+ public override int GetHashCode() => RequestId.GetHashCode();
+ public override bool Equals(object? obj) => obj is DownloadedResponse other && Equals(other);
+ public bool Equals(DownloadedResponse? other) => other is not null && RequestId == other.RequestId;
+}
+
+public class NetworkCondition
+{
+ ///
+ /// True to emulate Internet disconnection.
+ ///
+ public bool Offline { get; set; }
+
+ ///
+ /// Minimum latency from request sent to response headers received(ms).
+ ///
+ public int Latency { get; set; }
+
+ ///
+ /// Maximal aggregated download throughput(bytes/sec). -1 disables download throttling.
+ ///
+ public int DownloadThroughput { get; set; }
+
+ ///
+ /// Maximal aggregated upload throughput(bytes/sec). -1 disables upload throttling.
+ ///
+ public int UploadThroughput { get; set; }
+
+ ///
+ /// The underlying connection technology that the browser is supposedly using.
+ ///
+ public ConnectionType ConnectionType { get; set; }
+
+ public static readonly NetworkCondition IsOffline = new()
+ {
+ Offline = true,
+ DownloadThroughput = 0,
+ UploadThroughput = 0,
+ Latency = 0,
+ ConnectionType = ConnectionType.None
+ };
+
+ public static readonly NetworkCondition NoThrottle = new()
+ {
+ Offline = false,
+ DownloadThroughput = -1,
+ UploadThroughput = -1,
+ Latency = 0,
+ ConnectionType = ConnectionType.None
+ };
+
+ public static readonly NetworkCondition Regular2G = new()
+ {
+ Offline = false,
+ DownloadThroughput = (250 * 1024) / 8,
+ UploadThroughput = (50 * 1024) / 8,
+ Latency = 300,
+ ConnectionType = ConnectionType.Cellular2G
+ };
+
+ public static readonly NetworkCondition Fast2G = new()
+ {
+ Offline = false,
+ DownloadThroughput = (450 * 1024) / 8,
+ UploadThroughput = (150 * 1024) / 8,
+ Latency = 150,
+ ConnectionType = ConnectionType.Cellular2G
+ };
+
+ public static readonly NetworkCondition Regular3G = new()
+ {
+ Offline = false,
+ DownloadThroughput = (750 * 1024) / 8,
+ UploadThroughput = (250 * 1024) / 8,
+ Latency = 100,
+ ConnectionType = ConnectionType.Cellular3G
+ };
+
+ public static readonly NetworkCondition Good3G = new()
+ {
+ Offline = false,
+ DownloadThroughput = (1500 * 1024) / 8,
+ UploadThroughput = (750 * 1024) / 8,
+ Latency = 40,
+ ConnectionType = ConnectionType.Cellular3G
+ };
+
+ public static readonly NetworkCondition Regular4G = new()
+ {
+ Offline = false,
+ DownloadThroughput = (4 * 1024 * 1024) / 8,
+ UploadThroughput = (3 * 1024 * 1024) / 8,
+ Latency = 20,
+ ConnectionType = ConnectionType.Cellular4G
+ };
+
+ public static readonly NetworkCondition WiFi = new()
+ {
+ Offline = false,
+ DownloadThroughput = (30 * 1024 * 1024) / 8,
+ UploadThroughput = (15 * 1024 * 1024) / 8,
+ Latency = 2,
+ ConnectionType = ConnectionType.Wifi
+ };
+}
+
+public enum ConnectionType
+{
+ None,
+ Cellular2G,
+ Cellular3G,
+ Cellular4G,
+ Bluetooth,
+ Ethernet,
+ Wifi,
+ Wimax,
+ Other
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightVideoRecordingExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightVideoRecordingExtensions.cs
index 3f9c4c25da..f65e686e77 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightVideoRecordingExtensions.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightVideoRecordingExtensions.cs
@@ -5,47 +5,34 @@ namespace Boilerplate.Tests.Extensions;
public static class PlaywrightVideoRecordingExtensions
{
- public static async Task FinalizeVideoRecording(this PageTest page)
- {
- await FinalizeVideoRecording(page.Context, page.TestContext);
- }
-
- public static async Task FinalizeVideoRecording(this IBrowserContext browserContext, TestContext testContext)
+ //Pass full name of the test method to 'testMethodFullName' param or it will be inferred from the test context
+ public static async Task FinalizeVideoRecording(this IBrowserContext browserContext, TestContext testContext, string? testMethodFullName = null)
{
await browserContext.CloseAsync();
if (testContext.CurrentTestOutcome is not UnitTestOutcome.Failed)
{
- var directory = GetVideoDirectory(testContext);
+ var directory = GetVideoDirectory(testContext, testMethodFullName);
if (Directory.Exists(directory))
Directory.Delete(directory, true);
}
}
- public static BrowserNewContextOptions EnableVideoRecording(this BrowserNewContextOptions options, TestContext testContext)
- {
- options.RecordVideoDir = GetVideoDirectory(testContext);
- return options;
- }
-
- public static BrowserNewContextOptions EnableVideoRecording(this BrowserNewContextOptions options, TestContext testContext, string testMethodFullName)
+ //Pass full name of the test method to 'testMethodFullName' param or it will be inferred from the test context
+ public static BrowserNewContextOptions EnableVideoRecording(this BrowserNewContextOptions options, TestContext testContext, string? testMethodFullName = null)
{
options.RecordVideoDir = GetVideoDirectory(testContext, testMethodFullName);
return options;
}
- private static string GetVideoDirectory(TestContext testContext)
+ private static string GetVideoDirectory(TestContext testContext, string? testMethodFullName = null)
{
- var testMethodFullName = $"{testContext.FullyQualifiedTestClassName}.{GetTestMethodName(testContext)}";
- return GetVideoDirectory(testContext, testMethodFullName);
- }
+ testMethodFullName ??= $"{testContext.FullyQualifiedTestClassName}.{GetTestMethodName(testContext)}";
- private static string GetVideoDirectory(TestContext testContext, string testMethodFullName)
- {
// Remove invalid characters from the test method name
char[] notAllowedChars = [')', '"', '<', '>', '|', '*', '?', '\r', '\n', .. Path.GetInvalidFileNameChars()];
testMethodFullName = new string(testMethodFullName.Where(ch => !notAllowedChars.Contains(ch)).ToArray()).Replace('(', '_').Replace(',', '_');
- var dir = Path.Combine(testContext.TestResultsDirectory!, "..", "..", "Videos", testMethodFullName);
+ var dir = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "TestResults", "Videos", testMethodFullName);
return Path.GetFullPath(dir);
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityPagesTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityPagesTests.cs
index c1aa75e2b8..5d3344678c 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityPagesTests.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityPagesTests.cs
@@ -49,5 +49,5 @@ public async Task SignIn_Should_WorkAsExpected()
public override BrowserNewContextOptions ContextOptions() => base.ContextOptions().EnableVideoRecording(TestContext);
[TestCleanup]
- public async ValueTask Cleanup() => await this.FinalizeVideoRecording();
+ public async ValueTask Cleanup() => await Context.FinalizeVideoRecording(TestContext);
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorServerPreRenderingTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorServerPreRenderingTests.cs
new file mode 100644
index 0000000000..dcaed1f5ca
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorServerPreRenderingTests.cs
@@ -0,0 +1,13 @@
+namespace Boilerplate.Tests.PageTests.BlazorServer.PreRendering;
+
+[TestClass]
+public partial class IdentityPagesTests : BlazorServer.IdentityPagesTests
+{
+ public override bool PreRenderEnabled => true;
+}
+
+[TestClass]
+public partial class LocalizationTests : BlazorServer.LocalizationTests
+{
+ public override bool PreRenderEnabled => true;
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyPreRenderingTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyPreRenderingTests.cs
new file mode 100644
index 0000000000..0b1bb2c1c0
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyPreRenderingTests.cs
@@ -0,0 +1,13 @@
+namespace Boilerplate.Tests.PageTests.BlazorWebAssembly.PreRendering;
+
+[TestClass]
+public partial class IdentityPagesTests : BlazorWebAssembly.IdentityPagesTests
+{
+ public override bool PreRenderEnabled => true;
+}
+
+[TestClass]
+public partial class LocalizationTests : BlazorWebAssembly.LocalizationTests
+{
+ public override bool PreRenderEnabled => true;
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyTests.cs
index b39c56c77e..8184ab661c 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyTests.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyTests.cs
@@ -1,4 +1,8 @@
-using Boilerplate.Client.Web;
+//+:cnd:noEmit
+using Boilerplate.Client.Web;
+using Boilerplate.Tests.Extensions;
+using Boilerplate.Tests.PageTests.PageModels;
+using Boilerplate.Tests.PageTests.PageModels.Layout;
namespace Boilerplate.Tests.PageTests.BlazorWebAssembly;
@@ -13,17 +17,85 @@ public partial class LocalizationTests : BlazorServer.LocalizationTests
{
public override BlazorWebAppMode BlazorRenderMode => BlazorWebAppMode.BlazorWebAssembly;
-#if MultilingualEnabled == false
[TestMethod]
[TestCategory("MultilingualDisabled")]
public async Task MultilingualDisabled()
{
- var homePage = new PageModels.MainHomePage(Page, WebAppServerAddress);
+ if (CultureInfoManager.MultilingualEnabled is false)
+ {
+ Assert.Inconclusive("Multilingual is disabled. " +
+ "You can enable it via true setting in Directiory.Build.props.");
+ return;
+ }
+
+ var homePage = new MainHomePage(Page, WebAppServerAddress);
await homePage.Open();
await homePage.AssertOpen();
- var contains = Extensions.PlaywrightCacheExtensions.ContainsAsset(new(@"\/_framework\/icudt_hybrid\.dat\?v=sha256-.+"));
+ var contains = PlaywrightAssetCachingExtensions.ContainsAsset("icudt_hybrid.dat");
Assert.IsFalse(contains, "The 'icudt_hybrid.dat' file must not be loaded when Multilingual is disabled.");
}
-#endif
+}
+
+[TestClass]
+public partial class DownloadSizeTests : PageTestBase
+{
+ public override BlazorWebAppMode BlazorRenderMode => BlazorWebAppMode.BlazorWebAssembly;
+ public override Uri WebAppServerAddress => new("http://localhost:5000/");
+ public override bool EnableBlazorWasmCaching => false;
+
+ //#if (sample == "Todo")
+ [TestMethod]
+ [AutoAuthenticate]
+ [AutoStartTestServer(false)]
+ public async Task TodoDownloadSize()
+ {
+ await AssertDownloadSize(new IdentityHomePage(Page, WebAppServerAddress), expectedTotalSizeKB: 3_200, expectedWasmSizeKB: 2_700);
+ }
+ //#elif (sample == "Admin")
+ [TestMethod]
+ [AutoAuthenticate]
+ [AutoStartTestServer(false)]
+ public async Task AdminDownloadSize()
+ {
+ await AssertDownloadSize(new IdentityHomePage(Page, WebAppServerAddress), expectedTotalSizeKB: 3_200, expectedWasmSizeKB: 2_700);
+ }
+ //#else
+ [TestMethod]
+ [AutoStartTestServer(false)]
+ public async Task SimpleDownloadSize()
+ {
+ await AssertDownloadSize(new MainHomePage(Page, WebAppServerAddress), expectedTotalSizeKB: 3_200, expectedWasmSizeKB: 2_700);
+ }
+ //#endif
+
+ private async Task AssertDownloadSize(TPage page, int expectedTotalSizeKB, int expectedWasmSizeKB, int toleranceKB = 50)
+ where TPage : RootLayout
+ {
+ var downloadSizeTests = Environment.GetEnvironmentVariable(nameof(DownloadSizeTests));
+ if (Convert.ToBoolean(downloadSizeTests) is false)
+ {
+ Assert.Inconclusive("Download size tests are disabled. " +
+ "You can enable it via an environment variable `DownloadSizeTests=true`");
+ return;
+ }
+
+ await using var networkSession = await Page.OpenNetworkSession();
+
+ await page.Open();
+ await page.AssertOpen();
+
+ await Page.WaitForHydrationToComplete();
+ await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+
+ var totalSizeKB = networkSession.TotalDownloaded / 1000;
+ var wasmSizeKB = networkSession.DownloadedResponses
+ .Where(x => PlaywrightAssetCachingExtensions.BlazorWasmRegex().IsMatch(new Uri(x.Url).PathAndQuery))
+ .Sum(x => x.EncodedDataLength) / 1000;
+
+ Assert.AreEqual(expectedTotalSizeKB, totalSizeKB, toleranceKB, "Total size is not within tolerance.");
+ Assert.AreEqual(expectedWasmSizeKB, wasmSizeKB, toleranceKB, "Wasm size is not within tolerance.");
+
+ Console.WriteLine($"Total size: {totalSizeKB} KB");
+ }
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/IdentityPagesTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/IdentityPagesTests.cs
index 4e39e94f5b..d41d0b335e 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/IdentityPagesTests.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/IdentityPagesTests.cs
@@ -3,6 +3,8 @@
using Boilerplate.Server.Api.Data;
using Boilerplate.Tests.PageTests.PageModels;
using Boilerplate.Tests.PageTests.PageModels.Identity;
+using Boilerplate.Tests.Extensions;
+using Microsoft.EntityFrameworkCore;
namespace Boilerplate.Tests.PageTests.BlazorServer;
@@ -13,9 +15,12 @@ public partial class IdentityPagesTests : PageTestBase
public async Task UnauthorizedUser_Should_RedirectToSignInPage()
{
var response = await Page.GotoAsync(new Uri(WebAppServerAddress, Urls.SettingsPage).ToString());
+ await Page.WaitForHydrationToComplete();
Assert.IsNotNull(response);
- Assert.AreEqual(StatusCodes.Status200OK, response.Status);
+ //NOTE: Status code differs between pre-render Disabled (200) and Enabled(401)
+ //Once it resolved we can uncomment this line
+ //Assert.AreEqual(StatusCodes.Status200OK, response.Status);
await Expect(Page).ToHaveURLAsync(new Uri(WebAppServerAddress, "/sign-in?return-url=settings").ToString());
}
@@ -310,6 +315,39 @@ public async Task ChangePhone(string mode)
}
}
+ [TestMethod]
+ public async Task DeleteUser()
+ {
+ await using var scope = TestServer.WebApp.Services.CreateAsyncScope();
+
+ var dbContext = scope.ServiceProvider.GetRequiredService();
+ var userService = new UserService(dbContext);
+ var email = $"{Guid.NewGuid()}@gmail.com";
+ await userService.AddUser(email);
+
+ var signInPage = new SignInPage(Page, WebAppServerAddress);
+
+ await signInPage.Open();
+ await signInPage.AssertOpen();
+
+ var identityHomePage = await signInPage.SignInWithEmail(email);
+ await identityHomePage.AssertSignInSuccess(email, userFullName: null);
+
+ var settingsPage = new SettingsPage(Page, WebAppServerAddress);
+
+ await settingsPage.Open();
+ await settingsPage.AssertOpen();
+
+ await settingsPage.ExpandAccount();
+ await settingsPage.ClickOnDeleteTab();
+
+ signInPage = await settingsPage.DeleteUser();
+ await signInPage.AssertSignOut();
+
+ var exists = await dbContext.Users.AnyAsync(u => u.Email == email);
+ Assert.IsFalse(exists, "User must be deleted.");
+ }
+
private async Task CreateNewUser()
{
await using var scope = TestServer.WebApp.Services.CreateAsyncScope();
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/LocalizationTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/LocalizationTests.cs
index 59fe5c20bf..dd5954a1c1 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/LocalizationTests.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/LocalizationTests.cs
@@ -1,6 +1,7 @@
using System.Reflection;
using Boilerplate.Tests.Services;
using Boilerplate.Tests.PageTests.PageModels;
+using Boilerplate.Tests.Extensions;
namespace Boilerplate.Tests.PageTests.BlazorServer;
@@ -37,6 +38,7 @@ public async Task QueryString(string cultureName, string cultureDisplayName)
var localizer = StringLocalizerFactory.Create(cultureName);
var homePage = new MainHomePage(Page, WebAppServerAddress);
await Page.GotoAsync($"{WebAppServerAddress}?culture={cultureName}");
+ await Page.WaitForHydrationToComplete();
await homePage.AssertLocalized(localizer, cultureName, cultureDisplayName);
}
@@ -47,6 +49,7 @@ public async Task UrlSegment(string cultureName, string cultureDisplayName)
var localizer = StringLocalizerFactory.Create(cultureName);
var homePage = new MainHomePage(Page, WebAppServerAddress);
await Page.GotoAsync(new Uri(WebAppServerAddress, cultureName).ToString());
+ await Page.WaitForHydrationToComplete();
await homePage.AssertLocalized(localizer, cultureName, cultureDisplayName);
}
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/TokenMagicLinkEmail.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/TokenMagicLinkEmail.cs
index 57c2b2d857..33b71f15a0 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/TokenMagicLinkEmail.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Email/TokenMagicLinkEmail.cs
@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
+using Boilerplate.Tests.Extensions;
using Boilerplate.Tests.PageTests.PageModels.Layout;
using Boilerplate.Tests.Services;
@@ -61,6 +62,8 @@ public virtual async Task OpenMagicLink()
await Page.WaitForURLAsync(url => url != href);
}
+ await Page.WaitForHydrationToComplete();
+
return (TFinalPage)Activator.CreateInstance(typeof(TFinalPage), Page, WebAppServerAddress)!;
}
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/HomePage.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/HomePage.cs
index e1edf64c08..b39c9a2495 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/HomePage.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/HomePage.cs
@@ -18,7 +18,7 @@ public async Task AssertLocalized(IStringLocalizer localizer, string cultureName
{
await Assertions.Expect(Page).ToHaveTitleAsync(localizer[nameof(AppStrings.HomePageTitle)]);
- await Assertions.Expect(Page.GetByText(localizer[nameof(AppStrings.HomePanelTitle)] + " " + localizer[nameof(AppStrings.HomePanelSubtitle)])).ToBeVisibleAsync();
+ await Assertions.Expect(Page.GetByRole(AriaRole.Heading, new() { Level = 4, Name = localizer[nameof(AppStrings.HomePanelTitle)] + " " + localizer[nameof(AppStrings.HomePanelSubtitle)] })).ToBeVisibleAsync();
await Assertions.Expect(Page.GetByText(localizer[nameof(AppStrings.HomeMessage)])).ToBeVisibleAsync();
@@ -42,10 +42,10 @@ public partial class IdentityHomePage(IPage page, Uri serverAddress)
public override string PagePath => Urls.HomePage;
public override string PageTitle => AppStrings.HomePageTitle;
- public async Task SignOut()
+ public new async Task SignOut()
{
- await Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.SignOut }).ClickAsync();
- await Page.GetByRole(AriaRole.Dialog).GetByRole(AriaRole.Button, new() { Name = AppStrings.SignOut }).ClickAsync();
+ await Page.Locator(".bit-crd.panel").GetByRole(AriaRole.Button, new() { Name = AppStrings.SignOut }).ClickAsync();
+ await Page.GetByRole(AriaRole.Dialog).GetByRole(AriaRole.Button, new() { Name = AppStrings.SignOut, Exact = true }).ClickAsync();
return new MainHomePage(Page, WebAppServerAddress);
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Delete.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Delete.cs
new file mode 100644
index 0000000000..52aee1fb4c
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Delete.cs
@@ -0,0 +1,25 @@
+namespace Boilerplate.Tests.PageTests.PageModels.Identity;
+
+public partial class SettingsPage
+{
+ public async Task ClickOnDeleteTab()
+ {
+ await Page.GetByRole(AriaRole.Tab, new() { Name = AppStrings.Delete }).ClickAsync();
+ }
+
+ public async Task AssertDeleteTab()
+ {
+ await Assertions.Expect(Page.GetByLabel(AppStrings.Delete).GetByRole(AriaRole.Heading)).ToContainTextAsync(AppStrings.DeleteAccount);
+ await Assertions.Expect(Page.GetByLabel(AppStrings.Delete).GetByRole(AriaRole.Paragraph)).ToContainTextAsync(AppStrings.DeleteAccountPrompt);
+ await Assertions.Expect(Page.GetByLabel(AppStrings.Delete).GetByRole(AriaRole.Button, new() { Name = AppStrings.DeleteAccount })).ToBeVisibleAsync();
+ }
+
+ public async Task DeleteUser()
+ {
+ await Page.GetByLabel(AppStrings.Delete).GetByRole(AriaRole.Button, new() { Name = AppStrings.DeleteAccount }).ClickAsync();
+ var currentUrl = Page.Url;
+ await Page.GetByRole(AriaRole.Dialog).GetByRole(AriaRole.Button, new() { Name = AppStrings.Yes }).ClickAsync();
+ await Page.WaitForURLAsync(url => url != currentUrl);
+ return new(Page, WebAppServerAddress) { ReturnUrl = new Uri(currentUrl).PathAndQuery.TrimStart('/') };
+ }
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Email.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Email.cs
index c00cf39407..f9d5e0540a 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Email.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Identity/SettingsPage.Account.Email.cs
@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
+using Boilerplate.Tests.Extensions;
using Boilerplate.Tests.PageTests.PageModels.Email;
namespace Boilerplate.Tests.PageTests.PageModels.Identity;
@@ -64,6 +65,7 @@ public async Task ConfirmEmailByToken(string token)
{
await Page.GetByPlaceholder(AppStrings.EmailTokenPlaceholder).FillAsync(token);
await Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.EmailTokenConfirmButtonText }).ClickAsync();
+ await Page.WaitForHydrationToComplete();
}
public async Task AssertConfirmEmailSuccess()
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Layout/IdentityLayout.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Layout/IdentityLayout.cs
index 90df31e737..4456346a23 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Layout/IdentityLayout.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Layout/IdentityLayout.cs
@@ -19,8 +19,8 @@ public async Task AssertSignInSuccess(string userEmail = TestData.DefaultTestEma
public async Task SignOut()
{
- await Page.GetByRole(AriaRole.Button, new() { Name = AppStrings.SignOut }).ClickAsync();
- await Page.GetByRole(AriaRole.Dialog).GetByRole(AriaRole.Button, new() { Name = AppStrings.SignOut }).ClickAsync();
+ await Page.Locator(".bit-crd.panel").GetByRole(AriaRole.Button, new() { Name = AppStrings.SignOut }).ClickAsync();
+ await Page.GetByRole(AriaRole.Dialog).GetByRole(AriaRole.Button, new() { Name = AppStrings.SignOut, Exact = true }).ClickAsync();
return new SignInPage(Page, WebAppServerAddress) { ReturnUrl = new Uri(Page.Url).PathAndQuery.TrimStart('/') };
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Layout/RootLayout.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Layout/RootLayout.cs
index 69ae2b9d13..bc72939cbd 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Layout/RootLayout.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/Layout/RootLayout.cs
@@ -1,4 +1,6 @@
-namespace Boilerplate.Tests.PageTests.PageModels.Layout;
+using Boilerplate.Tests.Extensions;
+
+namespace Boilerplate.Tests.PageTests.PageModels.Layout;
public abstract partial class RootLayout(IPage page, Uri serverAddress)
{
@@ -10,6 +12,7 @@ public abstract partial class RootLayout(IPage page, Uri serverAddress)
public virtual async Task Open()
{
await Page.GotoAsync(new Uri(WebAppServerAddress, PagePath).ToString());
+ await Page.WaitForHydrationToComplete();
}
public virtual async Task AssertOpen()
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageTestBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageTestBase.cs
index a00681d4bd..c7a75baa3c 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageTestBase.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageTestBase.cs
@@ -10,13 +10,21 @@ public abstract partial class PageTestBase : PageTest
{
public AppTestServer TestServer { get; set; } = new();
public WebApplication WebApp => TestServer.WebApp;
- public Uri WebAppServerAddress => TestServer.WebAppServerAddress;
+ public virtual Uri WebAppServerAddress => TestServer.WebAppServerAddress;
public virtual BlazorWebAppMode BlazorRenderMode => BlazorWebAppMode.BlazorServer;
+ public virtual bool PreRenderEnabled => false;
+ public virtual bool EnableBlazorWasmCaching => true;
[TestInitialize]
public async Task InitializeTestServer()
{
- await Context.EnableBlazorWasmCaching();
+ if (PreRenderEnabled)
+ await Context.EnableHydrationCheck();
+
+ if (EnableBlazorWasmCaching)
+ await Context.EnableBlazorWasmCaching();
+
+ await Context.SetBlazorWebAssemblyServerAddress(WebAppServerAddress.OriginalString);
var currentTestMethod = GetType().GetMethod(TestContext.TestName!);
@@ -32,10 +40,11 @@ public async Task InitializeTestServer()
}
var autoStartTestServer = currentTestMethod!.GetCustomAttribute();
- if (autoStartTestServer is null || autoStartTestServer.AutoStart)
+ if (autoStartTestServer?.AutoStart != false)
{
await TestServer.Build(configureTestConfigurations: configuration =>
{
+ configuration["WebAppRender:PrerenderEnabled"] = PreRenderEnabled.ToString();
configuration["WebAppRender:BlazorMode"] = BlazorRenderMode.ToString();
}).Start();
}
@@ -44,7 +53,7 @@ await TestServer.Build(configureTestConfigurations: configuration =>
[TestCleanup]
public async ValueTask CleanupTestServer()
{
- await this.FinalizeVideoRecording();
+ await Context.FinalizeVideoRecording(TestContext);
if (TestServer is not null)
{
@@ -62,7 +71,7 @@ public override BrowserNewContextOptions ContextOptions()
var isAuthenticated = currentTestMethod!.GetCustomAttribute() is not null;
if (isAuthenticated)
{
- options.StorageState = TestsInitializer.AuthenticationState.Replace("[ServerAddress]", WebAppServerAddress.OriginalString);
+ options.StorageState = TestsInitializer.AuthenticationState.Replace("[ServerAddress]", WebAppServerAddress.ToString());
}
var configureBrowserContext = currentTestMethod!.GetCustomAttribute();
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestsInitializer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestsInitializer.cs
index db15392677..6cfa7f8119 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestsInitializer.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestsInitializer.cs
@@ -70,10 +70,13 @@ private static async Task InitializeAuthenticationState(AppTestServer testServer
var playwrightPage = new PageTest() { TestContext = testContext };
await playwrightPage.Setup();
await playwrightPage.BrowserSetup();
+
var currentMethodFullName = $"{typeof(TestsInitializer).FullName}.{(nameof(InitializeAuthenticationState))}";
var options = new BrowserNewContextOptions().EnableVideoRecording(testContext, currentMethodFullName);
var context = await playwrightPage.NewContextAsync(options);
+
await context.EnableBlazorWasmCaching();
+ await context.SetBlazorWebAssemblyServerAddress(testServer.WebAppServerAddress.ToString());
var page = await context.NewPageAsync();
var signinPage = new SignInPage(page, testServer.WebAppServerAddress);
@@ -94,6 +97,7 @@ private static async Task InitializeAuthenticationState(AppTestServer testServer
AuthenticationState = state.Replace(testServer.WebAppServerAddress.OriginalString.TrimEnd('/'), "[ServerAddress]");
+ await context.FinalizeVideoRecording(testContext, currentMethodFullName);
await context.Browser!.CloseAsync();
await context.Browser!.DisposeAsync();
}
diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj
index 6ae9ab3a5a..71a5eca69c 100644
--- a/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj
+++ b/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj
@@ -1,12 +1,10 @@
- net8.0
+ net9.0trueDefaulttrue
-
- false
BeforeBuildTasks;
$(ResolveStaticWebAssetsInputsDependsOn)
@@ -14,7 +12,7 @@
-
+
@@ -24,19 +22,19 @@
-
-
+
+
-
-
+
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj
index f5ed83a289..51d7ea4526 100644
--- a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj
+++ b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj
@@ -1,23 +1,24 @@
-
+
- net8.0
+ net9.0
-
-
-
+
+
+
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Startup/Middlewares.cs b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Startup/Middlewares.cs
index d3e05f5f0b..b8d762807b 100644
--- a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Startup/Middlewares.cs
+++ b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Startup/Middlewares.cs
@@ -74,6 +74,7 @@ public static void Use(WebApplication app, IWebHostEnvironment env, IConfigurati
UseSiteMap(app);
+ app.MapStaticAssets();
app.MapRazorComponents()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj
index 35e5fb020d..72da2da997 100644
--- a/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj
+++ b/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj
@@ -1,22 +1,22 @@
- net8.0
+ net9.0en-US
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+ compile; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/Websites/Careers/src/Directory.Build.props b/src/Websites/Careers/src/Directory.Build.props
index 8f121b27da..8fd44b1f06 100644
--- a/src/Websites/Careers/src/Directory.Build.props
+++ b/src/Websites/Careers/src/Directory.Build.props
@@ -1,4 +1,4 @@
-
+
12.0
diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj
index 0e70844fd8..5f7ccd28c1 100644
--- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj
+++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj
@@ -1,12 +1,10 @@
- net8.0
+ net9.0trueDefaulttrue
-
- false
BeforeBuildTasks;
$(ResolveStaticWebAssetsInputsDependsOn)
@@ -14,7 +12,7 @@
-
+
@@ -24,20 +22,20 @@
-
-
-
+
+
+
-
-
+
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Besql/Besql01OverviewPage.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Besql/Besql01OverviewPage.razor
index 6487a42771..d06c99990a 100644
--- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Besql/Besql01OverviewPage.razor
+++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Besql/Besql01OverviewPage.razor
@@ -1,4 +1,5 @@
-@page "/besql/overview"
+@page "/besql"
+@page "/besql/overview"
@inherits AppComponentBase
diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Lcnc/LcncPage.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Lcnc/LcncPage.razor
index 6022abcec3..04a83d651d 100644
--- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Lcnc/LcncPage.razor
+++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Lcnc/LcncPage.razor
@@ -154,7 +154,7 @@
-
+4.8M NuGet downloads
+
+5.5M NuGet downloads
+1K GitHub stars
+125 contributors
diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates01OverviewPage.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates01OverviewPage.razor
index 17df98a396..9b74c4ba39 100644
--- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates01OverviewPage.razor
+++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates01OverviewPage.razor
@@ -36,16 +36,23 @@
Blazor is supported in the browsers shown in the following list on both mobile and desktop platforms.
+
+ .NET 8
-
Apple Safari (Current† version)
-
Google Chrome (Current† version)
-
Microsoft Edge (Current† version)
-
Mozilla Firefox (Current† version)
+
Safari 15+
+
Firefox 100+
+
Chrome/Edge 95+
+
+
+ .NET 9
+
+
Safari 16.4+
+
Firefox 100+
+
Chrome/Edge 95+
- Supported platforms
@@ -53,11 +60,21 @@
Blazor Hybrid apps have the following additional platform requirements:
+
+ .NET 8
+
Note:
-
+
This setting does not affect the enabling or disabling of Blazor Server.
-
+
File Storage
@@ -202,7 +215,7 @@
}
-
+
Offline Database
@@ -218,7 +231,7 @@
Warning: It is advisable to use this option only when necessary, as integrating Entity Framework Core can increase application size and potentially reduce startup performance.
-
+
Server Database
@@ -235,8 +248,6 @@
Supported options include SqlServer, PostgreSQL, MySQL, Cosmos, and Other.
- If you require a database not listed among the options, select Other. After project creation, you can add the desired database
- package and configure it in the AddDbContextPool method which is located in the Program.Services.cs file within the Server.Api project.
For the default Sqlite option, we'd recommend installing sqlite package for Visual Studio and Visual Studio Code.
@@ -279,7 +290,7 @@
}
-
+