Browse Source

Merge branch 'dev' into Shared-User-Account

pull/24456/head
maliming 9 months ago
parent
commit
1cbf5aa0d8
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 2
      docs/en/Community-Articles/2025-09-02-training-campaign/post.md
  2. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png
  3. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png
  4. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png
  5. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png
  6. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png
  7. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png
  8. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png
  9. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png
  10. 2
      docs/en/tutorials/book-store-with-abp-suite/part-02.md
  11. 76
      framework/src/Volo.Abp.AspNetCore.Components.Server/Microsoft/AspNetCore/Authentication/Cookies/CookieAuthenticationOptionsExtensions.cs
  12. 7
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs
  13. 1
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Button/AbpButtonTagHelperService.cs
  14. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Button/AbpButtonTagHelperServiceBase.cs
  15. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardBodyTagHelperService.cs
  16. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Carousel/AbpCarouselItemTagHelperService.cs
  17. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Collapse/AbpAccordionItemTagHelperService.cs
  18. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Collapse/AbpAccordionTagHelperService.cs
  19. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs
  20. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs
  21. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs
  22. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs
  23. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs
  24. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabDropdownTagHelperService.cs
  25. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabLinkTagHelperService.cs
  26. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabTagHelperService.cs
  27. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs
  28. 6
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs
  29. 6
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs
  30. 38
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs
  31. 2
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinderProvider.cs
  32. 10
      framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs
  33. 54
      framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs
  34. 28
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/BasicAggregateRoot.cs
  35. 49
      framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs
  36. 69
      framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverter.cs
  37. 116
      framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs
  38. 67
      framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpNullableDateTimeConverter.cs
  39. 39
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController.cs
  40. 104
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs
  41. 2
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManager.cs
  42. 80
      npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.html
  43. 9
      npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.scss
  44. 232
      npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.ts
  45. 52
      npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/permission-checkbox-list/permission-checkbox-list.component.html
  46. 14
      npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.html
  47. 90
      npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.ts
  48. 41
      npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-form/resource-permission-form.component.html
  49. 107
      npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.html
  50. 385
      npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.ts
  51. 32
      text-template-management/src/Volo.Abp.TextTemplateManagement.Domain/Volo/Abp/TextTemplateManagement/StaticTemplateDefinitionChangedEventHandler.cs

2
docs/en/Community-Articles/2025-09-02-training-campaign/post.md

@ -1,6 +1,6 @@
# IMPROVE YOUR ABP SKILLS WITH 33% OFF LIVE TRAININGS! # IMPROVE YOUR ABP SKILLS WITH 33% OFF LIVE TRAININGS!
We have exciting news to share\! As you know, we offer live training packages to help you improve your skills and knowledge of ABP. From September 8th to 19th, we are giving you 33% OFF our live trainings, so you can learn more about the product at a discounted price\! We have exciting news to share\! As you know, we offer live training packages to help you improve your skills and knowledge of ABP. For a limited time, we are giving you 33% OFF our live trainings, so you can learn more about the product at a discounted price\!
#### Why Join ABP.IO Training? #### Why Join ABP.IO Training?

BIN
docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 29 KiB

2
docs/en/tutorials/book-store-with-abp-suite/part-02.md

@ -96,7 +96,7 @@ Here is the all details for the `Book` entity:
* `Name` is **required**, it's a **string** property and maximum length is **128**. * `Name` is **required**, it's a **string** property and maximum length is **128**.
* `Type` is an **enum** and the enum file path is *\Acme.BookStore.Domain.Shared\Books\BookType.cs*. * `Type` is an **enum** and the enum file path is *\Acme.BookStore.Domain.Shared\Books\BookType.cs*.
* `PublishDate` is a **DateTime** property and **not nullable**. * `PublishDate` is a **DateTime** property and **not nullable**.
* `Price` is a **float** property and **required**. * `Price` is a **float** property.
You can leave the other configurations as default. You can leave the other configurations as default.

76
framework/src/Volo.Abp.AspNetCore.Components.Server/Microsoft/AspNetCore/Authentication/Cookies/CookieAuthenticationOptionsExtensions.cs

@ -1,86 +1,16 @@
using System; using System;
using System.Threading.Tasks;
using Duende.IdentityModel.Client;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.Threading;
namespace Microsoft.AspNetCore.Authentication.Cookies; namespace Microsoft.AspNetCore.Authentication.Cookies;
public static class CookieAuthenticationOptionsExtensions public static class CookieAuthenticationOptionsExtensions
{ {
/// <summary> /// <summary>
/// Introspect access token on validating the principal. /// Check the access_token is expired or inactive.
/// </summary> /// </summary>
/// <param name="options"></param> [Obsolete("Use CheckTokenExpiration method instead.")]
/// <param name="oidcAuthenticationScheme"></param>
/// <returns></returns>
public static CookieAuthenticationOptions IntrospectAccessToken(this CookieAuthenticationOptions options, string oidcAuthenticationScheme = "oidc") public static CookieAuthenticationOptions IntrospectAccessToken(this CookieAuthenticationOptions options, string oidcAuthenticationScheme = "oidc")
{ {
options.Events.OnValidatePrincipal = async principalContext => return options.CheckTokenExpiration(oidcAuthenticationScheme, null, TimeSpan.FromMinutes(1));
{
if (principalContext.Principal == null || principalContext.Principal.Identity == null || !principalContext.Principal.Identity.IsAuthenticated)
{
return;
}
var logger = principalContext.HttpContext.RequestServices.GetRequiredService<ILogger<CookieAuthenticationOptions>>();
var accessToken = principalContext.Properties.GetTokenValue("access_token");
if (!accessToken.IsNullOrWhiteSpace())
{
var openIdConnectOptions = await GetOpenIdConnectOptions(principalContext, oidcAuthenticationScheme);
var response = await openIdConnectOptions.Backchannel.IntrospectTokenAsync(new TokenIntrospectionRequest
{
Address = openIdConnectOptions.Configuration?.IntrospectionEndpoint ?? openIdConnectOptions.Authority!.EnsureEndsWith('/') + "connect/introspect",
ClientId = openIdConnectOptions.ClientId!,
ClientSecret = openIdConnectOptions.ClientSecret,
Token = accessToken
});
if (response.IsError)
{
logger.LogError(response.Error);
await SignOutAsync(principalContext);
return;
}
if (!response.IsActive)
{
logger.LogError("The access_token is not active.");
await SignOutAsync(principalContext);
return;
}
logger.LogInformation("The access_token is active.");
}
else
{
logger.LogError("The access_token is not found in the cookie properties, Please make sure SaveTokens of OpenIdConnectOptions is set as true.");
await SignOutAsync(principalContext);
}
};
return options;
}
private async static Task<OpenIdConnectOptions> GetOpenIdConnectOptions(CookieValidatePrincipalContext principalContext, string oidcAuthenticationScheme)
{
var openIdConnectOptions = principalContext.HttpContext.RequestServices.GetRequiredService<IOptionsMonitor<OpenIdConnectOptions>>().Get(oidcAuthenticationScheme);
if (openIdConnectOptions.Configuration == null && openIdConnectOptions.ConfigurationManager != null)
{
var cancellationTokenProvider = principalContext.HttpContext.RequestServices.GetRequiredService<ICancellationTokenProvider>();
openIdConnectOptions.Configuration = await openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(cancellationTokenProvider.Token);
}
return openIdConnectOptions;
}
private async static Task SignOutAsync(CookieValidatePrincipalContext principalContext)
{
principalContext.RejectPrincipal();
await principalContext.HttpContext.SignOutAsync(principalContext.Scheme.Name);
} }
} }

7
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc.Rendering; using System;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
@ -41,12 +42,12 @@ public class AbpBreadcrumbItemTagHelperService : AbpTagHelperService<AbpBreadcru
if (string.IsNullOrWhiteSpace(TagHelper.Href)) if (string.IsNullOrWhiteSpace(TagHelper.Href))
{ {
output.Attributes.Add("aria-current", "page"); output.Attributes.Add("aria-current", "page");
return _encoder.Encode(TagHelper.Title); return _encoder.Encode(TagHelper.Title ?? string.Empty);
} }
var link = new TagBuilder("a"); var link = new TagBuilder("a");
link.Attributes.Add("href", TagHelper.Href); link.Attributes.Add("href", TagHelper.Href);
link.InnerHtml.AppendHtml(TagHelper.Title); link.InnerHtml.Append(TagHelper.Title);
return link.ToHtmlString(); return link.ToHtmlString();
} }
} }

1
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Button/AbpButtonTagHelperService.cs

@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using System; using System;
using System.Text.Encodings.Web;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Button; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Button;

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Button/AbpButtonTagHelperServiceBase.cs

@ -69,7 +69,7 @@ public abstract class AbpButtonTagHelperServiceBase<TTagHelper> : AbpTagHelperSe
} }
var span = new TagBuilder("span"); var span = new TagBuilder("span");
span.InnerHtml.AppendHtml(TagHelper.Text!); span.InnerHtml.Append(TagHelper.Text!);
output.Content.AppendHtml(span); output.Content.AppendHtml(span);
} }

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardBodyTagHelperService.cs

@ -22,7 +22,7 @@ public class AbpCardBodyTagHelperService : AbpTagHelperService<AbpCardBodyTagHel
{ {
var cardTitle = new TagBuilder(AbpCardTitleTagHelper.DefaultHeading.ToHtmlTag()); var cardTitle = new TagBuilder(AbpCardTitleTagHelper.DefaultHeading.ToHtmlTag());
cardTitle.AddCssClass("card-title"); cardTitle.AddCssClass("card-title");
cardTitle.InnerHtml.AppendHtml(TagHelper.Title!); cardTitle.InnerHtml.Append(TagHelper.Title!);
output.PreContent.AppendHtml(cardTitle); output.PreContent.AppendHtml(cardTitle);
} }
} }
@ -33,7 +33,7 @@ public class AbpCardBodyTagHelperService : AbpTagHelperService<AbpCardBodyTagHel
{ {
var cardSubtitle = new TagBuilder(AbpCardSubtitleTagHelper.DefaultHeading.ToHtmlTag()); var cardSubtitle = new TagBuilder(AbpCardSubtitleTagHelper.DefaultHeading.ToHtmlTag());
cardSubtitle.AddCssClass("card-subtitle mb-2"); cardSubtitle.AddCssClass("card-subtitle mb-2");
cardSubtitle.InnerHtml.AppendHtml(TagHelper.Subtitle!); cardSubtitle.InnerHtml.Append(TagHelper.Subtitle!);
output.PreContent.AppendHtml(cardSubtitle); output.PreContent.AppendHtml(cardSubtitle);
} }
} }

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Carousel/AbpCarouselItemTagHelperService.cs

@ -66,10 +66,10 @@ public class AbpCarouselItemTagHelperService : AbpTagHelperService<AbpCarouselIt
} }
var title = new TagBuilder("h5"); var title = new TagBuilder("h5");
title.InnerHtml.AppendHtml(TagHelper.CaptionTitle!); title.InnerHtml.Append(TagHelper.CaptionTitle!);
var caption = new TagBuilder("p"); var caption = new TagBuilder("p");
caption.InnerHtml.AppendHtml(TagHelper.Caption!); caption.InnerHtml.Append(TagHelper.Caption!);
var wrapper = new TagBuilder("div"); var wrapper = new TagBuilder("div");
wrapper.AddCssClass("carousel-caption d-none d-md-block"); wrapper.AddCssClass("carousel-caption d-none d-md-block");

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Collapse/AbpAccordionItemTagHelperService.cs

@ -32,7 +32,7 @@ public class AbpAccordionItemTagHelperService : AbpTagHelperService<AbpAccordion
button.Attributes.Add("data-bs-target", "#" + GetContentId()); button.Attributes.Add("data-bs-target", "#" + GetContentId());
button.Attributes.Add("aria-expanded", "true"); button.Attributes.Add("aria-expanded", "true");
button.Attributes.Add("aria-controls", GetContentId()); button.Attributes.Add("aria-controls", GetContentId());
button.InnerHtml.AppendHtml(TagHelper.Title); button.InnerHtml.Append(TagHelper.Title);
var h5 = new TagBuilder("h5"); var h5 = new TagBuilder("h5");
h5.AddCssClass("mb-0"); h5.AddCssClass("mb-0");

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Collapse/AbpAccordionTagHelperService.cs

@ -37,7 +37,7 @@ public class AbpAccordionTagHelperService : AbpTagHelperService<AbpAccordionTagH
{ {
foreach (var item in items) foreach (var item in items)
{ {
var content = item.Replace(AbpAccordionParentIdPlaceholder, HtmlGenerator.Encode(TagHelper.Id)); var content = item.Replace(AbpAccordionParentIdPlaceholder, HtmlGenerator.Encode(TagHelper.Id ?? string.Empty));
var wrapper = new TagBuilder("div"); var wrapper = new TagBuilder("div");
wrapper.AddCssClass("card"); wrapper.AddCssClass("card");

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs

@ -287,7 +287,7 @@ public class AbpInputTagHelperService : AbpTagHelperService<AbpInputTagHelper>
var label = new TagBuilder("label"); var label = new TagBuilder("label");
label.Attributes.Add("for", GetIdAttributeValue(inputTag)); label.Attributes.Add("for", GetIdAttributeValue(inputTag));
label.InnerHtml.AppendHtml(_encoder.Encode(TagHelper.Label)); label.InnerHtml.Append(TagHelper.Label);
label.AddCssClass(isCheckbox ? "form-check-label" : "form-label"); label.AddCssClass(isCheckbox ? "form-check-label" : "form-label");

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs

@ -107,7 +107,7 @@ public class AbpRadioInputTagHelperService : AbpTagHelperService<AbpRadioInputTa
var label = new TagBuilder("label"); var label = new TagBuilder("label");
label.AddCssClass("form-check-label"); label.AddCssClass("form-check-label");
label.Attributes.Add("for", id); label.Attributes.Add("for", id);
label.InnerHtml.AppendHtml(selectItem.Text); label.InnerHtml.Append(selectItem.Text);
var wrapper = new TagBuilder("div"); var wrapper = new TagBuilder("div");
wrapper.AddCssClass("form-check mb-1" + inlineClass); wrapper.AddCssClass("form-check mb-1" + inlineClass);

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs

@ -187,7 +187,7 @@ public class AbpSelectTagHelperService : AbpTagHelperService<AbpSelectTagHelper>
var label = new TagBuilder("label"); var label = new TagBuilder("label");
label.AddCssClass("form-label"); label.AddCssClass("form-label");
label.Attributes.Add("for", GetIdAttributeValue(selectTag)); label.Attributes.Add("for", GetIdAttributeValue(selectTag));
label.InnerHtml.AppendHtml(_encoder.Encode(TagHelper.Label)); label.InnerHtml.Append(TagHelper.Label);
label.InnerHtml.AppendHtml(GetRequiredSymbol(context, output)); label.InnerHtml.AppendHtml(GetRequiredSymbol(context, output));
return label.ToHtmlString(); return label.ToHtmlString();

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs

@ -557,7 +557,7 @@ public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelp
var label = new TagBuilder("label"); var label = new TagBuilder("label");
label.Attributes.Add("for", GetIdAttributeValue(inputTag)); label.Attributes.Add("for", GetIdAttributeValue(inputTag));
label.InnerHtml.AppendHtml(Encoder.Encode(TagHelper.Label)); label.InnerHtml.Append(TagHelper.Label);
label.AddCssClass("form-label"); label.AddCssClass("form-label");

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs

@ -27,7 +27,7 @@ public class AbpModalHeaderTagHelperService : AbpTagHelperService<AbpModalHeader
{ {
var title = new TagBuilder("h5"); var title = new TagBuilder("h5");
title.AddCssClass("modal-title"); title.AddCssClass("modal-title");
title.InnerHtml.AppendHtml(TagHelper.Title); title.InnerHtml.Append(TagHelper.Title);
return title.ToHtmlString(); return title.ToHtmlString();
} }

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabDropdownTagHelperService.cs

@ -40,7 +40,7 @@ public class AbpTabDropdownTagHelperService : AbpTagHelperService<AbpTabDropdown
anchor.Attributes.Add("role", "button"); anchor.Attributes.Add("role", "button");
anchor.Attributes.Add("aria-haspopup", "true"); anchor.Attributes.Add("aria-haspopup", "true");
anchor.Attributes.Add("aria-expanded", "false"); anchor.Attributes.Add("aria-expanded", "false");
anchor.InnerHtml.AppendHtml(title); anchor.InnerHtml.Append(title);
var menu = new TagBuilder("div"); var menu = new TagBuilder("div");
menu.AddCssClass("dropdown-menu"); menu.AddCssClass("dropdown-menu");

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabLinkTagHelperService.cs

@ -35,7 +35,7 @@ public class AbpTabLinkTagHelperService : AbpTagHelperService<AbpTabLinkTagHelpe
anchor.AddCssClass("dropdown-item"); anchor.AddCssClass("dropdown-item");
anchor.Attributes.Add("id", id); anchor.Attributes.Add("id", id);
anchor.Attributes.Add("href", href); anchor.Attributes.Add("href", href);
anchor.InnerHtml.AppendHtml(title); anchor.InnerHtml.Append(title);
return anchor.ToHtmlString(); return anchor.ToHtmlString();
} }
@ -45,7 +45,7 @@ public class AbpTabLinkTagHelperService : AbpTagHelperService<AbpTabLinkTagHelpe
anchor.AddCssClass("nav-link " + AbpTabItemActivePlaceholder); anchor.AddCssClass("nav-link " + AbpTabItemActivePlaceholder);
anchor.Attributes.Add("id", id); anchor.Attributes.Add("id", id);
anchor.Attributes.Add("href", href); anchor.Attributes.Add("href", href);
anchor.InnerHtml.AppendHtml(title); anchor.InnerHtml.Append(title);
var listItem = new TagBuilder("li"); var listItem = new TagBuilder("li");
listItem.AddCssClass("nav-item"); listItem.AddCssClass("nav-item");

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabTagHelperService.cs

@ -53,7 +53,7 @@ public class AbpTabTagHelperService : AbpTagHelperService<AbpTabTagHelper>
anchor.Attributes.Add(attr.Name, attr.Value.ToString()); anchor.Attributes.Add(attr.Name, attr.Value.ToString());
} }
anchor.InnerHtml.AppendHtml(title); anchor.InnerHtml.Append(title);
return anchor.ToHtmlString(); return anchor.ToHtmlString();
} }
@ -73,7 +73,7 @@ public class AbpTabTagHelperService : AbpTagHelperService<AbpTabTagHelper>
anchor.Attributes.Add(attr.Name, attr.Value.ToString()); anchor.Attributes.Add(attr.Name, attr.Value.ToString());
} }
anchor.InnerHtml.AppendHtml(title); anchor.InnerHtml.Append(title);
var listItem = new TagBuilder("li"); var listItem = new TagBuilder("li");
listItem.AddCssClass("nav-item"); listItem.AddCssClass("nav-item");

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs

@ -225,6 +225,6 @@ public class AbpTabsTagHelperService : AbpTagHelperService<AbpTabsTagHelper>
protected virtual string SetTabItemNameIfNotProvided(string content, int index) protected virtual string SetTabItemNameIfNotProvided(string content, int index)
{ {
return content.Replace(TabItemNamePlaceHolder, HtmlGenerator.Encode(TagHelper.Name) + "_" + index); return content.Replace(TabItemNamePlaceHolder, HtmlGenerator.Encode(TagHelper.Name ?? string.Empty) + "_" + index);
} }
} }

6
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs

@ -19,9 +19,9 @@ public class AbpTagHelperScriptService : AbpTagHelperResourceService
IBundleManager bundleManager, IBundleManager bundleManager,
IOptions<AbpBundlingOptions> options, IOptions<AbpBundlingOptions> options,
IWebHostEnvironment hostingEnvironment) : base( IWebHostEnvironment hostingEnvironment) : base(
bundleManager, bundleManager,
options, options,
hostingEnvironment) hostingEnvironment)
{ {
} }

6
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs

@ -22,9 +22,9 @@ public class AbpTagHelperStyleService : AbpTagHelperResourceService
IOptions<AbpBundlingOptions> options, IOptions<AbpBundlingOptions> options,
IWebHostEnvironment hostingEnvironment, IWebHostEnvironment hostingEnvironment,
IOptions<AbpSecurityHeadersOptions> securityHeadersOptions) : base( IOptions<AbpSecurityHeadersOptions> securityHeadersOptions) : base(
bundleManager, bundleManager,
options, options,
hostingEnvironment) hostingEnvironment)
{ {
SecurityHeadersOptions = securityHeadersOptions.Value; SecurityHeadersOptions = securityHeadersOptions.Value;
} }

38
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs

@ -2,27 +2,57 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.Extensions.Logging;
using Volo.Abp.Timing; using Volo.Abp.Timing;
namespace Volo.Abp.AspNetCore.Mvc.ModelBinding; namespace Volo.Abp.AspNetCore.Mvc.ModelBinding;
public class AbpDateTimeModelBinder : IModelBinder public class AbpDateTimeModelBinder : IModelBinder
{ {
private readonly ILogger<AbpDateTimeModelBinder> _logger;
private readonly DateTimeModelBinder _dateTimeModelBinder; private readonly DateTimeModelBinder _dateTimeModelBinder;
private readonly IClock _clock; private readonly IClock _clock;
private readonly ICurrentTimezoneProvider _currentTimezoneProvider;
private readonly ITimezoneProvider _timezoneProvider;
public AbpDateTimeModelBinder(IClock clock, DateTimeModelBinder dateTimeModelBinder) public AbpDateTimeModelBinder(
ILogger<AbpDateTimeModelBinder> logger,
DateTimeModelBinder dateTimeModelBinder,
IClock clock,
ICurrentTimezoneProvider currentTimezoneProvider,
ITimezoneProvider timezoneProvider)
{ {
_clock = clock; _logger = logger;
_dateTimeModelBinder = dateTimeModelBinder; _dateTimeModelBinder = dateTimeModelBinder;
_clock = clock;
_currentTimezoneProvider = currentTimezoneProvider;
_timezoneProvider = timezoneProvider;
} }
public async Task BindModelAsync(ModelBindingContext bindingContext) public async Task BindModelAsync(ModelBindingContext bindingContext)
{ {
await _dateTimeModelBinder.BindModelAsync(bindingContext); await _dateTimeModelBinder.BindModelAsync(bindingContext);
if (bindingContext.Result.IsModelSet && bindingContext.Result.Model is DateTime dateTime)
if (!bindingContext.Result.IsModelSet || bindingContext.Result.Model is not DateTime dateTime)
{ {
bindingContext.Result = ModelBindingResult.Success(_clock.Normalize(dateTime)); return;
} }
if (dateTime.Kind == DateTimeKind.Unspecified &&
_clock.SupportsMultipleTimezone &&
!_currentTimezoneProvider.TimeZone.IsNullOrWhiteSpace())
{
try
{
var timezoneInfo = _timezoneProvider.GetTimeZoneInfo(_currentTimezoneProvider.TimeZone);
dateTime = new DateTimeOffset(dateTime, timezoneInfo.GetUtcOffset(dateTime)).UtcDateTime;
}
catch
{
_logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", _currentTimezoneProvider.TimeZone);
}
}
bindingContext.Result = ModelBindingResult.Success(_clock.Normalize(dateTime));
} }
} }

2
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinderProvider.cs

@ -50,6 +50,6 @@ public class AbpDateTimeModelBinderProvider : IModelBinderProvider
{ {
const DateTimeStyles supportedStyles = DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AdjustToUniversal; const DateTimeStyles supportedStyles = DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AdjustToUniversal;
var dateTimeModelBinder = new DateTimeModelBinder(supportedStyles, context.Services.GetRequiredService<ILoggerFactory>()); var dateTimeModelBinder = new DateTimeModelBinder(supportedStyles, context.Services.GetRequiredService<ILoggerFactory>());
return new AbpDateTimeModelBinder(context.Services.GetRequiredService<IClock>(), dateTimeModelBinder); return ActivatorUtilities.CreateInstance<AbpDateTimeModelBinder>(context.Services, dateTimeModelBinder);
} }
} }

10
framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -33,6 +34,15 @@ public abstract class AbpWebApplicationFactoryIntegratedTest<TProgram> : WebAppl
return base.CreateHost(builder); return base.CreateHost(builder);
} }
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
hostingContext.HostingEnvironment.EnvironmentName = "Production";
});
base.ConfigureWebHost(builder);
}
protected virtual T? GetService<T>() protected virtual T? GetService<T>()
{ {
return Services.GetService<T>(); return Services.GetService<T>();

54
framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs

@ -14,41 +14,56 @@ namespace Microsoft.Extensions.DependencyInjection;
public static class CookieAuthenticationOptionsExtensions public static class CookieAuthenticationOptionsExtensions
{ {
/// <summary> /// <summary>
/// Check the access_token is expired or inactive. /// Check if the access_token is expired or inactive.
/// </summary> /// </summary>
public static CookieAuthenticationOptions CheckTokenExpiration(this CookieAuthenticationOptions options, string oidcAuthenticationScheme = "oidc", TimeSpan? advance = null, TimeSpan? validationInterval = null) public static CookieAuthenticationOptions CheckTokenExpiration(this CookieAuthenticationOptions options, string oidcAuthenticationScheme = "oidc", TimeSpan? advance = null, TimeSpan? validationInterval = null)
{ {
advance ??= TimeSpan.FromMinutes(3); advance ??= TimeSpan.FromMinutes(3);
validationInterval ??= TimeSpan.FromMinutes(1); validationInterval ??= TimeSpan.FromMinutes(1);
var previousHandler = options.Events.OnValidatePrincipal;
options.Events.OnValidatePrincipal = async principalContext => options.Events.OnValidatePrincipal = async principalContext =>
{ {
if (principalContext.Principal == null || principalContext.Principal.Identity == null || !principalContext.Principal.Identity.IsAuthenticated) if (principalContext.Principal == null || principalContext.Principal.Identity == null || !principalContext.Principal.Identity.IsAuthenticated)
{ {
await InvokePreviousHandlerAsync(principalContext, previousHandler);
return; return;
} }
var logger = principalContext.HttpContext.RequestServices.GetRequiredService<ILogger<CookieAuthenticationOptions>>(); var logger = principalContext.HttpContext.RequestServices.GetRequiredService<ILogger<CookieAuthenticationOptions>>();
var tokenExpiresAt = principalContext.Properties.GetString(".Token.expires_at"); var tokenExpiresAt = principalContext.Properties.GetString(".Token.expires_at");
if (!tokenExpiresAt.IsNullOrWhiteSpace() && DateTimeOffset.TryParseExact(tokenExpiresAt, "o", null, DateTimeStyles.RoundtripKind, out var expiresAt) && if (!tokenExpiresAt.IsNullOrWhiteSpace() && DateTimeOffset.TryParseExact(tokenExpiresAt, "o", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var expiresAt) &&
expiresAt < DateTimeOffset.UtcNow.Subtract(advance.Value)) expiresAt <= DateTimeOffset.UtcNow.Add(advance.Value))
{ {
logger.LogInformation("The access_token is expired."); logger.LogInformation("The access_token expires within {AdvanceSeconds}s; signing out.", advance.Value.TotalSeconds);
await SignOutAsync(principalContext); await SignOutAndInvokePreviousHandlerAsync(principalContext, previousHandler);
return; return;
} }
if (principalContext.Properties.IssuedUtc != null && DateTimeOffset.UtcNow.Subtract(principalContext.Properties.IssuedUtc.Value) > validationInterval) if (principalContext.Properties.IssuedUtc != null && DateTimeOffset.UtcNow.Subtract(principalContext.Properties.IssuedUtc.Value) > validationInterval)
{ {
logger.LogInformation($"Check the access_token is active every {validationInterval.Value.TotalSeconds} seconds."); logger.LogInformation("Checking access_token activity every {Seconds} seconds.", validationInterval.Value.TotalSeconds);
var accessToken = principalContext.Properties.GetTokenValue("access_token"); var accessToken = principalContext.Properties.GetTokenValue("access_token");
if (!accessToken.IsNullOrWhiteSpace()) if (!accessToken.IsNullOrWhiteSpace())
{ {
var openIdConnectOptions = await GetOpenIdConnectOptions(principalContext, oidcAuthenticationScheme); var openIdConnectOptions = await GetOpenIdConnectOptions(principalContext, oidcAuthenticationScheme);
var introspectionEndpoint = openIdConnectOptions.Configuration?.IntrospectionEndpoint;
if (introspectionEndpoint.IsNullOrWhiteSpace() && !openIdConnectOptions.Authority.IsNullOrWhiteSpace())
{
introspectionEndpoint = openIdConnectOptions.Authority.EnsureEndsWith('/') + "connect/introspect";
}
if (introspectionEndpoint.IsNullOrWhiteSpace())
{
logger.LogWarning("No introspection endpoint configured. Skipping token activity check.");
await InvokePreviousHandlerAsync(principalContext, previousHandler);
return;
}
var response = await openIdConnectOptions.Backchannel.IntrospectTokenAsync(new TokenIntrospectionRequest var response = await openIdConnectOptions.Backchannel.IntrospectTokenAsync(new TokenIntrospectionRequest
{ {
Address = openIdConnectOptions.Configuration?.IntrospectionEndpoint ?? openIdConnectOptions.Authority!.EnsureEndsWith('/') + "connect/introspect", Address = introspectionEndpoint,
ClientId = openIdConnectOptions.ClientId!, ClientId = openIdConnectOptions.ClientId!,
ClientSecret = openIdConnectOptions.ClientSecret, ClientSecret = openIdConnectOptions.ClientSecret,
Token = accessToken Token = accessToken
@ -56,15 +71,15 @@ public static class CookieAuthenticationOptionsExtensions
if (response.IsError) if (response.IsError)
{ {
logger.LogError(response.Error); logger.LogError("Token introspection error: {Error}", response.Error);
await SignOutAsync(principalContext); await SignOutAndInvokePreviousHandlerAsync(principalContext, previousHandler);
return; return;
} }
if (!response.IsActive) if (!response.IsActive)
{ {
logger.LogError("The access_token is not active."); logger.LogError("The access_token is not active.");
await SignOutAsync(principalContext); await SignOutAndInvokePreviousHandlerAsync(principalContext, previousHandler);
return; return;
} }
@ -73,16 +88,18 @@ public static class CookieAuthenticationOptionsExtensions
} }
else else
{ {
logger.LogError("The access_token is not found in the cookie properties, Please make sure SaveTokens of OpenIdConnectOptions is set as true."); logger.LogError("The access_token is not found in the cookie properties. Ensure SaveTokens of OpenIdConnectOptions is true.");
await SignOutAsync(principalContext); await SignOutAsync(principalContext);
} }
} }
await InvokePreviousHandlerAsync(principalContext, previousHandler);
}; };
return options; return options;
} }
private async static Task<OpenIdConnectOptions> GetOpenIdConnectOptions(CookieValidatePrincipalContext principalContext, string oidcAuthenticationScheme) private static async Task<OpenIdConnectOptions> GetOpenIdConnectOptions(CookieValidatePrincipalContext principalContext, string oidcAuthenticationScheme)
{ {
var openIdConnectOptions = principalContext.HttpContext.RequestServices.GetRequiredService<IOptionsMonitor<OpenIdConnectOptions>>().Get(oidcAuthenticationScheme); var openIdConnectOptions = principalContext.HttpContext.RequestServices.GetRequiredService<IOptionsMonitor<OpenIdConnectOptions>>().Get(oidcAuthenticationScheme);
var cancellationTokenProvider = principalContext.HttpContext.RequestServices.GetRequiredService<ICancellationTokenProvider>(); var cancellationTokenProvider = principalContext.HttpContext.RequestServices.GetRequiredService<ICancellationTokenProvider>();
@ -94,9 +111,20 @@ public static class CookieAuthenticationOptionsExtensions
return openIdConnectOptions; return openIdConnectOptions;
} }
private async static Task SignOutAsync(CookieValidatePrincipalContext principalContext) private static async Task SignOutAsync(CookieValidatePrincipalContext principalContext)
{ {
principalContext.RejectPrincipal(); principalContext.RejectPrincipal();
await principalContext.HttpContext.SignOutAsync(principalContext.Scheme.Name); await principalContext.HttpContext.SignOutAsync(principalContext.Scheme.Name);
} }
private static Task InvokePreviousHandlerAsync(CookieValidatePrincipalContext principalContext, Func<CookieValidatePrincipalContext, Task>? previousHandler)
{
return previousHandler != null ? previousHandler(principalContext) : Task.CompletedTask;
}
private static async Task SignOutAndInvokePreviousHandlerAsync(CookieValidatePrincipalContext principalContext, Func<CookieValidatePrincipalContext, Task>? previousHandler)
{
await SignOutAsync(principalContext);
await InvokePreviousHandlerAsync(principalContext, previousHandler);
}
} }

28
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/BasicAggregateRoot.cs

@ -10,36 +10,38 @@ public abstract class BasicAggregateRoot : Entity,
IAggregateRoot, IAggregateRoot,
IGeneratesDomainEvents IGeneratesDomainEvents
{ {
private readonly ICollection<DomainEventRecord> _distributedEvents = new Collection<DomainEventRecord>(); private ICollection<DomainEventRecord>? _distributedEvents;
private readonly ICollection<DomainEventRecord> _localEvents = new Collection<DomainEventRecord>(); private ICollection<DomainEventRecord>? _localEvents;
public virtual IEnumerable<DomainEventRecord> GetLocalEvents() public virtual IEnumerable<DomainEventRecord> GetLocalEvents()
{ {
return _localEvents; return _localEvents ?? Array.Empty<DomainEventRecord>();
} }
public virtual IEnumerable<DomainEventRecord> GetDistributedEvents() public virtual IEnumerable<DomainEventRecord> GetDistributedEvents()
{ {
return _distributedEvents; return _distributedEvents ?? Array.Empty<DomainEventRecord>();
} }
public virtual void ClearLocalEvents() public virtual void ClearLocalEvents()
{ {
_localEvents.Clear(); _localEvents?.Clear();
} }
public virtual void ClearDistributedEvents() public virtual void ClearDistributedEvents()
{ {
_distributedEvents.Clear(); _distributedEvents?.Clear();
} }
protected virtual void AddLocalEvent(object eventData) protected virtual void AddLocalEvent(object eventData)
{ {
_localEvents ??= new Collection<DomainEventRecord>();
_localEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext())); _localEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext()));
} }
protected virtual void AddDistributedEvent(object eventData) protected virtual void AddDistributedEvent(object eventData)
{ {
_distributedEvents ??= new Collection<DomainEventRecord>();
_distributedEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext())); _distributedEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext()));
} }
} }
@ -49,8 +51,8 @@ public abstract class BasicAggregateRoot<TKey> : Entity<TKey>,
IAggregateRoot<TKey>, IAggregateRoot<TKey>,
IGeneratesDomainEvents IGeneratesDomainEvents
{ {
private readonly ICollection<DomainEventRecord> _distributedEvents = new Collection<DomainEventRecord>(); private ICollection<DomainEventRecord>? _distributedEvents;
private readonly ICollection<DomainEventRecord> _localEvents = new Collection<DomainEventRecord>(); private ICollection<DomainEventRecord>? _localEvents;
protected BasicAggregateRoot() protected BasicAggregateRoot()
{ {
@ -65,31 +67,33 @@ public abstract class BasicAggregateRoot<TKey> : Entity<TKey>,
public virtual IEnumerable<DomainEventRecord> GetLocalEvents() public virtual IEnumerable<DomainEventRecord> GetLocalEvents()
{ {
return _localEvents; return _localEvents ?? Array.Empty<DomainEventRecord>();
} }
public virtual IEnumerable<DomainEventRecord> GetDistributedEvents() public virtual IEnumerable<DomainEventRecord> GetDistributedEvents()
{ {
return _distributedEvents; return _distributedEvents ?? Array.Empty<DomainEventRecord>();
} }
public virtual void ClearLocalEvents() public virtual void ClearLocalEvents()
{ {
_localEvents.Clear(); _localEvents?.Clear();
} }
public virtual void ClearDistributedEvents() public virtual void ClearDistributedEvents()
{ {
_distributedEvents.Clear(); _distributedEvents?.Clear();
} }
protected virtual void AddLocalEvent(object eventData) protected virtual void AddLocalEvent(object eventData)
{ {
_localEvents ??= new Collection<DomainEventRecord>();
_localEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext())); _localEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext()));
} }
protected virtual void AddDistributedEvent(object eventData) protected virtual void AddDistributedEvent(object eventData)
{ {
_distributedEvents ??= new Collection<DomainEventRecord>();
_distributedEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext())); _distributedEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext()));
} }
} }

49
framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs

@ -2,6 +2,8 @@
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
@ -15,15 +17,24 @@ namespace Volo.Abp.Json.Newtonsoft;
public class AbpDateTimeConverter : DateTimeConverterBase, ITransientDependency public class AbpDateTimeConverter : DateTimeConverterBase, ITransientDependency
{ {
private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK";
public ILogger<AbpDateTimeConverter> Logger { get; set; }
private readonly DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind; private readonly DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind;
private readonly CultureInfo _culture = CultureInfo.InvariantCulture; private readonly CultureInfo _culture = CultureInfo.InvariantCulture;
private readonly IClock _clock; private readonly IClock _clock;
private readonly AbpJsonOptions _options; private readonly AbpJsonOptions _options;
private readonly ICurrentTimezoneProvider _currentTimezoneProvider;
private readonly ITimezoneProvider _timezoneProvider;
private bool _skipDateTimeNormalization; private bool _skipDateTimeNormalization;
public AbpDateTimeConverter(IClock clock, IOptions<AbpJsonOptions> options) public AbpDateTimeConverter(IClock clock, IOptions<AbpJsonOptions> options, ICurrentTimezoneProvider currentTimezoneProvider, ITimezoneProvider timezoneProvider)
{ {
Logger = NullLogger<AbpDateTimeConverter>.Instance;
_clock = clock; _clock = clock;
_currentTimezoneProvider = currentTimezoneProvider;
_timezoneProvider = timezoneProvider;
_options = options.Value; _options = options.Value;
} }
@ -41,19 +52,14 @@ public class AbpDateTimeConverter : DateTimeConverterBase, ITransientDependency
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
var nullable = Nullable.GetUnderlyingType(objectType) != null; var nullable = Nullable.GetUnderlyingType(objectType) != null;
if (reader.TokenType == JsonToken.Null) switch (reader.TokenType)
{ {
if (!nullable) case JsonToken.Null when !nullable:
{
throw new JsonSerializationException($"Cannot convert null value to {objectType.FullName}."); throw new JsonSerializationException($"Cannot convert null value to {objectType.FullName}.");
} case JsonToken.Null:
return null;
return null; case JsonToken.Date:
} return Normalize(reader.Value!.To<DateTime>());
if (reader.TokenType == JsonToken.Date)
{
return Normalize(reader.Value!.To<DateTime>());
} }
if (reader.TokenType != JsonToken.String) if (reader.TokenType != JsonToken.String)
@ -108,7 +114,7 @@ public class AbpDateTimeConverter : DateTimeConverterBase, ITransientDependency
} }
} }
static internal bool ShouldNormalize(MemberInfo member, JsonProperty property) internal static bool ShouldNormalize(MemberInfo member, JsonProperty property)
{ {
if (property.PropertyType != typeof(DateTime) && if (property.PropertyType != typeof(DateTime) &&
property.PropertyType != typeof(DateTime?)) property.PropertyType != typeof(DateTime?))
@ -121,6 +127,23 @@ public class AbpDateTimeConverter : DateTimeConverterBase, ITransientDependency
protected virtual DateTime Normalize(DateTime dateTime) protected virtual DateTime Normalize(DateTime dateTime)
{ {
if (dateTime.Kind != DateTimeKind.Unspecified ||
!_clock.SupportsMultipleTimezone ||
_currentTimezoneProvider.TimeZone.IsNullOrWhiteSpace())
{
return _skipDateTimeNormalization ? dateTime : _clock.Normalize(dateTime);
}
try
{
var timezoneInfo = _timezoneProvider.GetTimeZoneInfo(_currentTimezoneProvider.TimeZone);
dateTime = new DateTimeOffset(dateTime, timezoneInfo.GetUtcOffset(dateTime)).UtcDateTime;
}
catch
{
Logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", _currentTimezoneProvider.TimeZone);
}
return _skipDateTimeNormalization return _skipDateTimeNormalization
? dateTime ? dateTime
: _clock.Normalize(dateTime); : _clock.Normalize(dateTime);

69
framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverter.cs

@ -1,84 +1,43 @@
using System; using System;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Timing; using Volo.Abp.Timing;
namespace Volo.Abp.Json.SystemTextJson.JsonConverters; namespace Volo.Abp.Json.SystemTextJson.JsonConverters;
public class AbpDateTimeConverter : JsonConverter<DateTime>, ITransientDependency public class AbpDateTimeConverter : AbpDateTimeConverterBase<DateTime>, ITransientDependency
{ {
private readonly IClock _clock; public AbpDateTimeConverter(
private readonly AbpJsonOptions _options; IClock clock,
private bool _skipDateTimeNormalization; IOptions<AbpJsonOptions> abpJsonOptions,
ICurrentTimezoneProvider currentTimezoneProvider,
public AbpDateTimeConverter(IClock clock, IOptions<AbpJsonOptions> abpJsonOptions) ITimezoneProvider timezoneProvider)
: base(clock, abpJsonOptions, currentTimezoneProvider, timezoneProvider)
{ {
_clock = clock;
_options = abpJsonOptions.Value;
} }
public virtual AbpDateTimeConverter SkipDateTimeNormalization() public virtual AbpDateTimeConverter SkipDateTimeNormalization()
{ {
_skipDateTimeNormalization = true; IsSkipDateTimeNormalization = true;
return this; return this;
} }
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
if (_options.InputDateTimeFormats.Any()) if (Options.InputDateTimeFormats.Any() && reader.TokenType != JsonTokenType.String)
{
if (reader.TokenType == JsonTokenType.String)
{
foreach (var format in _options.InputDateTimeFormats)
{
var s = reader.GetString();
if (DateTime.TryParseExact(s, format, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d1))
{
return Normalize(d1);
}
}
}
else
{
throw new JsonException("Reader's TokenType is not String!");
}
}
if (reader.TryGetDateTime(out var d3))
{
return Normalize(d3);
}
var dateText = reader.GetString();
if (!dateText.IsNullOrWhiteSpace())
{ {
if (DateTime.TryParse(dateText, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d4)) throw new JsonException("Reader's TokenType is not String!");
{
return Normalize(d4);
}
} }
throw new JsonException("Can't get datetime from the reader!"); return TryReadDateTime(ref reader, out var result)
? result
: throw new JsonException("Can't get datetime from the reader!");
} }
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{ {
if (_options.OutputDateTimeFormat.IsNullOrWhiteSpace()) WriteDateTime(writer, value);
{
writer.WriteStringValue(Normalize(value));
}
else
{
writer.WriteStringValue(Normalize(value).ToString(_options.OutputDateTimeFormat, CultureInfo.CurrentUICulture));
}
}
protected virtual DateTime Normalize(DateTime dateTime)
{
return _skipDateTimeNormalization ? dateTime : _clock.Normalize(dateTime);
} }
} }

116
framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs

@ -0,0 +1,116 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.Timing;
namespace Volo.Abp.Json.SystemTextJson.JsonConverters;
public abstract class AbpDateTimeConverterBase<T> : JsonConverter<T>
{
public ILogger<AbpDateTimeConverterBase<T>> Logger { get; set; }
protected IClock Clock { get; }
protected AbpJsonOptions Options { get; }
protected ICurrentTimezoneProvider CurrentTimezoneProvider { get; }
protected ITimezoneProvider TimezoneProvider { get; }
protected bool IsSkipDateTimeNormalization { get; set; }
protected AbpDateTimeConverterBase(
IClock clock,
IOptions<AbpJsonOptions> abpJsonOptions,
ICurrentTimezoneProvider currentTimezoneProvider,
ITimezoneProvider timezoneProvider)
{
Logger = NullLogger<AbpDateTimeConverterBase<T>>.Instance;
Clock = clock;
CurrentTimezoneProvider = currentTimezoneProvider;
TimezoneProvider = timezoneProvider;
Options = abpJsonOptions.Value;
}
protected bool TryReadDateTime(ref Utf8JsonReader reader, out DateTime value)
{
value = default;
if (Options.InputDateTimeFormats.Any())
{
if (reader.TokenType != JsonTokenType.String)
{
return false;
}
var s = reader.GetString();
foreach (var format in Options.InputDateTimeFormats)
{
if (!DateTime.TryParseExact(s, format, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d1))
{
continue;
}
value = Normalize(d1);
return true;
}
}
if (reader.TryGetDateTime(out var d2))
{
value = Normalize(d2);
return true;
}
var dateText = reader.GetString();
if (dateText.IsNullOrWhiteSpace())
{
return false;
}
if (!DateTime.TryParse(dateText, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d3))
{
return false;
}
value = Normalize(d3);
return true;
}
protected void WriteDateTime(Utf8JsonWriter writer, DateTime value)
{
if (Options.OutputDateTimeFormat.IsNullOrWhiteSpace())
{
writer.WriteStringValue(Normalize(value));
}
else
{
writer.WriteStringValue(Normalize(value).ToString(Options.OutputDateTimeFormat, CultureInfo.CurrentUICulture));
}
}
protected virtual DateTime Normalize(DateTime dateTime)
{
if (dateTime.Kind != DateTimeKind.Unspecified ||
!Clock.SupportsMultipleTimezone ||
CurrentTimezoneProvider.TimeZone.IsNullOrWhiteSpace())
{
return IsSkipDateTimeNormalization ? dateTime : Clock.Normalize(dateTime);
}
try
{
var timezoneInfo = TimezoneProvider.GetTimeZoneInfo(CurrentTimezoneProvider.TimeZone);
dateTime = new DateTimeOffset(dateTime, timezoneInfo.GetUtcOffset(dateTime)).UtcDateTime;
}
catch
{
Logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", CurrentTimezoneProvider.TimeZone);
}
return IsSkipDateTimeNormalization ? dateTime : Clock.Normalize(dateTime);
}
}

67
framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpNullableDateTimeConverter.cs

@ -1,65 +1,39 @@
using System; using System;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Timing; using Volo.Abp.Timing;
namespace Volo.Abp.Json.SystemTextJson.JsonConverters; namespace Volo.Abp.Json.SystemTextJson.JsonConverters;
public class AbpNullableDateTimeConverter : JsonConverter<DateTime?>, ITransientDependency public class AbpNullableDateTimeConverter : AbpDateTimeConverterBase<DateTime?>, ITransientDependency
{ {
private readonly IClock _clock; public AbpNullableDateTimeConverter(
private readonly AbpJsonOptions _options; IClock clock,
private bool _skipDateTimeNormalization; IOptions<AbpJsonOptions> abpJsonOptions,
ICurrentTimezoneProvider currentTimezoneProvider,
public AbpNullableDateTimeConverter(IClock clock, IOptions<AbpJsonOptions> abpJsonOptions) ITimezoneProvider timezoneProvider)
: base(clock, abpJsonOptions, currentTimezoneProvider, timezoneProvider)
{ {
_clock = clock;
_options = abpJsonOptions.Value;
} }
public virtual AbpNullableDateTimeConverter SkipDateTimeNormalization() public virtual AbpNullableDateTimeConverter SkipDateTimeNormalization()
{ {
_skipDateTimeNormalization = true; IsSkipDateTimeNormalization = true;
return this; return this;
} }
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
if (_options.InputDateTimeFormats.Any()) if (Options.InputDateTimeFormats.Any() && reader.TokenType != JsonTokenType.String)
{
if (reader.TokenType == JsonTokenType.String)
{
foreach (var format in _options.InputDateTimeFormats)
{
var s = reader.GetString();
if (DateTime.TryParseExact(s, format, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d1))
{
return Normalize(d1);
}
}
}
else
{
throw new JsonException("Reader's TokenType is not String!");
}
}
if (reader.TryGetDateTime(out var d2))
{ {
return Normalize(d2); throw new JsonException("Reader's TokenType is not String!");
} }
var dateText = reader.GetString(); if (TryReadDateTime(ref reader, out var result))
if (!dateText.IsNullOrWhiteSpace())
{ {
if (DateTime.TryParse(dateText, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d3)) return result;
{
return Normalize(d3);
}
} }
return null; return null;
@ -70,22 +44,9 @@ public class AbpNullableDateTimeConverter : JsonConverter<DateTime?>, ITransient
if (value == null) if (value == null)
{ {
writer.WriteNullValue(); writer.WriteNullValue();
return;
} }
else
{
if (_options.OutputDateTimeFormat.IsNullOrWhiteSpace())
{
writer.WriteStringValue(Normalize(value.Value));
}
else
{
writer.WriteStringValue(Normalize(value.Value).ToString(_options.OutputDateTimeFormat, CultureInfo.CurrentUICulture));
}
}
}
protected virtual DateTime Normalize(DateTime dateTime) WriteDateTime(writer, value.Value);
{
return _skipDateTimeNormalization ? dateTime : _clock.Normalize(dateTime);
} }
} }

39
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController.cs

@ -13,12 +13,24 @@ public class ModelBindingController : AbpController
return input.Kind.ToString().ToLower(); return input.Kind.ToString().ToLower();
} }
[HttpGet("DateTimeKind_WithResult")]
public string DateTimeKind_WithResult(DateTime input)
{
return input.Kind.ToString().ToLower() + "_" + input.ToString("O").ToLower();
}
[HttpGet("NullableDateTimeKind")] [HttpGet("NullableDateTimeKind")]
public string NullableDateTimeKind(DateTime? input) public string NullableDateTimeKind(DateTime? input)
{ {
return input.Value.Kind.ToString().ToLower(); return input.Value.Kind.ToString().ToLower();
} }
[HttpGet("NullableDateTimeKind_WithResult")]
public string NullableDateTimeKind_WithResult(DateTime? input)
{
return input.Value.Kind.ToString().ToLower() + "_" + input.Value.ToString("O").ToLower();
}
[HttpGet("DisableDateTimeNormalizationDateTimeKind")] [HttpGet("DisableDateTimeNormalizationDateTimeKind")]
public string DisableDateTimeNormalizationDateTimeKind([DisableDateTimeNormalization] DateTime input) public string DisableDateTimeNormalizationDateTimeKind([DisableDateTimeNormalization] DateTime input)
{ {
@ -40,6 +52,19 @@ public class ModelBindingController : AbpController
input.InnerModel.Time4.Kind.ToString().ToLower(); input.InnerModel.Time4.Kind.ToString().ToLower();
} }
[HttpGet("ComplexTypeDateTimeKind_WithResult")]
public string ComplexTypeDateTimeKind_WithResult(GetDateTimeKindModel input)
{
return input.Time1.Kind.ToString().ToLower() + "_" +
input.Time1.ToString("O").ToLower() + "_" +
input.Time2.Kind.ToString().ToLower() + "_" +
input.Time2.ToString("O").ToLower() + "_" +
input.Time3.Value.Kind.ToString().ToLower() + "_" +
input.Time3.Value.ToString("O").ToLower() + "_" +
input.InnerModel.Time4.Kind.ToString().ToLower() + "_" +
input.InnerModel.Time4.ToString("O").ToLower();
}
//JSON input and output. //JSON input and output.
[HttpPost("ComplexTypeDateTimeKind_JSON")] [HttpPost("ComplexTypeDateTimeKind_JSON")]
public string ComplexTypeDateTimeKind_JSON([FromBody] GetDateTimeKindModel input) public string ComplexTypeDateTimeKind_JSON([FromBody] GetDateTimeKindModel input)
@ -50,6 +75,20 @@ public class ModelBindingController : AbpController
input.InnerModel.Time4.Kind.ToString().ToLower(); input.InnerModel.Time4.Kind.ToString().ToLower();
} }
//JSON input and output.
[HttpPost("ComplexTypeDateTimeKind_JSON_WithResult")]
public string ComplexTypeDateTimeKind_JSON_WithResult([FromBody] GetDateTimeKindModel input)
{
return input.Time1.Kind.ToString().ToLower() + "_" +
input.Time1.ToString("O").ToLower() + "_" +
input.Time2.Kind.ToString().ToLower() + "_" +
input.Time2.ToString("O").ToLower() + "_" +
input.Time3.Value.Kind.ToString().ToLower() + "_" +
input.Time3.Value.ToString("O").ToLower() + "_" +
input.InnerModel.Time4.Kind.ToString().ToLower() + "_" +
input.InnerModel.Time4.ToString("O").ToLower();
}
[HttpPost("Guid_Json_Test")] [HttpPost("Guid_Json_Test")]
public GuidJsonModel Guid_Json_Test([FromBody] GuidJsonModel input) public GuidJsonModel Guid_Json_Test([FromBody] GuidJsonModel input)
{ {

104
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs

@ -19,34 +19,79 @@ public abstract class ModelBindingController_Tests : AspNetCoreMvcTestBase
[Fact] [Fact]
public async Task DateTimeKind_Test() public async Task DateTimeKind_Test()
{ {
var response = await Client.GetAsync("/api/model-Binding-test/DateTimeKind?input=2010-01-01T00:00:00Z"); var response = await Client.GetAsync("/api/model-Binding-test/DateTimeKind?input=2020-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK); response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync(); var resultAsString = await response.Content.ReadAsStringAsync();
resultAsString.ShouldBe(Kind.ToString().ToLower()); resultAsString.ShouldBe(Kind.ToString().ToLower());
} }
[Fact]
public async Task DateTimeKind_WithTimezone_Test()
{
var response = await Client.GetAsync("/api/model-Binding-test/DateTimeKind_WithResult?input=2020-01-01T00:00:00&__timezone=Europe/Istanbul");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
var dateTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Unspecified);
switch (Kind)
{
case DateTimeKind.Utc:
dateTime = new DateTime(2019, 12, 31, 21, 0, 0, DateTimeKind.Utc); //Turkey is UTC+3
break;
case DateTimeKind.Local:
dateTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Local);
break;
}
resultAsString.ShouldBe($"{Kind.ToString().ToLower()}_{dateTime.ToString("O").ToLower()}");
}
[Fact] [Fact]
public async Task NullableDateTimeKind_Test() public async Task NullableDateTimeKind_Test()
{ {
var response = var response =
await Client.GetAsync("/api/model-Binding-test/NullableDateTimeKind?input=2010-01-01T00:00:00Z"); await Client.GetAsync("/api/model-Binding-test/NullableDateTimeKind?input=2020-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK); response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync(); var resultAsString = await response.Content.ReadAsStringAsync();
resultAsString.ShouldBe(Kind.ToString().ToLower()); resultAsString.ShouldBe(Kind.ToString().ToLower());
} }
[Fact]
public async Task NullableDateTimeKind_WithTimezone_Test()
{
var response =
await Client.GetAsync("/api/model-Binding-test/NullableDateTimeKind_WithResult?input=2020-01-01T00:00:00&__timezone=Europe/Istanbul");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
var dateTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Unspecified);
switch (Kind)
{
case DateTimeKind.Utc:
dateTime = new DateTime(2019, 12, 31, 21, 0, 0, DateTimeKind.Utc); //Turkey is UTC+3
break;
case DateTimeKind.Local:
dateTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Local);
break;
}
resultAsString.ShouldBe($"{Kind.ToString().ToLower()}_{dateTime.ToString("O").ToLower()}");
}
[Fact] [Fact]
public async Task DisableDateTimeNormalizationDateTimeKind_Test() public async Task DisableDateTimeNormalizationDateTimeKind_Test()
{ {
var response = var response =
await Client.GetAsync( await Client.GetAsync(
"/api/model-Binding-test/DisableDateTimeNormalizationDateTimeKind?input=2010-01-01T00:00:00Z"); "/api/model-Binding-test/DisableDateTimeNormalizationDateTimeKind?input=2020-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK); response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync(); var resultAsString = await response.Content.ReadAsStringAsync();
//Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is UTC //Time parameter(2020-01-01T00:00:00Z) with time zone information, so the default Kind is UTC
//https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50?view=aspnetcore-3.1&tabs=visual-studio#datetime-values-are-model-bound-as-utc-times //https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50?view=aspnetcore-3.1&tabs=visual-studio#datetime-values-are-model-bound-as-utc-times
resultAsString.ShouldBe(DateTimeKind.Utc.ToString().ToLower()); resultAsString.ShouldBe(DateTimeKind.Utc.ToString().ToLower());
} }
@ -56,11 +101,11 @@ public abstract class ModelBindingController_Tests : AspNetCoreMvcTestBase
{ {
var response = var response =
await Client.GetAsync( await Client.GetAsync(
"/api/model-Binding-test/DisableDateTimeNormalizationNullableDateTimeKind?input=2010-01-01T00:00:00Z"); "/api/model-Binding-test/DisableDateTimeNormalizationNullableDateTimeKind?input=2020-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK); response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync(); var resultAsString = await response.Content.ReadAsStringAsync();
//Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is UTC //Time parameter(2020-01-01T00:00:00Z) with time zone information, so the default Kind is UTC
//https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50?view=aspnetcore-3.1&tabs=visual-studio#datetime-values-are-model-bound-as-utc-times //https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50?view=aspnetcore-3.1&tabs=visual-studio#datetime-values-are-model-bound-as-utc-times
resultAsString.ShouldBe(DateTimeKind.Utc.ToString().ToLower()); resultAsString.ShouldBe(DateTimeKind.Utc.ToString().ToLower());
} }
@ -69,14 +114,14 @@ public abstract class ModelBindingController_Tests : AspNetCoreMvcTestBase
public async Task ComplexTypeDateTimeKind_Test() public async Task ComplexTypeDateTimeKind_Test()
{ {
var response = await Client.GetAsync("/api/model-Binding-test/ComplexTypeDateTimeKind?" + var response = await Client.GetAsync("/api/model-Binding-test/ComplexTypeDateTimeKind?" +
"Time1=2010-01-01T00:00:00Z&" + "Time1=2020-01-01T00:00:00Z&" +
"Time2=2010-01-01T00:00:00Z&" + "Time2=2020-01-01T00:00:00Z&" +
"Time3=2010-01-01T00:00:00Z&" + "Time3=2020-01-01T00:00:00Z&" +
"InnerModel.Time4=2010-01-01T00:00:00Z"); "InnerModel.Time4=2020-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK); response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync(); var resultAsString = await response.Content.ReadAsStringAsync();
//Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is UTC //Time parameter(2020-01-01T00:00:00Z) with time zone information, so the default Kind is UTC
//https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50?view=aspnetcore-3.1&tabs=visual-studio#datetime-values-are-model-bound-as-utc-times //https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50?view=aspnetcore-3.1&tabs=visual-studio#datetime-values-are-model-bound-as-utc-times
resultAsString.ShouldBe($"utc_{Kind.ToString().ToLower()}_{Kind.ToString().ToLower()}_utc"); resultAsString.ShouldBe($"utc_{Kind.ToString().ToLower()}_{Kind.ToString().ToLower()}_utc");
} }
@ -84,7 +129,7 @@ public abstract class ModelBindingController_Tests : AspNetCoreMvcTestBase
[Fact] [Fact]
public async Task ComplexTypeDateTimeKind_JSON_Test() public async Task ComplexTypeDateTimeKind_JSON_Test()
{ {
var time = DateTime.Parse("2010-01-01T00:00:00Z"); var time = DateTime.Parse("2020-01-01T00:00:00Z");
var response = await Client.PostAsync("/api/model-Binding-test/ComplexTypeDateTimeKind_JSON", var response = await Client.PostAsync("/api/model-Binding-test/ComplexTypeDateTimeKind_JSON",
new StringContent(JsonSerializer.Serialize( new StringContent(JsonSerializer.Serialize(
new GetDateTimeKindModel new GetDateTimeKindModel
@ -104,6 +149,41 @@ public abstract class ModelBindingController_Tests : AspNetCoreMvcTestBase
resultAsString.ShouldBe($"local_{Kind.ToString().ToLower()}_{Kind.ToString().ToLower()}_local"); resultAsString.ShouldBe($"local_{Kind.ToString().ToLower()}_{Kind.ToString().ToLower()}_local");
} }
[Fact]
public async Task ComplexTypeDateTimeKind_JSON_WithTimezone_Test()
{
var time = DateTime.Parse("2020-01-01T00:00:00");
var response = await Client.PostAsync("/api/model-Binding-test/ComplexTypeDateTimeKind_JSON_WithResult?__timezone=Europe/Istanbul",
new StringContent(JsonSerializer.Serialize(
new GetDateTimeKindModel
{
Time1 = time,
Time2 = time,
Time3 = time,
InnerModel = new GetDateTimeKindModel.GetDateTimeKindInnerModel
{
Time4 = time
}
}
), Encoding.UTF8, MimeTypes.Application.Json));
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
var dateTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Unspecified);
switch (Kind)
{
case DateTimeKind.Utc:
dateTime = new DateTime(2019, 12, 31, 21, 0, 0, DateTimeKind.Utc); //Turkey is UTC+3
break;
case DateTimeKind.Local:
dateTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Local);
break;
}
resultAsString.ShouldBe($"unspecified_{time.ToString("O").ToLower()}_{Kind.ToString().ToLower()}_{dateTime.ToString("O").ToLower()}_{Kind.ToString().ToLower()}_{dateTime.ToString("O").ToLower()}_unspecified_{time.ToString("O").ToLower()}");
}
[Fact] [Fact]
public async Task Guid_Json_Test() public async Task Guid_Json_Test()
{ {

2
modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManager.cs

@ -270,7 +270,7 @@ public class ResourcePermissionManager : IResourcePermissionManager, ISingletonD
} }
var currentGrantInfo = await GetInternalAsync(permission, resourceName, resourceKey, providerName, providerKey); var currentGrantInfo = await GetInternalAsync(permission, resourceName, resourceKey, providerName, providerKey);
if (currentGrantInfo.IsGranted == isGranted) if (currentGrantInfo.IsGranted == isGranted && currentGrantInfo.Providers.Any(x => x.Name == providerName && x.Key == providerKey))
{ {
return; return;
} }

80
npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.html

@ -1,45 +1,61 @@
<div class="abp-lookup-container position-relative"> <div class="abp-lookup-container position-relative">
@if (label()) { @if (label()) {
<label class="form-label">{{ label() | abpLocalization }}</label> <label class="form-label">{{ label() | abpLocalization }}</label>
} }
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" [placeholder]="placeholder() | abpLocalization" [ngModel]="displayValue()" <input
(ngModelChange)="onSearchInput($event)" (focus)="onSearchFocus()" (blur)="onSearchBlur($event)" type="text"
[disabled]="disabled()" /> class="form-control"
[placeholder]="placeholder() | abpLocalization"
[ngModel]="displayValue()"
(ngModelChange)="onSearchInput($event)"
(focus)="onSearchFocus()"
(blur)="onSearchBlur($event)"
[disabled]="disabled()"
/>
@if (displayValue() && !disabled()) { @if (displayValue() && !disabled()) {
<button type="button" class="btn btn-outline-secondary" (mousedown)="clearSelection()" tabindex="-1"> <button
<i class="fa fa-times"></i> type="button"
</button> class="btn btn-outline-secondary"
(mousedown)="clearSelection()"
tabindex="-1"
>
<i class="fa fa-times"></i>
</button>
} }
</div> </div>
@if (showDropdown() && !disabled()) { @if (showDropdown() && !disabled()) {
<div class="abp-lookup-dropdown list-group position-absolute w-100 shadow"> <div class="abp-lookup-dropdown list-group position-absolute w-100">
@if (isLoading()) { @if (isLoading()) {
<div class="list-group-item text-center py-3"> <div class="list-group-item text-center py-3">
<i class="fa fa-spinner fa-spin me-2"></i> <i class="fa fa-spinner fa-spin me-2"></i>
{{ 'AbpUi::Loading' | abpLocalization }} {{ 'AbpUi::Loading' | abpLocalization }}
</div> </div>
} @else if (searchResults().length > 0) { } @else if (searchResults().length > 0) {
@for (item of searchResults(); track item.key) { @for (item of searchResults(); track item.key) {
<button type="button" class="list-group-item list-group-item-action" (mousedown)="selectItem(item)"> <button
@if (itemTemplate()) { type="button"
<ng-container *ngTemplateOutlet="itemTemplate()!; context: { $implicit: item }" /> class="list-group-item list-group-item-action"
} @else { (mousedown)="selectItem(item)"
{{ getDisplayValue(item) }} >
@if (itemTemplate()) {
<ng-container *ngTemplateOutlet="itemTemplate()!; context: { $implicit: item }" />
} @else {
{{ getDisplayValue(item) }}
}
</button>
}
} @else if (displayValue()) {
@if (noResultsTemplate()) {
<ng-container *ngTemplateOutlet="noResultsTemplate()!" />
} @else {
<div class="list-group-item text-muted">
{{ 'AbpUi::NoDataAvailableInDatatable' | abpLocalization }}
</div>
}
} }
</button>
}
} @else if (displayValue()) {
@if (noResultsTemplate()) {
<ng-container *ngTemplateOutlet="noResultsTemplate()!" />
} @else {
<div class="list-group-item text-muted">
{{ 'AbpUi::NoRecordsFound' | abpLocalization }}
</div> </div>
}
}
</div>
} }
</div> </div>

9
npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.scss

@ -1,5 +1,8 @@
.abp-lookup-dropdown { .abp-lookup-dropdown {
z-index: 1050; z-index: 1060;
max-height: 200px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
top: 100%;
margin-top: 0.25rem;
background-color: var(--lpx-content-bg);
} }

232
npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.ts

@ -1,15 +1,15 @@
import { import {
Component, Component,
input, input,
output, output,
model, model,
signal, signal,
OnInit, OnInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
TemplateRef, TemplateRef,
contentChild, contentChild,
DestroyRef, DestroyRef,
inject, inject,
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@ -18,119 +18,123 @@ import { LocalizationPipe } from '@abp/ng.core';
import { Subject, Observable, debounceTime, distinctUntilChanged, of, finalize } from 'rxjs'; import { Subject, Observable, debounceTime, distinctUntilChanged, of, finalize } from 'rxjs';
export interface LookupItem { export interface LookupItem {
key: string; key: string;
displayName: string; displayName: string;
[key: string]: unknown; [key: string]: unknown;
} }
export type LookupSearchFn<T = LookupItem> = (filter: string) => Observable<T[]>; export type LookupSearchFn<T = LookupItem> = (filter: string) => Observable<T[]>;
@Component({ @Component({
selector: 'abp-lookup-search', selector: 'abp-lookup-search',
templateUrl: './lookup-search.component.html', templateUrl: './lookup-search.component.html',
styleUrl: './lookup-search.component.scss', styleUrl: './lookup-search.component.scss',
imports: [CommonModule, FormsModule, LocalizationPipe], imports: [CommonModule, FormsModule, LocalizationPipe],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class LookupSearchComponent<T extends LookupItem = LookupItem> implements OnInit { export class LookupSearchComponent<T extends LookupItem = LookupItem> implements OnInit {
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
readonly label = input<string>(); readonly label = input<string>();
readonly placeholder = input<string>(''); readonly placeholder = input<string>('');
readonly debounceTime = input<number>(300); readonly debounceTime = input<number>(300);
readonly minSearchLength = input<number>(0); readonly minSearchLength = input<number>(0);
readonly displayKey = input<keyof T>('displayName' as keyof T); readonly displayKey = input<keyof T>('displayName' as keyof T);
readonly valueKey = input<keyof T>('key' as keyof T); readonly valueKey = input<keyof T>('key' as keyof T);
readonly disabled = input<boolean>(false); readonly disabled = input<boolean>(false);
readonly searchFn = input<LookupSearchFn<T>>(() => of([])); readonly searchFn = input<LookupSearchFn<T>>(() => of([]));
readonly selectedValue = model<string>(''); readonly selectedValue = model<string>('');
readonly displayValue = model<string>(''); readonly displayValue = model<string>('');
readonly itemSelected = output<T>(); readonly itemSelected = output<T>();
readonly searchChanged = output<string>(); readonly searchChanged = output<string>();
readonly itemTemplate = contentChild<TemplateRef<{ $implicit: T }>>('itemTemplate'); readonly itemTemplate = contentChild<TemplateRef<{ $implicit: T }>>('itemTemplate');
readonly noResultsTemplate = contentChild<TemplateRef<void>>('noResultsTemplate'); readonly noResultsTemplate = contentChild<TemplateRef<void>>('noResultsTemplate');
readonly searchResults = signal<T[]>([]); readonly searchResults = signal<T[]>([]);
readonly showDropdown = signal(false); readonly showDropdown = signal(true);
readonly isLoading = signal(false); readonly isLoading = signal(false);
private readonly searchSubject = new Subject<string>(); private readonly searchSubject = new Subject<string>();
ngOnInit() { ngOnInit() {
this.searchSubject.pipe( this.searchSubject
debounceTime(this.debounceTime()), .pipe(
distinctUntilChanged(), debounceTime(this.debounceTime()),
takeUntilDestroyed(this.destroyRef) distinctUntilChanged(),
).subscribe(filter => { takeUntilDestroyed(this.destroyRef),
this.performSearch(filter); )
}); .subscribe(filter => {
} this.performSearch(filter);
});
onSearchInput(filter: string) { }
this.displayValue.set(filter);
this.showDropdown.set(true); onSearchInput(filter: string) {
this.searchChanged.emit(filter); this.displayValue.set(filter);
this.showDropdown.set(true);
if (filter.length >= this.minSearchLength()) { this.searchChanged.emit(filter);
this.searchSubject.next(filter);
} else { if (filter.length >= this.minSearchLength()) {
this.searchResults.set([]); this.searchSubject.next(filter);
} } else {
} this.searchResults.set([]);
onSearchFocus() {
this.showDropdown.set(true);
const currentFilter = this.displayValue() || '';
if (currentFilter.length >= this.minSearchLength()) {
this.performSearch(currentFilter);
}
}
onSearchBlur(event: FocusEvent) {
const relatedTarget = event.relatedTarget as HTMLElement;
if (!relatedTarget?.closest('.abp-lookup-dropdown')) {
this.showDropdown.set(false);
}
}
selectItem(item: T) {
const displayKeyValue = String(item[this.displayKey()] ?? '');
const valueKeyValue = String(item[this.valueKey()] ?? '');
this.displayValue.set(displayKeyValue);
this.selectedValue.set(valueKeyValue);
this.searchResults.set([]);
this.showDropdown.set(false);
this.itemSelected.emit(item);
}
clearSelection() {
this.displayValue.set('');
this.selectedValue.set('');
this.searchResults.set([]);
} }
}
private performSearch(filter: string) { onSearchFocus() {
this.isLoading.set(true); this.showDropdown.set(true);
const currentFilter = this.displayValue() || '';
this.searchFn()(filter).pipe( if (currentFilter.length >= this.minSearchLength()) {
takeUntilDestroyed(this.destroyRef), this.performSearch(currentFilter);
finalize(() => this.isLoading.set(false))
).subscribe({
next: results => {
this.searchResults.set(results);
},
error: () => {
this.searchResults.set([]);
}
});
} }
}
getDisplayValue(item: T): string { onSearchBlur(event: FocusEvent) {
return String(item[this.displayKey()] ?? item[this.valueKey()] ?? ''); const relatedTarget = event.relatedTarget as HTMLElement;
if (!relatedTarget?.closest('.abp-lookup-dropdown')) {
this.showDropdown.set(false);
} }
}
selectItem(item: T) {
const displayKeyValue = String(item[this.displayKey()] ?? '');
const valueKeyValue = String(item[this.valueKey()] ?? '');
this.displayValue.set(displayKeyValue);
this.selectedValue.set(valueKeyValue);
this.searchResults.set([]);
this.showDropdown.set(false);
this.itemSelected.emit(item);
}
clearSelection() {
this.displayValue.set('');
this.selectedValue.set('');
this.searchResults.set([]);
}
private performSearch(filter: string) {
this.isLoading.set(true);
this.searchFn()(filter)
.pipe(
takeUntilDestroyed(this.destroyRef),
finalize(() => this.isLoading.set(false)),
)
.subscribe({
next: results => {
this.searchResults.set(results);
},
error: () => {
this.searchResults.set([]);
},
});
}
getDisplayValue(item: T): string {
return String(item[this.displayKey()] ?? item[this.valueKey()] ?? '');
}
} }

52
npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/permission-checkbox-list/permission-checkbox-list.component.html

@ -1,25 +1,33 @@
<div class="mb-3"> <div class="mb-3">
@if (showTitle()) { @if (showTitle()) {
<h5>{{ title() | abpLocalization }}</h5> <h5>{{ title() | abpLocalization }}</h5>
} }
<div class="form-check form-switch mb-2"> <div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="grantAll-{{ idPrefix() }}" <input
[checked]="state.allPermissionsSelected()" class="form-check-input"
(change)="state.toggleAllPermissions(!state.allPermissionsSelected())" /> type="checkbox"
<label class="form-check-label" for="grantAll-{{ idPrefix() }}"> id="grantAll-{{ idPrefix() }}"
{{ 'AbpPermissionManagement::GrantAllResourcePermissions' | abpLocalization }} [checked]="state.allPermissionsSelected()"
(change)="state.toggleAllPermissions(!state.allPermissionsSelected())"
/>
<label class="form-check-label" for="grantAll-{{ idPrefix() }}">
{{ 'AbpPermissionManagement::GrantAllResourcePermissions' | abpLocalization }}
</label>
</div>
<div class="abp-permission-list-container border rounded p-3">
@for (perm of permissions(); track perm.name) {
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="perm-{{ idPrefix() }}-{{ perm.name }}"
[checked]="state.isPermissionSelected(perm.name || '')"
(change)="state.togglePermission(perm.name || '')"
/>
<label class="form-check-label" for="perm-{{ idPrefix() }}-{{ perm.name }}">
{{ perm.displayName }}
</label> </label>
</div> </div>
<div class="abp-permission-list-container border rounded p-3"> }
@for (perm of permissions(); track perm.name) { </div>
<div class="form-check"> </div>
<input class="form-check-input" type="checkbox" id="perm-{{ idPrefix() }}-{{ perm.name }}"
[checked]="state.isPermissionSelected(perm.name || '')"
(change)="state.togglePermission(perm.name || '')" />
<label class="form-check-label" for="perm-{{ idPrefix() }}-{{ perm.name }}">
{{ perm.displayName }}
</label>
</div>
}
</div>
</div>

14
npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.html

@ -1,4 +1,10 @@
<abp-lookup-search label="AbpPermissionManagement::SearchProviderKey" <abp-lookup-search
placeholder="AbpPermissionManagement::SearchProviderKey" [searchFn]="searchFn" [displayValue]="state.searchFilter()" label="AbpPermissionManagement::SearchProviderKey"
(displayValueChange)="state.searchFilter.set($event)" [selectedValue]="state.selectedProviderKey()" placeholder="AbpPermissionManagement::SearchProviderKey"
(selectedValueChange)="state.selectedProviderKey.set($event)" (itemSelected)="onItemSelected($event)" /> [searchFn]="searchFn"
[displayValue]="state.searchFilter()"
(displayValueChange)="state.searchFilter.set($event)"
[selectedValue]="state.selectedProviderKey()"
(selectedValueChange)="state.selectedProviderKey.set($event)"
(itemSelected)="onItemSelected($event)"
/>

90
npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.ts

@ -1,4 +1,11 @@
import { Component, input, inject, OnInit, ChangeDetectionStrategy, DestroyRef } from '@angular/core'; import {
Component,
input,
inject,
OnInit,
ChangeDetectionStrategy,
DestroyRef,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { PermissionsService } from '@abp/ng.permission-management/proxy'; import { PermissionsService } from '@abp/ng.permission-management/proxy';
import { LookupSearchComponent, LookupItem } from '@abp/ng.components/lookup'; import { LookupSearchComponent, LookupItem } from '@abp/ng.components/lookup';
@ -6,56 +13,55 @@ import { Observable, map } from 'rxjs';
import { ResourcePermissionStateService } from '../../../services/resource-permission-state.service'; import { ResourcePermissionStateService } from '../../../services/resource-permission-state.service';
interface ProviderKeyLookupItem extends LookupItem { interface ProviderKeyLookupItem extends LookupItem {
providerKey: string; providerKey: string;
providerDisplayName?: string; providerDisplayName?: string;
} }
@Component({ @Component({
selector: 'abp-provider-key-search', selector: 'abp-provider-key-search',
templateUrl: './provider-key-search.component.html', templateUrl: './provider-key-search.component.html',
imports: [LookupSearchComponent], imports: [LookupSearchComponent],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ProviderKeySearchComponent implements OnInit { export class ProviderKeySearchComponent implements OnInit {
readonly state = inject(ResourcePermissionStateService); readonly state = inject(ResourcePermissionStateService);
private readonly service = inject(PermissionsService); private readonly service = inject(PermissionsService);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
readonly resourceName = input.required<string>(); readonly resourceName = input.required<string>();
searchFn: (filter: string) => Observable<ProviderKeyLookupItem[]> = () => new Observable(); searchFn: (filter: string) => Observable<ProviderKeyLookupItem[]> = () => new Observable();
ngOnInit() { ngOnInit() {
this.searchFn = (filter: string) => this.loadProviderKeys(filter); this.searchFn = (filter: string) => this.loadProviderKeys(filter);
} }
onItemSelected(item: ProviderKeyLookupItem) { onItemSelected(item: ProviderKeyLookupItem) {
// State is already updated via displayValue and selectedValue bindings // State is already updated via displayValue and selectedValue bindings
// This handler can be used for additional side effects if needed // This handler can be used for additional side effects if needed
} }
private loadProviderKeys(filter: string): Observable<ProviderKeyLookupItem[]> { private loadProviderKeys(filter: string): Observable<ProviderKeyLookupItem[]> {
const providerName = this.state.selectedProviderName(); const providerName = this.state.selectedProviderName();
if (!providerName) { if (!providerName) {
return new Observable(subscriber => { return new Observable(subscriber => {
subscriber.next([]); subscriber.next([]);
subscriber.complete(); subscriber.complete();
}); });
}
return this.service.searchResourceProviderKey(
this.resourceName(),
providerName,
filter,
1
).pipe(
map(res => (res.keys || []).map(k => ({
key: k.providerKey || '',
displayName: k.providerDisplayName || k.providerKey || '',
providerKey: k.providerKey || '',
providerDisplayName: k.providerDisplayName || undefined,
}))),
takeUntilDestroyed(this.destroyRef)
);
} }
return this.service
.searchResourceProviderKey(this.resourceName(), providerName, filter, 1)
.pipe(
map(res =>
(res.keys || []).map(k => ({
key: k.providerKey || '',
displayName: k.providerDisplayName || k.providerKey || '',
providerKey: k.providerKey || '',
providerDisplayName: k.providerDisplayName || undefined,
})),
),
takeUntilDestroyed(this.destroyRef),
);
}
} }

41
npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-form/resource-permission-form.component.html

@ -1,28 +1,37 @@
@if (mode() === eResourcePermissionViewModes.Add) { @if (mode() === eResourcePermissionViewModes.Add) {
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-bold"> <label class="form-label fw-bold">
{{ 'AbpPermissionManagement::SelectProvider' | abpLocalization }} {{ 'AbpPermissionManagement::SelectProvider' | abpLocalization }}
</label> </label>
<div class="mb-2"> <div class="mb-2">
@for (provider of state.providers(); track provider.name; let i = $index) { @for (provider of state.providers(); track provider.name; let i = $index) {
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="radio" [id]="'provider-' + i" [value]="provider.name" <input
[checked]="state.selectedProviderName() === provider.name" class="form-check-input"
(change)="state.onProviderChange(provider.name || '')" /> type="radio"
<label class="form-check-label" [for]="'provider-' + i"> [id]="'provider-' + i"
{{ provider.displayName }} [value]="provider.name"
</label> [checked]="state.selectedProviderName() === provider.name"
(change)="state.onProviderChange(provider.name || '')"
/>
<label class="form-check-label" [for]="'provider-' + i">
{{ provider.displayName }}
</label>
</div> </div>
} }
</div> </div>
<abp-provider-key-search [resourceName]="resourceName()" /> <abp-provider-key-search [resourceName]="resourceName()" />
</div> </div>
<abp-permission-checkbox-list [permissions]="state.permissionDefinitions()" idPrefix="add" /> <abp-permission-checkbox-list [permissions]="state.permissionDefinitions()" idPrefix="add" />
} @else { } @else {
<div class="mb-3" id="permissionList"> <div class="mb-3" id="permissionList">
<h4>{{ 'AbpPermissionManagement::Permissions' | abpLocalization }}</h4> <h4>{{ 'AbpPermissionManagement::Permissions' | abpLocalization }}</h4>
<abp-permission-checkbox-list [permissions]="state.permissionsWithProvider()" idPrefix="edit" [showTitle]="false" /> <abp-permission-checkbox-list
</div> [permissions]="state.permissionsWithProvider()"
} idPrefix="edit"
[showTitle]="false"
/>
</div>
}

107
npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.html

@ -1,60 +1,67 @@
<abp-modal [(visible)]="visible" [busy]="state.modalBusy()" [options]="{ size: 'xl', scrollable: false }"> <abp-modal
<ng-template #abpHeader> [(visible)]="visible"
<h5 class="modal-title"> [busy]="state.modalBusy()"
@switch (state.viewMode()) { [options]="{ size: 'xl', scrollable: false }"
@case (eResourcePermissionViewModes.Edit) { >
{{ 'AbpPermissionManagement::UpdatePermission' | abpLocalization }} <ng-template #abpHeader>
} <h5 class="modal-title">
@case (eResourcePermissionViewModes.Add) { @switch (state.viewMode()) {
{{ 'AbpPermissionManagement::AddResourcePermission' | abpLocalization }} @case (eResourcePermissionViewModes.Edit) {
} {{ 'AbpPermissionManagement::UpdatePermission' | abpLocalization }}
@default { }
{{ 'AbpPermissionManagement::ResourcePermissions' | abpLocalization }} @case (eResourcePermissionViewModes.Add) {
@if (resourceDisplayName()) { {{ 'AbpPermissionManagement::AddResourcePermission' | abpLocalization }}
}
@default {
{{ 'AbpPermissionManagement::ResourcePermissions' | abpLocalization }}
@if (resourceDisplayName()) {
- {{ resourceDisplayName() }} - {{ resourceDisplayName() }}
} }
} }
} }
</h5> </h5>
</ng-template> </ng-template>
<ng-template #abpBody> <ng-template #abpBody>
@if (!state.hasResourcePermission() || !state.hasProviderKeyLookupService()) { @if (!state.hasResourcePermission() || !state.hasProviderKeyLookupService()) {
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
@if (!state.hasResourcePermission()) { @if (!state.hasResourcePermission()) {
{{ 'AbpPermissionManagement::NoResourcePermissionFound' | abpLocalization }} {{ 'AbpPermissionManagement::NoResourcePermissionFound' | abpLocalization }}
} @else {
{{ 'AbpPermissionManagement::NoResourceProviderKeyLookupServiceFound' | abpLocalization }}
}
</div>
} @else { } @else {
@switch (state.viewMode()) { {{ 'AbpPermissionManagement::NoResourceProviderKeyLookupServiceFound' | abpLocalization }}
}
</div>
} @else {
@switch (state.viewMode()) {
@case (eResourcePermissionViewModes.List) { @case (eResourcePermissionViewModes.List) {
<abp-resource-permission-list (addClicked)="onAddClicked()" (editClicked)="onEditClicked($event)" <abp-resource-permission-list
(deleteClicked)="onDeleteClicked($event)" /> (addClicked)="onAddClicked()"
(editClicked)="onEditClicked($event)"
(deleteClicked)="onDeleteClicked($event)"
/>
} }
@case (eResourcePermissionViewModes.Add) { @case (eResourcePermissionViewModes.Add) {
<abp-resource-permission-form mode="add" [resourceName]="resourceName()" /> <abp-resource-permission-form mode="add" [resourceName]="resourceName()" />
} }
@case (eResourcePermissionViewModes.Edit) { @case (eResourcePermissionViewModes.Edit) {
<abp-resource-permission-form mode="edit" [resourceName]="resourceName()" /> <abp-resource-permission-form mode="edit" [resourceName]="resourceName()" />
}
}
} }
</ng-template> }
}
</ng-template>
<ng-template #abpFooter> <ng-template #abpFooter>
@if (state.isListMode()) { @if (state.isListMode()) {
<button type="button" class="btn btn-outline-primary" abpClose> <button type="button" class="btn btn-outline-primary" abpClose>
{{ 'AbpUi::Close' | abpLocalization }} {{ 'AbpUi::Close' | abpLocalization }}
</button> </button>
} @else { } @else {
<button type="button" class="btn btn-outline-secondary" (click)="state.goToListMode()"> <button type="button" class="btn btn-outline-secondary" (click)="state.goToListMode()">
{{ 'AbpUi::Cancel' | abpLocalization }} {{ 'AbpUi::Cancel' | abpLocalization }}
</button> </button>
<abp-button iconClass="fa fa-check" (click)="savePermission()"> <abp-button iconClass="fa fa-check" (click)="savePermission()">
{{ 'AbpUi::Save' | abpLocalization }} {{ 'AbpUi::Save' | abpLocalization }}
</abp-button> </abp-button>
} }
</ng-template> </ng-template>
</abp-modal> </abp-modal>

385
npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.ts

@ -1,26 +1,17 @@
import { ListService, LocalizationPipe } from '@abp/ng.core'; import { ListService, LocalizationPipe } from '@abp/ng.core';
import { import {
ButtonComponent, ButtonComponent,
Confirmation, Confirmation,
ConfirmationService, ConfirmationService,
ModalCloseDirective, ModalCloseDirective,
ModalComponent, ModalComponent,
ToasterService, ToasterService,
} from '@abp/ng.theme.shared'; } from '@abp/ng.theme.shared';
import { import {
PermissionsService, PermissionsService,
ResourcePermissionGrantInfoDto, ResourcePermissionGrantInfoDto,
} from '@abp/ng.permission-management/proxy'; } from '@abp/ng.permission-management/proxy';
import { import { Component, inject, input, model, OnInit, effect, untracked, signal } from '@angular/core';
Component,
inject,
input,
model,
OnInit,
effect,
untracked,
signal,
} from '@angular/core';
import { finalize, switchMap, of } from 'rxjs'; import { finalize, switchMap, of } from 'rxjs';
import { ResourcePermissionStateService } from '../../services/resource-permission-state.service'; import { ResourcePermissionStateService } from '../../services/resource-permission-state.service';
import { ResourcePermissionListComponent } from './resource-permission-list/resource-permission-list.component'; import { ResourcePermissionListComponent } from './resource-permission-list/resource-permission-list.component';
@ -31,187 +22,195 @@ import { eResourcePermissionViewModes } from '../../enums/view-modes';
const DEFAULT_MAX_RESULT_COUNT = 10; const DEFAULT_MAX_RESULT_COUNT = 10;
@Component({ @Component({
selector: 'abp-resource-permission-management', selector: 'abp-resource-permission-management',
templateUrl: './resource-permission-management.component.html', templateUrl: './resource-permission-management.component.html',
exportAs: 'abpResourcePermissionManagement', exportAs: 'abpResourcePermissionManagement',
providers: [ResourcePermissionStateService, ListService], providers: [ResourcePermissionStateService, ListService],
imports: [ imports: [
ModalComponent, ModalComponent,
LocalizationPipe, LocalizationPipe,
ButtonComponent, ButtonComponent,
ModalCloseDirective, ModalCloseDirective,
ResourcePermissionListComponent, ResourcePermissionListComponent,
ResourcePermissionFormComponent, ResourcePermissionFormComponent,
], ],
}) })
export class ResourcePermissionManagementComponent implements OnInit { export class ResourcePermissionManagementComponent implements OnInit {
readonly eResourcePermissionViewModes = eResourcePermissionViewModes; readonly eResourcePermissionViewModes = eResourcePermissionViewModes;
protected readonly service = inject(PermissionsService); protected readonly service = inject(PermissionsService);
protected readonly toasterService = inject(ToasterService); protected readonly toasterService = inject(ToasterService);
protected readonly confirmationService = inject(ConfirmationService); protected readonly confirmationService = inject(ConfirmationService);
protected readonly state = inject(ResourcePermissionStateService); protected readonly state = inject(ResourcePermissionStateService);
private readonly list = inject(ListService); private readonly list = inject(ListService);
readonly resourceName = input.required<string>(); readonly resourceName = input.required<string>();
readonly resourceKey = input.required<string>(); readonly resourceKey = input.required<string>();
readonly resourceDisplayName = input<string>(); readonly resourceDisplayName = input<string>();
readonly visible = model<boolean>(false); readonly visible = model<boolean>(false);
private readonly previousVisible = signal(false); private readonly previousVisible = signal(false);
constructor() { constructor() {
effect(() => { effect(() => {
const resourceName = this.resourceName(); const resourceName = this.resourceName();
const resourceKey = this.resourceKey(); const resourceKey = this.resourceKey();
const resourceDisplayName = this.resourceDisplayName(); const resourceDisplayName = this.resourceDisplayName();
untracked(() => { untracked(() => {
this.state.resourceName.set(resourceName); this.state.resourceName.set(resourceName);
this.state.resourceKey.set(resourceKey); this.state.resourceKey.set(resourceKey);
this.state.resourceDisplayName.set(resourceDisplayName); this.state.resourceDisplayName.set(resourceDisplayName);
}); });
}); });
effect(() => { effect(() => {
const isVisible = this.visible(); const isVisible = this.visible();
const wasVisible = this.previousVisible(); const wasVisible = this.previousVisible();
if (isVisible && !wasVisible) { if (isVisible && !wasVisible) {
this.openModal(); this.openModal();
} else if (!isVisible && wasVisible) { } else if (!isVisible && wasVisible) {
this.state.reset(); this.state.reset();
} }
untracked(() => this.previousVisible.set(isVisible)); untracked(() => this.previousVisible.set(isVisible));
}); });
} }
ngOnInit() { ngOnInit() {
this.list.maxResultCount = DEFAULT_MAX_RESULT_COUNT; this.list.maxResultCount = DEFAULT_MAX_RESULT_COUNT;
this.list.hookToQuery(query => { this.list
const allData = this.state.allResourcePermissions(); .hookToQuery(query => {
const skipCount = query.skipCount || 0; const allData = this.state.allResourcePermissions();
const maxResultCount = query.maxResultCount || DEFAULT_MAX_RESULT_COUNT; const skipCount = query.skipCount || 0;
const maxResultCount = query.maxResultCount || DEFAULT_MAX_RESULT_COUNT;
const paginatedData = allData.slice(skipCount, skipCount + maxResultCount);
const paginatedData = allData.slice(skipCount, skipCount + maxResultCount);
return of({
items: paginatedData, return of({
totalCount: allData.length items: paginatedData,
}); totalCount: allData.length,
}).subscribe(result => {
this.state.resourcePermissions.set(result.items);
this.state.totalCount.set(result.totalCount);
}); });
} })
.subscribe(result => {
openModal() { this.state.resourcePermissions.set(result.items);
this.state.modalBusy.set(true); this.state.totalCount.set(result.totalCount);
});
this.service.getResource(this.resourceName(), this.resourceKey()).pipe( }
switchMap(permRes => {
this.state.setResourceData(permRes.permissions || []); openModal() {
this.list.get(); this.state.modalBusy.set(true);
return this.service.getResourceProviderKeyLookupServices(this.resourceName());
}), this.service
switchMap(providerRes => { .getResource(this.resourceName(), this.resourceKey())
this.state.setProviders(providerRes.providers || []); .pipe(
return this.service.getResourceDefinitions(this.resourceName()); switchMap(permRes => {
}), this.state.setResourceData(permRes.permissions || []);
finalize(() => this.state.modalBusy.set(false)) this.list.get();
).subscribe({ return this.service.getResourceProviderKeyLookupServices(this.resourceName());
next: defRes => { }),
this.state.setDefinitions(defRes.permissions || []); switchMap(providerRes => {
}, this.state.setProviders(providerRes.providers || []);
error: () => { return this.service.getResourceDefinitions(this.resourceName());
this.toasterService.error('AbpPermissionManagement::ErrorLoadingPermissions'); }),
} finalize(() => this.state.modalBusy.set(false)),
}); )
} .subscribe({
next: defRes => {
onAddClicked() { this.state.setDefinitions(defRes.permissions || []);
this.state.goToAddMode(); },
} error: () => {
this.toasterService.error('AbpPermissionManagement::ErrorLoadingPermissions');
onEditClicked(grant: ResourcePermissionGrantInfoDto) { },
this.state.prepareEditMode(grant); });
this.state.modalBusy.set(true); }
this.service.getResourceByProvider( onAddClicked() {
this.resourceName(), this.state.goToAddMode();
this.resourceKey(), }
grant.providerName || '',
grant.providerKey || '' onEditClicked(grant: ResourcePermissionGrantInfoDto) {
).pipe( this.state.prepareEditMode(grant);
finalize(() => this.state.modalBusy.set(false)) this.state.modalBusy.set(true);
).subscribe({
next: res => { this.service
this.state.setEditModePermissions(res.permissions || []); .getResourceByProvider(
} this.resourceName(),
}); this.resourceKey(),
} grant.providerName || '',
grant.providerKey || '',
onDeleteClicked(grant: ResourcePermissionGrantInfoDto) { )
this.confirmationService .pipe(finalize(() => this.state.modalBusy.set(false)))
.warn( .subscribe({
'AbpPermissionManagement::PermissionDeletionConfirmationMessage', next: res => {
'AbpPermissionManagement::AreYouSure', this.state.setEditModePermissions(res.permissions || []);
{ },
messageLocalizationParams: [grant.providerKey || ''], });
} }
onDeleteClicked(grant: ResourcePermissionGrantInfoDto) {
this.confirmationService
.warn(
'AbpPermissionManagement::ResourcePermissionDeletionConfirmationMessage',
'AbpPermissionManagement::AreYouSure',
{
messageLocalizationParams: [grant.providerKey || ''],
},
)
.subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) {
this.state.modalBusy.set(true);
this.service
.deleteResource(
this.resourceName(),
this.resourceKey(),
grant.providerName || '',
grant.providerKey || '',
) )
.subscribe((status: Confirmation.Status) => { .pipe(
if (status === Confirmation.Status.confirm) { switchMap(() => this.service.getResource(this.resourceName(), this.resourceKey())),
this.state.modalBusy.set(true); finalize(() => this.state.modalBusy.set(false)),
this.service.deleteResource( )
this.resourceName(), .subscribe({
this.resourceKey(), next: res => {
grant.providerName || '', this.state.setResourceData(res.permissions || []);
grant.providerKey || '' this.list.get();
).pipe( this.toasterService.success('AbpUi::DeletedSuccessfully');
switchMap(() => this.service.getResource(this.resourceName(), this.resourceKey())), },
finalize(() => this.state.modalBusy.set(false))
).subscribe({
next: res => {
this.state.setResourceData(res.permissions || []);
this.list.get();
this.toasterService.success('AbpUi::SuccessfullyDeleted');
}
});
}
}); });
}
savePermission() {
const isEdit = this.state.isEditMode();
const providerName = isEdit ? this.state.editProviderName() : this.state.selectedProviderName();
const providerKey = isEdit ? this.state.editProviderKey() : this.state.selectedProviderKey();
if (!isEdit && !this.state.canSave()) {
this.toasterService.warn('AbpPermissionManagement::PleaseSelectProviderAndPermissions');
return;
} }
});
}
this.state.modalBusy.set(true); savePermission() {
this.service.updateResource( const isEdit = this.state.isEditMode();
this.resourceName(), const providerName = isEdit ? this.state.editProviderName() : this.state.selectedProviderName();
this.resourceKey(), const providerKey = isEdit ? this.state.editProviderKey() : this.state.selectedProviderKey();
{
providerName, if (!isEdit && !this.state.canSave()) {
providerKey, this.toasterService.warn('AbpPermissionManagement::PleaseSelectProviderAndPermissions');
permissions: this.state.selectedPermissions() return;
}
).pipe(
switchMap(() => this.service.getResource(this.resourceName(), this.resourceKey())),
finalize(() => this.state.modalBusy.set(false))
).subscribe({
next: res => {
this.state.setResourceData(res.permissions || []);
this.list.get();
this.toasterService.success('AbpUi::SavedSuccessfully');
this.state.goToListMode();
}
});
} }
this.state.modalBusy.set(true);
this.service
.updateResource(this.resourceName(), this.resourceKey(), {
providerName,
providerKey,
permissions: this.state.selectedPermissions(),
})
.pipe(
switchMap(() => this.service.getResource(this.resourceName(), this.resourceKey())),
finalize(() => this.state.modalBusy.set(false)),
)
.subscribe({
next: res => {
this.state.setResourceData(res.permissions || []);
this.list.get();
this.toasterService.success('AbpUi::SavedSuccessfully');
this.state.goToListMode();
},
});
}
} }

32
text-template-management/src/Volo.Abp.TextTemplateManagement.Domain/Volo/Abp/TextTemplateManagement/StaticTemplateDefinitionChangedEventHandler.cs

@ -1,32 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
using Volo.Abp.StaticDefinitions;
using Volo.Abp.TextTemplating;
using Volo.Abp.Threading;
namespace Volo.Abp.TextTemplateManagement;
public class StaticTemplateDefinitionChangedEventHandler : ILocalEventHandler<StaticTemplateDefinitionChangedEvent>, ITransientDependency
{
protected IStaticDefinitionCache<TemplateDefinition, Dictionary<string, TemplateDefinition>> DefinitionCache { get; }
protected TextTemplateDynamicInitializer TextTemplateDynamicInitializer { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
public StaticTemplateDefinitionChangedEventHandler(
IStaticDefinitionCache<TemplateDefinition, Dictionary<string, TemplateDefinition>> definitionCache,
TextTemplateDynamicInitializer textTemplateDynamicInitializer,
ICancellationTokenProvider cancellationTokenProvider)
{
DefinitionCache = definitionCache;
TextTemplateDynamicInitializer = textTemplateDynamicInitializer;
CancellationTokenProvider = cancellationTokenProvider;
}
public virtual async Task HandleEventAsync(StaticTemplateDefinitionChangedEvent eventData)
{
await DefinitionCache.ClearAsync();
await TextTemplateDynamicInitializer.InitializeAsync(false, CancellationTokenProvider.Token);
}
}
Loading…
Cancel
Save